Detect storage space issues during restore.
This commit is contained in:
parent
b4472833b8
commit
7f1a866e79
14 changed files with 163 additions and 35 deletions
|
@ -42,6 +42,7 @@ import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||||
import org.signal.ringrtc.CallManager;
|
import org.signal.ringrtc.CallManager;
|
||||||
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||||
|
@ -253,6 +254,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||||
KeyCachingService.onAppForegrounded(this);
|
KeyCachingService.onAppForegrounded(this);
|
||||||
AppDependencies.getShakeToReport().enable();
|
AppDependencies.getShakeToReport().enable();
|
||||||
checkBuildExpiration();
|
checkBuildExpiration();
|
||||||
|
checkFreeDiskSpace();
|
||||||
MemoryTracker.start();
|
MemoryTracker.start();
|
||||||
BackupSubscriptionCheckJob.enqueueIfAble();
|
BackupSubscriptionCheckJob.enqueueIfAble();
|
||||||
|
|
||||||
|
@ -289,6 +291,13 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void checkFreeDiskSpace() {
|
||||||
|
if (RemoteConfig.messageBackups()) {
|
||||||
|
long availableBytes = BackupRepository.INSTANCE.getFreeStorageSpace().getBytes();
|
||||||
|
SignalStore.backup().setSpaceAvailableOnDiskBytes(availableBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: this is purposefully "started" twice -- once during application create, and once during foreground.
|
* Note: this is purposefully "started" twice -- once during application create, and once during foreground.
|
||||||
* This is so we can capture ANR's that happen on boot before the foreground event.
|
* This is so we can capture ANR's that happen on boot before the foreground event.
|
||||||
|
|
|
@ -64,6 +64,7 @@ import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||||
|
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||||
|
@ -131,6 +132,9 @@ object BackupRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the free storage space in the device's data partition.
|
||||||
|
*/
|
||||||
fun getFreeStorageSpace(): ByteSize {
|
fun getFreeStorageSpace(): ByteSize {
|
||||||
val statFs = StatFs(Environment.getDataDirectory().absolutePath)
|
val statFs = StatFs(Environment.getDataDirectory().absolutePath)
|
||||||
val free = (statFs.availableBlocksLong) * statFs.blockSizeLong
|
val free = (statFs.availableBlocksLong) * statFs.blockSizeLong
|
||||||
|
@ -138,9 +142,33 @@ object BackupRepository {
|
||||||
return free.bytes
|
return free.bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether or not we do not have enough storage space for our remaining attachments to be downloaded.
|
||||||
|
* Called from the attachment / thumbnail download jobs.
|
||||||
|
*/
|
||||||
|
fun checkForOutOfStorageError(tag: String): Boolean {
|
||||||
|
val availableSpace = getFreeStorageSpace()
|
||||||
|
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes
|
||||||
|
|
||||||
|
return if (availableSpace < remainingAttachmentSize) {
|
||||||
|
Log.w(tag, "Possibly out of space. ${availableSpace.toUnitString()} available.", true)
|
||||||
|
SignalStore.backup.spaceAvailableOnDiskBytes = availableSpace.bytes
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels any relevant jobs for media restore
|
||||||
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun skipMediaRestore() {
|
fun skipMediaRestore() {
|
||||||
// TODO [backups] -- Clear the error as necessary, cancel anything remaining in the restore
|
SignalStore.backup.userManuallySkippedMediaRestore = true
|
||||||
|
|
||||||
|
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.RESTORE_OFFLOADED))
|
||||||
|
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.INITIAL_RESTORE))
|
||||||
|
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.MANUAL))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -172,7 +172,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||||
private fun displaySkipRestoreDialog() {
|
private fun displaySkipRestoreDialog() {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle((R.string.BackupAlertBottomSheet__skip_restore_question))
|
.setTitle((R.string.BackupAlertBottomSheet__skip_restore_question))
|
||||||
.setMessage(R.string.BackupAlertBottomSheet__if_you_skip_restore)
|
.setMessage(R.string.BackupAlertBottomSheet__if_you_skip_restore_the)
|
||||||
.setPositiveButton(R.string.BackupAlertBottomSheet__skip) { _, _ ->
|
.setPositiveButton(R.string.BackupAlertBottomSheet__skip) { _, _ ->
|
||||||
BackupRepository.skipMediaRestore()
|
BackupRepository.skipMediaRestore()
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getActiveRestoreFlow(): Flow<BackupStatusData.RestoringMedia> {
|
private fun getActiveRestoreFlow(): Flow<BackupStatusData> {
|
||||||
val flow: Flow<Unit> = callbackFlow {
|
val flow: Flow<Unit> = callbackFlow {
|
||||||
val onChange = { trySend(Unit) }
|
val onChange = { trySend(Unit) }
|
||||||
|
|
||||||
|
@ -115,11 +115,16 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
|
||||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||||
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
||||||
|
val availableBytes = SignalStore.backup.spaceAvailableOnDiskBytes
|
||||||
|
|
||||||
|
if (availableBytes > -1L && remainingAttachmentSize > availableBytes) {
|
||||||
|
BackupStatusData.NotEnoughFreeSpace(requiredSpace = remainingAttachmentSize.bytes)
|
||||||
|
} else {
|
||||||
BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes)
|
BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,10 @@
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.jobs
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
|
import org.signal.core.util.billing.BillingPurchaseResult
|
||||||
|
import org.signal.core.util.billing.BillingPurchaseState
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.donations.InAppPaymentType
|
import org.signal.donations.InAppPaymentType
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||||
|
@ -19,6 +22,7 @@ import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager.Chain
|
import org.thoughtcrime.securesms.jobmanager.JobManager.Chain
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submits a purchase token to the server to link it with a subscriber id.
|
* Submits a purchase token to the server to link it with a subscriber id.
|
||||||
|
@ -26,7 +30,7 @@ import kotlin.concurrent.withLock
|
||||||
class InAppPaymentPurchaseTokenJob private constructor(
|
class InAppPaymentPurchaseTokenJob private constructor(
|
||||||
private val inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
private val inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||||
parameters: Parameters
|
parameters: Parameters
|
||||||
) : BaseJob(parameters) {
|
) : Job(parameters) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(InAppPaymentPurchaseTokenJob::class)
|
private val TAG = Log.tag(InAppPaymentPurchaseTokenJob::class)
|
||||||
|
@ -39,7 +43,7 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
||||||
parameters = Parameters.Builder()
|
parameters = Parameters.Builder()
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.setQueue(InAppPaymentsRepository.resolveJobQueueKey(inAppPayment))
|
.setQueue(InAppPaymentsRepository.resolveJobQueueKey(inAppPayment))
|
||||||
.setLifespan(InAppPaymentsRepository.resolveContextJobLifespan(inAppPayment).inWholeMilliseconds)
|
.setLifespan(3.days.inWholeMilliseconds)
|
||||||
.setMaxAttempts(Parameters.UNLIMITED)
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
@ -76,14 +80,34 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRun() {
|
override fun run(): Result {
|
||||||
InAppPaymentsRepository.resolveLock(inAppPaymentId).withLock {
|
return InAppPaymentsRepository.resolveLock(inAppPaymentId).withLock {
|
||||||
doRun()
|
runBlocking { linkPurchaseToken() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doRun() {
|
private suspend fun linkPurchaseToken(): Result {
|
||||||
val inAppPayment = getAndValidateInAppPayment()
|
if (!AppDependencies.billingApi.isApiAvailable()) {
|
||||||
|
warning("Billing API is not available on this device. Exiting.")
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
val purchaseState: BillingPurchaseState = when (val purchase = AppDependencies.billingApi.queryPurchases()) {
|
||||||
|
is BillingPurchaseResult.Success -> purchase.purchaseState
|
||||||
|
else -> BillingPurchaseState.UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
if (purchaseState != BillingPurchaseState.PURCHASED) {
|
||||||
|
warning("Billing purchase not in the PURCHASED state. Retrying later.")
|
||||||
|
return Result.retry(defaultBackoff())
|
||||||
|
}
|
||||||
|
|
||||||
|
val inAppPayment = try {
|
||||||
|
getAndValidateInAppPayment()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
warning("Failed to validate in-app payment.", e)
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
val response = AppDependencies.donationsService.linkGooglePlayBillingPurchaseTokenToSubscriberId(
|
val response = AppDependencies.donationsService.linkGooglePlayBillingPurchaseTokenToSubscriberId(
|
||||||
inAppPayment.subscriberId!!,
|
inAppPayment.subscriberId!!,
|
||||||
|
@ -92,12 +116,13 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response.applicationError.isPresent) {
|
if (response.applicationError.isPresent) {
|
||||||
handleApplicationError(response.applicationError.get(), response.status)
|
return handleApplicationError(response.applicationError.get(), response.status)
|
||||||
} else if (response.result.isPresent) {
|
} else if (response.result.isPresent) {
|
||||||
info("Successfully linked purchase token to subscriber id.")
|
info("Successfully linked purchase token to subscriber id.")
|
||||||
|
return Result.success()
|
||||||
} else {
|
} else {
|
||||||
warning("Encountered a retryable exception.", response.executionError.get())
|
warning("Encountered a retryable exception.", response.executionError.get())
|
||||||
throw InAppPaymentRetryException(response.executionError.get())
|
return Result.retry(defaultBackoff())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,22 +166,21 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
||||||
return inAppPayment
|
return inAppPayment
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleApplicationError(applicationError: Throwable, status: Int) {
|
private fun handleApplicationError(applicationError: Throwable, status: Int): Result {
|
||||||
when (status) {
|
return when (status) {
|
||||||
402 -> {
|
402 -> {
|
||||||
warning("The purchaseToken payment is incomplete or invalid.", applicationError)
|
warning("The purchaseToken payment is incomplete or invalid.", applicationError)
|
||||||
// TODO [backups] -- Is this a recoverable failure?
|
Result.retry(defaultBackoff())
|
||||||
throw IOException("TODO -- recoverable?")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
403 -> {
|
403 -> {
|
||||||
warning("subscriberId authentication failure OR account authentication is present", applicationError)
|
warning("subscriberId authentication failure OR account authentication is present", applicationError)
|
||||||
throw IOException("subscriberId authentication failure OR account authentication is present")
|
Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
404 -> {
|
404 -> {
|
||||||
warning("No such subscriberId exists or subscriberId is malformed or the purchaseToken does not exist", applicationError)
|
warning("No such subscriberId exists or subscriberId is malformed or the purchaseToken does not exist", applicationError)
|
||||||
throw IOException("No such subscriberId exists or subscriberId is malformed or the purchaseToken does not exist")
|
Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
409 -> {
|
409 -> {
|
||||||
|
@ -165,9 +189,6 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
||||||
try {
|
try {
|
||||||
info("Generating a new subscriber id.")
|
info("Generating a new subscriber id.")
|
||||||
RecurringInAppPaymentRepository.ensureSubscriberId(InAppPaymentSubscriberRecord.Type.BACKUP, true).blockingAwait()
|
RecurringInAppPaymentRepository.ensureSubscriberId(InAppPaymentSubscriberRecord.Type.BACKUP, true).blockingAwait()
|
||||||
} catch (e: Exception) {
|
|
||||||
throw InAppPaymentRetryException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
info("Writing the new subscriber id to the InAppPayment.")
|
info("Writing the new subscriber id to the InAppPayment.")
|
||||||
val latest = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
val latest = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||||
|
@ -176,18 +197,20 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
info("Scheduling retry.")
|
info("Scheduling retry.")
|
||||||
throw InAppPaymentRetryException()
|
} catch (e: Exception) {
|
||||||
|
warning("Failed to generate and update subscriber id. Retrying later.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.retry(defaultBackoff())
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
warning("An unknown error occurred.", applicationError)
|
warning("An unknown error occurred.", applicationError)
|
||||||
throw IOException(applicationError)
|
Result.failure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShouldRetry(e: Exception): Boolean = e is InAppPaymentRetryException
|
|
||||||
|
|
||||||
private fun info(message: String, throwable: Throwable? = null) {
|
private fun info(message: String, throwable: Throwable? = null) {
|
||||||
Log.i(TAG, "InAppPayment[$inAppPaymentId]: $message", throwable, true)
|
Log.i(TAG, "InAppPayment[$inAppPaymentId]: $message", throwable, true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,7 +131,15 @@ class RestoreAttachmentJob private constructor(
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun onRun() {
|
override fun onRun() {
|
||||||
|
try {
|
||||||
doWork()
|
doWork()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (BackupRepository.checkForOutOfStorageError(TAG)) {
|
||||||
|
throw RetryLaterException(e)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!SignalDatabase.messages.isStory(messageId)) {
|
if (!SignalDatabase.messages.isStory(messageId)) {
|
||||||
AppDependencies.messageNotifier.updateNotification(context, forConversation(0))
|
AppDependencies.messageNotifier.updateNotification(context, forConversation(0))
|
||||||
|
|
|
@ -79,6 +79,15 @@ class RestoreAttachmentThumbnailJob private constructor(
|
||||||
|
|
||||||
@Throws(Exception::class, IOException::class, InvalidAttachmentException::class, InvalidMessageException::class, MissingConfigurationException::class)
|
@Throws(Exception::class, IOException::class, InvalidAttachmentException::class, InvalidMessageException::class, MissingConfigurationException::class)
|
||||||
public override fun onRun() {
|
public override fun onRun() {
|
||||||
|
try {
|
||||||
|
doWork()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
BackupRepository.checkForOutOfStorageError(TAG)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doWork() {
|
||||||
Log.i(TAG, "onRun() messageId: $messageId attachmentId: $attachmentId")
|
Log.i(TAG, "onRun() messageId: $messageId attachmentId: $attachmentId")
|
||||||
|
|
||||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||||
|
|
|
@ -41,7 +41,7 @@ class RestoreOptimizedMediaJob private constructor(parameters: Parameters) : Job
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun run(): Result {
|
override fun run(): Result {
|
||||||
if (SignalStore.backup.optimizeStorage) {
|
if (SignalStore.backup.optimizeStorage && !SignalStore.backup.userManuallySkippedMediaRestore) {
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,9 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||||
private const val KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_TIME = "backup.failed.acknowledged.snooze.time"
|
private const val KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_TIME = "backup.failed.acknowledged.snooze.time"
|
||||||
private const val KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_COUNT = "backup.failed.acknowledged.snooze.count"
|
private const val KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_COUNT = "backup.failed.acknowledged.snooze.count"
|
||||||
private const val KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME = "backup.failed.sheet.snooze"
|
private const val KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME = "backup.failed.sheet.snooze"
|
||||||
|
private const val KEY_BACKUP_FAIL_SPACE_REMAINING = "backup.failed.space.remaining"
|
||||||
|
|
||||||
|
private const val KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE = "backup.user.manually.skipped.media.restore"
|
||||||
|
|
||||||
private const val KEY_MEDIA_ROOT_BACKUP_KEY = "backup.mediaRootBackupKey"
|
private const val KEY_MEDIA_ROOT_BACKUP_KEY = "backup.mediaRootBackupKey"
|
||||||
|
|
||||||
|
@ -89,6 +92,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||||
var lastMediaSyncTime: Long by longValue(KEY_LAST_BACKUP_MEDIA_SYNC_TIME, -1)
|
var lastMediaSyncTime: Long by longValue(KEY_LAST_BACKUP_MEDIA_SYNC_TIME, -1)
|
||||||
var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer)
|
var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer)
|
||||||
|
|
||||||
|
var userManuallySkippedMediaRestore: Boolean by booleanValue(KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE, false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key used to backup messages.
|
* Key used to backup messages.
|
||||||
*/
|
*/
|
||||||
|
@ -172,10 +177,19 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||||
/** True if we believe we have successfully uploaded a backup, otherwise false. */
|
/** True if we believe we have successfully uploaded a backup, otherwise false. */
|
||||||
var hasBackupBeenUploaded: Boolean by booleanValue(KEY_BACKUP_UPLOADED, false)
|
var hasBackupBeenUploaded: Boolean by booleanValue(KEY_BACKUP_UPLOADED, false)
|
||||||
|
|
||||||
val hasBackupFailure: Boolean = getBoolean(KEY_BACKUP_FAIL, false)
|
val hasBackupFailure: Boolean get() = getBoolean(KEY_BACKUP_FAIL, false)
|
||||||
val nextBackupFailureSnoozeTime: Duration get() = getLong(KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_TIME, 0L).milliseconds
|
val nextBackupFailureSnoozeTime: Duration get() = getLong(KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_TIME, 0L).milliseconds
|
||||||
val nextBackupFailureSheetSnoozeTime: Duration get() = getLong(KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME, getNextBackupFailureSheetSnoozeTime(lastBackupTime.milliseconds).inWholeMilliseconds).milliseconds
|
val nextBackupFailureSheetSnoozeTime: Duration get() = getLong(KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME, getNextBackupFailureSheetSnoozeTime(lastBackupTime.milliseconds).inWholeMilliseconds).milliseconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denotes how many bytes are still available on the disk for writing. Used to display
|
||||||
|
* the disk full error and sheet. Set when we believe there might be an "out of space"
|
||||||
|
* failure in BackupRestoreMediaJob and each time the application is brought into the
|
||||||
|
* foreground. We never clear this value, so it can't be used as an indicator that
|
||||||
|
* something bad happened, it can only be utilized as a reference for comparison.
|
||||||
|
*/
|
||||||
|
var spaceAvailableOnDiskBytes: Long by longValue(KEY_BACKUP_FAIL_SPACE_REMAINING, -1L)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call when the user disables backups. Clears/resets all relevant fields.
|
* Call when the user disables backups. Clears/resets all relevant fields.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -7473,7 +7473,7 @@
|
||||||
<!-- Dialog title for skipping media restore -->
|
<!-- Dialog title for skipping media restore -->
|
||||||
<string name="BackupAlertBottomSheet__skip_restore_question">Skip restore?</string>
|
<string name="BackupAlertBottomSheet__skip_restore_question">Skip restore?</string>
|
||||||
<!-- Dialog text for skipping media restore -->
|
<!-- Dialog text for skipping media restore -->
|
||||||
<string name="BackupAlertBottomSheet__if_you_skip_restore">If you skip restore the remaining media and attachments in your backup will be deleted the next time your device completes a new backup.</string>
|
<string name="BackupAlertBottomSheet__if_you_skip_restore_the">If you skip restore the remaining media and attachments in your backup can be downloaded at a later time when storage space becomes available.</string>
|
||||||
|
|
||||||
<!-- BackupStatus -->
|
<!-- BackupStatus -->
|
||||||
<!-- Status title when user does not have enough free space to download their media. Placeholder is required disk space. -->
|
<!-- Status title when user does not have enough free space to download their media. Placeholder is required disk space. -->
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.android.billingclient.api.BillingResult
|
||||||
import com.android.billingclient.api.PendingPurchasesParams
|
import com.android.billingclient.api.PendingPurchasesParams
|
||||||
import com.android.billingclient.api.ProductDetails
|
import com.android.billingclient.api.ProductDetails
|
||||||
import com.android.billingclient.api.ProductDetailsResult
|
import com.android.billingclient.api.ProductDetailsResult
|
||||||
|
import com.android.billingclient.api.Purchase
|
||||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||||
import com.android.billingclient.api.QueryProductDetailsParams
|
import com.android.billingclient.api.QueryProductDetailsParams
|
||||||
import com.android.billingclient.api.QueryPurchasesParams
|
import com.android.billingclient.api.QueryPurchasesParams
|
||||||
|
@ -41,6 +42,7 @@ import org.signal.core.util.billing.BillingDependencies
|
||||||
import org.signal.core.util.billing.BillingError
|
import org.signal.core.util.billing.BillingError
|
||||||
import org.signal.core.util.billing.BillingProduct
|
import org.signal.core.util.billing.BillingProduct
|
||||||
import org.signal.core.util.billing.BillingPurchaseResult
|
import org.signal.core.util.billing.BillingPurchaseResult
|
||||||
|
import org.signal.core.util.billing.BillingPurchaseState
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
@ -80,6 +82,7 @@ internal class BillingApiImpl(
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "purchasesUpdatedListener: successful purchase at ${newestPurchase.purchaseTime}")
|
Log.d(TAG, "purchasesUpdatedListener: successful purchase at ${newestPurchase.purchaseTime}")
|
||||||
BillingPurchaseResult.Success(
|
BillingPurchaseResult.Success(
|
||||||
|
purchaseState = newestPurchase.purchaseState.toBillingPurchaseState(),
|
||||||
purchaseToken = newestPurchase.purchaseToken,
|
purchaseToken = newestPurchase.purchaseToken,
|
||||||
isAcknowledged = newestPurchase.isAcknowledged,
|
isAcknowledged = newestPurchase.isAcknowledged,
|
||||||
purchaseTime = newestPurchase.purchaseTime,
|
purchaseTime = newestPurchase.purchaseTime,
|
||||||
|
@ -202,6 +205,7 @@ internal class BillingApiImpl(
|
||||||
val purchase = result.purchasesList.maxByOrNull { it.purchaseTime } ?: return BillingPurchaseResult.None
|
val purchase = result.purchasesList.maxByOrNull { it.purchaseTime } ?: return BillingPurchaseResult.None
|
||||||
|
|
||||||
return BillingPurchaseResult.Success(
|
return BillingPurchaseResult.Success(
|
||||||
|
purchaseState = purchase.purchaseState.toBillingPurchaseState(),
|
||||||
purchaseTime = purchase.purchaseTime,
|
purchaseTime = purchase.purchaseTime,
|
||||||
purchaseToken = purchase.purchaseToken,
|
purchaseToken = purchase.purchaseToken,
|
||||||
isAcknowledged = purchase.isAcknowledged,
|
isAcknowledged = purchase.isAcknowledged,
|
||||||
|
@ -256,6 +260,14 @@ internal class BillingApiImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Int.toBillingPurchaseState(): BillingPurchaseState {
|
||||||
|
return when (this) {
|
||||||
|
Purchase.PurchaseState.PURCHASED -> BillingPurchaseState.PURCHASED
|
||||||
|
Purchase.PurchaseState.PENDING -> BillingPurchaseState.PENDING
|
||||||
|
else -> BillingPurchaseState.UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun queryProductsInternal(): ProductDetailsResult {
|
private suspend fun queryProductsInternal(): ProductDetailsResult {
|
||||||
val productList = listOf(
|
val productList = listOf(
|
||||||
QueryProductDetailsParams.Product.newBuilder()
|
QueryProductDetailsParams.Product.newBuilder()
|
||||||
|
|
|
@ -95,6 +95,10 @@ class ByteSize(val bytes: Long) {
|
||||||
return "${formatter.format(size)}${if (spaced) " " else ""}${unit.label}"
|
return "${formatter.format(size)}${if (spaced) " " else ""}${unit.label}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operator fun compareTo(other: ByteSize): Int {
|
||||||
|
return bytes.compareTo(other.bytes)
|
||||||
|
}
|
||||||
|
|
||||||
enum class Size(val label: String) {
|
enum class Size(val label: String) {
|
||||||
BYTE("B"),
|
BYTE("B"),
|
||||||
KIBIBYTE("KB"),
|
KIBIBYTE("KB"),
|
||||||
|
|
|
@ -14,6 +14,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||||
*/
|
*/
|
||||||
sealed interface BillingPurchaseResult {
|
sealed interface BillingPurchaseResult {
|
||||||
data class Success(
|
data class Success(
|
||||||
|
val purchaseState: BillingPurchaseState,
|
||||||
val purchaseToken: String,
|
val purchaseToken: String,
|
||||||
val isAcknowledged: Boolean,
|
val isAcknowledged: Boolean,
|
||||||
val purchaseTime: Long,
|
val purchaseTime: Long,
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.util.billing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BillingPurchaseState which aligns with the Google Play Billing purchased state.
|
||||||
|
*/
|
||||||
|
enum class BillingPurchaseState {
|
||||||
|
UNSPECIFIED,
|
||||||
|
PURCHASED,
|
||||||
|
PENDING
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue