Improve error reporting for SMS export.

This commit is contained in:
Cody Henthorne
2022-10-19 22:11:31 -04:00
committed by GitHub
parent 262f762d7f
commit 690e1e60ba
33 changed files with 933 additions and 95 deletions

View File

@@ -99,6 +99,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
public abstract List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp);
public abstract Set<Long> getAllRateLimitedMessageIds();
public abstract Cursor getUnexportedInsecureMessages(int limit);
public abstract long getUnexportedInsecureMessagesEstimatedSize();
public abstract void deleteExportedMessages();
public abstract void markExpireStarted(long messageId);
@@ -380,15 +381,15 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
}
protected String getInsecureMessageClause(long threadId) {
String isSent = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE;
String isReceived = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE;
String isSecure = "(" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
String isNotSecure = "(" + getTypeField() + " <= " + (Types.BASE_TYPE_MASK | Types.MESSAGE_ATTRIBUTE_MASK) + ")";
String isSent = "(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE;
String isReceived = "(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE;
String isSecure = "(" + getTableName() + "." + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
String isNotSecure = "(" + getTableName() + "." + getTypeField() + " <= " + (Types.BASE_TYPE_MASK | Types.MESSAGE_ATTRIBUTE_MASK) + ")";
String whereClause = String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s AND %s", isSent, isReceived, isSecure, isNotSecure);
if (threadId != -1) {
whereClause += " AND " + THREAD_ID + " = " + threadId;
whereClause += " AND " + getTableName() + "." + THREAD_ID + " = " + threadId;
}
return whereClause;
@@ -417,7 +418,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), getTableName())
.values(values)
.where(EXPORTED + " < ?", MessageExportStatus.UNEXPORTED.getCode())
.where(EXPORTED + " < ?", MessageExportStatus.UNEXPORTED)
.run();
}

View File

@@ -36,6 +36,7 @@ import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.signal.core.util.CursorExtensionsKt;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SQLiteDatabaseExtensionsKt;
import org.signal.core.util.SqlUtil;
@@ -2463,6 +2464,24 @@ public class MmsDatabase extends MessageDatabase {
);
}
@Override
public long getUnexportedInsecureMessagesEstimatedSize() {
Cursor messageTextSize = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "SUM(LENGTH(" + BODY + "))")
.from(TABLE_NAME)
.where(getInsecureMessageClause() + " AND " + EXPORTED + " < ?", MessageExportStatus.EXPORTED)
.run();
long bodyTextSize = CursorExtensionsKt.readToSingleLong(messageTextSize);
String select = "SUM(" + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ") AS s";
String fromJoin = TABLE_NAME + " INNER JOIN " + AttachmentDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID;
String where = getInsecureMessageClause() + " AND " + EXPORTED + " < " + MessageExportStatus.EXPORTED.serialize();
long fileSize = CursorExtensionsKt.readToSingleLong(getReadableDatabase().rawQuery("SELECT " + select + " FROM " + fromJoin + " WHERE " + where, null));
return bodyTextSize + fileSize;
}
@Override
public void deleteExportedMessages() {
beginTransaction();

View File

@@ -33,11 +33,13 @@ import com.google.protobuf.InvalidProtocolBufferException;
import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.signal.core.util.CursorExtensionsKt;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SQLiteDatabaseExtensionsKt;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.components.settings.app.chats.sms.SmsExportState;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
@@ -921,6 +923,16 @@ public class SmsDatabase extends MessageDatabase {
);
}
@Override
public long getUnexportedInsecureMessagesEstimatedSize() {
Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "SUM(LENGTH(" + BODY + "))")
.from(TABLE_NAME)
.where(getInsecureMessageClause() + " AND " + EXPORTED + " < ?", MessageExportStatus.EXPORTED)
.run();
return CursorExtensionsKt.readToSingleLong(cursor);
}
@Override
public void deleteExportedMessages() {
beginTransaction();

View File

@@ -6,6 +6,7 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.SmsExportDirections
import org.thoughtcrime.securesms.databinding.ExportSmsCompleteFragmentBinding
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -20,7 +21,7 @@ class ExportSmsCompleteFragment : Fragment(R.layout.export_sms_complete_fragment
val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount
val binding = ExportSmsCompleteFragmentBinding.bind(view)
binding.exportCompleteNext.setOnClickListener { findNavController().safeNavigate(ExportSmsCompleteFragmentDirections.actionExportingSmsMessagesFragmentToChooseANewDefaultSmsAppFragment()) }
binding.exportCompleteNext.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToChooseANewDefaultSmsAppFragment()) }
binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount)
}
}

