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) {