mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 21:24:42 +00:00
Add 30 day reminder for manual backups.
Co-authored-by: Michelle Tang <mtang@signal.org>
This commit is contained in:
committed by
Cody Henthorne
parent
82531630c7
commit
e705495638
@@ -88,6 +88,8 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
|
||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupDeleteJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
@@ -158,6 +160,7 @@ object BackupRepository {
|
||||
private const val LOCAL_MAIN_DB_SNAPSHOT_NAME = "local-signal-snapshot"
|
||||
private const val LOCAL_KEYVALUE_DB_SNAPSHOT_NAME = "local-signal-key-value-snapshot"
|
||||
private const val RECENT_RECIPIENTS_MAX = 50
|
||||
private val MANUAL_BACKUP_NOTIFICATION_THRESHOLD = 30.days
|
||||
|
||||
private val resetInitializedStateErrorAction: StatusCodeErrorAction = { error ->
|
||||
when (error.code) {
|
||||
@@ -321,6 +324,29 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fun displayManualBackupNotCreatedInThresholdNotification() {
|
||||
if (SignalStore.backup.lastBackupTime <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val daysSinceLastBackup = (System.currentTimeMillis().milliseconds - SignalStore.backup.lastBackupTime.milliseconds).inWholeDays.toInt()
|
||||
val context = AppDependencies.application
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, AppSettingsActivity.remoteBackups(context), cancelCurrent())
|
||||
val notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().APP_ALERTS)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(context.resources.getQuantityString(R.plurals.Notification_no_backup_for_d_days, daysSinceLastBackup, daysSinceLastBackup))
|
||||
.setContentText(context.resources.getQuantityString(R.plurals.Notification_you_have_not_completed_a_backup, daysSinceLastBackup, daysSinceLastBackup))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
ServiceUtil.getNotificationManager(context).notify(NotificationIds.MANUAL_BACKUP_NOT_CREATED, notification)
|
||||
}
|
||||
|
||||
fun cancelManualBackupNotCreatedInThresholdNotification() {
|
||||
ServiceUtil.getNotificationManager(AppDependencies.application).cancel(NotificationIds.MANUAL_BACKUP_NOT_CREATED)
|
||||
}
|
||||
|
||||
@Discouraged("This is only public to allow internal settings to call it directly.")
|
||||
fun displayInitialBackupFailureNotification() {
|
||||
val context = AppDependencies.application
|
||||
@@ -432,6 +458,45 @@ object BackupRepository {
|
||||
SignalStore.backup.hasBackupAlreadyRedeemedError = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the "No backup" for manual backups should be displayed.
|
||||
* This should only be displayed after a set threshold has passed and the user
|
||||
* has set the MANUAL backups frequency.
|
||||
*/
|
||||
fun shouldDisplayNoManualBackupForTimeoutSheet(): Boolean {
|
||||
if (shouldNotDisplayBackupFailedMessaging()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (SignalStore.backup.backupFrequency != BackupFrequency.MANUAL) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (SignalStore.backup.lastBackupTime <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isNetworkConstraintMet = if (SignalStore.backup.backupWithCellular) {
|
||||
NetworkConstraint.isMet(AppDependencies.application)
|
||||
} else {
|
||||
WifiConstraint.isMet(AppDependencies.application)
|
||||
}
|
||||
|
||||
if (!isNetworkConstraintMet) {
|
||||
return false
|
||||
}
|
||||
|
||||
val durationSinceLastBackup = System.currentTimeMillis().milliseconds - SignalStore.backup.lastBackupTime.milliseconds
|
||||
if (durationSinceLastBackup < MANUAL_BACKUP_NOTIFICATION_THRESHOLD) {
|
||||
return false
|
||||
}
|
||||
|
||||
val display = !SignalStore.backup.isNoBackupForManualUploadNotified
|
||||
SignalStore.backup.isNoBackupForManualUploadNotified = false
|
||||
|
||||
return display
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the watermark for the indicator display.
|
||||
*/
|
||||
|
||||
@@ -8,40 +8,26 @@ package org.thoughtcrime.securesms.backup.v2.ui
|
||||
import android.content.DialogInterface
|
||||
import android.os.Parcelable
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -51,8 +37,6 @@ import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
@@ -66,7 +50,6 @@ import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.BackupDownloadNotifierState
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* Notifies the user of an issue with their backup.
|
||||
@@ -96,14 +79,10 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val performPrimaryAction = remember(backupAlert) {
|
||||
createPrimaryAction()
|
||||
}
|
||||
|
||||
BackupAlertSheetContent(
|
||||
AlertContainer(
|
||||
backupAlert = backupAlert,
|
||||
onPrimaryActionClick = performPrimaryAction,
|
||||
onSecondaryActionClick = this::performSecondaryAction
|
||||
primaryActionButtonState = rememberPrimaryAction(backupAlert, remember(backupAlert) { createPrimaryAction() }),
|
||||
secondaryActionButtonState = rememberSecondaryAction(backupAlert) { performSecondaryAction() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -146,12 +125,14 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
is BackupAlert.DiskFull -> {
|
||||
displaySkipRestoreDialog()
|
||||
}
|
||||
|
||||
BackupAlert.BackupFailed -> {
|
||||
ContactSupportDialogFragment.create(
|
||||
subject = R.string.BackupAlertBottomSheet_network_failure_support_email,
|
||||
filter = R.string.BackupAlertBottomSheet_export_failure_filter
|
||||
).show(parentFragmentManager, null)
|
||||
}
|
||||
|
||||
BackupAlert.CouldNotRedeemBackup -> CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.backup_support_url)) // TODO [backups] final url
|
||||
}
|
||||
|
||||
@@ -192,105 +173,56 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackupAlertSheetContent(
|
||||
private fun AlertContainer(
|
||||
backupAlert: BackupAlert,
|
||||
onPrimaryActionClick: () -> Unit = {},
|
||||
onSecondaryActionClick: () -> Unit = {}
|
||||
primaryActionButtonState: BackupAlertActionButtonState,
|
||||
secondaryActionButtonState: BackupAlertActionButtonState? = null
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter))
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
BackupAlertBottomSheetContainer(
|
||||
icon = { AlertIcon(backupAlert) },
|
||||
title = titleString(backupAlert),
|
||||
primaryActionButtonState = primaryActionButtonState,
|
||||
secondaryActionButtonState = secondaryActionButtonState,
|
||||
content = { Body(backupAlert) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(26.dp))
|
||||
|
||||
when (backupAlert) {
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.FailedToRenew, BackupAlert.ExpiredAndDowngraded -> {
|
||||
Box {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.image_signal_backups),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.padding(2.dp)
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_error_circle_fill_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val iconColors = rememberBackupsIconColors(backupAlert = backupAlert)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_backup_light),
|
||||
contentDescription = null,
|
||||
tint = iconColors.foreground,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(color = iconColors.background, shape = CircleShape)
|
||||
.padding(20.dp)
|
||||
)
|
||||
}
|
||||
@Composable
|
||||
private fun AlertIcon(backupAlert: BackupAlert) {
|
||||
when (backupAlert) {
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.FailedToRenew, BackupAlert.ExpiredAndDowngraded -> {
|
||||
BackupAlertImage()
|
||||
}
|
||||
|
||||
Text(
|
||||
text = titleString(backupAlert = backupAlert),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
|
||||
)
|
||||
|
||||
when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> CouldNotCompleteBackup(
|
||||
daysSinceLastBackup = backupAlert.daysSinceLastBackup
|
||||
)
|
||||
|
||||
BackupAlert.FailedToRenew -> PaymentProcessingBody()
|
||||
is BackupAlert.DownloadYourBackupData -> DownloadYourBackupData(backupAlert.formattedSize)
|
||||
is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace)
|
||||
BackupAlert.BackupFailed -> BackupFailedBody()
|
||||
BackupAlert.CouldNotRedeemBackup -> CouldNotRedeemBackup()
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.ExpiredAndDowngraded -> SubscriptionExpired()
|
||||
}
|
||||
|
||||
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
|
||||
val padBottom = if (secondaryActionResource > 0) 16.dp else 56.dp
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onPrimaryActionClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(bottom = padBottom)
|
||||
) {
|
||||
Text(text = primaryActionString(backupAlert = backupAlert))
|
||||
}
|
||||
|
||||
if (secondaryActionResource > 0) {
|
||||
TextButton(
|
||||
onClick = onSecondaryActionClick,
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = secondaryActionResource))
|
||||
}
|
||||
else -> {
|
||||
val iconColors = rememberBackupsIconColors(backupAlert = backupAlert)
|
||||
BackupAlertIcon(iconColors = iconColors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Body(backupAlert: BackupAlert) {
|
||||
when (val alert = backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> CouldNotCompleteBackup(
|
||||
daysSinceLastBackup = alert.daysSinceLastBackup
|
||||
)
|
||||
|
||||
BackupAlert.FailedToRenew -> PaymentProcessingBody()
|
||||
is BackupAlert.DownloadYourBackupData -> DownloadYourBackupData(alert.formattedSize)
|
||||
is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = alert.requiredSpace)
|
||||
BackupAlert.BackupFailed -> BackupFailedBody()
|
||||
BackupAlert.CouldNotRedeemBackup -> CouldNotRedeemBackup()
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.ExpiredAndDowngraded -> SubscriptionExpired()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CouldNotRedeemBackup() {
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = stringResource(R.string.BackupAlertBottomSheet__too_many_devices_have_tried),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
@@ -338,17 +270,13 @@ private fun CouldNotRedeemBackup() {
|
||||
|
||||
@Composable
|
||||
private fun SubscriptionExpired() {
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__your_subscription_couldnt_be_renewed),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__youll_continue_to_have_access_to_the_free),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 36.dp)
|
||||
)
|
||||
}
|
||||
@@ -357,54 +285,42 @@ private fun SubscriptionExpired() {
|
||||
private fun CouldNotCompleteBackup(
|
||||
daysSinceLastBackup: Int
|
||||
) {
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = pluralStringResource(id = R.plurals.BackupAlertBottomSheet__your_device_hasnt, daysSinceLastBackup, daysSinceLastBackup),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 60.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentProcessingBody() {
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__check_to_make_sure_your_payment_method),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 60.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadYourBackupData(formattedSize: String) {
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__you_have_s_of_media_thats_not_on_this_device, formattedSize),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__you_can_begin_paying_for_backups_again),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 36.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DiskFullBody(requiredSpace: String) {
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__to_finish_downloading_your_signal_backup, requiredSpace),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = stringResource(R.string.BackupAlertBottomSheet__to_free_up_space_offload),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 36.dp)
|
||||
)
|
||||
}
|
||||
@@ -427,10 +343,8 @@ private fun BackupFailedBody() {
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
BackupAlertText(
|
||||
text = text,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 36.dp)
|
||||
)
|
||||
}
|
||||
@@ -459,6 +373,7 @@ private fun titleString(backupAlert: BackupAlert): String {
|
||||
stringResource(R.string.BackupAlertBottomSheet__download_your_backup_data)
|
||||
}
|
||||
}
|
||||
|
||||
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__free_up_s_on_this_device, backupAlert.requiredSpace)
|
||||
BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__backup_failed)
|
||||
BackupAlert.CouldNotRedeemBackup -> stringResource(R.string.BackupAlertBottomSheet__couldnt_redeem_your_backups_subscription)
|
||||
@@ -467,10 +382,11 @@ private fun titleString(backupAlert: BackupAlert): String {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun primaryActionString(
|
||||
backupAlert: BackupAlert
|
||||
): String {
|
||||
return when (backupAlert) {
|
||||
private fun rememberPrimaryAction(
|
||||
backupAlert: BackupAlert,
|
||||
callback: () -> Unit
|
||||
): BackupAlertActionButtonState {
|
||||
val label = when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__back_up_now)
|
||||
BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__manage_subscription)
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
|
||||
@@ -480,20 +396,41 @@ private fun primaryActionString(
|
||||
BackupAlert.CouldNotRedeemBackup -> stringResource(R.string.BackupAlertBottomSheet__got_it)
|
||||
BackupAlert.ExpiredAndDowngraded -> stringResource(R.string.BackupAlertBottomSheet__manage_backups)
|
||||
}
|
||||
|
||||
return remember(backupAlert, callback) {
|
||||
BackupAlertActionButtonState(
|
||||
label = label,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
|
||||
return remember(backupAlert) {
|
||||
when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> R.string.BackupAlertBottomSheet__try_later
|
||||
BackupAlert.FailedToRenew, BackupAlert.ExpiredAndDowngraded -> R.string.BackupAlertBottomSheet__not_now
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
|
||||
is BackupAlert.DownloadYourBackupData -> R.string.BackupAlertBottomSheet__dont_download_backup
|
||||
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
|
||||
is BackupAlert.BackupFailed -> R.string.BackupAlertBottomSheet__contact_support
|
||||
BackupAlert.CouldNotRedeemBackup -> R.string.BackupAlertBottomSheet__learn_more
|
||||
}
|
||||
private fun rememberSecondaryAction(
|
||||
backupAlert: BackupAlert,
|
||||
callback: () -> Unit
|
||||
): BackupAlertActionButtonState? {
|
||||
val labelResource = when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup -> R.string.BackupAlertBottomSheet__try_later
|
||||
BackupAlert.FailedToRenew, BackupAlert.ExpiredAndDowngraded -> R.string.BackupAlertBottomSheet__not_now
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
|
||||
is BackupAlert.DownloadYourBackupData -> R.string.BackupAlertBottomSheet__dont_download_backup
|
||||
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
|
||||
is BackupAlert.BackupFailed -> R.string.BackupAlertBottomSheet__contact_support
|
||||
BackupAlert.CouldNotRedeemBackup -> R.string.BackupAlertBottomSheet__learn_more
|
||||
}
|
||||
|
||||
if (labelResource <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val label = stringResource(labelResource)
|
||||
|
||||
return remember(backupAlert, callback) {
|
||||
BackupAlertActionButtonState(
|
||||
label = label,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,9 +438,11 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewGeneric() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = 7)
|
||||
)
|
||||
val backupAlert = BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = 7)
|
||||
val primaryActionButtonState = rememberPrimaryAction(backupAlert) { }
|
||||
val secondaryActionButtonState = rememberSecondaryAction(backupAlert) { }
|
||||
|
||||
AlertContainer(backupAlert, primaryActionButtonState, secondaryActionButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,9 +450,11 @@ private fun BackupAlertSheetContentPreviewGeneric() {
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewPayment() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.FailedToRenew
|
||||
)
|
||||
val backupAlert = BackupAlert.FailedToRenew
|
||||
val primaryActionButtonState = rememberPrimaryAction(backupAlert) { }
|
||||
val secondaryActionButtonState = rememberSecondaryAction(backupAlert) { }
|
||||
|
||||
AlertContainer(backupAlert, primaryActionButtonState, secondaryActionButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,12 +462,14 @@ private fun BackupAlertSheetContentPreviewPayment() {
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewDelete() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.DownloadYourBackupData(
|
||||
isLastDay = false,
|
||||
formattedSize = "2.3MB"
|
||||
)
|
||||
val backupAlert = BackupAlert.DownloadYourBackupData(
|
||||
isLastDay = false,
|
||||
formattedSize = "2.3MB"
|
||||
)
|
||||
val primaryActionButtonState = rememberPrimaryAction(backupAlert) { }
|
||||
val secondaryActionButtonState = rememberSecondaryAction(backupAlert) { }
|
||||
|
||||
AlertContainer(backupAlert, primaryActionButtonState, secondaryActionButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,9 +477,11 @@ private fun BackupAlertSheetContentPreviewDelete() {
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewDiskFull() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.DiskFull(requiredSpace = "12GB")
|
||||
)
|
||||
val backupAlert = BackupAlert.DiskFull(requiredSpace = "12GB")
|
||||
val primaryActionButtonState = rememberPrimaryAction(backupAlert) { }
|
||||
val secondaryActionButtonState = rememberSecondaryAction(backupAlert) { }
|
||||
|
||||
AlertContainer(backupAlert, primaryActionButtonState, secondaryActionButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,9 +489,11 @@ private fun BackupAlertSheetContentPreviewDiskFull() {
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewBackupFailed() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.BackupFailed
|
||||
)
|
||||
val backupAlert = BackupAlert.BackupFailed
|
||||
val primaryActionButtonState = rememberPrimaryAction(backupAlert) { }
|
||||
val secondaryActionButtonState = rememberSecondaryAction(backupAlert) { }
|
||||
|
||||
AlertContainer(backupAlert, primaryActionButtonState, secondaryActionButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,9 +501,11 @@ private fun BackupAlertSheetContentPreviewBackupFailed() {
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.CouldNotRedeemBackup
|
||||
)
|
||||
val backupAlert = BackupAlert.CouldNotRedeemBackup
|
||||
val primaryActionButtonState = rememberPrimaryAction(backupAlert) { }
|
||||
val secondaryActionButtonState = rememberSecondaryAction(backupAlert) { }
|
||||
|
||||
AlertContainer(backupAlert, primaryActionButtonState, secondaryActionButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,9 +513,11 @@ private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewSubscriptionExpired() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.ExpiredAndDowngraded
|
||||
)
|
||||
val backupAlert = BackupAlert.ExpiredAndDowngraded
|
||||
val primaryActionButtonState = rememberPrimaryAction(backupAlert) { }
|
||||
val secondaryActionButtonState = rememberSecondaryAction(backupAlert) { }
|
||||
|
||||
AlertContainer(backupAlert, primaryActionButtonState, secondaryActionButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ object BackupAlertDelegate {
|
||||
BackupAlertBottomSheet.create(BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = SignalStore.backup.daysSinceLastBackup)).show(fragmentManager, FRAGMENT_TAG)
|
||||
} else if (BackupRepository.shouldDisplayBackupExpiredAndDowngradedSheet()) {
|
||||
BackupAlertBottomSheet.create(BackupAlert.ExpiredAndDowngraded).show(fragmentManager, FRAGMENT_TAG)
|
||||
} else if (BackupRepository.shouldDisplayNoManualBackupForTimeoutSheet()) {
|
||||
NoManualBackupBottomSheet().show(fragmentManager, FRAGMENT_TAG)
|
||||
BackupRepository.displayManualBackupNotCreatedInThresholdNotification()
|
||||
}
|
||||
|
||||
displayBackupDownloadNotifier(fragmentManager)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* Container for a backup alert sheet.
|
||||
*
|
||||
* Primary action padding will change depending on presence of secondary action.
|
||||
*/
|
||||
@Composable
|
||||
fun BackupAlertBottomSheetContainer(
|
||||
icon: @Composable () -> Unit,
|
||||
title: String,
|
||||
primaryActionButtonState: BackupAlertActionButtonState,
|
||||
secondaryActionButtonState: BackupAlertActionButtonState? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter))
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Spacer(modifier = Modifier.size(26.dp))
|
||||
|
||||
icon()
|
||||
|
||||
BackupAlertTitle(title = title)
|
||||
|
||||
content()
|
||||
|
||||
BackupAlertPrimaryActionButton(
|
||||
text = primaryActionButtonState.label,
|
||||
onClick = primaryActionButtonState.callback,
|
||||
modifier = Modifier.padding(
|
||||
bottom = if (secondaryActionButtonState != null) {
|
||||
16.dp
|
||||
} else {
|
||||
56.dp
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (secondaryActionButtonState != null) {
|
||||
BackupAlertSecondaryActionButton(
|
||||
text = secondaryActionButtonState.label,
|
||||
onClick = secondaryActionButtonState.callback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup alert sheet icon for the top of the sheet, vector only.
|
||||
*/
|
||||
@Composable
|
||||
fun BackupAlertIcon(
|
||||
iconColors: BackupsIconColors
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_backup_light),
|
||||
contentDescription = null,
|
||||
tint = iconColors.foreground,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(color = iconColors.background, shape = CircleShape)
|
||||
.padding(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup alert sheet image for the top of the sheet displaying a backup icon and alert indicator.
|
||||
*/
|
||||
@Composable
|
||||
fun BackupAlertImage() {
|
||||
Box {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.image_signal_backups),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.padding(2.dp)
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_error_circle_fill_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertTitle(
|
||||
title: String
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly styled Text for Backup Alert sheets
|
||||
*/
|
||||
@Composable
|
||||
fun BackupAlertText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly styled Text for Backup Alert sheets
|
||||
*/
|
||||
@Composable
|
||||
fun BackupAlertText(
|
||||
text: AnnotatedString,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertPrimaryActionButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.then(modifier)
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertSecondaryActionButton(
|
||||
text: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupAlertBottomSheetContainerPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertBottomSheetContainer(
|
||||
icon = { BackupAlertIcon(iconColors = BackupsIconColors.Warning) },
|
||||
title = "Test backup alert",
|
||||
primaryActionButtonState = BackupAlertActionButtonState("Test Primary", callback = {}),
|
||||
secondaryActionButtonState = BackupAlertActionButtonState("Test Secondary", callback = {})
|
||||
) {
|
||||
BackupAlertText(text = "Content", modifier = Modifier.padding(bottom = 60.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable state class for alert sheet actions.
|
||||
*/
|
||||
@Immutable
|
||||
data class BackupAlertActionButtonState(
|
||||
val label: String,
|
||||
val callback: () -> Unit
|
||||
)
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Displays an alert to the user if they've passed a given threshold without
|
||||
* performing a manual backup.
|
||||
*/
|
||||
class NoManualBackupBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val durationSinceLastBackup = remember {
|
||||
System.currentTimeMillis().milliseconds - SignalStore.backup.lastBackupTime.milliseconds
|
||||
}
|
||||
|
||||
NoManualBackupSheetContent(
|
||||
durationSinceLastBackup = durationSinceLastBackup,
|
||||
onBackUpNowClick = {
|
||||
BackupMessagesJob.enqueue()
|
||||
startActivity(AppSettingsActivity.remoteBackups(requireActivity()))
|
||||
dismissAllowingStateLoss()
|
||||
},
|
||||
onNotNowClick = this::dismissAllowingStateLoss
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoManualBackupSheetContent(
|
||||
durationSinceLastBackup: Duration,
|
||||
onBackUpNowClick: () -> Unit = {},
|
||||
onNotNowClick: () -> Unit = {}
|
||||
) {
|
||||
val primaryActionLabel = stringResource(R.string.BackupAlertBottomSheet__back_up_now)
|
||||
val primaryAction = remember { BackupAlertActionButtonState(primaryActionLabel, onBackUpNowClick) }
|
||||
val secondaryActionLabel = stringResource(android.R.string.cancel)
|
||||
val secondaryAction = remember { BackupAlertActionButtonState(secondaryActionLabel, onNotNowClick) }
|
||||
val days: Int = durationSinceLastBackup.inWholeDays.toInt()
|
||||
|
||||
BackupAlertBottomSheetContainer(
|
||||
icon = { BackupAlertIcon(iconColors = BackupsIconColors.Warning) },
|
||||
title = pluralStringResource(R.plurals.NoManualBackupBottomSheet__no_backup_for_d_days, days, days),
|
||||
primaryActionButtonState = primaryAction,
|
||||
secondaryActionButtonState = secondaryAction
|
||||
) {
|
||||
BackupAlertText(
|
||||
text = pluralStringResource(R.plurals.NoManualBackupBottomSheet__you_have_not_completed_a_backup, days, days),
|
||||
modifier = Modifier.padding(bottom = 38.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun NoManualBackupSheetContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
NoManualBackupSheetContent(
|
||||
durationSinceLastBackup = 30.days
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NoRemoteArchiveGarbageCollectionPendingConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver
|
||||
@@ -72,6 +73,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
private const val KEY_BACKUP_ALREADY_REDEEMED = "backup.already.redeemed"
|
||||
private const val KEY_INVALID_BACKUP_VERSION = "backup.invalid.version"
|
||||
private const val KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE = "backup.not.enough.remote.storage.space"
|
||||
private const val KEY_MANUAL_NO_BACKUP_NOTIFIED = "backup.manual.no.backup.notified"
|
||||
|
||||
private const val KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE = "backup.user.manually.skipped.media.restore"
|
||||
private const val KEY_BACKUP_EXPIRED_AND_DOWNGRADED = "backup.expired.and.downgraded"
|
||||
@@ -115,6 +117,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
get() = getLong(KEY_LAST_BACKUP_TIME, -1)
|
||||
set(value) {
|
||||
putLong(KEY_LAST_BACKUP_TIME, value)
|
||||
isNoBackupForManualUploadNotified = false
|
||||
BackupRepository.cancelManualBackupNotCreatedInThresholdNotification()
|
||||
clearMessageBackupFailureSheetWatermark()
|
||||
}
|
||||
|
||||
@@ -317,6 +321,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
var isNotEnoughRemoteStorageSpace by booleanValue(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE, false)
|
||||
|
||||
var isNoBackupForManualUploadNotified by booleanValue(KEY_MANUAL_NO_BACKUP_NOTIFIED, false)
|
||||
|
||||
/**
|
||||
* If true, it means we have been told that remote storage is full, but we have not yet run any of our "garbage collection" tasks, like committing deletes
|
||||
* or pruning orphaned media.
|
||||
|
||||
@@ -36,6 +36,7 @@ public final class NotificationIds {
|
||||
public static final int NEW_LINKED_DEVICE = 120400;
|
||||
public static final int OUT_OF_REMOTE_STORAGE = 120500;
|
||||
public static final int INITIAL_BACKUP_FAILED = 120501;
|
||||
public static final int MANUAL_BACKUP_NOT_CREATED = 120502;
|
||||
|
||||
private NotificationIds() { }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user