Add re-export SMS support and hard code Phase 0.

main
Cody Henthorne 2022-11-16 11:42:41 -05:00 zatwierdzone przez Alex Hart
rodzic fd1d2ec8fc
commit 7c60c32918
13 zmienionych plików z 129 dodań i 55 usunięć

Wyświetl plik

@ -70,6 +70,16 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
} }
) )
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages_again),
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__exporting_again_can_result_in_duplicate_messages),
onClick = {
SmsExportDialogs.showSmsReExportDialog(requireContext()) {
smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext(), isReExport = true))
}
}
)
dividerPref() dividerPref()
} }
SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit

Wyświetl plik

@ -98,6 +98,16 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
} }
) )
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages_again),
summary = DSLSettingsText.from(R.string.SmsSettingsFragment__exporting_again_can_result_in_duplicate_messages),
onClick = {
SmsExportDialogs.showSmsReExportDialog(requireContext()) {
smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext(), isReExport = true))
}
}
)
dividerPref() dividerPref()
} }
SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit

Wyświetl plik

@ -417,6 +417,20 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
return 0; return 0;
} }
/**
* Resets the exported state and exported flag so messages can be re-exported.
*/
public void clearExportState() {
ContentValues values = new ContentValues(2);
values.putNull(EXPORT_STATE);
values.put(EXPORTED, MessageExportStatus.UNEXPORTED.serialize());
SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), getTableName())
.values(values)
.where(EXPORT_STATE + " IS NOT NULL OR " + EXPORTED + " != ?", MessageExportStatus.UNEXPORTED)
.run();
}
/** /**
* Reset the exported status (not state) to the default for clearing errors. * Reset the exported status (not state) to the default for clearing errors.
*/ */

Wyświetl plik

@ -31,8 +31,10 @@ class SignalSmsExportService : SmsExportService() {
/** /**
* Launches the export service and immediately begins exporting messages. * Launches the export service and immediately begins exporting messages.
*/ */
fun start(context: Context) { fun start(context: Context, clearPreviousExportState: Boolean) {
ContextCompat.startForegroundService(context, Intent(context, SignalSmsExportService::class.java)) val intent = Intent(context, SignalSmsExportService::class.java)
.apply { putExtra(CLEAR_PREVIOUS_EXPORT_STATE_EXTRA, clearPreviousExportState) }
ContextCompat.startForegroundService(context, intent)
} }
} }
@ -80,6 +82,11 @@ class SignalSmsExportService : SmsExportService() {
) )
} }
override fun clearPreviousExportState() {
SignalDatabase.sms.clearExportState()
SignalDatabase.mms.clearExportState()
}
override fun prepareForExport() { override fun prepareForExport() {
SignalDatabase.sms.clearInsecureMessageExportedErrorStatus() SignalDatabase.sms.clearInsecureMessageExportedErrorStatus()
SignalDatabase.mms.clearInsecureMessageExportedErrorStatus() SignalDatabase.mms.clearInsecureMessageExportedErrorStatus()

Wyświetl plik

@ -8,6 +8,7 @@ import android.view.ContextThemeWrapper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@ -28,6 +29,8 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
*/ */
class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fragment) { class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fragment) {
private val viewModel: SmsExportViewModel by activityViewModels()
private val lifecycleDisposable = LifecycleDisposable() private val lifecycleDisposable = LifecycleDisposable()
private var navigationDisposable = Disposable.disposed() private var navigationDisposable = Disposable.disposed()
@ -109,7 +112,7 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr
.request(Manifest.permission.READ_SMS) .request(Manifest.permission.READ_SMS)
.ifNecessary() .ifNecessary()
.withRationaleDialog(getString(R.string.ExportingSmsMessagesFragment__signal_needs_the_sms_permission_to_be_able_to_export_your_sms_messages), R.drawable.ic_messages_solid_24) .withRationaleDialog(getString(R.string.ExportingSmsMessagesFragment__signal_needs_the_sms_permission_to_be_able_to_export_your_sms_messages), R.drawable.ic_messages_solid_24)
.onAllGranted { SignalSmsExportService.start(requireContext()) } .onAllGranted { SignalSmsExportService.start(requireContext(), viewModel.isReExport) }
.withPermanentDenialDialog(getString(R.string.ExportingSmsMessagesFragment__signal_needs_the_sms_permission_to_be_able_to_export_your_sms_messages)) { requireActivity().finish() } .withPermanentDenialDialog(getString(R.string.ExportingSmsMessagesFragment__signal_needs_the_sms_permission_to_be_able_to_export_your_sms_messages)) { requireActivity().finish() }
.onAnyDenied { checkPermissionsAndStartExport() } .onAnyDenied { checkPermissionsAndStartExport() }
.execute() .execute()

Wyświetl plik

@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
@ -15,15 +16,21 @@ import org.thoughtcrime.securesms.util.WindowUtil
class SmsExportActivity : FragmentWrapperActivity() { class SmsExportActivity : FragmentWrapperActivity() {
private lateinit var viewModel: SmsExportViewModel
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
WindowUtil.setLightStatusBarFromTheme(this) WindowUtil.setLightStatusBarFromTheme(this)
NotificationManagerCompat.from(this).cancel(NotificationIds.SMS_EXPORT_COMPLETE) NotificationManagerCompat.from(this).cancel(NotificationIds.SMS_EXPORT_COMPLETE)
} }
@Suppress("ReplaceGetOrSet")
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready) super.onCreate(savedInstanceState, ready)
onBackPressedDispatcher.addCallback(this, OnBackPressed()) onBackPressedDispatcher.addCallback(this, OnBackPressed())
val factory = SmsExportViewModel.Factory(intent.getBooleanExtra(IS_RE_EXPORT, false))
viewModel = ViewModelProvider(this, factory).get(SmsExportViewModel::class.java)
} }
override fun getFragment(): Fragment { override fun getFragment(): Fragment {
@ -39,7 +46,14 @@ class SmsExportActivity : FragmentWrapperActivity() {
} }
companion object { companion object {
const val IS_RE_EXPORT = "is_re_export"
@JvmOverloads
@JvmStatic @JvmStatic
fun createIntent(context: Context): Intent = Intent(context, SmsExportActivity::class.java) fun createIntent(context: Context, isReExport: Boolean = false): Intent {
return Intent(context, SmsExportActivity::class.java).apply {
putExtra(IS_RE_EXPORT, isReExport)
}
}
} }
} }