View File

@@ -0,0 +1,44 @@
package org.thoughtcrime.securesms.exporter.flow
import android.content.Context
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.SmsExportDirections
import org.thoughtcrime.securesms.databinding.ExportSmsFullErrorFragmentBinding
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Fragment shown when all export messages failed.
*/
class ExportSmsFullErrorFragment : LoggingFragment(R.layout.export_sms_full_error_fragment) {
private val args: ExportSmsFullErrorFragmentArgs by navArgs()
override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
val inflater = super.onGetLayoutInflater(savedInstanceState)
val contextThemeWrapper: Context = ContextThemeWrapper(requireContext(), R.style.Signal_DayNight)
return inflater.cloneInContext(contextThemeWrapper)
}
@Suppress("UsePropertyAccessSyntax")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = ExportSmsFullErrorFragmentBinding.bind(view)
val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount
binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount)
binding.retryButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToExportYourSmsMessagesFragment()) }
binding.pleaseTryAgain.apply {
setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary))
setLearnMoreVisible(true, R.string.ExportSmsPartiallyComplete__contact_us)
setOnLinkClickListener {
findNavController().safeNavigate(SmsExportDirections.actionDirectToHelpFragment())
}
}
}
}

View File

@@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.exporter.flow
import android.content.Context
import android.os.Bundle
import android.text.format.Formatter
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.signal.core.util.concurrent.SimpleTask
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.SmsExportDirections
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.databinding.ExportSmsPartiallyCompleteFragmentBinding
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Fragment shown when some messages exported and some failed.
*/
class ExportSmsPartiallyCompleteFragment : LoggingFragment(R.layout.export_sms_partially_complete_fragment) {
private val args: ExportSmsPartiallyCompleteFragmentArgs by navArgs()
override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
val inflater = super.onGetLayoutInflater(savedInstanceState)
val contextThemeWrapper: Context = ContextThemeWrapper(requireContext(), R.style.Signal_DayNight)
return inflater.cloneInContext(contextThemeWrapper)
}
@Suppress("UsePropertyAccessSyntax")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = ExportSmsPartiallyCompleteFragmentBinding.bind(view)
val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount
binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount)
binding.retryButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToExportYourSmsMessagesFragment()) }
binding.continueButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToChooseANewDefaultSmsAppFragment()) }
binding.bullet3Text.apply {
setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary))
setLearnMoreVisible(true, R.string.ExportSmsPartiallyComplete__contact_us)
setOnLinkClickListener {
findNavController().safeNavigate(SmsExportDirections.actionDirectToHelpFragment())
}
}
SimpleTask.runWhenValid(
viewLifecycleOwner.lifecycle,
{ SignalDatabase.sms.getUnexportedInsecureMessagesEstimatedSize() + SignalDatabase.mms.getUnexportedInsecureMessagesEstimatedSize() },
{ totalSize ->
binding.bullet1Text.setText(getString(R.string.ExportSmsPartiallyComplete__ensure_you_have_an_additional_s_free_on_your_phone_to_export_your_messages, Formatter.formatFileSize(requireContext(), totalSize)))
}
)
}
}

View File

@@ -10,7 +10,6 @@ import org.signal.smsexporter.DefaultSmsHelper
import org.signal.smsexporter.SmsExportProgress
import org.signal.smsexporter.SmsExportService
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.SmsExportDirections
import org.thoughtcrime.securesms.databinding.ExportYourSmsMessagesFragmentBinding
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -46,9 +45,7 @@ class ExportYourSmsMessagesFragment : Fragment(R.layout.export_your_sms_messages
.progressState
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it is SmsExportProgress.Done) {
findNavController().safeNavigate(SmsExportDirections.actionDirectToExportSmsCompleteFragment(it.errorCount, it.total))
} else if (it is SmsExportProgress.InProgress) {
if (it !is SmsExportProgress.Init) {
findNavController().safeNavigate(ExportYourSmsMessagesFragmentDirections.actionExportYourSmsMessagesFragmentToExportingSmsMessagesFragment())
}
}

