mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 08:23:00 +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");
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M13.632,15.546L38.203,10.934A4,4 64.903,0 1,42.872 14.128L46.303,32.407A4,4 94.864,0 1,43.11 37.077L18.539,41.689A4,4 54.899,0 1,13.87 38.496L10.438,20.216A4,4 0,0 1,13.632 15.546z"
|
||||
android:fillColor="#E7EBFF"/>
|
||||
<path
|
||||
android:pathData="M24,23L49,23A4,4 0,0 1,53 27L53,47A4,4 0,0 1,49 51L24,51A4,4 0,0 1,20 47L20,27A4,4 0,0 1,24 23z"
|
||||
android:fillColor="#D2D8FE"/>
|
||||
<path
|
||||
android:pathData="M29.833,28.479C27.889,28.479 26.313,30.056 26.313,32C26.313,33.944 27.889,35.521 29.833,35.521C31.778,35.521 33.354,33.944 33.354,32C33.354,30.056 31.778,28.479 29.833,28.479Z"
|
||||
android:fillColor="#666EE5"/>
|
||||
<path
|
||||
android:pathData="M46.353,19.73L46.614,21.713C47.399,21.72 48.088,21.738 48.69,21.788C49.694,21.87 50.6,22.045 51.447,22.476C52.772,23.151 53.849,24.228 54.524,25.553C54.956,26.4 55.131,27.306 55.213,28.31C55.292,29.28 55.292,30.473 55.292,31.932V41.818C55.292,43.277 55.292,44.47 55.213,45.44C55.131,46.444 54.956,47.35 54.524,48.197C53.849,49.522 52.772,50.599 51.447,51.274C50.6,51.706 49.694,51.881 48.69,51.963C47.721,52.042 46.527,52.042 45.068,52.042H27.599C26.14,52.042 24.947,52.042 23.977,51.963C22.973,51.881 22.067,51.706 21.22,51.274C19.895,50.599 18.818,49.522 18.143,48.197C17.711,47.35 17.536,46.444 17.454,45.44C17.39,44.653 17.378,43.72 17.376,42.616C16.525,42.632 15.731,42.554 14.956,42.28C13.554,41.783 12.346,40.856 11.503,39.63C10.965,38.847 10.674,37.971 10.461,36.987C10.256,36.036 10.1,34.853 9.91,33.406L8.761,24.679C8.571,23.232 8.415,22.049 8.367,21.077C8.317,20.072 8.372,19.15 8.689,18.254C9.186,16.853 10.113,15.644 11.339,14.802C12.122,14.264 12.998,13.972 13.982,13.759C14.933,13.554 16.116,13.399 17.563,13.208L34.883,10.928C36.329,10.738 37.512,10.582 38.484,10.534C39.49,10.484 40.411,10.539 41.307,10.856C42.709,11.353 43.917,12.28 44.76,13.506C45.298,14.289 45.59,15.165 45.802,16.149C46.007,17.1 46.163,18.283 46.353,19.73ZM17.375,39.365C16.722,39.382 16.336,39.32 16.041,39.216C15.286,38.949 14.635,38.449 14.182,37.789C13.983,37.501 13.808,37.089 13.638,36.302C13.464,35.496 13.325,34.445 13.123,32.915L11.992,24.322C11.791,22.792 11.653,21.74 11.613,20.917C11.573,20.113 11.636,19.669 11.753,19.339C12.02,18.584 12.52,17.934 13.179,17.48C13.468,17.282 13.88,17.106 14.667,16.936C15.473,16.763 16.524,16.623 18.054,16.421L35.239,14.159C36.77,13.958 37.821,13.82 38.644,13.78C39.449,13.74 39.892,13.803 40.222,13.92C40.977,14.187 41.628,14.686 42.081,15.346C42.28,15.635 42.455,16.047 42.625,16.834C42.799,17.64 42.938,18.691 43.14,20.221L43.336,21.708L27.599,21.708C26.14,21.708 24.947,21.708 23.977,21.788C22.973,21.87 22.067,22.045 21.22,22.476C19.895,23.151 18.818,24.228 18.143,25.553C17.711,26.4 17.536,27.306 17.454,28.31C17.375,29.28 17.375,30.473 17.375,31.932L17.375,39.365ZM51.629,27.029C51.788,27.341 51.908,27.772 51.973,28.575C52.041,29.396 52.042,30.456 52.042,32V41.205L45.099,35.254C43.172,33.602 40.328,33.602 38.401,35.254L31.323,41.321L29.58,39.826C27.856,38.348 25.311,38.348 23.587,39.826L20.625,42.365C20.625,42.169 20.625,41.964 20.625,41.75L20.625,32C20.625,30.456 20.626,29.396 20.694,28.575C20.759,27.772 20.879,27.341 21.038,27.029C21.402,26.315 21.982,25.735 22.695,25.372C23.008,25.213 23.439,25.092 24.242,25.027C25.063,24.96 26.123,24.958 27.667,24.958L45,24.958C46.544,24.958 47.604,24.96 48.425,25.027C49.228,25.092 49.66,25.213 49.972,25.372C50.685,25.735 51.265,26.315 51.629,27.029ZM20.768,45.81C20.836,46.229 20.928,46.504 21.038,46.721C21.402,47.435 21.982,48.015 22.695,48.378C23.008,48.537 23.439,48.658 24.242,48.723C25.063,48.79 26.123,48.792 27.667,48.792H45C46.544,48.792 47.604,48.79 48.425,48.723C49.228,48.658 49.66,48.537 49.972,48.378C50.685,48.015 51.265,47.435 51.629,46.721C51.788,46.409 51.908,45.978 51.973,45.175C51.985,45.037 51.994,44.891 52.002,44.738L43.336,37.31C42.424,36.528 41.077,36.528 40.164,37.31L33.404,43.104L33.965,43.585C34.533,44.072 34.598,44.927 34.112,45.494C33.625,46.062 32.77,46.128 32.202,45.641L27.817,41.883C27.107,41.274 26.06,41.274 25.35,41.883L20.768,45.81Z"
|
||||
android:fillColor="#666EE5"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M32.588,13.118L24.159,9.505L18.74,10.107L11.516,15.526L8.505,22.751L12.118,32.986L13.322,42.017L20.546,38.405H25.965L34.394,35.394L38.609,28.17L36.802,20.343L32.588,13.118Z"
|
||||
android:strokeWidth="1.20414"
|
||||
android:fillColor="#D2D8FE"
|
||||
android:strokeColor="#666EE5"/>
|
||||
<path
|
||||
android:pathData="M23.557,8.607C32.365,8.608 39.507,15.748 39.507,24.557C39.507,33.365 32.365,40.507 23.557,40.507C22.444,40.507 21.358,40.39 20.309,40.173L14.977,43.881C13.098,45.186 10.548,43.714 10.738,41.435L11.298,34.758C8.995,31.993 7.607,28.435 7.607,24.557C7.608,15.748 14.748,8.608 23.557,8.607ZM23.557,11.607C16.405,11.608 10.608,17.405 10.607,24.557C10.607,27.734 11.75,30.641 13.648,32.895C14.1,33.431 14.36,34.145 14.297,34.901L13.78,41.058L18.69,37.646L18.813,37.564C19.356,37.23 19.979,37.109 20.573,37.177L20.845,37.222L21.174,37.287C21.946,37.431 22.742,37.507 23.557,37.507C30.708,37.507 36.507,31.708 36.507,24.557C36.507,17.405 30.708,11.608 23.557,11.607Z"
|
||||
android:fillColor="#666EE5"/>
|
||||
<path
|
||||
android:pathData="M48.242,23.353L42.221,22.751H36.2L32.588,25.761L28.976,31.18V38.404L32.588,46.833L42.823,51.048L48.844,52.252L53.66,54.058L54.262,51.048L55.466,44.425L56.671,35.394L54.864,28.169L48.242,23.353Z"
|
||||
android:fillColor="#D2D8FE"/>
|
||||
<path
|
||||
android:pathData="M42.824,22.149V20.649H42.824L42.824,22.149ZM28.373,36.598H26.873V36.598L28.373,36.598ZM42.824,51.048L42.824,52.548H42.824V51.048ZM57.273,36.598L58.773,36.598V36.598H57.273ZM53.903,45.873L52.754,44.909L53.903,45.873ZM52.303,54.846L53.159,53.614L52.303,54.846ZM45.72,50.756L45.421,49.286L45.72,50.756ZM46.694,50.947L45.838,52.179L46.694,50.947ZM42.824,22.149V23.649C35.672,23.649 29.874,29.446 29.873,36.598L28.373,36.598L26.873,36.598C26.874,27.789 34.015,20.649 42.824,20.649V22.149ZM28.373,36.598H29.873C29.873,43.75 35.672,49.548 42.824,49.548V51.048V52.548C34.015,52.548 26.873,45.407 26.873,36.598H28.373ZM42.824,51.048L42.823,49.548C43.714,49.548 44.582,49.458 45.421,49.286L45.72,50.756L46.02,52.226C44.987,52.437 43.918,52.548 42.824,52.548L42.824,51.048ZM46.694,50.947L47.55,49.716L53.159,53.614L52.303,54.846L51.446,56.077L45.838,52.179L46.694,50.947ZM54.19,53.756L52.695,53.882L52.111,46.911L53.605,46.785L55.1,46.66L55.685,53.631L54.19,53.756ZM53.903,45.873L52.754,44.909C54.639,42.66 55.773,39.763 55.773,36.598H57.273H58.773C58.773,40.494 57.374,44.066 55.053,46.836L53.903,45.873ZM57.273,36.598L55.773,36.598C55.772,29.447 49.975,23.649 42.823,23.649L42.824,22.149L42.824,20.649C51.632,20.649 58.772,27.79 58.773,36.598L57.273,36.598ZM53.605,46.785L52.111,46.911C52.047,46.157 52.305,45.445 52.754,44.909L53.903,45.873L55.053,46.836C55.075,46.81 55.108,46.751 55.1,46.66L53.605,46.785ZM52.303,54.846L53.159,53.614C52.953,53.471 52.674,53.632 52.695,53.882L54.19,53.756L55.685,53.631C55.876,55.911 53.325,57.383 51.446,56.077L52.303,54.846ZM45.72,50.756L45.421,49.286C46.13,49.142 46.907,49.268 47.55,49.716L46.694,50.947L45.838,52.179C45.916,52.233 45.986,52.233 46.02,52.226L45.72,50.756Z"
|
||||
android:fillColor="#666EE5"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,19 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M45.5,48.5L32.5,32H54L51,42.5L45.5,48.5Z"
|
||||
android:fillColor="#EFF2FF"/>
|
||||
<path
|
||||
android:pathData="M22.5,13.5L31,10H32.5V31L45,48L43.5,50.5L34.5,53.5L23.5,52.5L14.5,46L11.5,35.5V25.5L16.5,16.5L22.5,13.5Z"
|
||||
android:fillColor="#D2D8FE"/>
|
||||
<path
|
||||
android:pathData="M32.5,32L32,10.5L38,11L47,16L51.5,22L53.5,32H32.5Z"
|
||||
android:fillColor="#989DF9"/>
|
||||
<path
|
||||
android:pathData="M32,8.708C19.136,8.708 8.708,19.136 8.708,32C8.708,44.864 19.136,55.292 32,55.292C44.864,55.292 55.292,44.864 55.292,32C55.292,19.136 44.864,8.708 32,8.708ZM30.375,12.023C20.066,12.851 11.958,21.478 11.958,32C11.958,43.069 20.931,52.042 32,52.042C36.181,52.042 40.064,50.761 43.276,48.571L31.695,34.211C30.841,33.151 30.375,31.831 30.375,30.47L30.375,12.023ZM45.805,46.529C49.341,43.168 51.651,38.53 51.997,33.354H40.667L34.889,32.993L45.805,46.529ZM33.625,30.104L51.953,30.104C51.048,20.459 43.307,12.8 33.625,12.023L33.625,30.104Z"
|
||||
android:fillColor="#666EE5"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
24
app/src/main/res/drawable/megaphone_backup_media_size.xml
Normal file
24
app/src/main/res/drawable/megaphone_backup_media_size.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
~ Copyright 2026 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M13.632,15.546L38.203,10.934A4,4 64.903,0 1,42.872 14.128L46.248,32.113A4,4 107.165,0 1,43.055 36.782L18.484,41.394A4,4 50.655,0 1,13.814 38.201L10.438,20.216A4,4 0,0 1,13.632 15.546z"
|
||||
android:fillColor="#E7EBFF"/>
|
||||
<path
|
||||
android:pathData="M24,23L49,23A4,4 0,0 1,53 27L53,47A4,4 0,0 1,49 51L24,51A4,4 0,0 1,20 47L20,27A4,4 0,0 1,24 23z"
|
||||
android:fillColor="#D2D8FE"/>
|
||||
<path
|
||||
android:pathData="M29.833,28.479C27.889,28.479 26.313,30.056 26.313,32C26.313,33.944 27.889,35.521 29.833,35.521C31.778,35.521 33.354,33.944 33.354,32C33.354,30.056 31.778,28.479 29.833,28.479Z"
|
||||
android:fillColor="#3B45FD"/>
|
||||
<path
|
||||
android:pathData="M46.353,19.73L46.614,21.713C47.399,21.72 48.088,21.738 48.69,21.788C49.694,21.87 50.6,22.045 51.447,22.476C52.772,23.151 53.849,24.228 54.524,25.553C54.956,26.4 55.131,27.306 55.213,28.31C55.292,29.28 55.292,30.473 55.292,31.932V41.818C55.292,43.277 55.292,44.47 55.213,45.44C55.131,46.444 54.956,47.35 54.524,48.197C53.849,49.522 52.772,50.599 51.447,51.274C50.6,51.706 49.694,51.881 48.69,51.963C47.721,52.042 46.527,52.042 45.068,52.042H27.599C26.14,52.042 24.947,52.042 23.977,51.963C22.973,51.881 22.067,51.706 21.22,51.274C19.895,50.599 18.818,49.522 18.143,48.197C17.711,47.35 17.536,46.444 17.454,45.44C17.39,44.653 17.378,43.72 17.376,42.616C16.525,42.632 15.731,42.554 14.956,42.28C13.554,41.783 12.346,40.856 11.503,39.63C10.965,38.847 10.674,37.971 10.461,36.987C10.256,36.036 10.1,34.853 9.91,33.406L8.761,24.679C8.571,23.232 8.415,22.049 8.367,21.077C8.317,20.072 8.372,19.15 8.689,18.254C9.186,16.853 10.113,15.644 11.339,14.802C12.122,14.264 12.998,13.972 13.982,13.759C14.933,13.554 16.116,13.399 17.563,13.208L34.883,10.928C36.329,10.738 37.512,10.582 38.484,10.534C39.49,10.484 40.411,10.539 41.307,10.856C42.709,11.353 43.917,12.28 44.76,13.506C45.298,14.289 45.59,15.165 45.802,16.149C46.007,17.1 46.163,18.283 46.353,19.73ZM17.375,39.365C16.722,39.382 16.336,39.32 16.041,39.216C15.286,38.949 14.635,38.449 14.182,37.789C13.983,37.501 13.808,37.089 13.638,36.302C13.464,35.496 13.325,34.445 13.123,32.915L11.992,24.322C11.791,22.792 11.653,21.74 11.613,20.917C11.573,20.113 11.636,19.669 11.753,19.339C12.02,18.584 12.52,17.934 13.179,17.48C13.468,17.282 13.88,17.106 14.667,16.936C15.473,16.763 16.524,16.623 18.054,16.421L35.239,14.159C36.77,13.958 37.821,13.82 38.644,13.78C39.449,13.74 39.892,13.803 40.222,13.92C40.977,14.187 41.628,14.686 42.081,15.346C42.28,15.635 42.455,16.047 42.625,16.834C42.799,17.64 42.938,18.691 43.14,20.221L43.336,21.708L27.599,21.708C26.14,21.708 24.947,21.708 23.977,21.788C22.973,21.87 22.067,22.045 21.22,22.476C19.895,23.151 18.818,24.228 18.143,25.553C17.711,26.4 17.536,27.306 17.454,28.31C17.375,29.28 17.375,30.473 17.375,31.932L17.375,39.365ZM51.629,27.029C51.788,27.341 51.908,27.772 51.973,28.575C52.041,29.396 52.042,30.456 52.042,32V41.205L45.099,35.254C43.172,33.602 40.328,33.602 38.401,35.254L31.323,41.321L29.58,39.826C27.856,38.348 25.311,38.348 23.587,39.826L20.625,42.365C20.625,42.169 20.625,41.964 20.625,41.75L20.625,32C20.625,30.456 20.626,29.396 20.694,28.575C20.759,27.772 20.879,27.341 21.038,27.029C21.402,26.315 21.982,25.735 22.695,25.372C23.008,25.213 23.439,25.092 24.242,25.027C25.063,24.96 26.123,24.958 27.667,24.958L45,24.958C46.544,24.958 47.604,24.96 48.425,25.027C49.228,25.092 49.66,25.213 49.972,25.372C50.685,25.735 51.265,26.315 51.629,27.029ZM20.768,45.81C20.836,46.229 20.928,46.504 21.038,46.721C21.402,47.435 21.982,48.015 22.695,48.378C23.008,48.537 23.439,48.658 24.242,48.723C25.063,48.79 26.123,48.792 27.667,48.792H45C46.544,48.792 47.604,48.79 48.425,48.723C49.228,48.658 49.66,48.537 49.972,48.378C50.685,48.015 51.265,47.435 51.629,46.721C51.788,46.409 51.908,45.978 51.973,45.175C51.985,45.037 51.994,44.891 52.002,44.738L43.336,37.31C42.424,36.528 41.077,36.528 40.164,37.31L33.404,43.104L33.965,43.585C34.533,44.072 34.598,44.927 34.112,45.494C33.625,46.062 32.77,46.128 32.202,45.641L27.817,41.883C27.107,41.274 26.06,41.274 25.35,41.883L20.768,45.81Z"
|
||||
android:fillColor="#3B45FD"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
20
app/src/main/res/drawable/megaphone_backup_message_count.xml
Normal file
20
app/src/main/res/drawable/megaphone_backup_message_count.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M32.588,13.118L24.159,9.505L18.74,10.107L11.516,15.526L8.505,22.751L12.118,32.986L13.322,42.017L20.546,38.405H25.965L34.394,35.394L38.609,28.17L36.802,20.343L32.588,13.118Z"
|
||||
android:strokeWidth="1.20414"
|
||||
android:fillColor="#D2D8FE"
|
||||
android:strokeColor="#3B45FD"/>
|
||||
<path
|
||||
android:pathData="M23.557,8.607C32.365,8.608 39.507,15.748 39.507,24.557C39.507,33.365 32.365,40.507 23.557,40.507C22.444,40.507 21.358,40.39 20.309,40.173L14.977,43.881C13.098,45.186 10.548,43.714 10.738,41.435L11.298,34.758C8.995,31.993 7.607,28.435 7.607,24.557C7.608,15.748 14.748,8.608 23.557,8.607ZM23.557,11.607C16.405,11.608 10.608,17.405 10.607,24.557C10.607,27.734 11.75,30.641 13.648,32.895C14.1,33.431 14.36,34.145 14.297,34.901L13.78,41.058L18.69,37.646L18.813,37.564C19.356,37.23 19.979,37.109 20.573,37.177L20.845,37.222L21.174,37.287C21.946,37.431 22.742,37.507 23.557,37.507C30.708,37.507 36.507,31.708 36.507,24.557C36.507,17.405 30.708,11.608 23.557,11.607Z"
|
||||
android:fillColor="#3B45FD"/>
|
||||
<path
|
||||
android:pathData="M48.242,23.353L42.221,22.751H36.2L32.588,25.761L28.976,31.18V38.404L32.588,46.833L42.823,51.048L48.844,52.252L53.66,54.058L54.262,51.048L55.466,44.425L56.671,35.394L54.864,28.169L48.242,23.353Z"
|
||||
android:fillColor="#D2D8FE"/>
|
||||
<path
|
||||
android:pathData="M42.824,22.149V20.649H42.824L42.824,22.149ZM28.373,36.598H26.873V36.598L28.373,36.598ZM42.824,51.048L42.824,52.548H42.824V51.048ZM57.273,36.598L58.773,36.598V36.598H57.273ZM53.903,45.873L52.754,44.909L53.903,45.873ZM52.303,54.846L53.159,53.614L52.303,54.846ZM45.72,50.756L45.421,49.286L45.72,50.756ZM46.694,50.947L45.838,52.179L46.694,50.947ZM42.824,22.149V23.649C35.672,23.649 29.874,29.446 29.873,36.598L28.373,36.598L26.873,36.598C26.874,27.789 34.015,20.649 42.824,20.649V22.149ZM28.373,36.598H29.873C29.873,43.75 35.672,49.548 42.824,49.548V51.048V52.548C34.015,52.548 26.873,45.407 26.873,36.598H28.373ZM42.824,51.048L42.823,49.548C43.714,49.548 44.582,49.458 45.421,49.286L45.72,50.756L46.02,52.226C44.987,52.437 43.918,52.548 42.824,52.548L42.824,51.048ZM46.694,50.947L47.55,49.716L53.159,53.614L52.303,54.846L51.446,56.077L45.838,52.179L46.694,50.947ZM54.19,53.756L52.695,53.882L52.111,46.911L53.605,46.785L55.1,46.66L55.685,53.631L54.19,53.756ZM53.903,45.873L52.754,44.909C54.639,42.66 55.773,39.763 55.773,36.598H57.273H58.773C58.773,40.494 57.374,44.066 55.053,46.836L53.903,45.873ZM57.273,36.598L55.773,36.598C55.772,29.447 49.975,23.649 42.823,23.649L42.824,22.149L42.824,20.649C51.632,20.649 58.772,27.79 58.773,36.598L57.273,36.598ZM53.605,46.785L52.111,46.911C52.047,46.157 52.305,45.445 52.754,44.909L53.903,45.873L55.053,46.836C55.075,46.81 55.108,46.751 55.1,46.66L53.605,46.785ZM52.303,54.846L53.159,53.614C52.953,53.471 52.674,53.632 52.695,53.882L54.19,53.756L55.685,53.631C55.876,55.911 53.325,57.383 51.446,56.077L52.303,54.846ZM45.72,50.756L45.421,49.286C46.13,49.142 46.907,49.268 47.55,49.716L46.694,50.947L45.838,52.179C45.916,52.233 45.986,52.233 46.02,52.226L45.72,50.756Z"
|
||||
android:fillColor="#3B45FD"/>
|
||||
</vector>
|
||||
26
app/src/main/res/drawable/megaphone_backup_storage_low.xml
Normal file
26
app/src/main/res/drawable/megaphone_backup_storage_low.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
~ Copyright 2026 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M32.5,32L32,10.5L38,11L47,16L51.5,22L53.5,32H32.5Z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillColor="#3B45FD"
|
||||
android:fillAlpha="0.5"/>
|
||||
<path
|
||||
android:pathData="M45.5,48.5L32.5,32H54L51,42.5L45.5,48.5Z"
|
||||
android:fillColor="#EAEDFF"/>
|
||||
<path
|
||||
android:pathData="M22.5,13.5L31,10H32.5V31L45,48L43.5,50.5L34.5,53.5L23.5,52.5L14.5,46L11.5,35.5V25.5L16.5,16.5L22.5,13.5Z"
|
||||
android:fillColor="#D2D8FE"/>
|
||||
<path
|
||||
android:pathData="M32,8.708C19.136,8.708 8.708,19.136 8.708,32C8.708,44.864 19.136,55.292 32,55.292C44.864,55.292 55.292,44.864 55.292,32C55.292,19.136 44.864,8.708 32,8.708ZM30.375,12.023C20.066,12.851 11.958,21.478 11.958,32C11.958,43.069 20.931,52.042 32,52.042C36.181,52.042 40.064,50.761 43.276,48.571L31.695,34.211C30.841,33.151 30.375,31.831 30.375,30.47L30.375,12.023ZM45.805,46.529C49.341,43.168 51.651,38.53 51.997,33.354H40.667L34.889,32.993L45.805,46.529ZM33.625,30.104L51.953,30.104C51.048,20.459 43.307,12.8 33.625,12.023L33.625,30.104Z"
|
||||
android:fillColor="#3B45FD"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
@@ -7915,6 +7915,67 @@
|
||||
<!-- Text for a "toast" that pops up at the bottom of the user\'s screen after they respond "not now" to a prompt asking them to enable backups -->
|
||||
<string name="TurnOnSignalBackups__toast_not_now">You can enable backups in \"Settings\"</string>
|
||||
|
||||
<!-- Title of Megaphone B: upsell to enable backups when user has many messages -->
|
||||
<string name="BackupMessagesUpsell__title">Never lose a message</string>
|
||||
<!-- Body of Megaphone B: upsell to enable backups when user has many messages -->
|
||||
<string name="BackupMessagesUpsell__body">Turn on Signal Secure Backups to preserve your messages and media.</string>
|
||||
<!-- Primary button of Megaphone B -->
|
||||
<string name="BackupMessagesUpsell__turn_on">Turn on</string>
|
||||
<!-- Secondary button of Megaphone B -->
|
||||
<string name="BackupMessagesUpsell__not_now">Not now</string>
|
||||
|
||||
<!-- Title of Megaphone C: upsell to upgrade backups when user has lots of media -->
|
||||
<string name="BackupMediaUpsell__title">Back up all your media</string>
|
||||
<!-- Body of Megaphone C: upsell to upgrade backups when user has lots of media -->
|
||||
<string name="BackupMediaUpsell__body">Paid Signal Secure Backup plans preserve all your media, up to 100GB.</string>
|
||||
<!-- Primary button of Megaphone C -->
|
||||
<string name="BackupMediaUpsell__upgrade">Upgrade</string>
|
||||
<!-- Secondary button of Megaphone C -->
|
||||
<string name="BackupMediaUpsell__not_now">Not now</string>
|
||||
|
||||
<!-- Title of Megaphone D: upsell backups when device storage is low -->
|
||||
<string name="BackupStorageUpsell__title">Save space with paid backups</string>
|
||||
<!-- Body of Megaphone D: upsell backups when device storage is low -->
|
||||
<string name="BackupStorageUpsell__body">Paid secure backup plans can save storage space by offloading media.</string>
|
||||
<!-- Primary button of Megaphone D when user has free tier backups -->
|
||||
<string name="BackupStorageUpsell__upgrade">Upgrade</string>
|
||||
<!-- Primary button of Megaphone D when user has no backups -->
|
||||
<string name="BackupStorageUpsell__turn_on">Turn on</string>
|
||||
<!-- Secondary button of Megaphone D -->
|
||||
<string name="BackupStorageUpsell__not_now">Not now</string>
|
||||
|
||||
<!-- Title of the backup upsell bottom sheet promoting paid backup plans -->
|
||||
<string name="BackupUpsellBottomSheet__title">Upgrade to back up all media</string>
|
||||
<!-- Body of the backup upsell bottom sheet -->
|
||||
<string name="BackupUpsellBottomSheet__body">Paid Signal Secure Backup plans save all media you send and receive, up to a maximum of 100GB. Never lose an image or video when you get a new phone or reinstall Signal.</string>
|
||||
<!-- Label for the paid plan price shown in the feature card, e.g. "$1.99/month" -->
|
||||
<string name="BackupUpsellBottomSheet__price_per_month">%s/month</string>
|
||||
<!-- Subtitle for the paid plan feature card -->
|
||||
<string name="BackupUpsellBottomSheet__text_and_all_media">Text + all your media</string>
|
||||
<!-- Feature bullet: full text and media backup -->
|
||||
<string name="BackupUpsellBottomSheet__full_text_media_backup">Full text + media backup</string>
|
||||
<!-- Feature bullet: 100GB storage -->
|
||||
<string name="BackupUpsellBottomSheet__storage_100gb">100GB storage</string>
|
||||
<!-- Feature bullet: save on-device storage -->
|
||||
<string name="BackupUpsellBottomSheet__save_on_device_storage">Save on-device storage</string>
|
||||
<!-- Feature bullet: thanks for supporting Signal -->
|
||||
<string name="BackupUpsellBottomSheet__thanks_for_supporting">Thanks for supporting Signal</string>
|
||||
<!-- Primary button for the upsell bottom sheet. The %s is replaced by the subscription price, e.g. "$1.99/month" -->
|
||||
<string name="BackupUpsellBottomSheet__subscribe_for">Subscribe for %s/month</string>
|
||||
<!-- Secondary/dismiss button for the upsell bottom sheet -->
|
||||
<string name="BackupUpsellBottomSheet__no_thanks">No thanks</string>
|
||||
|
||||
<!-- Title of the backup setup complete bottom sheet -->
|
||||
<string name="BackupSetupCompleteBottomSheet__title">You\'re all set. Start your backup now.</string>
|
||||
<!-- Body of the backup setup complete bottom sheet -->
|
||||
<string name="BackupSetupCompleteBottomSheet__body">This could take a while. You can use Signal normally while backing up.</string>
|
||||
<!-- Label for the optimize storage toggle -->
|
||||
<string name="BackupSetupCompleteBottomSheet__optimize_storage">Optimize Signal storage</string>
|
||||
<!-- Subtitle for the optimize storage toggle -->
|
||||
<string name="BackupSetupCompleteBottomSheet__optimize_subtitle">Older media will be offloaded, but can be downloaded from your backup anytime.</string>
|
||||
<!-- Primary button on the setup complete bottom sheet -->
|
||||
<string name="BackupSetupCompleteBottomSheet__back_up_now">Back up now</string>
|
||||
|
||||
<!-- Title of a megaphone shown to prompt the user to upgrade to the new local backups type -->
|
||||
<string name="UseNewOnDeviceBackups__title">Use new on-device backups</string>
|
||||
<!-- Body of a megaphone shown to prompt the user to upgrade to the new local backups type -->
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.thoughtcrime.securesms.megaphone
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones.Event
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
class BackupUpsellScheduleTest {
|
||||
|
||||
private val now = System.currentTimeMillis()
|
||||
|
||||
// -- First display (seenCount = 0) --
|
||||
|
||||
@Test
|
||||
fun `shows on first display when no other backup upsell seen`() {
|
||||
val schedule = schedule(emptyRecords())
|
||||
assertTrue(schedule.shouldDisplay(0, 0, 0, now))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shows on first display when other upsell was seen outside the threshold`() {
|
||||
val records = emptyRecords().toMutableMap().apply {
|
||||
put(Event.BACKUPS_GENERIC_UPSELL, record(Event.BACKUPS_GENERIC_UPSELL, lastSeen = now - 60.days.inWholeMilliseconds - 1))
|
||||
}
|
||||
val schedule = schedule(records)
|
||||
assertTrue(schedule.shouldDisplay(0, 0, 0, now))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suppressed on first display when another upsell was seen within the threshold`() {
|
||||
val records = emptyRecords().toMutableMap().apply {
|
||||
put(Event.BACKUPS_GENERIC_UPSELL, record(Event.BACKUPS_GENERIC_UPSELL, lastSeen = now - 60.days.inWholeMilliseconds + 1.hours.inWholeMilliseconds))
|
||||
}
|
||||
val schedule = schedule(records)
|
||||
assertFalse(schedule.shouldDisplay(0, 0, 0, now))
|
||||
}
|
||||
|
||||
// -- Recurring gap logic (seenCount > 0) --
|
||||
|
||||
@Test
|
||||
fun `shows after gap elapsed since last seen`() {
|
||||
val gapMs = 60.days.inWholeMilliseconds
|
||||
val schedule = schedule(emptyRecords(), gapMs)
|
||||
assertTrue(schedule.shouldDisplay(1, now - gapMs - 1, 0, now))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suppressed when gap has not elapsed since last seen`() {
|
||||
val gapMs = 60.days.inWholeMilliseconds
|
||||
val schedule = schedule(emptyRecords(), gapMs)
|
||||
assertFalse(schedule.shouldDisplay(1, now - gapMs + 1.hours.inWholeMilliseconds, 0, now))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uses second gap for second snooze`() {
|
||||
val firstGap = 60.days.inWholeMilliseconds
|
||||
val secondGap = 120.days.inWholeMilliseconds
|
||||
val schedule = schedule(emptyRecords(), firstGap, secondGap)
|
||||
|
||||
// seenCount=2 -> uses gaps[1] = 120 days
|
||||
assertFalse(schedule.shouldDisplay(2, now - 100.days.inWholeMilliseconds, 0, now))
|
||||
assertTrue(schedule.shouldDisplay(2, now - secondGap - 1, 0, now))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `repeats last gap for high seen counts`() {
|
||||
val firstGap = 60.days.inWholeMilliseconds
|
||||
val secondGap = 120.days.inWholeMilliseconds
|
||||
val schedule = schedule(emptyRecords(), firstGap, secondGap)
|
||||
|
||||
// seenCount=5 -> clamps to gaps[1] = 120 days
|
||||
assertFalse(schedule.shouldDisplay(5, now - 100.days.inWholeMilliseconds, 0, now))
|
||||
assertTrue(schedule.shouldDisplay(5, now - secondGap - 1, 0, now))
|
||||
}
|
||||
|
||||
// -- Combined: cross-event snooze AND recurring gap --
|
||||
|
||||
@Test
|
||||
fun `cross-event snooze blocks even when recurring gap is satisfied`() {
|
||||
val gapMs = 60.days.inWholeMilliseconds
|
||||
val records = emptyRecords().toMutableMap().apply {
|
||||
put(Event.BACKUP_LOW_STORAGE_UPSELL, record(Event.BACKUP_LOW_STORAGE_UPSELL, lastSeen = now - 30.days.inWholeMilliseconds))
|
||||
}
|
||||
val schedule = schedule(records, gapMs)
|
||||
|
||||
// Own gap satisfied (last seen 90 days ago) but another upsell was seen 30 days ago
|
||||
assertFalse(schedule.shouldDisplay(1, now - 90.days.inWholeMilliseconds, 0, now))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shows when both cross-event snooze and recurring gap are satisfied`() {
|
||||
val gapMs = 60.days.inWholeMilliseconds
|
||||
val records = emptyRecords().toMutableMap().apply {
|
||||
put(Event.BACKUP_LOW_STORAGE_UPSELL, record(Event.BACKUP_LOW_STORAGE_UPSELL, lastSeen = now - 60.days.inWholeMilliseconds - 1))
|
||||
}
|
||||
val schedule = schedule(records, gapMs)
|
||||
assertTrue(schedule.shouldDisplay(1, now - gapMs - 1, 0, now))
|
||||
}
|
||||
|
||||
// -- Ignores non-backup events --
|
||||
|
||||
@Test
|
||||
fun `ignores non-backup upsell events in cross-event check`() {
|
||||
val records = emptyRecords().toMutableMap().apply {
|
||||
put(Event.PIN_REMINDER, record(Event.PIN_REMINDER, lastSeen = now))
|
||||
put(Event.NOTIFICATIONS, record(Event.NOTIFICATIONS, lastSeen = now))
|
||||
}
|
||||
val schedule = schedule(records)
|
||||
assertTrue(schedule.shouldDisplay(0, 0, 0, now))
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
private fun schedule(records: Map<Event, MegaphoneRecord>, vararg gaps: Long): BackupUpsellSchedule {
|
||||
return BackupUpsellSchedule(records, *gaps)
|
||||
}
|
||||
|
||||
private fun record(event: Event, seenCount: Int = 1, lastSeen: Long = 0, firstVisible: Long = 0, finished: Boolean = false): MegaphoneRecord {
|
||||
return MegaphoneRecord(event, seenCount, lastSeen, firstVisible, finished)
|
||||
}
|
||||
|
||||
private fun emptyRecords(): Map<Event, MegaphoneRecord> {
|
||||
return BackupUpsellSchedule.BACKUP_UPSELL_EVENTS.associateWith { record(it, seenCount = 0) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user