Support for using Signal without Play Services

This is now possible with beta calling, so non-GCM users are a
part of beta calling by default.

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2017-02-20 12:00:03 -08:00
parent 4112f23f33
commit 1669731329
15 changed files with 354 additions and 114 deletions

View File

@@ -5,9 +5,11 @@ import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.gcm.GcmBroadcastReceiver;
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob;
@@ -24,16 +26,20 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
public class MessageRetrievalService extends Service implements Runnable, InjectableType, RequirementListener {
public class MessageRetrievalService extends Service implements InjectableType, RequirementListener {
private static final String TAG = MessageRetrievalService.class.getSimpleName();
public static final String ACTION_ACTIVITY_STARTED = "ACTIVITY_STARTED";
public static final String ACTION_ACTIVITY_FINISHED = "ACTIVITY_FINISHED";
public static final String ACTION_PUSH_RECEIVED = "PUSH_RECEIVED";
public static final String ACTION_INITIALIZE = "INITIALIZE";
public static final int FOREGROUND_ID = 313399;
private static final long REQUEST_TIMEOUT_MINUTES = 1;
private NetworkRequirement networkRequirement;
@@ -42,8 +48,9 @@ public class MessageRetrievalService extends Service implements Runnable, Inject
@Inject
public SignalServiceMessageReceiver receiver;
private int activeActivities = 0;
private List<Intent> pushPending = new LinkedList<>();
private int activeActivities = 0;
private List<Intent> pushPending = new LinkedList<>();
private MessageRetrievalThread retrievalThread = null;
public static SignalServiceMessagePipe pipe = null;
@@ -56,7 +63,11 @@ public class MessageRetrievalService extends Service implements Runnable, Inject
networkRequirementProvider = new NetworkRequirementProvider(this);
networkRequirementProvider.setListener(this);
new Thread(this, "MessageRetrievalService").start();
retrievalThread = new MessageRetrievalThread();
retrievalThread.start();
setForegroundIfNecessary();
}
public int onStartCommand(Intent intent, int flags, int startId) {
@@ -70,44 +81,11 @@ public class MessageRetrievalService extends Service implements Runnable, Inject
}
@Override
public void run() {
while (true) {
Log.w(TAG, "Waiting for websocket state change....");
waitForConnectionNecessary();
public void onDestroy() {
super.onDestroy();
Log.w(TAG, "Making websocket connection....");
pipe = receiver.createMessagePipe();
try {
while (isConnectionNecessary()) {
try {
Log.w(TAG, "Reading message...");
pipe.read(REQUEST_TIMEOUT_MINUTES, TimeUnit.MINUTES,
new SignalServiceMessagePipe.MessagePipeCallback() {
@Override
public void onMessage(SignalServiceEnvelope envelope) {
Log.w(TAG, "Retrieved envelope! " + envelope.getSource());
PushContentReceiveJob receiveJob = new PushContentReceiveJob(MessageRetrievalService.this);
receiveJob.handle(envelope, false);
decrementPushReceived();
}
});
} catch (TimeoutException e) {
Log.w(TAG, "Application level read timeout...");
} catch (InvalidVersionException e) {
Log.w(TAG, e);
}
}
} catch (Throwable e) {
Log.w(TAG, e);
} finally {
Log.w(TAG, "Shutting down pipe...");
shutdown(pipe);
}
Log.w(TAG, "Looping...");
if (retrievalThread != null) {
retrievalThread.stopThread();
}
}
@@ -123,6 +101,18 @@ public class MessageRetrievalService extends Service implements Runnable, Inject
return null;
}
private void setForegroundIfNecessary() {
if (TextSecurePreferences.isGcmDisabled(this)) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentTitle(getString(R.string.MessageRetrievalService_signal));
builder.setContentText(getString(R.string.MessageRetrievalService_background_connection_enabled));
builder.setPriority(NotificationCompat.PRIORITY_MIN);
builder.setWhen(0);
builder.setSmallIcon(R.drawable.ic_signal_grey_24dp);
startForeground(FOREGROUND_ID, builder.build());
}
}
private synchronized void incrementActive() {
activeActivities++;
Log.w(TAG, "Active Count: " + activeActivities);
@@ -149,11 +139,13 @@ public class MessageRetrievalService extends Service implements Runnable, Inject
}
private synchronized boolean isConnectionNecessary() {
Log.w(TAG, String.format("Network requirement: %s, active activities: %s, push pending: %s",
networkRequirement.isPresent(), activeActivities, pushPending.size()));
boolean isGcmDisabled = TextSecurePreferences.isGcmDisabled(this);
return TextSecurePreferences.isWebsocketRegistered(this) &&
(activeActivities > 0 || !pushPending.isEmpty()) &&
Log.w(TAG, String.format("Network requirement: %s, active activities: %s, push pending: %s, gcm disabled: %b",
networkRequirement.isPresent(), activeActivities, pushPending.size(), isGcmDisabled));
return TextSecurePreferences.isWebsocketRegistered(this) &&
(activeActivities > 0 || !pushPending.isEmpty() || isGcmDisabled) &&
networkRequirement.isPresent();
}
@@ -188,4 +180,59 @@ public class MessageRetrievalService extends Service implements Runnable, Inject
public static @Nullable SignalServiceMessagePipe getPipe() {
return pipe;
}
private class MessageRetrievalThread extends Thread {
private AtomicBoolean stopThread = new AtomicBoolean(false);
@Override
public void run() {
while (!stopThread.get()) {
Log.w(TAG, "Waiting for websocket state change....");
waitForConnectionNecessary();
Log.w(TAG, "Making websocket connection....");
pipe = receiver.createMessagePipe();
SignalServiceMessagePipe localPipe = pipe;
try {
while (isConnectionNecessary() && !stopThread.get()) {
try {
Log.w(TAG, "Reading message...");
localPipe.read(REQUEST_TIMEOUT_MINUTES, TimeUnit.MINUTES,
new SignalServiceMessagePipe.MessagePipeCallback() {
@Override
public void onMessage(SignalServiceEnvelope envelope) {
Log.w(TAG, "Retrieved envelope! " + envelope.getSource());
PushContentReceiveJob receiveJob = new PushContentReceiveJob(MessageRetrievalService.this);
receiveJob.handle(envelope, false);
decrementPushReceived();
}
});
} catch (TimeoutException e) {
Log.w(TAG, "Application level read timeout...");
} catch (InvalidVersionException e) {
Log.w(TAG, e);
}
}
} catch (Throwable e) {
Log.w(TAG, e);
} finally {
Log.w(TAG, "Shutting down pipe...");
shutdown(localPipe);
}
Log.w(TAG, "Looping...");
}
Log.w(TAG, "Exiting...");
}
public void stopThread() {
stopThread.set(true);
}
}
}

