From 46344776a4d89dcdded0aa5d2c47e1e1be1470cb Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 2 Feb 2021 16:42:47 -0500 Subject: [PATCH] Add UI support for configuring a proxy. --- app/src/main/AndroidManifest.xml | 11 +- .../securesms/ApplicationContext.java | 12 +- .../ApplicationPreferencesActivity.java | 4 + .../thoughtcrime/securesms/MainActivity.java | 9 + .../conversation/ConversationFragment.java | 5 +- .../ConversationListFragment.java | 38 ++++ .../ConversationListViewModel.java | 5 + .../dependencies/ApplicationDependencies.java | 31 +++- .../ApplicationDependencyProvider.java | 60 +++--- .../securesms/keyvalue/ProxyValues.java | 70 +++++++ .../securesms/keyvalue/SignalStore.java | 7 + .../messages/IncomingMessageObserver.java | 24 ++- .../net/PipeConnectivityListener.java | 86 +++++++++ .../DataAndStoragePreferenceFragment.java | 8 + .../preferences/EditProxyFragment.java | 174 ++++++++++++++++++ .../preferences/EditProxyViewModel.java | 99 ++++++++++ .../profiles/manage/EditAboutFragment.java | 3 + .../manage/EditProfileNameFragment.java | 3 + .../proxy/ProxyBottomSheetFragment.java | 117 ++++++++++++ .../push/SignalServiceNetworkAccess.java | 2 +- .../securesms/util/CommunicationActions.java | 16 ++ .../securesms/util/SignalProxyUtil.java | 123 +++++++++++++ .../drawable-night/ic_proxy_connected_24.xml | 13 ++ .../drawable-night/ic_proxy_connecting_24.xml | 9 + .../res/drawable-night/ic_proxy_failed_24.xml | 9 + .../res/drawable/ic_proxy_connected_24.xml | 12 ++ .../res/drawable/ic_proxy_connecting_24.xml | 15 ++ .../main/res/drawable/ic_proxy_failed_24.xml | 15 ++ app/src/main/res/drawable/proxy_avatar_96.xml | 15 ++ .../res/layout/conversation_list_fragment.xml | 17 +- .../main/res/layout/edit_proxy_fragment.xml | 93 ++++++++++ .../main/res/layout/proxy_bottom_sheet.xml | 118 ++++++++++++ app/src/main/res/values/strings.xml | 23 +++ .../res/xml/preferences_data_and_storage.xml | 11 ++ .../api/util/TlsProxySocketFactory.java | 2 - .../api/websocket/ConnectivityListener.java | 3 + .../websocket/WebSocketConnection.java | 10 +- 37 files changed, 1217 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/keyvalue/ProxyValues.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/proxy/ProxyBottomSheetFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java create mode 100644 app/src/main/res/drawable-night/ic_proxy_connected_24.xml create mode 100644 app/src/main/res/drawable-night/ic_proxy_connecting_24.xml create mode 100644 app/src/main/res/drawable-night/ic_proxy_failed_24.xml create mode 100644 app/src/main/res/drawable/ic_proxy_connected_24.xml create mode 100644 app/src/main/res/drawable/ic_proxy_connecting_24.xml create mode 100644 app/src/main/res/drawable/ic_proxy_failed_24.xml create mode 100644 app/src/main/res/drawable/proxy_avatar_96.xml create mode 100644 app/src/main/res/layout/edit_proxy_fragment.xml create mode 100644 app/src/main/res/layout/proxy_bottom_sheet.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index db55855b0c..580f23a221 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -254,6 +254,14 @@ + + + + + + + + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 0d97c39acb..681d9223e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -17,7 +17,6 @@ package org.thoughtcrime.securesms; import android.content.Context; -import android.hardware.SensorManager; import android.os.Build; import androidx.annotation.NonNull; @@ -33,7 +32,6 @@ import com.google.android.gms.security.ProviderInstaller; import org.conscrypt.Conscrypt; import org.signal.aesgcmprovider.AesGcmProvider; -import org.signal.core.util.ShakeDetector; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.AndroidLogger; import org.signal.core.util.logging.Log; @@ -46,7 +44,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider; import org.thoughtcrime.securesms.gcm.FcmJobService; -import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob; @@ -71,7 +68,6 @@ import org.thoughtcrime.securesms.service.LocalBackupListener; import org.thoughtcrime.securesms.service.RotateSenderCertificateListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; -import org.thoughtcrime.securesms.shakereport.ShakeToReport; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -145,6 +141,12 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); } }) + .addBlocking("proxy-init", () -> { + if (SignalStore.proxy().isProxyEnabled()) { + Log.w(TAG, "Proxy detected. Enabling Conscrypt.setUseEngineSocketByDefault()"); + Conscrypt.setUseEngineSocketByDefault(true); + } + }) .addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializeGcmCheck) .addNonBlocking(this::initializeSignedPreKeyCheck) @@ -272,7 +274,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi } private void initializeAppDependencies() { - ApplicationDependencies.init(this, new ApplicationDependencyProvider(this, new SignalServiceNetworkAccess(this))); + ApplicationDependencies.init(this, new ApplicationDependencyProvider(this)); } private void initializeFirstEverAppLaunch() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index bf79472b94..03a94645cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment; import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment; import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment; import org.thoughtcrime.securesms.preferences.DataAndStoragePreferenceFragment; +import org.thoughtcrime.securesms.preferences.EditProxyFragment; import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment; import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment; import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference; @@ -67,6 +68,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity { public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment"; public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment"; + public static final String LAUNCH_TO_PROXY_FRAGMENT = "launch.to.proxy.fragment"; @SuppressWarnings("unused") private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName(); @@ -108,6 +110,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity initFragment(android.R.id.content, new BackupsPreferenceFragment()); } else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_HELP_FRAGMENT, false)) { initFragment(android.R.id.content, new HelpFragment()); + } else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_PROXY_FRAGMENT, false)) { + initFragment(android.R.id.content, EditProxyFragment.newInstance()); } else if (icicle == null) { initFragment(android.R.id.content, new ApplicationPreferenceFragment()); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java index 4a34710d1c..1584412926 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java @@ -41,6 +41,7 @@ public class MainActivity extends PassphraseRequiredActivity { navigator.onCreate(savedInstanceState); handleGroupLinkInIntent(getIntent()); + handleProxyInIntent(getIntent()); CachedInflater.from(this).clear(); } @@ -56,6 +57,7 @@ public class MainActivity extends PassphraseRequiredActivity { protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handleGroupLinkInIntent(intent); + handleProxyInIntent(intent); } @Override @@ -95,4 +97,11 @@ public class MainActivity extends PassphraseRequiredActivity { CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString()); } } + + private void handleProxyInIntent(Intent intent) { + Uri data = intent.getData(); + if (data != null) { + CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString()); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 57bf93c9a9..328b606b34 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -131,6 +131,7 @@ import org.thoughtcrime.securesms.util.HtmlUtil; import org.thoughtcrime.securesms.util.RemoteDeleteUtil; import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SetUtil; +import org.thoughtcrime.securesms.util.SignalProxyUtil; import org.thoughtcrime.securesms.util.SnapToTopDataObserver; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.StorageUtil; @@ -336,6 +337,7 @@ public class ConversationFragment extends LoggingFragment { public void onStart() { super.onStart(); initializeTypingObserver(); + SignalProxyUtil.startListeningToWebsocket(); } @Override @@ -1429,7 +1431,8 @@ public class ConversationFragment extends LoggingFragment { @Override public boolean onUrlClicked(@NonNull String url) { - return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url); + return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url) || + CommunicationActions.handlePotentialProxyLinkUrl(requireActivity(), url); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 20d0331ed3..ea0edeeea0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -72,6 +72,7 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.MainFragment; import org.thoughtcrime.securesms.MainNavigator; import org.thoughtcrime.securesms.NewConversationActivity; @@ -108,6 +109,7 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneActionController; import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder; import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.net.PipeConnectivityListener; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.recipients.Recipient; @@ -118,6 +120,7 @@ import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.AvatarUtil; import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.SignalProxyUtil; import org.thoughtcrime.securesms.util.SnapToTopDataObserver; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.Stopwatch; @@ -161,6 +164,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private PulsingFloatingActionButton fab; private PulsingFloatingActionButton cameraFab; private Stub searchToolbar; + private ImageView proxyStatus; private ImageView searchAction; private View toolbarShadow; private ConversationListViewModel viewModel; @@ -199,6 +203,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode searchEmptyState = view.findViewById(R.id.search_no_results); searchAction = view.findViewById(R.id.search_action); toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow); + proxyStatus = view.findViewById(R.id.conversation_list_proxy_status); reminderView = new Stub<>(view.findViewById(R.id.reminder)); emptyState = new Stub<>(view.findViewById(R.id.empty_state)); searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar)); @@ -208,6 +213,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode toolbar.setVisibility(View.VISIBLE); ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar); + proxyStatus.setOnClickListener(v -> onProxyStatusClicked()); + fab.show(); cameraFab.show(); @@ -262,6 +269,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode if (activeAdapter != null) { activeAdapter.notifyDataSetChanged(); } + + SignalProxyUtil.startListeningToWebsocket(); } @Override @@ -543,6 +552,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged); viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList); viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState); + viewModel.getPipeState().observe(getViewLifecycleOwner(), this::updateProxyStatus); visibilityLifecycleObserver = new DefaultLifecycleObserver() { @Override @@ -856,6 +866,34 @@ public class ConversationListFragment extends MainFragment implements ActionMode } } + private void updateProxyStatus(@NonNull PipeConnectivityListener.State state) { + if (SignalStore.proxy().isProxyEnabled()) { + proxyStatus.setVisibility(View.VISIBLE); + + switch (state) { + case CONNECTING: + case DISCONNECTED: + proxyStatus.setImageResource(R.drawable.ic_proxy_connecting_24); + break; + case CONNECTED: + proxyStatus.setImageResource(R.drawable.ic_proxy_connected_24); + break; + case FAILURE: + proxyStatus.setImageResource(R.drawable.ic_proxy_failed_24); + break; + } + } else { + proxyStatus.setVisibility(View.GONE); + } + } + + private void onProxyStatusClicked() { + Intent intent = new Intent(requireContext(), ApplicationPreferencesActivity.class); + intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_PROXY_FRAGMENT, true); + + startActivity(intent); + } + protected void onPostSubmitList(int conversationCount) { if (conversationCount >= 6 && (SignalStore.onboarding().shouldShowInviteFriends() || SignalStore.onboarding().shouldShowNewGroup())) { SignalStore.onboarding().clearAll(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java index 716f109329..234133ff73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.megaphone.Megaphone; import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.megaphone.Megaphones; +import org.thoughtcrime.securesms.net.PipeConnectivityListener; import org.thoughtcrime.securesms.search.SearchRepository; import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.Util; @@ -100,6 +101,10 @@ class ConversationListViewModel extends ViewModel { return pagedData.getController(); } + @NonNull LiveData getPipeState() { + return ApplicationDependencies.getPipeListener().getState(); + } + public int getPinnedCount() { return pinnedCount; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 0c52c1a00f..a60f37855e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; import org.thoughtcrime.securesms.messages.IncomingMessageProcessor; +import org.thoughtcrime.securesms.net.PipeConnectivityListener; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; @@ -79,9 +80,9 @@ public class ApplicationDependencies { throw new IllegalStateException("Already initialized!"); } - ApplicationDependencies.application = application; - ApplicationDependencies.provider = provider; - ApplicationDependencies.messageNotifier = provider.provideMessageNotifier(); + ApplicationDependencies.application = application; + ApplicationDependencies.provider = provider; + ApplicationDependencies.messageNotifier = provider.provideMessageNotifier(); } } @@ -89,6 +90,10 @@ public class ApplicationDependencies { return application; } + public static @NonNull PipeConnectivityListener getPipeListener() { + return provider.providePipeListener(); + } + public static @NonNull SignalServiceAccountManager getSignalServiceAccountManager() { if (accountManager == null) { synchronized (LOCK) { @@ -179,6 +184,25 @@ public class ApplicationDependencies { } } + public static void resetNetworkConnectionsAfterProxyChange() { + synchronized (LOCK) { + getPipeListener().reset(); + + if (incomingMessageObserver != null) { + incomingMessageObserver.terminate(); + } + + if (messageSender != null) { + messageSender.cancelInFlightRequests(); + } + + incomingMessageObserver = null; + messageReceiver = null; + accountManager = null; + messageSender = null; + } + } + public static @NonNull SignalServiceNetworkAccess getSignalServiceNetworkAccess() { return provider.provideSignalServiceNetworkAccess(); } @@ -336,6 +360,7 @@ public class ApplicationDependencies { } public interface Provider { + @NonNull PipeConnectivityListener providePipeListener(); @NonNull GroupsV2Operations provideGroupsV2Operations(); @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(); @NonNull SignalServiceMessageSender provideSignalServiceMessageSender(); 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 da6dd27118..18498a3598 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -4,6 +4,7 @@ import android.app.Application; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.greenrobot.eventbus.EventBus; import org.signal.core.util.concurrent.SignalExecutors; @@ -35,10 +36,12 @@ import org.thoughtcrime.securesms.jobs.PushProcessMessageJob; import org.thoughtcrime.securesms.jobs.PushTextSendJob; import org.thoughtcrime.securesms.jobs.ReactionSendJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; import org.thoughtcrime.securesms.messages.IncomingMessageProcessor; +import org.thoughtcrime.securesms.net.PipeConnectivityListener; import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier; @@ -53,12 +56,14 @@ import org.thoughtcrime.securesms.util.EarlyMessageCache; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FrameRateTracker; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -66,6 +71,8 @@ import org.whispersystems.signalservice.api.websocket.ConnectivityListener; import java.util.UUID; +import okhttp3.Response; + /** * Implementation of {@link ApplicationDependencies.Provider} that provides real app dependencies. */ @@ -73,16 +80,21 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr private static final String TAG = Log.tag(ApplicationDependencyProvider.class); - private final Application context; - private final SignalServiceNetworkAccess networkAccess; + private final Application context; + private final PipeConnectivityListener pipeListener; - public ApplicationDependencyProvider(@NonNull Application context, @NonNull SignalServiceNetworkAccess networkAccess) { - this.context = context; - this.networkAccess = networkAccess; + public ApplicationDependencyProvider(@NonNull Application context) { + this.context = context; + this.pipeListener = new PipeConnectivityListener(context); } private @NonNull ClientZkOperations provideClientZkOperations() { - return ClientZkOperations.create(networkAccess.getConfiguration(context)); + return ClientZkOperations.create(provideSignalServiceNetworkAccess().getConfiguration(context)); + } + + @Override + public @NonNull PipeConnectivityListener providePipeListener() { + return pipeListener; } @Override @@ -92,7 +104,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr @Override public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager() { - return new SignalServiceAccountManager(networkAccess.getConfiguration(context), + return new SignalServiceAccountManager(provideSignalServiceNetworkAccess().getConfiguration(context), new DynamicCredentialsProvider(context), BuildConfig.SIGNAL_AGENT, provideGroupsV2Operations(), @@ -101,7 +113,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr @Override public @NonNull SignalServiceMessageSender provideSignalServiceMessageSender() { - return new SignalServiceMessageSender(networkAccess.getConfiguration(context), + return new SignalServiceMessageSender(provideSignalServiceNetworkAccess().getConfiguration(context), new DynamicCredentialsProvider(context), new SignalProtocolStoreImpl(context), BuildConfig.SIGNAL_AGENT, @@ -119,10 +131,10 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr public @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver() { SleepTimer sleepTimer = TextSecurePreferences.isFcmDisabled(context) ? new AlarmSleepTimer(context) : new UptimeSleepTimer(); - return new SignalServiceMessageReceiver(networkAccess.getConfiguration(context), + return new SignalServiceMessageReceiver(provideSignalServiceNetworkAccess().getConfiguration(context), new DynamicCredentialsProvider(context), BuildConfig.SIGNAL_AGENT, - new PipeConnectivityListener(), + pipeListener, sleepTimer, provideClientZkOperations().getProfileOperations(), FeatureFlags.okHttpAutomaticRetry()); @@ -130,7 +142,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr @Override public @NonNull SignalServiceNetworkAccess provideSignalServiceNetworkAccess() { - return networkAccess; + return new SignalServiceNetworkAccess(context); } @Override @@ -240,30 +252,4 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr return TextSecurePreferences.getSignalingKey(context); } } - - private class PipeConnectivityListener implements ConnectivityListener { - - @Override - public void onConnected() { - Log.i(TAG, "onConnected()"); - TextSecurePreferences.setUnauthorizedReceived(context, false); - } - - @Override - public void onConnecting() { - Log.i(TAG, "onConnecting()"); - } - - @Override - public void onDisconnected() { - Log.w(TAG, "onDisconnected()"); - } - - @Override - public void onAuthenticationFailure() { - Log.w(TAG, "onAuthenticationFailure()"); - TextSecurePreferences.setUnauthorizedReceived(context, true); - EventBus.getDefault().post(new ReminderUpdateEvent()); - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ProxyValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ProxyValues.java new file mode 100644 index 0000000000..d45c2881cc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ProxyValues.java @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.keyvalue; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.whispersystems.signalservice.internal.configuration.SignalProxy; + +public final class ProxyValues extends SignalStoreValues { + + private static final String KEY_PROXY_ENABLED = "proxy.enabled"; + private static final String KEY_HOST = "proxy.host"; + private static final String KEY_PORT = "proxy.port"; + + ProxyValues(@NonNull KeyValueStore store) { + super(store); + } + + @Override + void onFirstEverAppLaunch() { + } + + + public void enableProxy(@NonNull SignalProxy proxy) { + getStore().beginWrite() + .putBoolean(KEY_PROXY_ENABLED, true) + .putString(KEY_HOST, proxy.getHost()) + .putInteger(KEY_PORT, proxy.getPort()) + .apply(); + } + + /** + * Disables the proxy, but does not clear out the last-chosen host. + */ + public void disableProxy() { + putBoolean(KEY_PROXY_ENABLED, false); + } + + public boolean isProxyEnabled() { + return getBoolean(KEY_PROXY_ENABLED, false); + } + + /** + * Sets the proxy. This does not *enable* the proxy. This is because the user may want to set a + * proxy and then enabled it and disable it at will. + */ + public void setProxy(@Nullable SignalProxy proxy) { + if (proxy != null) { + getStore().beginWrite() + .putString(KEY_HOST, proxy.getHost()) + .putInteger(KEY_PORT, proxy.getPort()) + .apply(); + } else { + getStore().beginWrite() + .remove(KEY_HOST) + .remove(KEY_PORT) + .apply(); + } + } + + public @Nullable SignalProxy getProxy() { + String host = getString(KEY_HOST, null); + int port = getInteger(KEY_PORT, 0); + + if (host != null) { + return new SignalProxy(host, port); + } else { + return null; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index d6a31b436c..aaab35b719 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -30,6 +30,7 @@ public final class SignalStore { private final PhoneNumberPrivacyValues phoneNumberPrivacyValues; private final OnboardingValues onboardingValues; private final WallpaperValues wallpaperValues; + private final ProxyValues proxyValues; private SignalStore() { this.store = new KeyValueStore(ApplicationDependencies.getApplication()); @@ -48,6 +49,7 @@ public final class SignalStore { this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store); this.onboardingValues = new OnboardingValues(store); this.wallpaperValues = new WallpaperValues(store); + this.proxyValues = new ProxyValues(store); } public static void onFirstEverAppLaunch() { @@ -65,6 +67,7 @@ public final class SignalStore { phoneNumberPrivacy().onFirstEverAppLaunch(); onboarding().onFirstEverAppLaunch(); wallpaper().onFirstEverAppLaunch(); + proxy().onFirstEverAppLaunch(); } public static @NonNull KbsValues kbsValues() { @@ -127,6 +130,10 @@ public final class SignalStore { return INSTANCE.wallpaperValues; } + public static @NonNull ProxyValues proxy() { + return INSTANCE.proxyValues; + } + public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() { return new GroupsV2AuthorizationSignalStoreCache(getStore()); } 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 1a9e596a0f..c9f8924c04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobs.PushDecryptDrainedJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.messages.IncomingMessageProcessor.Processor; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; @@ -56,6 +57,7 @@ public class IncomingMessageObserver { private volatile boolean networkDrained; private volatile boolean decryptionDrained; + private volatile boolean terminated; public IncomingMessageObserver(@NonNull Application context) { this.context = context; @@ -138,9 +140,10 @@ public class IncomingMessageObserver { boolean websocketRegistered = TextSecurePreferences.isWebsocketRegistered(context); boolean isGcmDisabled = TextSecurePreferences.isFcmDisabled(context); boolean hasNetwork = NetworkConstraint.isMet(context); + boolean hasProxy = SignalStore.proxy().isProxyEnabled(); - Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Censored: %s, Registered: %s, Websocket Registered: %s", - hasNetwork, appVisible, !isGcmDisabled, networkAccess.isCensored(context), registered, websocketRegistered)); + Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Censored: %s, Registered: %s, Websocket Registered: %s, Proxy: %s", + hasNetwork, appVisible, !isGcmDisabled, networkAccess.isCensored(context), registered, websocketRegistered, hasProxy)); return registered && websocketRegistered && @@ -157,10 +160,19 @@ public class IncomingMessageObserver { } } + public void terminate() { + Log.w(TAG, "Beginning termination."); + terminated = true; + shutdown(pipe, unidentifiedPipe); + } + private void shutdown(@Nullable SignalServiceMessagePipe pipe, @Nullable SignalServiceMessagePipe unidentifiedPipe) { try { if (pipe != null) { + Log.w(TAG, "Shutting down normal pipe."); pipe.shutdown(); + } else { + Log.w(TAG, "No need to shutdown normal pipe, it doesn't exist."); } } catch (Throwable t) { Log.w(TAG, "Closing normal pipe failed!", t); @@ -168,7 +180,10 @@ public class IncomingMessageObserver { try { if (unidentifiedPipe != null) { + Log.w(TAG, "Shutting down unidentified pipe."); unidentifiedPipe.shutdown(); + } else { + Log.w(TAG, "No need to shutdown unidentified pipe, it doesn't exist."); } } catch (Throwable t) { Log.w(TAG, "Closing unidentified pipe failed!", t); @@ -187,12 +202,13 @@ public class IncomingMessageObserver { MessageRetrievalThread() { super("MessageRetrievalService"); + Log.i(TAG, "Initializing! (" + this.hashCode() + ")"); setUncaughtExceptionHandler(this); } @Override public void run() { - while (true) { + while (!terminated) { Log.i(TAG, "Waiting for websocket state change...."); waitForConnectionNecessary(); @@ -236,6 +252,8 @@ public class IncomingMessageObserver { Log.i(TAG, "Looping..."); } + + Log.w(TAG, "Terminated! (" + this.hashCode() + ")"); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java b/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java new file mode 100644 index 0000000000..ed3fad2212 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java @@ -0,0 +1,86 @@ +package org.thoughtcrime.securesms.net; + +import android.app.Application; + +import androidx.annotation.NonNull; + +import org.greenrobot.eventbus.EventBus; +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.events.ReminderUpdateEvent; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.util.DefaultValueLiveData; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.api.websocket.ConnectivityListener; + +import okhttp3.Response; + +/** + * Our standard listener for reacting to the state of the websocket. Translates the state into a + * LiveData for observation. + */ +public class PipeConnectivityListener implements ConnectivityListener { + + private static final String TAG = Log.tag(PipeConnectivityListener.class); + + private final Application application; + private final DefaultValueLiveData state; + + public PipeConnectivityListener(@NonNull Application application) { + this.application = application; + this.state = new DefaultValueLiveData<>(State.DISCONNECTED); + } + + @Override + public void onConnected() { + Log.i(TAG, "onConnected()"); + TextSecurePreferences.setUnauthorizedReceived(application, false); + state.postValue(State.CONNECTED); + } + + @Override + public void onConnecting() { + Log.i(TAG, "onConnecting()"); + state.postValue(State.CONNECTING); + } + + @Override + public void onDisconnected() { + Log.w(TAG, "onDisconnected()"); + state.postValue(State.DISCONNECTED); + } + + @Override + public void onAuthenticationFailure() { + Log.w(TAG, "onAuthenticationFailure()"); + TextSecurePreferences.setUnauthorizedReceived(application, true); + EventBus.getDefault().post(new ReminderUpdateEvent()); + state.postValue(State.FAILURE); + } + + @Override + public boolean onGenericFailure(Response response, Throwable throwable) { + Log.w(TAG, "onGenericFailure() Response: " + response, throwable); + state.postValue(State.FAILURE); + + if (SignalStore.proxy().isProxyEnabled()) { + Log.w(TAG, "Encountered an error while we had a proxy set! Terminating the connection to prevent retry spam."); + ApplicationDependencies.getIncomingMessageObserver().terminate(); + return false; + } else { + return true; + } + } + + public void reset() { + state.postValue(State.DISCONNECTED); + } + + public @NonNull DefaultValueLiveData getState() { + return state; + } + + public enum State { + DISCONNECTED, CONNECTING, CONNECTED, FAILURE + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/DataAndStoragePreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/DataAndStoragePreferenceFragment.java index 39c1259a75..f7b0b0459e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/DataAndStoragePreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/DataAndStoragePreferenceFragment.java @@ -25,6 +25,7 @@ public class DataAndStoragePreferenceFragment extends ListSummaryPreferenceFragm private static final String TAG = Log.tag(DataAndStoragePreferenceFragment.class); private static final String MANAGE_STORAGE_KEY = "pref_data_manage"; + private static final String USE_PROXY_KEY = "pref_use_proxy"; @Override public void onCreate(Bundle icicle) { @@ -52,6 +53,12 @@ public class DataAndStoragePreferenceFragment extends ListSummaryPreferenceFragm viewModel.getStorageBreakdown() .observe(requireActivity(), breakdown -> manageStorage.setSummary(Util.getPrettyFileSize(breakdown.getTotalSize()))); + + + findPreference(USE_PROXY_KEY).setOnPreferenceClickListener(unused -> { + requireApplicationPreferencesActivity().pushFragment(EditProxyFragment.newInstance()); + return false; + }); } @Override @@ -65,6 +72,7 @@ public class DataAndStoragePreferenceFragment extends ListSummaryPreferenceFragm requireApplicationPreferencesActivity().getSupportActionBar().setTitle(R.string.preferences__data_and_storage); setMediaDownloadSummaries(); ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(requireActivity()).refreshStorageBreakdown(requireContext()); + findPreference(USE_PROXY_KEY).setSummary(SignalStore.proxy().isProxyEnabled() ? R.string.preferences_on : R.string.preferences_off); } private @NonNull ApplicationPreferencesActivity requireApplicationPreferencesActivity() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java new file mode 100644 index 0000000000..a001fac050 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java @@ -0,0 +1,174 @@ +package org.thoughtcrime.securesms.preferences; + +import android.app.AlertDialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.app.ShareCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProviders; +import androidx.navigation.Navigation; + +import com.dd.CircularProgressButton; + +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.net.PipeConnectivityListener; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.internal.configuration.SignalProxy; + +public class EditProxyFragment extends Fragment { + + private SwitchCompat proxySwitch; + private EditText proxyText; + private TextView proxyTitle; + private TextView proxyStatus; + private View shareButton; + private CircularProgressButton saveButton; + private EditProxyViewModel viewModel; + + public static EditProxyFragment newInstance() { + return new EditProxyFragment(); + } + + @Override + public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.edit_proxy_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + this.proxySwitch = view.findViewById(R.id.edit_proxy_switch); + this.proxyTitle = view.findViewById(R.id.edit_proxy_address_title); + this.proxyText = view.findViewById(R.id.edit_proxy_host); + this.proxyStatus = view.findViewById(R.id.edit_proxy_status); + this.saveButton = view.findViewById(R.id.edit_proxy_save); + this.shareButton = view.findViewById(R.id.edit_proxy_share); + + this.proxyText.setText(Optional.fromNullable(SignalStore.proxy().getProxy()).transform(SignalProxy::getHost).or("")); + this.proxySwitch.setChecked(SignalStore.proxy().isProxyEnabled()); + + initViewModel(); + + saveButton.setOnClickListener(v -> onSaveClicked()); + shareButton.setOnClickListener(v -> onShareClicked()); + proxySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> viewModel.onToggleProxy(isChecked)); + } + + @Override + public void onResume() { + super.onResume(); + ((ApplicationPreferencesActivity) requireActivity()).requireSupportActionBar().setTitle(R.string.preferences_use_proxy); + } + + private void initViewModel() { + viewModel = ViewModelProviders.of(this).get(EditProxyViewModel.class); + + viewModel.getUiState().observe(getViewLifecycleOwner(), this::presentUiState); + viewModel.getProxyState().observe(getViewLifecycleOwner(), this::presentProxyState); + viewModel.getEvents().observe(getViewLifecycleOwner(), this::presentEvent); + viewModel.getSaveState().observe(getViewLifecycleOwner(), this::presentSaveState); + } + + private void presentUiState(@NonNull EditProxyViewModel.UiState uiState) { + switch (uiState) { + case ALL_ENABLED: + proxyText.setEnabled(true); + proxyText.setAlpha(1); + saveButton.setEnabled(true); + saveButton.setAlpha(1); + shareButton.setEnabled(true); + shareButton.setAlpha(1); + proxyTitle.setAlpha(1); + proxyStatus.setVisibility(View.VISIBLE); + break; + case ALL_DISABLED: + proxyText.setEnabled(false); + proxyText.setAlpha(0.5f); + saveButton.setEnabled(false); + saveButton.setAlpha(0.5f); + shareButton.setEnabled(false); + shareButton.setAlpha(0.5f); + proxyTitle.setAlpha(0.5f); + proxyStatus.setVisibility(View.GONE); + break; + } + } + + private void presentProxyState(@NonNull PipeConnectivityListener.State proxyState) { + switch (proxyState) { + case DISCONNECTED: + case CONNECTING: + proxyStatus.setText(R.string.preferences_connecting_to_proxy); + proxyStatus.setTextColor(getResources().getColor(R.color.signal_text_secondary)); + break; + case CONNECTED: + proxyStatus.setText(R.string.preferences_connected_to_proxy); + proxyStatus.setTextColor(getResources().getColor(R.color.signal_accent_green)); + break; + case FAILURE: + proxyStatus.setText(R.string.preferences_connection_failed); + proxyStatus.setTextColor(getResources().getColor(R.color.signal_alert_primary)); + break; + } + } + + private void presentEvent(@NonNull EditProxyViewModel.Event event) { + switch (event) { + case PROXY_SUCCESS: + proxyStatus.setVisibility(View.VISIBLE); + proxyText.setText(Optional.fromNullable(SignalStore.proxy().getProxy()).transform(SignalProxy::getHost).or("")); + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.preferences_success) + .setMessage(R.string.preferences_you_are_connected_to_the_proxy) + .setPositiveButton(android.R.string.ok, (d, i) -> d.dismiss()) + .show(); + break; + case PROXY_FAILURE: + proxyStatus.setVisibility(View.GONE); + proxyText.setText(Optional.fromNullable(SignalStore.proxy().getProxy()).transform(SignalProxy::getHost).or("")); + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.preferences_failed_to_connect) + .setMessage(R.string.preferences_couldnt_connect_to_the_proxy) + .setPositiveButton(android.R.string.ok, (d, i) -> d.dismiss()) + .show(); + break; + } + } + + private void presentSaveState(@NonNull EditProxyViewModel.SaveState state) { + switch (state) { + case IDLE: + saveButton.setClickable(true); + saveButton.setIndeterminateProgressMode(false); + saveButton.setProgress(0); + break; + case IN_PROGRESS: + saveButton.setClickable(false); + saveButton.setIndeterminateProgressMode(true); + saveButton.setProgress(50); + break; + } + } + + private void onSaveClicked() { + viewModel.onSaveClicked(proxyText.getText().toString()); + } + + private void onShareClicked() { + String host = proxyText.getText().toString(); + ShareCompat.IntentBuilder.from(requireActivity()) + .setText(host) + .setType("text/plain") + .startChooser(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java new file mode 100644 index 0000000000..e67dafbf37 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java @@ -0,0 +1,99 @@ +package org.thoughtcrime.securesms.preferences; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import org.signal.core.util.concurrent.SignalExecutors; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.net.PipeConnectivityListener; +import org.thoughtcrime.securesms.util.SignalProxyUtil; +import org.thoughtcrime.securesms.util.SingleLiveEvent; +import org.whispersystems.signalservice.internal.configuration.SignalProxy; + +import java.util.concurrent.TimeUnit; + +public class EditProxyViewModel extends ViewModel { + + private final SingleLiveEvent events; + private final MutableLiveData uiState; + private final MutableLiveData saveState; + + public EditProxyViewModel() { + this.events = new SingleLiveEvent<>(); + this.uiState = new MutableLiveData<>(); + this.saveState = new MutableLiveData<>(SaveState.IDLE); + + if (SignalStore.proxy().isProxyEnabled()) { + uiState.setValue(UiState.ALL_ENABLED); + } else { + uiState.setValue(UiState.ALL_DISABLED); + } + } + + void onToggleProxy(boolean enabled) { + if (enabled) { + SignalProxy currentProxy = SignalStore.proxy().getProxy(); + + if (currentProxy != null) { + SignalProxyUtil.enableProxy(currentProxy); + } + uiState.postValue(UiState.ALL_ENABLED); + } else { + SignalProxyUtil.disableProxy(); + uiState.postValue(UiState.ALL_DISABLED); + } + } + + public void onSaveClicked(@NonNull String host) { + String parsedHost = SignalProxyUtil.parseHostFromProxyLink(host); + String trueHost = parsedHost != null ? parsedHost : host; + + saveState.postValue(SaveState.IN_PROGRESS); + + SignalExecutors.BOUNDED.execute(() -> { + SignalProxyUtil.enableProxy(new SignalProxy(trueHost, 443)); + + boolean success = SignalProxyUtil.testWebsocketConnection(TimeUnit.SECONDS.toMillis(10)); + + if (success) { + events.postValue(Event.PROXY_SUCCESS); + } else { + SignalProxyUtil.disableProxy(); + events.postValue(Event.PROXY_FAILURE); + } + + saveState.postValue(SaveState.IDLE); + }); + } + + @NonNull LiveData getUiState() { + return uiState; + } + + public @NonNull LiveData getEvents() { + return events; + } + + @NonNull LiveData getProxyState() { + return ApplicationDependencies.getPipeListener().getState(); + } + + public @NonNull LiveData getSaveState() { + return saveState; + } + + enum UiState { + ALL_DISABLED, ALL_ENABLED + } + + public enum Event { + PROXY_SUCCESS, PROXY_FAILURE + } + + public enum SaveState { + IDLE, IN_PROGRESS + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java index 3df411b75a..6ed6f1d1be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java @@ -167,14 +167,17 @@ public class EditAboutFragment extends Fragment implements ManageProfileActivity private void presentSaveState(@NonNull EditAboutViewModel.SaveState state) { switch (state) { case IDLE: + saveButton.setClickable(true); saveButton.setIndeterminateProgressMode(false); saveButton.setProgress(0); break; case IN_PROGRESS: + saveButton.setClickable(false); saveButton.setIndeterminateProgressMode(true); saveButton.setProgress(50); break; case DONE: + saveButton.setClickable(false); Navigation.findNavController(requireView()).popBackStack(); break; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java index 015cc26368..4f1c0125de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java @@ -81,14 +81,17 @@ public class EditProfileNameFragment extends Fragment { private void presentSaveState(@NonNull EditProfileNameViewModel.SaveState state) { switch (state) { case IDLE: + saveButton.setClickable(true); saveButton.setIndeterminateProgressMode(false); saveButton.setProgress(0); break; case IN_PROGRESS: + saveButton.setClickable(false); saveButton.setIndeterminateProgressMode(true); saveButton.setProgress(50); break; case DONE: + saveButton.setClickable(false); Navigation.findNavController(requireView()).popBackStack(); break; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/proxy/ProxyBottomSheetFragment.java b/app/src/main/java/org/thoughtcrime/securesms/proxy/ProxyBottomSheetFragment.java new file mode 100644 index 0000000000..18a6859914 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/proxy/ProxyBottomSheetFragment.java @@ -0,0 +1,117 @@ +package org.thoughtcrime.securesms.proxy; + +import android.app.AlertDialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProviders; + +import com.dd.CircularProgressButton; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.preferences.EditProxyViewModel; +import org.thoughtcrime.securesms.util.BottomSheetUtil; +import org.thoughtcrime.securesms.util.ThemeUtil; + +/** + * A bottom sheet shown in response to a deep link. Allows a user to set a proxy. + */ +public final class ProxyBottomSheetFragment extends BottomSheetDialogFragment { + + private static final String TAG = Log.tag(ProxyBottomSheetFragment.class); + + private static final String ARG_PROXY_LINK = "proxy_link"; + + private TextView proxyText; + private View cancelButton; + private CircularProgressButton useProxyButton; + private EditProxyViewModel viewModel; + + public static void showForProxy(@NonNull FragmentManager manager, @NonNull String proxyLink) { + ProxyBottomSheetFragment fragment = new ProxyBottomSheetFragment(); + + Bundle args = new Bundle(); + args.putString(ARG_PROXY_LINK, proxyLink); + fragment.setArguments(args); + + fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + setStyle(DialogFragment.STYLE_NORMAL, + ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_RoundedBottomSheet + : R.style.Theme_Signal_RoundedBottomSheet_Light); + + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.proxy_bottom_sheet, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + this.proxyText = view.findViewById(R.id.proxy_sheet_host); + this.useProxyButton = view.findViewById(R.id.proxy_sheet_use_proxy); + this.cancelButton = view.findViewById(R.id.proxy_sheet_cancel); + + String host = getArguments().getString(ARG_PROXY_LINK); + proxyText.setText(host); + + initViewModel(); + + useProxyButton.setOnClickListener(v -> viewModel.onSaveClicked(host)); + cancelButton.setOnClickListener(v -> dismiss()); + } + + private void initViewModel() { + this.viewModel = ViewModelProviders.of(this).get(EditProxyViewModel.class); + + viewModel.getSaveState().observe(getViewLifecycleOwner(), this::presentSaveState); + viewModel.getEvents().observe(getViewLifecycleOwner(), this::presentEvents); + } + + private void presentSaveState(@NonNull EditProxyViewModel.SaveState state) { + switch (state) { + case IDLE: + useProxyButton.setClickable(true); + useProxyButton.setIndeterminateProgressMode(false); + useProxyButton.setProgress(0); + break; + case IN_PROGRESS: + useProxyButton.setClickable(false); + useProxyButton.setIndeterminateProgressMode(true); + useProxyButton.setProgress(50); + break; + } + } + + private void presentEvents(@NonNull EditProxyViewModel.Event event) { + switch (event) { + case PROXY_SUCCESS: + Toast.makeText(requireContext(), R.string.ProxyBottomSheetFragment_successfully_connected_to_proxy, Toast.LENGTH_LONG).show(); + dismiss(); + break; + case PROXY_FAILURE: + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.preferences_failed_to_connect) + .setMessage(R.string.preferences_couldnt_connect_to_the_proxy) + .setPositiveButton(android.R.string.ok, (d, i) -> d.dismiss()) + .show(); + dismiss(); + break; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java index 4826e2cea1..2b580b790b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java @@ -231,7 +231,7 @@ public class SignalServiceNetworkAccess { new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))}, interceptors, dns, - Optional.absent(), + SignalStore.proxy().isProxyEnabled() ? Optional.of(SignalStore.proxy().getProxy()) : Optional.absent(), zkGroupServerPublicParams); this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index 91c7b50a35..992da00341 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.groups.ui.invitesandrequests.joining.GroupJoin import org.thoughtcrime.securesms.groups.ui.invitesandrequests.joining.GroupJoinUpdateRequiredBottomSheetDialogFragment; import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl; import org.thoughtcrime.securesms.permissions.Permissions; +import org.thoughtcrime.securesms.proxy.ProxyBottomSheetFragment; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.WebRtcCallService; @@ -207,6 +208,21 @@ public class CommunicationActions { }); } + /** + * If the url is a proxy link it will handle it. + * Otherwise returns false, indicating was not a proxy link. + */ + public static boolean handlePotentialProxyLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialProxyLinkUrl) { + String proxy = SignalProxyUtil.parseHostFromProxyLink(potentialProxyLinkUrl); + + if (proxy != null) { + ProxyBottomSheetFragment.showForProxy(activity.getSupportFragmentManager(), proxy); + return true; + } else { + return false; + } + } + private static void startInsecureCallInternal(@NonNull Activity activity, @NonNull Recipient recipient) { try { Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.requireSmsAddress())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java new file mode 100644 index 0000000000..8cda94a817 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java @@ -0,0 +1,123 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.lifecycle.Observer; + +import org.conscrypt.Conscrypt; +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.net.PipeConnectivityListener; +import org.whispersystems.signalservice.internal.configuration.SignalProxy; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class SignalProxyUtil { + + private static final String TAG = Log.tag(SignalProxyUtil.class); + + private static final String PROXY_LINK_HOST = "signal.tube"; + + private SignalProxyUtil() {} + + public static void startListeningToWebsocket() { + ApplicationDependencies.getIncomingMessageObserver(); + } + + /** + * Handles all things related to enabling a proxy, including saving it and resetting the relevant + * network connections. + */ + public static void enableProxy(@NonNull SignalProxy proxy) { + SignalStore.proxy().enableProxy(proxy); + Conscrypt.setUseEngineSocketByDefault(true); + ApplicationDependencies.resetNetworkConnectionsAfterProxyChange(); + startListeningToWebsocket(); + } + + /** + * Handles all things related to disabling a proxy, including saving the change and resetting the + * relevant network connections. + */ + public static void disableProxy() { + SignalStore.proxy().disableProxy(); + Conscrypt.setUseEngineSocketByDefault(false); + ApplicationDependencies.resetNetworkConnectionsAfterProxyChange(); + startListeningToWebsocket(); + } + + /** + * A blocking call that will wait until the websocket either successfully connects, or fails. + * It is assumed that the app state is already configured how you would like it, e.g. you've + * already configured a proxy if relevant. + * + * @return True if the connection is successful within the specified timeout, otherwise false. + */ + @WorkerThread + public static boolean testWebsocketConnection(long timeout) { + startListeningToWebsocket(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean success = new AtomicBoolean(false); + + Observer observer = state -> { + if (state == PipeConnectivityListener.State.CONNECTED) { + success.set(true); + latch.countDown(); + } else if (state == PipeConnectivityListener.State.FAILURE) { + success.set(false); + latch.countDown(); + } + }; + + Util.runOnMainSync(() -> ApplicationDependencies.getPipeListener().getState().observeForever(observer)); + + try { + latch.await(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted!", e); + } finally { + Util.runOnMainSync(() -> ApplicationDependencies.getPipeListener().getState().removeObserver(observer)); + } + + return success.get(); + } + + /** + * If this is a valid proxy link, this will return the embedded host. If not, it will return + * null. + */ + public static @Nullable String parseHostFromProxyLink(@NonNull String proxyLink) { + try { + URI uri = new URI(proxyLink); + + if (!"https".equalsIgnoreCase(uri.getScheme())) { + return null; + } + + if (!PROXY_LINK_HOST.equalsIgnoreCase(uri.getHost())) { + return null; + } + + String path = uri.getPath(); + + if (Util.isEmpty(path) || "/".equals(path)) { + return null; + } + + if (path.startsWith("/")) { + return path.substring(1); + } else { + return path; + } + } catch (URISyntaxException e) { + return null; + } + } +} diff --git a/app/src/main/res/drawable-night/ic_proxy_connected_24.xml b/app/src/main/res/drawable-night/ic_proxy_connected_24.xml new file mode 100644 index 0000000000..c2427ddaad --- /dev/null +++ b/app/src/main/res/drawable-night/ic_proxy_connected_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable-night/ic_proxy_connecting_24.xml b/app/src/main/res/drawable-night/ic_proxy_connecting_24.xml new file mode 100644 index 0000000000..ea644a3478 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_proxy_connecting_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-night/ic_proxy_failed_24.xml b/app/src/main/res/drawable-night/ic_proxy_failed_24.xml new file mode 100644 index 0000000000..a08e2e7583 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_proxy_failed_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_proxy_connected_24.xml b/app/src/main/res/drawable/ic_proxy_connected_24.xml new file mode 100644 index 0000000000..beda943e83 --- /dev/null +++ b/app/src/main/res/drawable/ic_proxy_connected_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_proxy_connecting_24.xml b/app/src/main/res/drawable/ic_proxy_connecting_24.xml new file mode 100644 index 0000000000..5668ea09d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_proxy_connecting_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_proxy_failed_24.xml b/app/src/main/res/drawable/ic_proxy_failed_24.xml new file mode 100644 index 0000000000..c202dcea6e --- /dev/null +++ b/app/src/main/res/drawable/ic_proxy_failed_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/proxy_avatar_96.xml b/app/src/main/res/drawable/proxy_avatar_96.xml new file mode 100644 index 0000000000..1cc237f107 --- /dev/null +++ b/app/src/main/res/drawable/proxy_avatar_96.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index 636f2a967f..34f5aaaa88 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -39,6 +39,7 @@ tools:src="@drawable/ic_contact_picture" /> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/proxy_bottom_sheet.xml b/app/src/main/res/layout/proxy_bottom_sheet.xml new file mode 100644 index 0000000000..2899768984 --- /dev/null +++ b/app/src/main/res/layout/proxy_bottom_sheet.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4580b94940..1364b2c2fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1396,6 +1396,14 @@ Can\'t receive audio and video from %1$s This may be because they have not verified your safety number change, there\'s a problem with their device, or they have blocked you. + + Proxy server + Proxy address + Do you want to use this proxy address? + Use proxy + Successfully connected to proxy. + + Select your country You must specify your @@ -2303,6 +2311,21 @@ Enable sealed sender for incoming messages from non-contacts and people with whom you have not shared your profile. Learn more Setup a username + Proxy + Use proxy + Off + On + Proxy address + Share + Save + Connecting to proxy… + Connected to proxy + Connection failed + Couldn\'t connect to the proxy. Check the proxy address and try again. + You are connected to the proxy. You can turn the proxy off at any time from Settings. + Success + Failed to connect + Customize option diff --git a/app/src/main/res/xml/preferences_data_and_storage.xml b/app/src/main/res/xml/preferences_data_and_storage.xml index 26864ab56b..8c7b4345e5 100644 --- a/app/src/main/res/xml/preferences_data_and_storage.xml +++ b/app/src/main/res/xml/preferences_data_and_storage.xml @@ -44,4 +44,15 @@ + + + + + + + + \ No newline at end of file diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java index a5eb3f792a..a404a64744 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java @@ -9,13 +9,11 @@ import java.net.InetAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; -import java.net.SocketOption; import java.net.UnknownHostException; import java.nio.channels.SocketChannel; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.List; -import java.util.Set; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/websocket/ConnectivityListener.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/websocket/ConnectivityListener.java index 0f06de8b09..bae701edb4 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/websocket/ConnectivityListener.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/websocket/ConnectivityListener.java @@ -1,9 +1,12 @@ package org.whispersystems.signalservice.api.websocket; +import okhttp3.Response; + public interface ConnectivityListener { void onConnected(); void onConnecting(); void onDisconnected(); void onAuthenticationFailure(); + boolean onGenericFailure(Response response, Throwable throwable); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java index afb454040e..294a238ff4 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java @@ -304,7 +304,15 @@ public class WebSocketConnection extends WebSocketListener { Log.w(TAG, "onFailure()", t); if (response != null && (response.code() == 401 || response.code() == 403)) { - if (listener != null) listener.onAuthenticationFailure(); + if (listener != null) { + listener.onAuthenticationFailure(); + } + } else if (listener != null) { + boolean shouldRetryConnection = listener.onGenericFailure(response, t); + if (!shouldRetryConnection) { + Log.w(TAG, "Experienced a failure, and the listener indicated we should not retry the connection. Disconnecting."); + disconnect(); + } } if (client != null) {