Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 35 additions & 19 deletions app/src/main/java/com/nextcloud/client/logger/ui/LogsEmailSender.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.net.Uri
import android.os.Build
import android.text.format.Formatter
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider
Expand All @@ -24,13 +25,19 @@ import com.nextcloud.client.core.Cancellable
import com.nextcloud.client.core.Clock
import com.nextcloud.client.logger.LogEntry
import com.owncloud.android.R
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.notifications.NotificationUtils
import com.owncloud.android.utils.FileExportUtils
import java.io.File
import java.security.SecureRandom
import java.util.TimeZone
import javax.inject.Inject

class LogsEmailSender(private val context: Context, private val clock: Clock, private val runner: AsyncRunner) {
class LogsEmailSender @Inject constructor(
private val context: Context,
private val clock: Clock,
private val runner: AsyncRunner
) {

private companion object {
const val LOGS_MIME_TYPE = "text/plain"
Expand Down Expand Up @@ -74,10 +81,27 @@ class LogsEmailSender(private val context: Context, private val clock: Clock, pr
fun export(logs: List<LogEntry>) {
if (task == null) {
val outFile = File(context.cacheDir, "attachments/logs.txt")
task = runner.postQuickTask(Task(context, logs, outFile, clock.tz), onResult = {
task = null
export(outFile)
})
task = runner.postQuickTask(
task = {
Task(context, logs, outFile, clock.tz).invoke()
export(outFile)
outFile.length()
},
onResult = { size ->
task = null
val message = context.getString(
R.string.logs_export_success,
Formatter.formatFileSize(context, size)
)
showExportResult(message)
},
onError = { error ->
task = null
Log_OC.e(this, "Could not export logs", error)
val reason = error.localizedMessage ?: context.getString(R.string.common_error_unknown)
showExportResult(context.getString(R.string.logs_export_failed, reason))
}
)
}
}

Expand All @@ -89,35 +113,27 @@ class LogsEmailSender(private val context: Context, private val clock: Clock, pr
}

private fun export(file: File) {
FileExportUtils().exportFile(
FileExportUtils("Nextcloud").exportFile(
"Nextcloud Android Files Logs",
"text/plain",
context.contentResolver,
null,
file
)
showSuccessNotification(1)
}

fun showSuccessNotification(successfulExports: Int) {
showNotification(
context.resources.getQuantityString(
R.plurals.export_successful,
successfulExports,
successfulExports
)
)
}

private fun showNotification(message: String) {
private fun showExportResult(message: String) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
val notificationId = SecureRandom().nextInt()

val notificationBuilder = NotificationCompat.Builder(
context,
NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD
)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(message)
.setContentTitle(context.getString(R.string.logs_menu_export))
.setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setAutoCancel(true)

val actionIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@
*/
package com.owncloud.android.ui.activity;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
Expand All @@ -46,7 +49,9 @@
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.etm.EtmActivity;
import com.nextcloud.client.logger.LogsRepository;
import com.nextcloud.client.logger.ui.LogsActivity;
import com.nextcloud.client.logger.ui.LogsEmailSender;
import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.preferences.AppPreferences;
Expand Down Expand Up @@ -128,6 +133,7 @@ public class SettingsActivity extends PreferenceActivity
private static final int ACTION_REQUEST_CODE_DAVDROID_SETUP = 10;
private static final int ACTION_SHOW_MNEMONIC = 11;
private static final int ACTION_E2E = 12;
private static final int REQUEST_EXPORT_LOGS_PERMISSION = 101;
private static final int TRUE_VALUE = 1;

private static final String DAV_PATH = "/remote.php/dav";
Expand Down Expand Up @@ -155,6 +161,8 @@ public class SettingsActivity extends PreferenceActivity
@Inject ClientFactory clientFactory;
@Inject ViewThemeUtils viewThemeUtils;
@Inject ConnectivityService connectivityService;
@Inject LogsRepository logsRepository;
@Inject LogsEmailSender logsEmailSender;

@SuppressWarnings("deprecation")
@Override
Expand Down Expand Up @@ -416,6 +424,15 @@ private void setupImprintPreference(PreferenceCategory preferenceCategoryMore) {
}

private void setupLoggingPreference(PreferenceCategory preferenceCategoryMore) {
Preference pExport = findPreference("export_logs");
if (MDMConfig.INSTANCE.isLogEnabled(this)) {
pExport.setOnPreferenceClickListener(preference -> {
exportLogs();
return true;
});
} else {
preferenceCategoryMore.removePreference(pExport);
}
Preference pLogger = findPreference("logger");
if (pLogger != null) {
if (MDMConfig.INSTANCE.isLogEnabled(this)) {
Expand All @@ -431,6 +448,20 @@ private void setupLoggingPreference(PreferenceCategory preferenceCategoryMore) {
}
}

private void exportLogs() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
&& !PermissionUtil.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_EXPORT_LOGS_PERMISSION);
return;
}
logsRepository.load((entries, size) -> {
if (!isDestroyed()) {
logsEmailSender.export(entries);
}
return Unit.INSTANCE;
});
}


private void setupRecommendPreference(PreferenceCategory preferenceCategoryMore) {
boolean recommendEnabled = getResources().getBoolean(R.bool.recommend_enabled);
Expand Down Expand Up @@ -1239,10 +1270,20 @@ protected void onPostCreate(Bundle savedInstanceState) {

@Override
protected void onDestroy() {
logsEmailSender.stop();
super.onDestroy();
getDelegate().onDestroy();
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_EXPORT_LOGS_PERMISSION
&& grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
exportLogs();
}
}

@Override
protected void onStop() {
super.onStop();
Expand Down
43 changes: 21 additions & 22 deletions app/src/main/java/com/owncloud/android/utils/FileExportUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream

class FileExportUtils {
@Throws(IllegalStateException::class)
class FileExportUtils(private val subdirectory: String = "") {
@Throws(IllegalStateException::class, IOException::class)
fun exportFile(fileName: String, mimeType: String, contentResolver: ContentResolver, ocFile: OCFile?, file: File?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
exportFileAndroid10AndAbove(
Expand Down Expand Up @@ -56,27 +57,17 @@ class FileExportUtils {
val cv = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/$subdirectory")
}

var uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, cv)

if (uri == null) {
var count = INITIAL_RENAME_COUNT
do {
val name = generateNewName(fileName, count)
cv.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, cv)

count++
} while (uri == null)
}
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, cv)
?: throw IOException("Could not create export in Downloads")

copy(
ocFile,
file,
contentResolver,
FileOutputStream(contentResolver.openFileDescriptor(uri, "w")?.fileDescriptor)
contentResolver.openOutputStream(uri, "w") ?: throw IOException("Could not open export in Downloads")
)
}

Expand All @@ -87,8 +78,13 @@ class FileExportUtils {
file: File?
) {
try {
var target = File(
val directory = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
subdirectory
)
directory.mkdirs()
var target = File(
directory,
fileName
)

Expand All @@ -97,7 +93,7 @@ class FileExportUtils {
do {
val name = generateNewName(fileName, count)
target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
directory,
name
)

Expand All @@ -113,31 +109,34 @@ class FileExportUtils {
)
} catch (e: FileNotFoundException) {
Log_OC.e(this, "File not found", e)
throw e
} catch (e: IOException) {
Log_OC.e(this, "Cannot write file", e)
throw e
}
}

@Throws(IllegalStateException::class)
private fun copy(ocFile: OCFile?, file: File?, contentResolver: ContentResolver, outputStream: FileOutputStream) {
@Throws(IllegalStateException::class, IOException::class)
private fun copy(ocFile: OCFile?, file: File?, contentResolver: ContentResolver, outputStream: OutputStream) {
outputStream.use { fos ->
try {
val inputStream = when {
ocFile != null -> contentResolver.openInputStream(ocFile.storageUri)
file != null -> FileInputStream(file)
else -> error("ocFile and file both may not be null")
}!!
} ?: throw IOException("Could not open export source")

inputStream.use { fis ->
copyStream(fis, fos)
}
} catch (e: IOException) {
Log_OC.e(this, "Cannot write file", e)
throw e
}
}
}

private fun copyStream(inputStream: InputStream, outputStream: FileOutputStream) {
private fun copyStream(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(COPY_BUFFER_SIZE)
var len: Int
while (inputStream.read(buffer).also { len = it } != -1) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,8 @@
<string name="logs_menu_refresh">Refresh</string>
<string name="logs_menu_send">Send logs by email</string>
<string name="logs_menu_export">Export logs</string>
<string name="logs_export_success">Logs saved to Download/Nextcloud/ (%1$s)</string>
<string name="logs_export_failed">Could not save logs to Download/Nextcloud/: %1$s</string>
<string name="logs_menu_delete">Delete logs</string>
<string name="log_send_no_mail_app">No app for sending logs found. Please install an email client.</string>
<string name="log_send_mail_subject">%1$s Android app logs</string>
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@
<Preference
android:title="@string/logs_title"
android:key="logger" />
<Preference
android:title="@string/logs_menu_export"
android:key="export_logs" />
<Preference
android:title="@string/prefs_imprint"
android:key="imprint" />
Expand Down
Loading