View File

@@ -2,11 +2,13 @@ package org.thoughtcrime.securesms.exporter.flow
import android.content.Context
import android.os.Bundle
import android.text.format.Formatter
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import org.signal.smsexporter.SmsExportProgress
@@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.ExportingSmsMessagesFragmentBinding
import org.thoughtcrime.securesms.exporter.SignalSmsExportService
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.mb
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
@@ -32,14 +35,22 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr
return inflater.cloneInContext(contextThemeWrapper)
}
@Suppress("KotlinConstantConditions")
override fun onResume() {
super.onResume()
navigationDisposable = SmsExportService
.progressState
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it is SmsExportProgress.Done) {
findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsCompleteFragment(it.total, it.errorCount))
.subscribe { smsExportProgress ->
if (smsExportProgress is SmsExportProgress.Done) {
SmsExportService.clearProgressState()
if (smsExportProgress.errorCount == 0) {
findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsCompleteFragment(smsExportProgress.total, smsExportProgress.errorCount))
} else if (smsExportProgress.errorCount == smsExportProgress.total) {
findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsFullErrorFragment(smsExportProgress.total, smsExportProgress.errorCount))
} else {
findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsPartiallyCompleteFragment(smsExportProgress.total, smsExportProgress.errorCount))
}
}
}
}
@@ -55,18 +66,34 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += SmsExportService.progressState.observeOn(AndroidSchedulers.mainThread()).subscribe {
when (it) {
is SmsExportProgress.Done -> Unit
SmsExportProgress.Init -> binding.progress.isIndeterminate = true
SmsExportProgress.Starting -> binding.progress.isIndeterminate = true
is SmsExportProgress.InProgress -> {
binding.progress.isIndeterminate = false
binding.progress.max = it.total
binding.progress.progress = it.progress
binding.progressLabel.text = resources.getQuantityString(R.plurals.ExportingSmsMessagesFragment__exporting_d_of_d, it.total, it.progress, it.total)
}
SmsExportProgress.Init -> binding.progress.isIndeterminate = true
SmsExportProgress.Starting -> binding.progress.isIndeterminate = true
is SmsExportProgress.Done -> Unit
}
}
SignalSmsExportService.start(requireContext())
lifecycleDisposable += ExportingSmsRepository()
.getSmsExportSizeEstimations()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (internalFreeSpace, estimatedRequiredSpace) ->
val adjustedFreeSpace = internalFreeSpace - estimatedRequiredSpace - 100.mb
if (estimatedRequiredSpace > adjustedFreeSpace) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ExportingSmsMessagesFragment__you_may_not_have_enough_disk_space)
.setMessage(getString(R.string.ExportingSmsMessagesFragment__you_need_approximately_s_to_export_your_messages_ensure_you_have_enough_space_before_continuing, Formatter.formatFileSize(requireContext(), estimatedRequiredSpace)))
.setPositiveButton(R.string.ExportingSmsMessagesFragment__continue_anyway) { _, _ -> SignalSmsExportService.start(requireContext()) }
.setNegativeButton(android.R.string.cancel) { _, _ -> findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionDirectToExportYourSmsMessagesFragment()) }
.setCancelable(false)
.show()
} else {
SignalSmsExportService.start(requireContext())
}
}
}
}

View File

