mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-19 23:00:22 +01:00
Wire in inactive-primary websocket alert.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
~ Copyright (C) 2026 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M23.5,3.651L40.5,3.651A8.5,8.5 0,0 1,49 12.151L49,53.376A8.5,8.5 0,0 1,40.5 61.876L23.5,61.876A8.5,8.5 0,0 1,15 53.376L15,12.151A8.5,8.5 0,0 1,23.5 3.651z"/>
|
||||
<path
|
||||
android:pathData="M23.5,3.651L40.5,3.651A8.5,8.5 0,0 1,49 12.151L49,52.101A8.5,8.5 0,0 1,40.5 60.601L23.5,60.601A8.5,8.5 0,0 1,15 52.101L15,12.151A8.5,8.5 0,0 1,23.5 3.651z"
|
||||
android:fillColor="#C0CBE2"/>
|
||||
<path
|
||||
android:pathData="M16.275,11.726C16.275,7.971 19.319,4.927 23.075,4.927H39.65C42.467,4.927 44.75,7.21 44.75,10.026V52.951C44.75,55.768 42.467,58.051 39.65,58.051H21.375C18.558,58.051 16.275,55.768 16.275,52.951V11.726Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.4"/>
|
||||
<path
|
||||
android:pathData="M18.4,4.926m-10.2,0a10.2,10.2 0,1 1,20.4 0a10.2,10.2 0,1 1,-20.4 0"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.45"/>
|
||||
<path
|
||||
android:pathData="M28.5,7L35.5,7A1.5,1.5 0,0 1,37 8.5L37,8.5A1.5,1.5 0,0 1,35.5 10L28.5,10A1.5,1.5 0,0 1,27 8.5L27,8.5A1.5,1.5 0,0 1,28.5 7z"
|
||||
android:fillColor="#61739A"/>
|
||||
<path
|
||||
android:pathData="M22,37L31,22H33.5L42,36L42.5,38L40.5,40H23.5L22,39V37Z"
|
||||
android:fillColor="#FDDDD7"/>
|
||||
<path
|
||||
android:pathData="M32,26.375C31.231,26.375 30.625,27.03 30.685,27.797L31.107,33.175C31.144,33.64 31.533,34 32,34C32.467,34 32.856,33.64 32.893,33.175L33.314,27.797C33.375,27.03 32.769,26.375 32,26.375Z"
|
||||
android:fillColor="#CB4116"/>
|
||||
<path
|
||||
android:pathData="M30.625,36.5C30.625,35.741 31.241,35.125 32,35.125C32.759,35.125 33.375,35.741 33.375,36.5C33.375,37.259 32.759,37.875 32,37.875C31.241,37.875 30.625,37.259 30.625,36.5Z"
|
||||
android:fillColor="#CB4116"/>
|
||||
<path
|
||||
android:pathData="M28.979,22.681C30.331,20.371 33.669,20.371 35.021,22.681L42.66,35.732C44.026,38.065 42.343,41 39.64,41H24.36C21.657,41 19.974,38.065 21.34,35.732L28.979,22.681ZM33.295,23.691C32.715,22.701 31.285,22.701 30.705,23.691L23.066,36.742C22.48,37.742 23.202,39 24.36,39H39.64C40.799,39 41.52,37.742 40.934,36.742L33.295,23.691Z"
|
||||
android:fillColor="#CB4116"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M23.5,4.651L40.5,4.651A7.5,7.5 0,0 1,48 12.151L48,53.376A7.5,7.5 0,0 1,40.5 60.876L23.5,60.876A7.5,7.5 0,0 1,16 53.376L16,12.151A7.5,7.5 0,0 1,23.5 4.651z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#61739A"/>
|
||||
</vector>
|
||||
@@ -24,6 +24,7 @@
|
||||
<string name="export_account_data_url" translatable="false">https://support.signal.org/hc/articles/5538911756954</string>
|
||||
<string name="pending_transfer_url" translatable="false">https://support.signal.org/hc/articles/360031949872#pending</string>
|
||||
<string name="donate_faq_url" translatable="false">https://support.signal.org/hc/articles/360031949872#donate</string>
|
||||
<string name="inactive_primary_support" translatable="false">https://support.signal.org/hc/articles/9021007554074</string>
|
||||
|
||||
<!-- First placeholder is productId, second placeholder is app package -->
|
||||
<string name="backup_subscription_management_url">https://play.google.com/store/account/subscriptions?sku=%1$s&package=%2$s</string>
|
||||
@@ -8241,6 +8242,15 @@
|
||||
<!-- Button of a megaphone that will snooze the reminder to upgrade to the new local backups type -->
|
||||
<string name="UseNewOnDeviceBackups__not_now">Not now</string>
|
||||
|
||||
<!-- Title of a megaphone shown to prompt the user to open Signal on their primary device -->
|
||||
<string name="InactivePrimary__title">Open Signal on your phone</string>
|
||||
<!-- Body of a megaphone shown to prompt the user to open Signal on their primary device -->
|
||||
<string name="InactivePrimary__body">To keep your account active, open Signal on your primary device.</string>
|
||||
<!-- Button of a megaphone shown to prompt the user to open Signal on their primary device that will navigate to a support page -->
|
||||
<string name="InactivePrimary__learn_more">Learn more</string>
|
||||
<!-- Button of a megaphone shown to prompt the user to open Signal on their primary device that will snooze the megaphone -->
|
||||
<string name="InactivePrimary__got_it">Got it</string>
|
||||
|
||||
<!-- Text describing how to restore a backup displayed on the on-device backups screen -->
|
||||
<string name="OnDeviceBackupsScreen__to_restore_a_backup">To restore a backup, install a new copy of Signal. Open the app and tap "Restore backup", then locate a backup file.</string>
|
||||
|
||||
|
||||
+1
@@ -550,6 +550,7 @@ class DemoNetworkController(
|
||||
val healthMonitor = object : HealthMonitor {
|
||||
override fun onKeepAliveResponse(sentTimestamp: Long, isIdentifiedWebSocket: Boolean) {}
|
||||
override fun onMessageError(status: Int, isIdentifiedWebSocket: Boolean) {}
|
||||
override fun onReceivedAlerts(alerts: Array<out String>, isIdentifiedWebSocket: Boolean) {}
|
||||
}
|
||||
|
||||
val libSignalConnection = LibSignalChatConnection(
|
||||
|
||||
+2
@@ -7,4 +7,6 @@ interface HealthMonitor {
|
||||
fun onKeepAliveResponse(sentTimestamp: Long, isIdentifiedWebSocket: Boolean)
|
||||
|
||||
fun onMessageError(status: Int, isIdentifiedWebSocket: Boolean)
|
||||
|
||||
fun onReceivedAlerts(alerts: Array<out String>, isIdentifiedWebSocket: Boolean)
|
||||
}
|
||||
|
||||
+1
@@ -674,6 +674,7 @@ class LibSignalChatConnection(
|
||||
if (alerts.isNotEmpty()) {
|
||||
Log.i(TAG, "$name Received ${alerts.size} alerts from the server")
|
||||
}
|
||||
healthMonitor.onReceivedAlerts(alerts, isIdentifiedWebSocket = chat is AuthenticatedChatConnection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ class LibSignalChatConnectionTest {
|
||||
clearAllMocks()
|
||||
every { healthMonitor.onMessageError(any(), any()) }
|
||||
every { healthMonitor.onKeepAliveResponse(any(), any()) }
|
||||
every { healthMonitor.onReceivedAlerts(any(), any()) }
|
||||
|
||||
// NB: We provide default success behavior mocks here to cut down on boilerplate later, but it is
|
||||
// expected that some tests will override some of these to test failures.
|
||||
|
||||
Reference in New Issue
Block a user