From 852dcd97114e565298e70636d4082215a7ebe371 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 4 Mar 2022 13:30:22 -0500 Subject: [PATCH] Show megaphone to improve network reliability. --- .../securesms/ApplicationContext.java | 2 + .../AdvancedPrivacySettingsViewModel.kt | 1 + .../jobs/CheckServiceReachabilityJob.kt | 119 ++++++++++++++++++ .../securesms/jobs/JobManagerFactories.java | 1 + .../keyvalue/MiscellaneousValues.java | 18 +++ .../securesms/megaphone/Megaphones.java | 26 +++- .../megaphone/SignalPinReminderSchedule.java | 2 - .../push/SignalServiceNetworkAccess.kt | 2 +- .../drawable/ic_censorship_megaphone_64.xml | 41 ++++++ app/src/main/res/values/strings.xml | 9 ++ 10 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/CheckServiceReachabilityJob.kt create mode 100644 app/src/main/res/drawable/ic_censorship_megaphone_64.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index d21fc6103d..105ab74e2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider; import org.thoughtcrime.securesms.emoji.EmojiSource; import org.thoughtcrime.securesms.emoji.JumboEmoji; import org.thoughtcrime.securesms.gcm.FcmJobService; +import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob; import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob; @@ -199,6 +200,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr .addPostRender(RetrieveReleaseChannelJob::enqueue) .addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount()) .addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob())) + .addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary) .execute(); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt index 4a01287312..5c982b19b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt @@ -77,6 +77,7 @@ class AdvancedPrivacySettingsViewModel( fun setCensorshipCircumventionEnabled(enabled: Boolean) { SignalStore.settings().setCensorshipCircumventionEnabled(enabled) + SignalStore.misc().isServiceReachableWithoutCircumvention = false ApplicationDependencies.resetNetworkConnectionsAfterProxyChange() refresh() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckServiceReachabilityJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckServiceReachabilityJob.kt new file mode 100644 index 0000000000..1102612628 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckServiceReachabilityJob.kt @@ -0,0 +1,119 @@ +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.whispersystems.libsignal.util.guava.Optional +import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState +import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider +import org.whispersystems.signalservice.internal.websocket.WebSocketConnection +import java.util.concurrent.TimeUnit + +/** + * Checks to see if a censored user can establish a websocket connection with an uncensored network configuration. + */ +class CheckServiceReachabilityJob private constructor(params: Parameters) : BaseJob(params) { + + constructor() : this( + Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.HOURS.toMillis(12)) + .setMaxAttempts(1) + .build() + ) + + companion object { + private val TAG = Log.tag(CheckServiceReachabilityJob::class.java) + + const val KEY = "CheckServiceReachabilityJob" + + @JvmStatic + fun enqueueIfNecessary() { + val isCensored = ApplicationDependencies.getSignalServiceNetworkAccess().isCensored() + val timeSinceLastCheck = System.currentTimeMillis() - SignalStore.misc().lastCensorshipServiceReachabilityCheckTime + if (SignalStore.account().isRegistered && isCensored && timeSinceLastCheck > TimeUnit.DAYS.toMillis(1)) { + ApplicationDependencies.getJobManager().add(CheckServiceReachabilityJob()) + } + } + } + + override fun serialize(): Data { + return Data.EMPTY + } + + override fun getFactoryKey(): String { + return KEY + } + + override fun onRun() { + if (!SignalStore.account().isRegistered) { + Log.w(TAG, "Not registered, skipping.") + SignalStore.misc().lastCensorshipServiceReachabilityCheckTime = System.currentTimeMillis() + return + } + + if (!ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) { + Log.w(TAG, "Not currently censored, skipping.") + SignalStore.misc().lastCensorshipServiceReachabilityCheckTime = System.currentTimeMillis() + return + } + + SignalStore.misc().lastCensorshipServiceReachabilityCheckTime = System.currentTimeMillis() + + val uncensoredWebsocket = WebSocketConnection( + "uncensored-test", + ApplicationDependencies.getSignalServiceNetworkAccess().uncensoredConfiguration, + Optional.of( + StaticCredentialsProvider( + SignalStore.account().aci, + SignalStore.account().pni, + SignalStore.account().e164, + SignalStore.account().deviceId, + SignalStore.account().servicePassword + ) + ), + BuildConfig.SIGNAL_AGENT, + null, + "" + ) + + try { + val startTime = System.currentTimeMillis() + + val state: WebSocketConnectionState = uncensoredWebsocket.connect() + .filter { it == WebSocketConnectionState.CONNECTED || it == WebSocketConnectionState.FAILED } + .timeout(30, TimeUnit.SECONDS) + .blockingFirst(WebSocketConnectionState.FAILED) + + if (state == WebSocketConnectionState.CONNECTED) { + Log.i(TAG, "Established connection in ${System.currentTimeMillis() - startTime} ms! Service is reachable!") + SignalStore.misc().isServiceReachableWithoutCircumvention = true + } else { + Log.w(TAG, "Failed to establish a connection in ${System.currentTimeMillis() - startTime} ms.") + SignalStore.misc().isServiceReachableWithoutCircumvention = false + } + } catch (exception: Exception) { + Log.w(TAG, "Failed to connect to the websocket.", exception) + SignalStore.misc().isServiceReachableWithoutCircumvention = false + } finally { + uncensoredWebsocket.disconnect() + } + } + + override fun onShouldRetry(e: Exception): Boolean { + return false + } + + override fun onFailure() { + } + + class Factory : Job.Factory { + override fun create(parameters: Parameters, data: Data): CheckServiceReachabilityJob { + return CheckServiceReachabilityJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index d5207f986f..316d34f317 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -82,6 +82,7 @@ public final class JobManagerFactories { put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory()); put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory()); put(BoostReceiptRequestResponseJob.KEY, new BoostReceiptRequestResponseJob.Factory()); + put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory()); put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); put(ClearFallbackKbsEnclaveJob.KEY, new ClearFallbackKbsEnclaveJob.Factory()); put(ConversationShortcutUpdateJob.KEY, new ConversationShortcutUpdateJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index c659b51b97..f3ad137c73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -16,6 +16,8 @@ public final class MiscellaneousValues extends SignalStoreValues { private static final String OLD_DEVICE_TRANSFER_LOCKED = "misc.old_device.transfer.locked"; private static final String HAS_EVER_HAD_AN_AVATAR = "misc.has.ever.had.an.avatar"; private static final String CHANGE_NUMBER_LOCK = "misc.change_number.lock"; + private static final String CENSORSHIP_LAST_CHECK_TIME = "misc.censorship.last_check_time"; + private static final String CENSORSHIP_SERVICE_REACHABLE = "misc.censorship.service_reachable"; MiscellaneousValues(@NonNull KeyValueStore store) { super(store); @@ -110,4 +112,20 @@ public final class MiscellaneousValues extends SignalStoreValues { public void unlockChangeNumber() { putBoolean(CHANGE_NUMBER_LOCK, false); } + + public long getLastCensorshipServiceReachabilityCheckTime() { + return getLong(CENSORSHIP_LAST_CHECK_TIME, 0); + } + + public void setLastCensorshipServiceReachabilityCheckTime(long value) { + putLong(CENSORSHIP_LAST_CHECK_TIME, value); + } + + public boolean isServiceReachableWithoutCircumvention() { + return getBoolean(CENSORSHIP_SERVICE_REACHABLE, false); + } + + public void setServiceReachableWithoutCircumvention(boolean value) { + putBoolean(CENSORSHIP_SERVICE_REACHABLE, value); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index 57369bb373..167826f9ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -106,6 +106,7 @@ public final class Megaphones { put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER); put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER); put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : NEVER); + put(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION, shouldShowTurnOffCircumventionMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(7)) : NEVER); put(Event.BECOME_A_SUSTAINER, shouldShowDonateMegaphone(context, records) ? ShowForDurationSchedule.showForDays(7) : NEVER); put(Event.PIN_REMINDER, new SignalPinReminderSchedule()); @@ -136,6 +137,8 @@ public final class Megaphones { return buildBecomeASustainerMegaphone(context); case NOTIFICATION_PROFILES: return buildNotificationProfilesMegaphone(context); + case TURN_OFF_CENSORSHIP_CIRCUMVENTION: + return buildTurnOffCircumventionMegaphone(context); default: throw new IllegalArgumentException("Event not handled!"); } @@ -295,6 +298,21 @@ public final class Megaphones { .build(); } + private static @NonNull Megaphone buildTurnOffCircumventionMegaphone(@NonNull Context context) { + return new Megaphone.Builder(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION, Megaphone.Style.BASIC) + .setTitle(R.string.CensorshipCircumventionMegaphone_turn_off_censorship_circumvention) + .setImage(R.drawable.ic_censorship_megaphone_64) + .setBody(R.string.CensorshipCircumventionMegaphone_you_can_now_connect_to_the_signal_service) + .setActionButton(R.string.CensorshipCircumventionMegaphone_turn_off, (megaphone, listener) -> { + SignalStore.settings().setCensorshipCircumventionEnabled(false); + listener.onMegaphoneSnooze(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION); + }) + .setSecondaryButton(R.string.CensorshipCircumventionMegaphone_no_thanks, (megaphone, listener) -> { + listener.onMegaphoneSnooze(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION); + }) + .build(); + } + private static boolean shouldShowDonateMegaphone(@NonNull Context context, @NonNull Map records) { long timeSinceLastDonatePrompt = timeSinceLastDonatePrompt(records); @@ -313,6 +331,11 @@ public final class Megaphones { return SignalStore.onboarding().hasOnboarding(context); } + private static boolean shouldShowTurnOffCircumventionMegaphone() { + return ApplicationDependencies.getSignalServiceNetworkAccess().isCensored() && + SignalStore.misc().isServiceReachableWithoutCircumvention(); + } + private static boolean shouldShowNotificationsMegaphone(@NonNull Context context) { boolean shouldShow = !SignalStore.settings().isMessageNotificationsEnabled() || !NotificationChannels.isMessageChannelEnabled(context) || @@ -374,7 +397,8 @@ public final class Megaphones { ADD_A_PROFILE_PHOTO("add_a_profile_photo"), BECOME_A_SUSTAINER("become_a_sustainer"), VALENTINES_DONATIONS_2022("valentines_donations_2022"), - NOTIFICATION_PROFILES("notification_profiles"); + NOTIFICATION_PROFILES("notification_profiles"), + TURN_OFF_CENSORSHIP_CIRCUMVENTION("turn_off_censorship_circumvention"); private final String key; diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java index 6c183bf0fd..2b603fd41a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.megaphone; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.util.TextSecurePreferences; final class SignalPinReminderSchedule implements MegaphoneSchedule { diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt index 09b71dde10..553913f740 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt @@ -211,7 +211,7 @@ class SignalServiceNetworkAccess(context: Context) { COUNTRY_CODE_UZBEKISTAN, ) - private val uncensoredConfiguration: SignalServiceConfiguration = SignalServiceConfiguration( + val uncensoredConfiguration: SignalServiceConfiguration = SignalServiceConfiguration( arrayOf(SignalServiceUrl(BuildConfig.SIGNAL_URL, serviceTrustStore)), mapOf( 0 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, serviceTrustStore)), diff --git a/app/src/main/res/drawable/ic_censorship_megaphone_64.xml b/app/src/main/res/drawable/ic_censorship_megaphone_64.xml new file mode 100644 index 0000000000..49a1ab1619 --- /dev/null +++ b/app/src/main/res/drawable/ic_censorship_megaphone_64.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 232dadca27..afd8fd6b2f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4600,6 +4600,15 @@ Couldn\'t Load Content + + Turn off censorship circumvention? + + You can now connect to the Signal service directly for a better experience. + + No thanks + + Turn off + View more