From afedbf40e3c34d96090cffa2b9ed2fa2fd85bc0c Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 30 Sep 2022 16:41:00 -0400 Subject: [PATCH] Prepare the websocket keepalive for API 31. --- .../app/internal/InternalSettingsFragment.kt | 20 +++++++ .../app/internal/InternalSettingsState.kt | 1 + .../app/internal/InternalSettingsViewModel.kt | 6 ++ .../ApplicationDependencyProvider.java | 2 +- .../securesms/jobs/RefreshAttributesJob.java | 2 +- .../securesms/keyvalue/InternalValues.java | 12 ++++ .../messages/IncomingMessageObserver.java | 19 ++++--- .../securesms/util/AlarmSleepTimer.java | 55 ++++++++++++------- 8 files changed, 85 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index 738fd9c1de..af5a454a5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate import java.util.Optional import java.util.concurrent.TimeUnit import kotlin.math.max +import kotlin.time.Duration.Companion.seconds class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) { @@ -201,6 +202,25 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter sectionHeaderPref(R.string.preferences__internal_network) + switchPref( + title = DSLSettingsText.from("Force websocket mode"), + summary = DSLSettingsText.from("Pretend you have no Play Services. Ignores websocket messages and keeps the websocket open in a foreground service. You have to manually force-stop the app for changes to take effect."), + isChecked = state.forceWebsocketMode, + onClick = { + viewModel.setForceWebsocketMode(!state.forceWebsocketMode) + SimpleTask.run({ + val jobState = ApplicationDependencies.getJobManager().runSynchronously(RefreshAttributesJob(), 10.seconds.inWholeMilliseconds) + return@run jobState.isPresent && jobState.get().isComplete + }, { success -> + if (success) { + Toast.makeText(context, "Successfully refreshed attributes. Force-stop the app for changes to take effect.", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "Failed to refresh attributes.", Toast.LENGTH_SHORT).show() + } + }) + } + ) + switchPref( title = DSLSettingsText.from(R.string.preferences__internal_allow_censorship_toggle), summary = DSLSettingsText.from(R.string.preferences__internal_allow_censorship_toggle_description), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt index 0013621335..8def66aa89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt @@ -10,6 +10,7 @@ data class InternalSettingsState( val gv2ignoreServerChanges: Boolean, val gv2ignoreP2PChanges: Boolean, val allowCensorshipSetting: Boolean, + val forceWebsocketMode: Boolean, val callingServer: String, val callingAudioProcessingMethod: CallManager.AudioProcessingMethod, val callingBandwidthMode: CallManager.BandwidthMode, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt index a230cc732a..79ca353d33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt @@ -59,6 +59,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito refresh() } + fun setForceWebsocketMode(enabled: Boolean) { + preferenceDataStore.putBoolean(InternalValues.FORCE_WEBSOCKET_MODE, enabled) + refresh() + } + fun setUseBuiltInEmoji(enabled: Boolean) { preferenceDataStore.putBoolean(InternalValues.FORCE_BUILT_IN_EMOJI, enabled) refresh() @@ -109,6 +114,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito gv2ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(), gv2ignoreP2PChanges = SignalStore.internalValues().gv2IgnoreP2PChanges(), allowCensorshipSetting = SignalStore.internalValues().allowChangingCensorshipSetting(), + forceWebsocketMode = SignalStore.internalValues().isWebsocketModeForced, callingServer = SignalStore.internalValues().groupCallingServer(), callingAudioProcessingMethod = SignalStore.internalValues().callingAudioProcessingMethod(), callingBandwidthMode = SignalStore.internalValues().callingBandwidthMode(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 003a71266d..24511d0149 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -283,7 +283,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr @Override public @NonNull SignalWebSocket provideSignalWebSocket(@NonNull Supplier signalServiceConfigurationSupplier) { - SleepTimer sleepTimer = SignalStore.account().isFcmEnabled() ? new UptimeSleepTimer() : new AlarmSleepTimer(context); + SleepTimer sleepTimer = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced() ? new AlarmSleepTimer(context) : new UptimeSleepTimer() ; SignalWebSocketHealthMonitor healthMonitor = new SignalWebSocketHealthMonitor(context, sleepTimer); SignalWebSocket signalWebSocket = new SignalWebSocket(provideWebSocketFactory(signalServiceConfigurationSupplier, healthMonitor)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 58e8a7a7ad..524301f081 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -84,7 +84,7 @@ public class RefreshAttributesJob extends BaseJob { } int registrationId = SignalStore.account().getRegistrationId(); - boolean fetchesMessages = !SignalStore.account().isFcmEnabled(); + boolean fetchesMessages = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced(); byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context); String registrationLockV1 = null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java index 30cf73acdb..e27614a07a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java @@ -26,6 +26,7 @@ public final class InternalValues extends SignalStoreValues { public static final String CALLING_DISABLE_TELECOM = "internal.calling_disable_telecom"; public static final String SHAKE_TO_REPORT = "internal.shake_to_report"; public static final String DISABLE_STORAGE_SERVICE = "internal.disable_storage_service"; + public static final String FORCE_WEBSOCKET_MODE = "internal.force_websocket_mode"; InternalValues(KeyValueStore store) { super(store); @@ -164,4 +165,15 @@ public final class InternalValues extends SignalStoreValues { return false; } } + + /** + * Whether or not the system is forced to be in 'websocket mode', where FCM is ignored and we use a foreground service to keep the app alive. + */ + public boolean isWebsocketModeForced() { + if (FeatureFlags.internalUser()) { + return getBoolean(FORCE_WEBSOCKET_MODE, false); + } else { + return false; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java index bf9b647422..b0f876549c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java @@ -81,7 +81,7 @@ public class IncomingMessageObserver { new MessageRetrievalThread().start(); - if (!SignalStore.account().isFcmEnabled()) { + if (!SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced()) { ContextCompat.startForegroundService(context, new Intent(context, ForegroundService.class)); } @@ -157,22 +157,23 @@ public class IncomingMessageObserver { } private synchronized boolean isConnectionNecessary() { - boolean registered = SignalStore.account().isRegistered(); - boolean fcmEnabled = SignalStore.account().isFcmEnabled(); - boolean hasNetwork = NetworkConstraint.isMet(context); - boolean hasProxy = SignalStore.proxy().isProxyEnabled(); - long oldRequest = System.currentTimeMillis() - OLD_REQUEST_WINDOW_MS; + boolean registered = SignalStore.account().isRegistered(); + boolean fcmEnabled = SignalStore.account().isFcmEnabled(); + boolean hasNetwork = NetworkConstraint.isMet(context); + boolean hasProxy = SignalStore.proxy().isProxyEnabled(); + boolean forceWebsocket = SignalStore.internalValues().isWebsocketModeForced(); + long oldRequest = System.currentTimeMillis() - OLD_REQUEST_WINDOW_MS; boolean removedRequests = keepAliveTokens.entrySet().removeIf(e -> e.getValue() < oldRequest); if (removedRequests) { Log.d(TAG, "Removed old keep web socket open requests."); } - Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Stay open requests: [%s], Censored: %s, Registered: %s, Proxy: %s", - hasNetwork, appVisible, fcmEnabled, Util.join(keepAliveTokens.entrySet(), ","), networkAccess.isCensored(), registered, hasProxy)); + Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Stay open requests: [%s], Censored: %s, Registered: %s, Proxy: %s, Force websocket: %s", + hasNetwork, appVisible, fcmEnabled, Util.join(keepAliveTokens.entrySet(), ","), networkAccess.isCensored(), registered, hasProxy, forceWebsocket)); return registered && - (appVisible || !fcmEnabled || Util.hasItems(keepAliveTokens)) && + (appVisible || !fcmEnabled || forceWebsocket || Util.hasItems(keepAliveTokens)) && hasNetwork && !networkAccess.isCensored(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AlarmSleepTimer.java b/app/src/main/java/org/thoughtcrime/securesms/util/AlarmSleepTimer.java index 70ea8699bc..a50140358d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AlarmSleepTimer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AlarmSleepTimer.java @@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.util; import android.app.AlarmManager; import android.app.PendingIntent; +import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; import android.os.SystemClock; import androidx.core.app.AlarmManagerCompat; @@ -18,13 +20,12 @@ import org.whispersystems.signalservice.api.util.SleepTimer; import java.util.concurrent.ConcurrentSkipListSet; /** - * A sleep timer that is based on elapsed realtime, so - * that it works properly, even in low-power sleep modes. - * + * A sleep timer that is based on elapsed realtime, so that it works properly, even in low-power sleep modes. */ public class AlarmSleepTimer implements SleepTimer { private static final String TAG = Log.tag(AlarmSleepTimer.class); - private static ConcurrentSkipListSet actionIdList = new ConcurrentSkipListSet<>(); + + private static final ConcurrentSkipListSet actionIdList = new ConcurrentSkipListSet<>(); private final Context context; @@ -33,23 +34,25 @@ public class AlarmSleepTimer implements SleepTimer { } @Override - public void sleep(long millis) { - final AlarmReceiver alarmReceiver = new AlarmSleepTimer.AlarmReceiver(); - int actionId = 0; + public void sleep(long sleepDuration) { + AlarmReceiver alarmReceiver = new AlarmSleepTimer.AlarmReceiver(); + int actionId = 0; + while (!actionIdList.add(actionId)){ actionId++; } + try { - context.registerReceiver(alarmReceiver, - new IntentFilter(AlarmReceiver.WAKE_UP_THREAD_ACTION + "." + actionId)); + String actionName = buildActionName(actionId); + context.registerReceiver(alarmReceiver, new IntentFilter(actionName)); - final long startTime = System.currentTimeMillis(); - alarmReceiver.setAlarm(millis, AlarmReceiver.WAKE_UP_THREAD_ACTION + "." + actionId); + long startTime = System.currentTimeMillis(); + alarmReceiver.setAlarm(sleepDuration, actionName); - while (System.currentTimeMillis() - startTime < millis) { + while (System.currentTimeMillis() - startTime < sleepDuration) { try { synchronized (this) { - wait(millis - System.currentTimeMillis() + startTime); + wait(sleepDuration - (System.currentTimeMillis() - startTime)); } } catch (InterruptedException e) { Log.w(TAG, e); @@ -58,25 +61,35 @@ public class AlarmSleepTimer implements SleepTimer { context.unregisterReceiver(alarmReceiver); } catch(Exception e) { Log.w(TAG, "Exception during sleep ...",e); - }finally { + } finally { actionIdList.remove(actionId); } } + private static String buildActionName(int actionId) { + return AlarmReceiver.WAKE_UP_THREAD_ACTION + "." + actionId; + } + private class AlarmReceiver extends BroadcastReceiver { private static final String WAKE_UP_THREAD_ACTION = "org.thoughtcrime.securesms.util.AlarmSleepTimer.AlarmReceiver.WAKE_UP_THREAD"; private void setAlarm(long millis, String action) { final Intent intent = new Intent(action); final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntentFlags.mutable()); - final AlarmManager alarmManager = ContextCompat.getSystemService(context, AlarmManager.class); + final AlarmManager alarmManager = ServiceUtil.getAlarmManager(context); - Log.w(TAG, "Setting alarm to wake up in " + millis + "ms."); - - AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, - AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + millis, - pendingIntent); + if (Build.VERSION.SDK_INT < 31 || alarmManager.canScheduleExactAlarms()) { + Log.d(TAG, "Setting an exact alarm to wake up in " + millis + "ms."); + AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + millis, + pendingIntent); + } else { + Log.w(TAG, "Setting an inexact alarm to wake up in " + millis + "ms. CanScheduleAlarms: " + alarmManager.canScheduleExactAlarms()); + alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + millis, + pendingIntent); + } } @Override