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: = <=>
This commit is contained in:
Al Lansley 2024-04-03 09:53:20 +11:00 committed by GitHub
parent fef965bcb5
commit d3c8635748
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 108 additions and 51 deletions

View File

@ -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.");
}
});
}

View File

@ -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()
}

View File

@ -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) }
}
private fun shareLogs() {
// 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 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)
}
}

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/export_logs_button"
android:layout_gravity="center"
style="@style/Widget.Session.Button.Common.Filled"
android:textStyle="bold"
@ -11,5 +12,6 @@
android:paddingHorizontal="@dimen/medium_spacing"
android:paddingVertical="12dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content" />
</FrameLayout>

View File

@ -1,23 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp"
android:gravity="bottom">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/export_progress_container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" >
<ProgressBar android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"/>
<TextView android:id="@+id/progress_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
tools:text="1345 messages so far"/>
<ProgressBar
android:id="@+id/export_progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible" />
</LinearLayout>

View File

@ -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" />
<!-- Note: Having this as `android:layout` rather than `android:layoutWidget` allows it to fit the screen width -->
<Preference android:layout="@layout/preference_widget_progress" />
</PreferenceCategory>
<PreferenceCategory>
<Preference
android:key="translate_session"
android:title="@string/activity_help_settings__translate_session"
android:widgetLayout="@layout/preference_external_link"
/>
android:widgetLayout="@layout/preference_external_link" />
</PreferenceCategory>
<PreferenceCategory>
<Preference
android:key="feedback"
android:title="@string/activity_help_settings__feedback"
android:widgetLayout="@layout/preference_external_link"
/>
android:widgetLayout="@layout/preference_external_link" />
</PreferenceCategory>
<PreferenceCategory>
<Preference
android:key="faq"
android:title="@string/activity_help_settings__faq"
android:widgetLayout="@layout/preference_external_link"
/>
android:widgetLayout="@layout/preference_external_link" />
</PreferenceCategory>
<PreferenceCategory>
<Preference
android:key="support"
android:title="@string/activity_help_settings__support"
android:widgetLayout="@layout/preference_external_link"
/>
android:widgetLayout="@layout/preference_external_link" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -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<File> getAvatarFiles(@NonNull Context context) {

View File

@ -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);
}