View File

@@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PersistentConnectionBootListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (TextSecurePreferences.isGcmDisabled(context)) {
Intent serviceIntent = new Intent(context, MessageRetrievalService.class);
serviceIntent.setAction(MessageRetrievalService.ACTION_INITIALIZE);
context.startService(serviceIntent);
}
}
}

View File

@@ -71,7 +71,12 @@ public class RegistrationService extends Service {
public static final String CHALLENGE_EVENT = "org.thoughtcrime.securesms.CHALLENGE_EVENT";
public static final String REGISTRATION_EVENT = "org.thoughtcrime.securesms.REGISTRATION_EVENT";
public static final String CHALLENGE_EXTRA = "CAAChallenge";
public static final String NUMBER_EXTRA = "e164number";
public static final String MASTER_SECRET_EXTRA = "master_secret";
public static final String GCM_SUPPORTED_EXTRA = "gcm_supported";
public static final String PASSWORD_EXTRA = "password";
public static final String SIGNALING_KEY_EXTRA = "signaling_key";
public static final String CHALLENGE_EXTRA = "CAAChallenge";
private static final long REGISTRATION_TIMEOUT_MILLIS = 120000;
@@ -149,21 +154,22 @@ public class RegistrationService extends Service {
private void handleVoiceRequestedIntent(Intent intent) {
setState(new RegistrationState(RegistrationState.STATE_VOICE_REQUESTED,
intent.getStringExtra("e164number"),
intent.getStringExtra("password")));
intent.getStringExtra(NUMBER_EXTRA),
intent.getStringExtra(PASSWORD_EXTRA)));
}
private void handleVoiceRegistrationIntent(Intent intent) {
markAsVerifying(true);
String number = intent.getStringExtra("e164number");
String password = intent.getStringExtra("password");
String signalingKey = intent.getStringExtra("signaling_key");
String number = intent.getStringExtra(NUMBER_EXTRA);
String password = intent.getStringExtra(PASSWORD_EXTRA);
String signalingKey = intent.getStringExtra(SIGNALING_KEY_EXTRA);
boolean supportsGcm = intent.getBooleanExtra(GCM_SUPPORTED_EXTRA, true);
try {
SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(this, number, password);
handleCommonRegistration(accountManager, number, password, signalingKey);
handleCommonRegistration(accountManager, number, password, signalingKey, supportsGcm);
markAsVerified(number, password, signalingKey);
@@ -183,8 +189,10 @@ public class RegistrationService extends Service {
private void handleSmsRegistrationIntent(Intent intent) {
markAsVerifying(true);
String number = intent.getStringExtra("e164number");
int registrationId = TextSecurePreferences.getLocalRegistrationId(this);
String number = intent.getStringExtra(NUMBER_EXTRA);
boolean supportsGcm = intent.getBooleanExtra(GCM_SUPPORTED_EXTRA, true);
int registrationId = TextSecurePreferences.getLocalRegistrationId(this);
boolean supportsVideo = TextSecurePreferences.isWebrtcCallingEnabled(this) || !supportsGcm;
if (registrationId == 0) {
registrationId = KeyHelper.generateRegistrationId(false);
@@ -203,9 +211,9 @@ public class RegistrationService extends Service {
setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number));
String challenge = waitForChallenge();
accountManager.verifyAccountWithCode(challenge, signalingKey, registrationId, true, TextSecurePreferences.isWebrtcCallingEnabled(this));
accountManager.verifyAccountWithCode(challenge, signalingKey, registrationId, true, supportsVideo, !supportsGcm);
handleCommonRegistration(accountManager, number, password, signalingKey);
handleCommonRegistration(accountManager, number, password, signalingKey, supportsGcm);
markAsVerified(number, password, signalingKey);
setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number));
@@ -231,7 +239,7 @@ public class RegistrationService extends Service {
}
}
private void handleCommonRegistration(SignalServiceAccountManager accountManager, String number, String password, String signalingKey)
private void handleCommonRegistration(SignalServiceAccountManager accountManager, String number, String password, String signalingKey, boolean supportsGcm)
throws IOException
{
setState(new RegistrationState(RegistrationState.STATE_GENERATING_KEYS, number));
@@ -244,21 +252,29 @@ public class RegistrationService extends Service {
setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number));
String gcmRegistrationId = GoogleCloudMessaging.getInstance(this).register(GcmRefreshJob.REGISTRATION_ID);
accountManager.setGcmId(Optional.of(gcmRegistrationId));
if (supportsGcm) {
String gcmRegistrationId = GoogleCloudMessaging.getInstance(this).register(GcmRefreshJob.REGISTRATION_ID);
accountManager.setGcmId(Optional.of(gcmRegistrationId));
TextSecurePreferences.setGcmRegistrationId(this, gcmRegistrationId);
TextSecurePreferences.setGcmDisabled(this, false);
} else {
TextSecurePreferences.setGcmDisabled(this, true);
}
TextSecurePreferences.setGcmRegistrationId(this, gcmRegistrationId);
TextSecurePreferences.setWebsocketRegistered(this, true);
DatabaseFactory.getIdentityDatabase(this).saveIdentity(self.getRecipientId(), identityKey.getPublicKey());
DirectoryHelper.refreshDirectory(this, accountManager, number);
RedPhoneAccountManager redPhoneAccountManager = new RedPhoneAccountManager(BuildConfig.REDPHONE_MASTER_URL,
new RedPhoneTrustStore(this),
number, password);
if (supportsGcm) {
RedPhoneAccountManager redPhoneAccountManager = new RedPhoneAccountManager(BuildConfig.REDPHONE_MASTER_URL,
new RedPhoneTrustStore(this),
number, password);
String verificationToken = accountManager.getAccountVerificationToken();
redPhoneAccountManager.createAccount(verificationToken, new RedPhoneAccountAttributes(signalingKey, gcmRegistrationId));
String verificationToken = accountManager.getAccountVerificationToken();
redPhoneAccountManager.createAccount(verificationToken, new RedPhoneAccountAttributes(signalingKey, TextSecurePreferences.getGcmRegistrationId(this)));
}
DirectoryRefreshListener.schedule(this);
RotateSignedPreKeyListener.schedule(this);