Wire in inactive-primary websocket alert.

This commit is contained in:
Alex Hart
2026-05-07 11:27:33 -03:00
committed by Michelle Tang
parent dca4351b8b
commit f1b231ca38
9 changed files with 121 additions and 8 deletions
@@ -84,6 +84,7 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
private const val KEY_ACCOUNT_REGISTERED_AT = "account.registered_at"
private const val KEY_HAS_LINKED_DEVICES = "account.has_linked_devices"
private const val KEY_HAS_INACTIVE_PRIMARY_DEVICE_ALERT = "account.has_inactive_primary_device_alert"
private const val KEY_ACCOUNT_ENTROPY_POOL = "account.account_entropy_pool"
private const val KEY_RESTORED_ACCOUNT_ENTROPY_KEY = "account.restored_account_entropy_pool"
@@ -487,6 +488,9 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
val isLinkedDevice: Boolean
get() = !isPrimaryDevice
@get:JvmName("hasInactivePrimaryDeviceAlert")
var hasInactivePrimaryDeviceAlert: Boolean by booleanValue(KEY_HAS_INACTIVE_PRIMARY_DEVICE_ALERT, false)
/** The local user's full username (nickname.discriminator), if set. */
var username: String?
get() {
@@ -11,7 +11,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.app.NotificationManagerCompat;
import java.util.stream.Collectors;
import com.bumptech.glide.Glide;
import org.signal.core.util.DiskUtil;
@@ -44,6 +43,7 @@ import org.thoughtcrime.securesms.profiles.username.NewWaysToConnectDialogFragme
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.ByteUnit;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Environment;
import org.thoughtcrime.securesms.util.RemoteConfig;
@@ -59,6 +59,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Creating a new megaphone:
@@ -91,11 +92,11 @@ public final class Megaphones {
List<Megaphone> megaphones = buildDisplayOrder(context, records).entrySet().stream()
.filter(e -> {
MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey()));
MegaphoneSchedule schedule = e.getValue();
MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey()));
MegaphoneSchedule schedule = e.getValue();
return !record.isFinished() && schedule.shouldDisplay(record.getSeenCount(), record.getLastSeen(), record.getFirstVisible(), currentTime);
})
return !record.isFinished() && schedule.shouldDisplay(record.getSeenCount(), record.getLastSeen(), record.getFirstVisible(), currentTime);
})
.map(Map.Entry::getKey)
.map(records::get)
.map(record -> Megaphones.forRecord(context, record))
@@ -118,14 +119,14 @@ public final class Megaphones {
return new LinkedHashMap<>() {{
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
put(Event.NEW_LINKED_DEVICE, shouldShowNewLinkedDeviceMegaphone() ? ALWAYS: NEVER);
put(Event.NEW_LINKED_DEVICE, shouldShowNewLinkedDeviceMegaphone() ? ALWAYS : NEVER);
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER);
put(Event.GRANT_FULL_SCREEN_INTENT, shouldShowGrantFullScreenIntentPermission(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER);
put(Event.BACKUP_SCHEDULE_PERMISSION, shouldShowBackupSchedulePermissionMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER);
put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : NEVER);
put(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION, shouldShowTurnOffCircumventionMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(7)) : NEVER);
put(Event.REMOTE_MEGAPHONE, shouldShowRemoteMegaphone(records) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(1)) : NEVER);
put(Event.LINKED_DEVICE_INACTIVE, shouldShowLinkedDeviceInactiveMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)): NEVER);
put(Event.LINKED_DEVICE_INACTIVE, shouldShowLinkedDeviceInactiveMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER);
// Specifically putting backup reminders here, above PIN reminders
put(Event.BACKUP_LOW_STORAGE_UPSELL, shouldShowBackupLowStorageUpsell(context) ? new BackupUpsellSchedule(records, TimeUnit.DAYS.toMillis(60), TimeUnit.DAYS.toMillis(120)) : NEVER);
@@ -142,6 +143,7 @@ public final class Megaphones {
put(Event.SET_UP_YOUR_USERNAME, shouldShowSetUpYourUsernameMegaphone(records) ? ALWAYS : NEVER);
put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfilePhotoMegaphone(context) ? ALWAYS : NEVER);
put(Event.PNP_LAUNCH, shouldShowPnpLaunchMegaphone() ? ALWAYS : NEVER);
put(Event.INACTIVE_PRIMARY, shouldShowInactivePrimaryMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(7)) : NEVER);
}};
}
@@ -203,6 +205,8 @@ public final class Megaphones {
return buildVerifyBackupKeyMegaphone();
case USE_NEW_ON_DEVICE_BACKUPS:
return buildUseNewOnDeviceBackupsMegaphone();
case INACTIVE_PRIMARY:
return buildInactivePrimaryMegaphone();
default:
throw new IllegalArgumentException("Event not handled!");
}
@@ -527,6 +531,23 @@ public final class Megaphones {
.build();
}
private static @NonNull Megaphone buildInactivePrimaryMegaphone() {
return new Megaphone.Builder(Event.INACTIVE_PRIMARY, Megaphone.Style.BASIC)
.setImage(R.drawable.megaphone_inactive_primary)
.setTitle(R.string.InactivePrimary__title)
.setBody(R.string.InactivePrimary__body)
.setActionButton(R.string.InactivePrimary__got_it, (megaphone, controller) -> {
controller.onMegaphoneSnooze(Event.INACTIVE_PRIMARY);
})
.setSecondaryButton(R.string.InactivePrimary__learn_more, ((megaphone, controller) -> {
CommunicationActions.openBrowserLink(
controller.getMegaphoneActivity(),
controller.getMegaphoneActivity().getString(R.string.inactive_primary_support)
);
}))
.build();
}
private static boolean shouldShowOnboardingMegaphone(@NonNull Context context) {
return SignalStore.account().isPrimaryDevice() && SignalStore.onboarding().hasOnboarding(context);
}
@@ -595,6 +616,10 @@ public final class Megaphones {
return SignalStore.account().isPrimaryDevice() && TextUtils.isEmpty(SignalStore.account().getUsername()) && !SignalStore.uiHints().hasCompletedUsernameOnboarding();
}
private static boolean shouldShowInactivePrimaryMegaphone() {
return SignalStore.account().isLinkedDevice() && SignalStore.account().hasInactivePrimaryDeviceAlert();
}
private static boolean shouldShowGenericBackupsMegaphone(@NonNull Context context) {
if (!RemoteConfig.backupsMegaphone()) {
return false;
@@ -756,7 +781,8 @@ public final class Megaphones {
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");
USE_NEW_ON_DEVICE_BACKUPS("use_new_on_device_backups"),
INACTIVE_PRIMARY("inactive_primary");
private final String key;
@@ -39,6 +39,8 @@ class SignalWebSocketHealthMonitor(
private val KEEP_ALIVE_SEND_CADENCE: Duration = OkHttpWebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS.seconds
private val KEEP_ALIVE_SEND_CADENCE_BACKGROUND: Duration = 60.seconds
private const val ALERT_IDLE_PRIMARY_DEVICE = "idle-primary-device"
}
private val executor: Executor = Executors.newSingleThreadExecutor()
@@ -136,6 +138,15 @@ class SignalWebSocketHealthMonitor(
}
}
override fun onReceivedAlerts(alerts: Array<out String>, isIdentifiedWebSocket: Boolean) {
if (!isIdentifiedWebSocket) {
return
}
executor.execute {
SignalStore.account.hasInactivePrimaryDeviceAlert = SignalStore.account.isLinkedDevice && alerts.contains(ALERT_IDLE_PRIMARY_DEVICE)
}
}
private fun onConnectingTimeout() {
executor.execute {
webSocket?.forceNewWebSocket()