From d3c8635748b1c08bc93116e11cc8a1ee73a9bb6a Mon Sep 17 00:00:00 2001 From: Al Lansley Date: Wed, 3 Apr 2024 09:53:20 +1100 Subject: [PATCH] SES-697 - Add loading state when exporting logs (#1402) * WIP * Fixes #1401 * Cleanup from PR view * Final cleanup * Removed commented line of code & re-ordered comment * Addressed PR feedback * Re-allowed loading of avatars to throw exceptions rather than return null on failure --------- Co-authored-by: = <=> --- .../securesms/ApplicationContext.java | 5 +- .../preferences/HelpSettingsActivity.kt | 20 +++++- .../securesms/preferences/ShareLogsDialog.kt | 66 +++++++++++++++---- .../main/res/layout/export_logs_widget.xml | 8 ++- .../res/layout/preference_widget_progress.xml | 34 +++++----- app/src/main/res/xml/preferences_help.xml | 17 +++-- .../libsession/avatars/AvatarHelper.java | 6 +- .../avatars/ProfileContactPhoto.java | 3 +- 8 files changed, 108 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index f95e818237..5a8b03fdf8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -478,9 +478,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO Log.d("Loki-Avatar", "Uploading Avatar Finished"); return Unit.INSTANCE; }); - } catch (Exception exception) { - // Do nothing - Log.e("Loki-Avatar", "Uploading avatar failed", exception); + } catch (Exception e) { + Log.e("Loki-Avatar", "Uploading avatar failed."); } }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt index f6efd041cd..e7e5f2d5f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt @@ -5,9 +5,14 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle +import android.widget.ProgressBar +import android.widget.TextView import android.widget.Toast +import androidx.core.view.isInvisible import androidx.preference.Preference + import network.loki.messenger.R +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.permissions.Permissions @@ -67,6 +72,19 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() { } } + private fun updateExportButtonAndProgressBarUI(exportJobRunning: Boolean) { + this.activity?.runOnUiThread(Runnable { + // Change export logs button text + val exportLogsButton = this.activity?.findViewById(R.id.export_logs_button) as TextView? + if (exportLogsButton == null) { Log.w("Loki", "Could not find export logs button view.") } + exportLogsButton?.text = if (exportJobRunning) getString(R.string.cancel) else getString(R.string.activity_help_settings__export_logs) + + // Show progress bar + val exportProgressBar = this.activity?.findViewById(R.id.export_progress_bar) as ProgressBar? + exportProgressBar?.isInvisible = !exportJobRunning + }) + } + private fun shareLogs() { Permissions.with(this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) @@ -76,7 +94,7 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() { Toast.makeText(requireActivity(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show() } .onAllGranted { - ShareLogsDialog().show(parentFragmentManager,"Share Logs Dialog") + ShareLogsDialog(::updateExportButtonAndProgressBarUI).show(parentFragmentManager,"Share Logs Dialog") } .execute() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt index 2dc5e75d98..9bfc1dabf2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt @@ -11,55 +11,73 @@ import android.os.Bundle import android.os.Environment import android.provider.MediaStore import android.webkit.MimeTypeMap +import android.widget.ProgressBar +import android.widget.TextView import android.widget.Toast +import androidx.core.view.isInvisible import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope + import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext + import network.loki.messenger.BuildConfig import network.loki.messenger.R + import org.session.libsignal.utilities.ExternalStorageUtil import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.util.FileProviderUtil import org.thoughtcrime.securesms.util.StreamUtil + import java.io.File import java.io.FileOutputStream import java.io.IOException import java.util.Objects import java.util.concurrent.TimeUnit -class ShareLogsDialog : DialogFragment() { +class ShareLogsDialog(private val updateCallback: (Boolean)->Unit): DialogFragment() { + + private val TAG = "ShareLogsDialog" private var shareJob: Job? = null override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { title(R.string.dialog_share_logs_title) text(R.string.dialog_share_logs_explanation) - button(R.string.share, dismiss = false) { shareLogs() } - cancelButton { dismiss() } + button(R.string.share, dismiss = false) { runShareLogsJob() } + cancelButton { updateCallback(false) } + } + + // If the share logs dialog loses focus the job gets cancelled so we'll update the UI state + override fun onPause() { + super.onPause() + updateCallback(false) } - private fun shareLogs() { + private fun runShareLogsJob() { + // Cancel any existing share job that might already be running to start anew shareJob?.cancel() + + updateCallback(true) + shareJob = lifecycleScope.launch(Dispatchers.IO) { val persistentLogger = ApplicationContext.getInstance(context).persistentLogger try { + Log.d(TAG, "Starting share logs job...") + val context = requireContext() val outputUri: Uri = ExternalStorageUtil.getDownloadUri() - val mediaUri = getExternalFile() - if (mediaUri == null) { - // show toast saying media saved - dismiss() - return@launch - } + val mediaUri = getExternalFile() ?: return@launch val inputStream = persistentLogger.logs.get().byteInputStream() val updateValues = ContentValues() + + // Add details into the output or media files as appropriate if (outputUri.scheme == ContentResolver.SCHEME_FILE) { FileOutputStream(mediaUri.path).use { outputStream -> StreamUtil.copy(inputStream, outputStream) @@ -73,6 +91,7 @@ class ShareLogsDialog : DialogFragment() { } } } + if (Build.VERSION.SDK_INT > 28) { updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0) } @@ -95,13 +114,35 @@ class ShareLogsDialog : DialogFragment() { } startActivity(Intent.createChooser(shareIntent, getString(R.string.share))) } - - dismiss() } catch (e: Exception) { withContext(Main) { Log.e("Loki", "Error saving logs", e) Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show() } + } + }.also { shareJob -> + shareJob.invokeOnCompletion { handler -> + // Note: Don't show Toasts here directly - use `withContext(Main)` or such if req'd + handler?.message.let { msg -> + if (shareJob.isCancelled) { + if (msg.isNullOrBlank()) { + Log.w(TAG, "Share logs job was cancelled.") + } else { + Log.d(TAG, "Share logs job was cancelled. Reason: $msg") + } + + } + else if (shareJob.isCompleted) { + Log.d(TAG, "Share logs job completed. Msg: $msg") + } + else { + Log.w(TAG, "Share logs job finished while still Active. Msg: $msg") + } + } + + // Regardless of the job's success it has now completed so update the UI + updateCallback(false) + dismiss() } } @@ -158,5 +199,4 @@ class ShareLogsDialog : DialogFragment() { return context.contentResolver.insert(outputUri, contentValues) } - } \ No newline at end of file diff --git a/app/src/main/res/layout/export_logs_widget.xml b/app/src/main/res/layout/export_logs_widget.xml index 95c681d397..56f6bc07df 100644 --- a/app/src/main/res/layout/export_logs_widget.xml +++ b/app/src/main/res/layout/export_logs_widget.xml @@ -1,9 +1,10 @@ - + + android:layout_height="wrap_content" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_widget_progress.xml b/app/src/main/res/layout/preference_widget_progress.xml index 3cce4b9483..4b44b9a55a 100644 --- a/app/src/main/res/layout/preference_widget_progress.xml +++ b/app/src/main/res/layout/preference_widget_progress.xml @@ -1,23 +1,19 @@ - + - - - + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_help.xml b/app/src/main/res/xml/preferences_help.xml index 0372619ea3..9bc9bd13e1 100644 --- a/app/src/main/res/xml/preferences_help.xml +++ b/app/src/main/res/xml/preferences_help.xml @@ -6,39 +6,38 @@ android:key="export_logs" android:title="@string/activity_help_settings__report_bug_title" android:summary="@string/activity_help_settings__report_bug_summary" - android:widgetLayout="@layout/export_logs_widget"/> + android:widgetLayout="@layout/export_logs_widget" /> + + + + android:widgetLayout="@layout/preference_external_link" /> + android:widgetLayout="@layout/preference_external_link" /> + android:widgetLayout="@layout/preference_external_link" /> + android:widgetLayout="@layout/preference_external_link" /> \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/avatars/AvatarHelper.java b/libsession/src/main/java/org/session/libsession/avatars/AvatarHelper.java index 1588289017..cc0909a592 100644 --- a/libsession/src/main/java/org/session/libsession/avatars/AvatarHelper.java +++ b/libsession/src/main/java/org/session/libsession/avatars/AvatarHelper.java @@ -8,9 +8,11 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; import org.session.libsession.utilities.Address; +import org.session.libsignal.utilities.Log; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -22,9 +24,9 @@ public class AvatarHelper { private static final String AVATAR_DIRECTORY = "avatars"; public static InputStream getInputStreamFor(@NonNull Context context, @NonNull Address address) - throws IOException + throws FileNotFoundException { - return new FileInputStream(getAvatarFile(context, address)); + return new FileInputStream(getAvatarFile(context, address)); } public static List getAvatarFiles(@NonNull Context context) { diff --git a/libsession/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java index f8675b031f..76a3449625 100644 --- a/libsession/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java +++ b/libsession/src/main/java/org/session/libsession/avatars/ProfileContactPhoto.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import org.session.libsession.utilities.Address; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; @@ -24,7 +25,7 @@ public class ProfileContactPhoto implements ContactPhoto { } @Override - public InputStream openInputStream(Context context) throws IOException { + public InputStream openInputStream(Context context) throws FileNotFoundException { return AvatarHelper.getInputStreamFor(context, address); }