@@ -0,0 +1,38 @@
package org.thoughtcrime.securesms.exporter.flow
import android.app.Application
import android.os.Build
import android.os.storage.StorageManager
import androidx.core.content.ContextCompat
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import java.io.File
class ExportingSmsRepository(private val context: Application = ApplicationDependencies.getApplication()) {
@Suppress("UsePropertyAccessSyntax")
fun getSmsExportSizeEstimations(): Single<SmsExportSizeEstimations> {
return Single.fromCallable {
val internalStorageFile = if (Build.VERSION.SDK_INT < 24) {
File(context.applicationInfo.dataDir)
} else {
context.dataDir
}
val internalFreeSpace: Long = if (Build.VERSION.SDK_INT < 26) {
internalStorageFile.usableSpace
} else {
val storageManagerFreeSpace = ContextCompat.getSystemService(context, StorageManager::class.java)?.let { storageManager ->
storageManager.getAllocatableBytes(storageManager.getUuidForPath(internalStorageFile))
}
storageManagerFreeSpace ?: internalStorageFile.usableSpace
}
SmsExportSizeEstimations(internalFreeSpace, SignalDatabase.sms.getUnexportedInsecureMessagesEstimatedSize() + SignalDatabase.mms.getUnexportedInsecureMessagesEstimatedSize())
}.subscribeOn(Schedulers.io())
}
data class SmsExportSizeEstimations(val estimatedInternalFreeSpace: Long, val estimatedRequiredSpace: Long)
}

View File

@@ -2,8 +2,11 @@ package org.thoughtcrime.securesms.exporter.flow
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
@@ -18,10 +21,23 @@ class SmsExportActivity : FragmentWrapperActivity() {
NotificationManagerCompat.from(this).cancel(NotificationIds.SMS_EXPORT_COMPLETE)
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
onBackPressedDispatcher.addCallback(this, OnBackPressed())
}
override fun getFragment(): Fragment {
return NavHostFragment.create(R.navigation.sms_export)
}
private inner class OnBackPressed : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!findNavController(R.id.fragment_container).popBackStack()) {
finish()
}
}
}
companion object {
@JvmStatic
fun createIntent(context: Context): Intent = Intent(context, SmsExportActivity::class.java)

View File

@@ -0,0 +1,30 @@
package org.thoughtcrime.securesms.exporter.flow
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.SmsExportHelpFragmentBinding
import org.thoughtcrime.securesms.help.HelpFragment
/**
* Fragment wrapper around the app settings help fragment to provide a toolbar and set default category for sms export.
*/
class SmsExportHelpFragment : LoggingFragment(R.layout.sms_export_help_fragment) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = SmsExportHelpFragmentBinding.bind(view)
binding.toolbar.setOnClickListener {
if (!findNavController().popBackStack()) {
requireActivity().finish()
}
}
childFragmentManager
.beginTransaction()
.replace(binding.smsExportHelpFragmentFragment.id, HelpFragment().apply { arguments = bundleOf(HelpFragment.START_CATEGORY_INDEX to HelpFragment.SMS_EXPORT_INDEX) })
.commitNow()
}
}

View File

@@ -40,6 +40,7 @@ public class HelpFragment extends LoggingFragment {
public static final String START_CATEGORY_INDEX = "start_category_index";
public static final int PAYMENT_INDEX = 6;
public static final int DONATION_INDEX = 7;
public static final int SMS_EXPORT_INDEX = 8;
private EditText problem;
private CheckBox includeDebugLogs;
@@ -93,7 +94,7 @@ public class HelpFragment extends LoggingFragment {
emoji.add(view.findViewById(feeling.getViewId()));
}
categoryAdapter = ArrayAdapter.createFromResource(requireContext(), R.array.HelpFragment__categories_4, android.R.layout.simple_spinner_item);
categoryAdapter = ArrayAdapter.createFromResource(requireContext(), R.array.HelpFragment__categories_5, android.R.layout.simple_spinner_item);
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
categorySpinner.setAdapter(categoryAdapter);
@@ -209,7 +210,7 @@ public class HelpFragment extends LoggingFragment {
suffix.append(getString(feeling.getStringId()));
}
String[] englishCategories = ResourceUtil.getEnglishResources(requireContext()).getStringArray(R.array.HelpFragment__categories_4);
String[] englishCategories = ResourceUtil.getEnglishResources(requireContext()).getStringArray(R.array.HelpFragment__categories_5);
String category = (helpViewModel.getCategoryIndex() >= 0 && helpViewModel.getCategoryIndex() < englishCategories.length) ? englishCategories[helpViewModel.getCategoryIndex()]
: categoryAdapter.getItem(helpViewModel.getCategoryIndex()).toString();