mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Add some megaphones to encourage users to try backups.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.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.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
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.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
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.ComposeBottomSheetDialogFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* Bottom sheet shown after a successful paid backup subscription from a storage upsell megaphone.
|
||||
* Allows the user to start their first backup and optionally enable storage optimization.
|
||||
*/
|
||||
class BackupSetupCompleteBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.75f
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
SetupCompleteSheetContent(
|
||||
onBackUpNowClick = { optimizeStorage ->
|
||||
SignalStore.backup.optimizeStorage = optimizeStorage
|
||||
BackupMessagesJob.enqueue()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetupCompleteSheetContent(
|
||||
onBackUpNowClick: (optimizeStorage: Boolean) -> Unit
|
||||
) {
|
||||
var optimizeStorage by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter))
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Spacer(modifier = Modifier.size(26.dp))
|
||||
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.image_signal_backups_subscribed),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.padding(2.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.BackupSetupCompleteBottomSheet__title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.BackupSetupCompleteBottomSheet__body),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
checked = optimizeStorage,
|
||||
text = stringResource(R.string.BackupSetupCompleteBottomSheet__optimize_storage),
|
||||
label = stringResource(R.string.BackupSetupCompleteBottomSheet__optimize_subtitle),
|
||||
onCheckChanged = { optimizeStorage = it },
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onBackUpNowClick(optimizeStorage) },
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(bottom = 56.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.BackupSetupCompleteBottomSheet__back_up_now))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupSetupCompleteBottomSheetPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
SetupCompleteSheetContent(
|
||||
onBackUpNowClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.RoundedCornerShape
|
||||
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.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.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.util.gibiBytes
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.billing.upgrade.UpgradeToPaidTierBottomSheet
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* Bottom sheet that upsells paid backup plans to users.
|
||||
*/
|
||||
class BackupUpsellBottomSheet : UpgradeToPaidTierBottomSheet() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_SHOW_POST_PAYMENT = "show_post_payment"
|
||||
|
||||
@JvmStatic
|
||||
fun create(showPostPaymentSheet: Boolean): DialogFragment {
|
||||
return BackupUpsellBottomSheet().apply {
|
||||
arguments = bundleOf(ARG_SHOW_POST_PAYMENT to showPostPaymentSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val showPostPaymentSheet: Boolean by lazy(LazyThreadSafetyMode.NONE) {
|
||||
requireArguments().getBoolean(ARG_SHOW_POST_PAYMENT, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (showPostPaymentSheet) {
|
||||
parentFragmentManager.setFragmentResultListener(RESULT_KEY, requireActivity()) { _, bundle ->
|
||||
if (bundle.getBoolean(RESULT_KEY, false)) {
|
||||
BackupSetupCompleteBottomSheet().show(parentFragmentManager, "backup_setup_complete")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun UpgradeSheetContent(
|
||||
paidBackupType: MessageBackupsType.Paid,
|
||||
freeBackupType: MessageBackupsType.Free,
|
||||
isSubscribeEnabled: Boolean,
|
||||
onSubscribeClick: () -> Unit
|
||||
) {
|
||||
UpsellSheetContent(
|
||||
paidBackupType = paidBackupType,
|
||||
isSubscribeEnabled = isSubscribeEnabled,
|
||||
onSubscribeClick = onSubscribeClick,
|
||||
onNoThanksClick = { dismissAllowingStateLoss() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpsellSheetContent(
|
||||
paidBackupType: MessageBackupsType.Paid,
|
||||
isSubscribeEnabled: Boolean,
|
||||
onSubscribeClick: () -> Unit,
|
||||
onNoThanksClick: () -> Unit
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val pricePerMonth = remember(paidBackupType) {
|
||||
FiatMoneyUtil.format(resources, paidBackupType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter))
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Spacer(modifier = Modifier.size(26.dp))
|
||||
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.image_signal_backups),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.padding(2.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.BackupUpsellBottomSheet__title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.BackupUpsellBottomSheet__body),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
FeatureCard(pricePerMonth = pricePerMonth)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
enabled = isSubscribeEnabled,
|
||||
onClick = onSubscribeClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.BackupUpsellBottomSheet__subscribe_for, pricePerMonth))
|
||||
}
|
||||
|
||||
TextButton(
|
||||
enabled = isSubscribeEnabled,
|
||||
onClick = onNoThanksClick,
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.BackupUpsellBottomSheet__no_thanks))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureCard(pricePerMonth: String) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.BackupUpsellBottomSheet__price_per_month, pricePerMonth),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.BackupUpsellBottomSheet__text_and_all_media),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
FeatureBullet(text = stringResource(R.string.BackupUpsellBottomSheet__full_text_media_backup))
|
||||
FeatureBullet(text = stringResource(R.string.BackupUpsellBottomSheet__storage_100gb))
|
||||
FeatureBullet(text = stringResource(R.string.BackupUpsellBottomSheet__save_on_device_storage))
|
||||
FeatureBullet(text = stringResource(R.string.BackupUpsellBottomSheet__thanks_for_supporting))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureBullet(text: String) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(vertical = 2.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_check_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupUpsellBottomSheetPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
UpsellSheetContent(
|
||||
paidBackupType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal("1.99"), Currency.getInstance("USD")),
|
||||
mediaTtl = 30.days,
|
||||
storageAllowanceBytes = 100.gibiBytes.inWholeBytes
|
||||
),
|
||||
isSubscribeEnabled = true,
|
||||
onSubscribeClick = {},
|
||||
onNoThanksClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -706,6 +706,17 @@ class AttachmentTable(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is "approximate" because it doesn't account for things like duplicates. Only useful as a heuristic.
|
||||
*/
|
||||
fun getApproximateTotalMediaSize(): Long {
|
||||
return readableDatabase
|
||||
.select("SUM($DATA_SIZE)")
|
||||
.from(TABLE_NAME)
|
||||
.run()
|
||||
.readToSingleLong(0L)
|
||||
}
|
||||
|
||||
fun getOptimizedMediaAttachmentSize(): Long {
|
||||
return readableDatabase
|
||||
.select("SUM($DATA_SIZE)")
|
||||
|
||||
@@ -1915,9 +1915,15 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of thread ids, return the count of all messages in the table that match that thread id. This will include *all* messages, and is
|
||||
* explicitly for use as a "fuzzy total"
|
||||
* "Approximate" because we're not filtering out stuff like message edits. Only useful as a heuristic.
|
||||
*/
|
||||
fun getApproximateTotalMessageCount(): Long {
|
||||
return readableDatabase.count()
|
||||
.from(TABLE_NAME)
|
||||
.run()
|
||||
.readToSingleLong(0L)
|
||||
}
|
||||
|
||||
fun getApproximateExportableMessageCount(threadIds: Set<Long>): Long {
|
||||
val queries = SqlUtil.buildCollectionQuery(THREAD_ID, threadIds)
|
||||
return queries.sumOf {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.megaphone
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Schedule for backup upsell megaphones that combines:
|
||||
* 1. Per-megaphone recurring display timing (like [RecurringSchedule])
|
||||
* 2. Cross-megaphone shared snooze: if any other backup upsell was seen recently, suppress display
|
||||
*
|
||||
* @param records All megaphone records, used to find the most recent lastSeen across backup upsell events.
|
||||
* @param gaps Per-megaphone recurring gaps, same semantics as [RecurringSchedule].
|
||||
*/
|
||||
class BackupUpsellSchedule(
|
||||
private val records: Map<Megaphones.Event, MegaphoneRecord>,
|
||||
vararg val gaps: Long
|
||||
) : MegaphoneSchedule {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val BACKUP_UPSELL_EVENTS: Set<Megaphones.Event> = setOf(
|
||||
Megaphones.Event.BACKUPS_GENERIC_UPSELL,
|
||||
Megaphones.Event.BACKUP_MESSAGE_COUNT_UPSELL,
|
||||
Megaphones.Event.BACKUP_MEDIA_SIZE_UPSELL,
|
||||
Megaphones.Event.BACKUP_LOW_STORAGE_UPSELL
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val MIN_TIME_BETWEEN_BACKUP_UPSELLS: Long = 60.days.inWholeMilliseconds
|
||||
}
|
||||
|
||||
override fun shouldDisplay(seenCount: Int, lastSeen: Long, firstVisible: Long, currentTime: Long): Boolean {
|
||||
val lastSeen = lastSeen.coerceAtMost(currentTime)
|
||||
|
||||
val lastSeenAnyBackupUpsell: Long = records.entries
|
||||
.filter { it.key in BACKUP_UPSELL_EVENTS }
|
||||
.mapNotNull { it.value.lastSeen.takeIf { t -> t > 0 } }
|
||||
.maxOrNull() ?: 0L
|
||||
|
||||
if (currentTime - lastSeenAnyBackupUpsell <= MIN_TIME_BETWEEN_BACKUP_UPSELLS) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (seenCount == 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
val gap = gaps[minOf(seenCount - 1, gaps.size - 1)]
|
||||
return lastSeen + gap <= currentTime
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,14 @@ import androidx.core.app.NotificationManagerCompat;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import org.signal.core.util.DiskUtil;
|
||||
import org.signal.core.util.MapUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.signal.core.util.TranslationDetection;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupUpsellBottomSheet;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
@@ -40,6 +43,7 @@ import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.profiles.username.NewWaysToConnectDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.ByteUnit;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Environment;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
@@ -129,7 +133,10 @@ public final class Megaphones {
|
||||
// Feature-introduction megaphones should *probably* be added below this divider
|
||||
put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfilePhotoMegaphone(context) ? ALWAYS : NEVER);
|
||||
put(Event.PNP_LAUNCH, shouldShowPnpLaunchMegaphone() ? ALWAYS : NEVER);
|
||||
put(Event.TURN_ON_SIGNAL_BACKUPS, shouldShowTurnOnBackupsMegaphone(context) ? new RecurringSchedule(TimeUnit.DAYS.toMillis(30), TimeUnit.DAYS.toMillis(90)) : NEVER);
|
||||
put(Event.BACKUP_LOW_STORAGE_UPSELL, shouldShowBackupLowStorageUpsell(context) ? new BackupUpsellSchedule(records, TimeUnit.DAYS.toMillis(60), TimeUnit.DAYS.toMillis(120)) : NEVER);
|
||||
put(Event.BACKUP_MEDIA_SIZE_UPSELL, shouldShowBackupMediaSizeUpsell() ? new BackupUpsellSchedule(records, TimeUnit.DAYS.toMillis(60), TimeUnit.DAYS.toMillis(120)) : NEVER);
|
||||
put(Event.BACKUP_MESSAGE_COUNT_UPSELL, shouldShowBackupMessageCountUpsell(context) ? new BackupUpsellSchedule(records, TimeUnit.DAYS.toMillis(60)) : NEVER);
|
||||
put(Event.BACKUPS_GENERIC_UPSELL, shouldShowGenericBackupsMegaphone(context) ? new BackupUpsellSchedule(records, TimeUnit.DAYS.toMillis(60)) : NEVER);
|
||||
put(Event.VERIFY_BACKUP_KEY, new VerifyBackupKeyReminderSchedule());
|
||||
put(Event.USE_NEW_ON_DEVICE_BACKUPS, shouldShowUseNewOnDeviceBackupsMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(14)) : NEVER);
|
||||
}};
|
||||
@@ -181,8 +188,14 @@ public final class Megaphones {
|
||||
return buildPnpLaunchMegaphone();
|
||||
case NEW_LINKED_DEVICE:
|
||||
return buildNewLinkedDeviceMegaphone(context);
|
||||
case TURN_ON_SIGNAL_BACKUPS:
|
||||
return buildTurnOnSignalBackupsMegaphone();
|
||||
case BACKUPS_GENERIC_UPSELL:
|
||||
return buildBackupGenericUpsellMegaphone();
|
||||
case BACKUP_MESSAGE_COUNT_UPSELL:
|
||||
return buildBackupMessageCountUpsellMegaphone();
|
||||
case BACKUP_MEDIA_SIZE_UPSELL:
|
||||
return buildBackupMediaSizeUpsellMegaphone();
|
||||
case BACKUP_LOW_STORAGE_UPSELL:
|
||||
return buildBackupLowStorageUpsellMegaphone();
|
||||
case VERIFY_BACKUP_KEY:
|
||||
return buildVerifyBackupKeyMegaphone();
|
||||
case USE_NEW_ON_DEVICE_BACKUPS:
|
||||
@@ -454,8 +467,8 @@ public final class Megaphones {
|
||||
.build();
|
||||
}
|
||||
|
||||
public static @NonNull Megaphone buildTurnOnSignalBackupsMegaphone() {
|
||||
return new Megaphone.Builder(Event.TURN_ON_SIGNAL_BACKUPS, Megaphone.Style.BASIC)
|
||||
public static @NonNull Megaphone buildBackupGenericUpsellMegaphone() {
|
||||
return new Megaphone.Builder(Event.BACKUPS_GENERIC_UPSELL, Megaphone.Style.BASIC)
|
||||
.setImage(R.drawable.backups_megaphone_image)
|
||||
.setTitle(R.string.TurnOnSignalBackups__title)
|
||||
.setBody(R.string.TurnOnSignalBackups__body)
|
||||
@@ -463,11 +476,11 @@ public final class Megaphones {
|
||||
Intent intent = AppSettingsActivity.remoteBackups(controller.getMegaphoneActivity());
|
||||
|
||||
controller.onMegaphoneNavigationRequested(intent);
|
||||
controller.onMegaphoneSnooze(Event.TURN_ON_SIGNAL_BACKUPS);
|
||||
controller.onMegaphoneSnooze(Event.BACKUPS_GENERIC_UPSELL);
|
||||
})
|
||||
.setSecondaryButton(R.string.TurnOnSignalBackups__not_now, (megaphone, controller) -> {
|
||||
controller.onMegaphoneToastRequested(controller.getMegaphoneActivity().getString(R.string.TurnOnSignalBackups__toast_not_now));
|
||||
controller.onMegaphoneSnooze(Event.TURN_ON_SIGNAL_BACKUPS);
|
||||
controller.onMegaphoneSnooze(Event.BACKUPS_GENERIC_UPSELL);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
@@ -579,7 +592,7 @@ public final class Megaphones {
|
||||
return SignalStore.account().isPrimaryDevice() && TextUtils.isEmpty(SignalStore.account().getUsername()) && !SignalStore.uiHints().hasCompletedUsernameOnboarding();
|
||||
}
|
||||
|
||||
private static boolean shouldShowTurnOnBackupsMegaphone(@NonNull Context context) {
|
||||
private static boolean shouldShowGenericBackupsMegaphone(@NonNull Context context) {
|
||||
if (!RemoteConfig.backupsMegaphone()) {
|
||||
return false;
|
||||
}
|
||||
@@ -631,6 +644,92 @@ public final class Megaphones {
|
||||
return System.currentTimeMillis() - lastSeenDonatePrompt;
|
||||
}
|
||||
|
||||
private static boolean shouldShowBackupMessageCountUpsell(@NonNull Context context) {
|
||||
if (!SignalStore.account().isRegistered() || TextSecurePreferences.isUnauthorizedReceived(context) || SignalStore.account().isLinkedDevice()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SignalStore.backup().getLatestBackupTier() != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SignalDatabase.messages().getApproximateTotalMessageCount() > 1000;
|
||||
}
|
||||
|
||||
private static boolean shouldShowBackupMediaSizeUpsell() {
|
||||
if (!SignalStore.account().isRegistered() || SignalStore.account().isLinkedDevice() || !Environment.Backups.supportsGooglePlayBilling()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SignalStore.backup().getLatestBackupTier() != MessageBackupTier.FREE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SignalDatabase.attachments().getApproximateTotalMediaSize() > ByteUnit.GIGABYTES.toBytes(1);
|
||||
}
|
||||
|
||||
private static boolean shouldShowBackupLowStorageUpsell(@NonNull Context context) {
|
||||
if (!SignalStore.account().isRegistered() || TextSecurePreferences.isUnauthorizedReceived(context) || SignalStore.account().isLinkedDevice() || !Environment.Backups.supportsGooglePlayBilling()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SignalStore.backup().getLatestBackupTier() == MessageBackupTier.PAID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long available = DiskUtil.getAvailableSpace(context).getBytes();
|
||||
long total = DiskUtil.getTotalDiskSize(context).getBytes();
|
||||
|
||||
return total > 0 && ((double) available / total) < 0.10;
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildBackupMessageCountUpsellMegaphone() {
|
||||
return new Megaphone.Builder(Event.BACKUP_MESSAGE_COUNT_UPSELL, Megaphone.Style.BASIC)
|
||||
.setImage(R.drawable.megaphone_backup_message_count)
|
||||
.setTitle(R.string.BackupMessagesUpsell__title)
|
||||
.setBody(R.string.BackupMessagesUpsell__body)
|
||||
.setActionButton(R.string.BackupMessagesUpsell__turn_on, (megaphone, controller) -> {
|
||||
Intent intent = AppSettingsActivity.remoteBackups(controller.getMegaphoneActivity());
|
||||
controller.onMegaphoneNavigationRequested(intent);
|
||||
controller.onMegaphoneSnooze(Event.BACKUP_MESSAGE_COUNT_UPSELL);
|
||||
})
|
||||
.setSecondaryButton(R.string.BackupMessagesUpsell__not_now, (megaphone, controller) -> {
|
||||
controller.onMegaphoneSnooze(Event.BACKUP_MESSAGE_COUNT_UPSELL);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildBackupMediaSizeUpsellMegaphone() {
|
||||
return new Megaphone.Builder(Event.BACKUP_MEDIA_SIZE_UPSELL, Megaphone.Style.BASIC)
|
||||
.setImage(R.drawable.megaphone_backup_media_size)
|
||||
.setTitle(R.string.BackupMediaUpsell__title)
|
||||
.setBody(R.string.BackupMediaUpsell__body)
|
||||
.setActionButton(R.string.BackupMediaUpsell__upgrade, (megaphone, controller) -> {
|
||||
controller.onMegaphoneDialogFragmentRequested(BackupUpsellBottomSheet.create(false));
|
||||
controller.onMegaphoneSnooze(Event.BACKUP_MEDIA_SIZE_UPSELL);
|
||||
})
|
||||
.setSecondaryButton(R.string.BackupMediaUpsell__not_now, (megaphone, controller) -> {
|
||||
controller.onMegaphoneSnooze(Event.BACKUP_MEDIA_SIZE_UPSELL);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildBackupLowStorageUpsellMegaphone() {
|
||||
boolean hasBackups = SignalStore.backup().getLatestBackupTier() != null;
|
||||
|
||||
return new Megaphone.Builder(Event.BACKUP_LOW_STORAGE_UPSELL, Megaphone.Style.BASIC)
|
||||
.setImage(R.drawable.megaphone_backup_storage_low)
|
||||
.setTitle(R.string.BackupStorageUpsell__title)
|
||||
.setBody(R.string.BackupStorageUpsell__body)
|
||||
.setActionButton(hasBackups ? R.string.BackupStorageUpsell__upgrade : R.string.BackupStorageUpsell__turn_on, (megaphone, controller) -> {
|
||||
controller.onMegaphoneDialogFragmentRequested(BackupUpsellBottomSheet.create(true));
|
||||
controller.onMegaphoneSnooze(Event.BACKUP_LOW_STORAGE_UPSELL);
|
||||
})
|
||||
.setSecondaryButton(R.string.BackupStorageUpsell__not_now, (megaphone, controller) -> {
|
||||
controller.onMegaphoneSnooze(Event.BACKUP_LOW_STORAGE_UPSELL);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
public enum Event {
|
||||
PINS_FOR_ALL("pins_for_all"),
|
||||
@@ -649,7 +748,10 @@ public final class Megaphones {
|
||||
PNP_LAUNCH("pnp_launch"),
|
||||
GRANT_FULL_SCREEN_INTENT("grant_full_screen_intent"),
|
||||
NEW_LINKED_DEVICE("new_linked_device"),
|
||||
TURN_ON_SIGNAL_BACKUPS("turn_on_signal_backups"),
|
||||
BACKUPS_GENERIC_UPSELL("turn_on_signal_backups"),
|
||||
BACKUP_MESSAGE_COUNT_UPSELL("backup_messages_upsell"),
|
||||
BACKUP_MEDIA_SIZE_UPSELL("backup_media_upsell"),
|
||||
BACKUP_LOW_STORAGE_UPSELL("backup_storage_upsell"),
|
||||
VERIFY_BACKUP_KEY("verify_backup_key"),
|
||||
USE_NEW_ON_DEVICE_BACKUPS("use_new_on_device_backups");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user