Wyświetl plik

@ -26,4 +26,14 @@ object SmsExportDialogs {
} }
.show() .show()
} }
@JvmStatic
fun showSmsReExportDialog(context: Context, continueCallback: Runnable) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.ReExportSmsMessagesDialogFragment__export_sms_again)
.setMessage(R.string.ReExportSmsMessagesDialogFragment__you_already_exported_your_sms_messages)
.setPositiveButton(R.string.ReExportSmsMessagesDialogFragment__continue) { _, _ -> continueCallback.run() }
.setNegativeButton(R.string.ReExportSmsMessagesDialogFragment__cancel, null)
.show()
}
} }

Wyświetl plik

@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.exporter.flow
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
/**
* Hold shared state for the SMS export flow.
*
* Note: Will be expanded on eventually to support different behavior when entering via megaphone.
*/
class SmsExportViewModel(val isReExport: Boolean) : ViewModel() {
class Factory(private val isReExport: Boolean) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(SmsExportViewModel(isReExport)))
}
}
}

Wyświetl plik

@ -6,6 +6,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata; import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -28,8 +29,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
private static final String LAST_FCM_FOREGROUND_TIME = "misc.last_fcm_foreground_time"; private static final String LAST_FCM_FOREGROUND_TIME = "misc.last_fcm_foreground_time";
private static final String LAST_FOREGROUND_TIME = "misc.last_foreground_time"; private static final String LAST_FOREGROUND_TIME = "misc.last_foreground_time";
private static final String PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices"; private static final String PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices";
private static final String SMS_PHASE_1_START_MS = "misc.sms_export.phase_1_start.2"; private static final String SMS_PHASE_1_START_MS = "misc.sms_export.phase_1_start.3";
private static final String STORIES_FEATURE_AVAILABLE_MS = "misc.stories_feature_available_ms";
MiscellaneousValues(@NonNull KeyValueStore store) { MiscellaneousValues(@NonNull KeyValueStore store) {
super(store); super(store);
@ -42,10 +42,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
@Override @Override
@NonNull List<String> getKeysToIncludeInBackup() { @NonNull List<String> getKeysToIncludeInBackup() {
return Arrays.asList( return Collections.singletonList(SMS_PHASE_1_START_MS);
SMS_PHASE_1_START_MS,
STORIES_FEATURE_AVAILABLE_MS
);
} }
public long getLastPrekeyRefreshTime() { public long getLastPrekeyRefreshTime() {
@ -234,20 +231,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
} }
} }
public long getStoriesFeatureAvailableTimestamp() {
return getLong(STORIES_FEATURE_AVAILABLE_MS, 0);
}
public void setStoriesFeatureAvailableTimestamp(long timestamp) {
putLong(STORIES_FEATURE_AVAILABLE_MS, timestamp);
}
public @NonNull SmsExportPhase getSmsExportPhase() { public @NonNull SmsExportPhase getSmsExportPhase() {
if (getLong(SMS_PHASE_1_START_MS, 0) == 0) { return SmsExportPhase.PHASE_0;
return SmsExportPhase.PHASE_0;
}
long now = System.currentTimeMillis();
return SmsExportPhase.getCurrentPhase(now - getLong(SMS_PHASE_1_START_MS, now));
} }
} }

Wyświetl plik

@ -4,8 +4,6 @@ import android.content.Context
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.SmsExportPhase import org.thoughtcrime.securesms.keyvalue.SmsExportPhase
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.Util
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
class SmsExportReminderSchedule(private val context: Context) : MegaphoneSchedule { class SmsExportReminderSchedule(private val context: Context) : MegaphoneSchedule {
@ -32,17 +30,8 @@ class SmsExportReminderSchedule(private val context: Context) : MegaphoneSchedul
} }
} }
@Suppress("UsePropertyAccessSyntax")
@WorkerThread @WorkerThread
fun shouldShowMegaphone(): Boolean { private fun shouldShowMegaphone(): Boolean {
return if (SignalStore.misc().storiesFeatureAvailableTimestamp == 0L) { return false
SignalStore.misc().storiesFeatureAvailableTimestamp = System.currentTimeMillis()
false
} else if (System.currentTimeMillis() > (SignalStore.misc().storiesFeatureAvailableTimestamp + FeatureFlags.smsExportMegaphoneDelayDays().days.inWholeMilliseconds)) {
SignalStore.misc().startSmsPhase1()
FeatureFlags.smsExporter() && Util.isDefaultSmsProvider(context)
} else {
false
}
} }
} }

Wyświetl plik

@ -99,7 +99,6 @@ public final class FeatureFlags {
private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2"; private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2";
private static final String SMS_EXPORTER = "android.sms.exporter.2"; private static final String SMS_EXPORTER = "android.sms.exporter.2";
private static final String HIDE_CONTACTS = "android.hide.contacts"; private static final String HIDE_CONTACTS = "android.hide.contacts";
private static final String SMS_EXPORT_MEGAPHONE_DELAY_DAYS = "android.smsExport.megaphoneDelayDays.2";
public static final String CREDIT_CARD_PAYMENTS = "android.credit.card.payments.3"; public static final String CREDIT_CARD_PAYMENTS = "android.credit.card.payments.3";
private static final String PAYMENTS_REQUEST_ACTIVATE_FLOW = "android.payments.requestActivateFlow"; private static final String PAYMENTS_REQUEST_ACTIVATE_FLOW = "android.payments.requestActivateFlow";
private static final String KEEP_MUTED_CHATS_ARCHIVED = "android.keepMutedChatsArchived"; private static final String KEEP_MUTED_CHATS_ARCHIVED = "android.keepMutedChatsArchived";
@ -160,7 +159,6 @@ public final class FeatureFlags {
RECIPIENT_MERGE_V2, RECIPIENT_MERGE_V2,
SMS_EXPORTER, SMS_EXPORTER,
HIDE_CONTACTS, HIDE_CONTACTS,
SMS_EXPORT_MEGAPHONE_DELAY_DAYS,
CREDIT_CARD_PAYMENTS, CREDIT_CARD_PAYMENTS,
PAYMENTS_REQUEST_ACTIVATE_FLOW, PAYMENTS_REQUEST_ACTIVATE_FLOW,
KEEP_MUTED_CHATS_ARCHIVED, KEEP_MUTED_CHATS_ARCHIVED,
@ -231,7 +229,6 @@ public final class FeatureFlags {
TELECOM_MODEL_BLOCKLIST, TELECOM_MODEL_BLOCKLIST,
CAMERAX_MODEL_BLOCKLIST, CAMERAX_MODEL_BLOCKLIST,
RECIPIENT_MERGE_V2, RECIPIENT_MERGE_V2,
SMS_EXPORT_MEGAPHONE_DELAY_DAYS,
CREDIT_CARD_PAYMENTS, CREDIT_CARD_PAYMENTS,
PAYMENTS_REQUEST_ACTIVATE_FLOW, PAYMENTS_REQUEST_ACTIVATE_FLOW,
KEEP_MUTED_CHATS_ARCHIVED, KEEP_MUTED_CHATS_ARCHIVED,
@ -549,13 +546,6 @@ public final class FeatureFlags {
return getBoolean(HIDE_CONTACTS, false); return getBoolean(HIDE_CONTACTS, false);
} }
/**
* Number of days to postpone the sms export megaphone and Phase 1 start.
*/
public static int smsExportMegaphoneDelayDays() {
return getInteger(SMS_EXPORT_MEGAPHONE_DELAY_DAYS, 14);
}
/** /**
* Whether or not we should allow credit card payments for donations * Whether or not we should allow credit card payments for donations
* *

Wyświetl plik

@ -4029,6 +4029,8 @@
<string name="SmsSettingsFragment__use_as_default_sms_app">Use as default SMS app</string> <string name="SmsSettingsFragment__use_as_default_sms_app">Use as default SMS app</string>
<!-- Preference title to export sms --> <!-- Preference title to export sms -->
<string name="SmsSettingsFragment__export_sms_messages">Export SMS messages</string> <string name="SmsSettingsFragment__export_sms_messages">Export SMS messages</string>
<!-- Preference title to re-export sms -->
<string name="SmsSettingsFragment__export_sms_messages_again">Export SMS messages again</string>
<!-- Preference title to delete sms --> <!-- Preference title to delete sms -->
<string name="SmsSettingsFragment__remove_sms_messages">Remove SMS messages</string> <string name="SmsSettingsFragment__remove_sms_messages">Remove SMS messages</string>
<!-- Snackbar text to confirm deletion --> <!-- Snackbar text to confirm deletion -->
@ -4037,6 +4039,8 @@
<string name="SmsSettingsFragment__you_can_remove_sms_messages_from_signal_in_settings">You can remove SMS messages from Signal in Settings at any time.</string> <string name="SmsSettingsFragment__you_can_remove_sms_messages_from_signal_in_settings">You can remove SMS messages from Signal in Settings at any time.</string>
<!-- Description for export sms preference --> <!-- Description for export sms preference -->
<string name="SmsSettingsFragment__you_can_export_your_sms_messages_to_your_phones_sms_database">You can export your SMS messages to your phone\'s SMS database</string> <string name="SmsSettingsFragment__you_can_export_your_sms_messages_to_your_phones_sms_database">You can export your SMS messages to your phone\'s SMS database</string>
<!-- Description for re-export sms preference -->
<string name="SmsSettingsFragment__exporting_again_can_result_in_duplicate_messages">Exporting again can result in duplicate messages.</string>
<!-- Description for remove sms preference --> <!-- Description for remove sms preference -->
<string name="SmsSettingsFragment__remove_sms_messages_from_signal_to_clear_up_storage_space">Remove SMS messages from Signal to clear up storage space.</string> <string name="SmsSettingsFragment__remove_sms_messages_from_signal_to_clear_up_storage_space">Remove SMS messages from Signal to clear up storage space.</string>
<!-- Information message shown at the top of sms settings to indicate it is being removed soon. --> <!-- Information message shown at the top of sms settings to indicate it is being removed soon. -->
@ -5442,6 +5446,16 @@
<!-- Message of dialog --> <!-- Message of dialog -->
<string name="RemoveSmsMessagesDialogFragment__you_can_now_remove_sms_messages_from_signal">You can now remove SMS messages from Signal to clear up storage space. They will still be available to other SMS apps on your phone even if you remove them.</string> <string name="RemoveSmsMessagesDialogFragment__you_can_now_remove_sms_messages_from_signal">You can now remove SMS messages from Signal to clear up storage space. They will still be available to other SMS apps on your phone even if you remove them.</string>
<!-- ReExportSmsMessagesDialogFragment -->
<!-- Action button to re-export messages -->
<string name="ReExportSmsMessagesDialogFragment__continue">Continue</string>
<!-- Action button to cancel re-export process -->
<string name="ReExportSmsMessagesDialogFragment__cancel">Cancel</string>
<!-- Title of dialog -->
<string name="ReExportSmsMessagesDialogFragment__export_sms_again">Export SMS again?</string>
<!-- Message of dialog -->
<string name="ReExportSmsMessagesDialogFragment__you_already_exported_your_sms_messages">You already exported your SMS messages.\nWARNING: If you continue, you may end up with duplicate messages.</string>
<!-- SetSignalAsDefaultSmsAppFragment --> <!-- SetSignalAsDefaultSmsAppFragment -->
<!-- Title of the screen --> <!-- Title of the screen -->
<string name="SetSignalAsDefaultSmsAppFragment__set_signal_as_the_default_sms_app">Set Signal as the default SMS app</string> <string name="SetSignalAsDefaultSmsAppFragment__set_signal_as_the_default_sms_app">Set Signal as the default SMS app</string>

Wyświetl plik

@ -25,16 +25,17 @@ import java.util.concurrent.Executors
abstract class SmsExportService : Service() { abstract class SmsExportService : Service() {
companion object { companion object {
fun clearProgressState() {
progressState.onNext(SmsExportProgress.Init)
}
private val TAG = Log.tag(SmsExportService::class.java) private val TAG = Log.tag(SmsExportService::class.java)
const val CLEAR_PREVIOUS_EXPORT_STATE_EXTRA = "clear_previous_export_state"
/** /**
* Progress state which can be listened to by interested components, such as fragments. * Progress state which can be listened to by interested components, such as fragments.
*/ */
val progressState: BehaviorProcessor<SmsExportProgress> = BehaviorProcessor.createDefault(SmsExportProgress.Init) val progressState: BehaviorProcessor<SmsExportProgress> = BehaviorProcessor.createDefault(SmsExportProgress.Init)
fun clearProgressState() {
progressState.onNext(SmsExportProgress.Init)
}
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
@ -47,18 +48,18 @@ abstract class SmsExportService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "Got start command in SMS Export Service") Log.d(TAG, "Got start command in SMS Export Service")
startExport() startExport(intent?.getBooleanExtra(CLEAR_PREVIOUS_EXPORT_STATE_EXTRA, false) ?: false)
return START_NOT_STICKY return START_NOT_STICKY
} }
private fun startExport() { private fun startExport(clearExportState: Boolean) {
if (isStarted) { if (isStarted) {
Log.d(TAG, "Already running exporter.") Log.d(TAG, "Already running exporter.")
return return
} }
Log.d(TAG, "Running export...") Log.d(TAG, "Running export clearExportState: $clearExportState")
isStarted = true isStarted = true
updateNotification(-1, -1) updateNotification(-1, -1)
@ -67,6 +68,10 @@ abstract class SmsExportService : Service() {
var progress = 0 var progress = 0
var errorCount = 0 var errorCount = 0
executor.execute { executor.execute {
if (clearExportState) {
clearPreviousExportState()
}
prepareForExport() prepareForExport()
val totalCount = getUnexportedMessageCount() val totalCount = getUnexportedMessageCount()
getUnexportedMessages().forEach { message -> getUnexportedMessages().forEach { message ->
@ -124,7 +129,14 @@ abstract class SmsExportService : Service() {
*/ */
protected abstract fun getExportCompleteNotification(): ExportNotification? protected abstract fun getExportCompleteNotification(): ExportNotification?
/** Called prior to starting export for any task setup that may need to occur. */ /**
* Called prior to starting export if the user has requested previous export state to be cleared.
*/
protected open fun clearPreviousExportState() = Unit
/**
* Called prior to starting export for any task setup that may need to occur.
*/
protected open fun prepareForExport() = Unit protected open fun prepareForExport() = Unit
/** /**