session-android/src/org/thoughtcrime/securesms/service/RegistrationService.java
Moxie Marlinspike d7070e7ecf Inserted the Registration activity into initial setup flow.
1) Added push messaging toggle to preferences.

2) Added push messaging registration screen to setup flow.

3) Migrated rest of SharedPreferences accessors to
   TextSecurePreferences.
2014-01-06 14:30:03 -08:00

397 lines
14 KiB
Java

package org.thoughtcrime.securesms.service;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.android.gcm.GCMRegistrar;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.gcm.GcmIntentService;
import org.thoughtcrime.securesms.gcm.GcmRegistrationTimeoutException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* The RegisterationService handles the process of PushService registration and verification.
* If it receives an intent with a REGISTER_NUMBER_ACTION, it does the following through
* an executor:
*
* 1) Generate secrets.
* 2) Register the specified number and those secrets with the server.
* 3) Wait for a challenge SMS.
* 4) Verify the challenge with the server.
* 5) Start the GCM registration process.
* 6) Retrieve the current directory.
*
* The RegistrationService broadcasts its state throughout this process, and also makes its
* state available through service binding. This enables a View to display progress.
*
* @author Moxie Marlinspike
*
*/
public class RegistrationService extends Service {
public static final String REGISTER_NUMBER_ACTION = "org.thoughtcrime.securesms.RegistrationService.REGISTER_NUMBER";
public static final String VOICE_REQUESTED_ACTION = "org.thoughtcrime.securesms.RegistrationService.VOICE_REQUESTED";
public static final String VOICE_REGISTER_ACTION = "org.thoughtcrime.securesms.RegistrationService.VOICE_REGISTER";
public static final String NOTIFICATION_TITLE = "org.thoughtcrime.securesms.NOTIFICATION_TITLE";
public static final String NOTIFICATION_TEXT = "org.thoughtcrime.securesms.NOTIFICATION_TEXT";
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 GCM_REGISTRATION_EVENT = "org.thoughtcrime.securesms.GCM_REGISTRATION_EVENT";
public static final String CHALLENGE_EXTRA = "CAAChallenge";
public static final String GCM_REGISTRATION_ID = "GCMRegistrationId";
private static final long REGISTRATION_TIMEOUT_MILLIS = 120000;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Binder binder = new RegistrationServiceBinder();
private volatile RegistrationState registrationState = new RegistrationState(RegistrationState.STATE_IDLE);
private volatile Handler registrationStateHandler;
private volatile ChallengeReceiver challengeReceiver;
private volatile GcmRegistrationReceiver gcmRegistrationReceiver;
private String challenge;
private String gcmRegistrationId;
private long verificationStartTime;
@Override
public int onStartCommand(final Intent intent, int flags, int startId) {
if (intent != null) {
executor.execute(new Runnable() {
@Override
public void run() {
if (intent.getAction().equals(REGISTER_NUMBER_ACTION)) handleRegistrationIntent(intent);
else if (intent.getAction().equals(VOICE_REQUESTED_ACTION)) handleVoiceRequestedIntent(intent);
else if (intent.getAction().equals(VOICE_REGISTER_ACTION)) handleVoiceRegisterIntent(intent);
}
});
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
executor.shutdown();
shutdown();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public void shutdown() {
shutdownChallengeListener();
markAsVerifying(false);
registrationState = new RegistrationState(RegistrationState.STATE_IDLE);
}
public synchronized int getSecondsRemaining() {
long millisPassed;
if (verificationStartTime == 0) millisPassed = 0;
else millisPassed = System.currentTimeMillis() - verificationStartTime;
return Math.max((int)(REGISTRATION_TIMEOUT_MILLIS - millisPassed) / 1000, 0);
}
public RegistrationState getRegistrationState() {
return registrationState;
}
private void initializeChallengeListener() {
this.challenge = null;
challengeReceiver = new ChallengeReceiver();
IntentFilter filter = new IntentFilter(CHALLENGE_EVENT);
registerReceiver(challengeReceiver, filter);
}
private void initializeGcmRegistrationListener() {
this.gcmRegistrationId = null;
gcmRegistrationReceiver = new GcmRegistrationReceiver();
IntentFilter filter = new IntentFilter(GCM_REGISTRATION_EVENT);
registerReceiver(gcmRegistrationReceiver, filter);
}
private synchronized void shutdownChallengeListener() {
if (challengeReceiver != null) {
unregisterReceiver(challengeReceiver);
challengeReceiver = null;
}
}
private synchronized void shutdownGcmRegistrationListener() {
if (gcmRegistrationReceiver != null) {
unregisterReceiver(gcmRegistrationReceiver);
gcmRegistrationReceiver = null;
}
}
private void handleVoiceRequestedIntent(Intent intent) {
setState(new RegistrationState(RegistrationState.STATE_VOICE_REQUESTED,
intent.getStringExtra("e164number"),
intent.getStringExtra("password")));
}
private void handleVoiceRegisterIntent(Intent intent) {
markAsVerifying(true);
String number = intent.getStringExtra("e164number");
String password = intent.getStringExtra("password");
try {
initializeGcmRegistrationListener();
PushServiceSocket socket = new PushServiceSocket(this, number, password);
setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number));
GCMRegistrar.register(this, GcmIntentService.GCM_SENDER_ID);
String gcmRegistrationId = waitForGcmRegistrationId();
socket.registerGcmId(gcmRegistrationId);
socket.retrieveDirectory(this);
markAsVerified(number, password);
setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number));
broadcastComplete(true);
} catch (UnsupportedOperationException uoe) {
Log.w("RegistrationService", uoe);
setState(new RegistrationState(RegistrationState.STATE_GCM_UNSUPPORTED, number));
broadcastComplete(false);
} catch (IOException e) {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_NETWORK_ERROR, number));
broadcastComplete(false);
} catch (GcmRegistrationTimeoutException e) {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_GCM_TIMEOUT));
broadcastComplete(false);
} catch (RateLimitException e) {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_NETWORK_ERROR));
broadcastComplete(false);
} finally {
shutdownGcmRegistrationListener();
}
}
private void handleRegistrationIntent(Intent intent) {
markAsVerifying(true);
String number = intent.getStringExtra("e164number");
try {
String password = Util.getSecret(18);
initializeChallengeListener();
initializeGcmRegistrationListener();
setState(new RegistrationState(RegistrationState.STATE_CONNECTING, number));
PushServiceSocket socket = new PushServiceSocket(this, number, password);
socket.createAccount(false);
setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number));
String challenge = waitForChallenge();
socket.verifyAccount(challenge);
setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number));
GCMRegistrar.register(this, GcmIntentService.GCM_SENDER_ID);
String gcmRegistrationId = waitForGcmRegistrationId();
socket.registerGcmId(gcmRegistrationId);
socket.retrieveDirectory(this);
markAsVerified(number, password);
setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number));
broadcastComplete(true);
} catch (UnsupportedOperationException uoe) {
Log.w("RegistrationService", uoe);
setState(new RegistrationState(RegistrationState.STATE_GCM_UNSUPPORTED, number));
broadcastComplete(false);
} catch (AccountVerificationTimeoutException avte) {
Log.w("RegistrationService", avte);
setState(new RegistrationState(RegistrationState.STATE_TIMEOUT, number));
broadcastComplete(false);
} catch (IOException e) {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_NETWORK_ERROR, number));
broadcastComplete(false);
} catch (GcmRegistrationTimeoutException e) {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_GCM_TIMEOUT));
broadcastComplete(false);
} catch (RateLimitException e) {
Log.w("RegistrationService", e);
setState(new RegistrationState(RegistrationState.STATE_NETWORK_ERROR));
broadcastComplete(false);
} finally {
shutdownChallengeListener();
shutdownGcmRegistrationListener();
}
}
private synchronized String waitForChallenge() throws AccountVerificationTimeoutException {
this.verificationStartTime = System.currentTimeMillis();
if (this.challenge == null) {
try {
wait(REGISTRATION_TIMEOUT_MILLIS);
} catch (InterruptedException e) {
throw new IllegalArgumentException(e);
}
}
if (this.challenge == null)
throw new AccountVerificationTimeoutException();
return this.challenge;
}
private synchronized String waitForGcmRegistrationId() throws GcmRegistrationTimeoutException {
if (this.gcmRegistrationId == null) {
try {
wait(10 * 60 * 1000);
} catch (InterruptedException e) {
throw new IllegalArgumentException(e);
}
}
if (this.gcmRegistrationId == null)
throw new GcmRegistrationTimeoutException();
return this.gcmRegistrationId;
}
private synchronized void challengeReceived(String challenge) {
this.challenge = challenge;
notifyAll();
}
private synchronized void gcmRegistrationReceived(String gcmRegistrationId) {
this.gcmRegistrationId = gcmRegistrationId;
notifyAll();
}
private void markAsVerifying(boolean verifying) {
TextSecurePreferences.setVerifying(this, verifying);
if (verifying) {
TextSecurePreferences.setPushRegistered(this, false);
}
}
private void markAsVerified(String number, String password) {
TextSecurePreferences.setVerifying(this, false);
TextSecurePreferences.setPushRegistered(this, true);
TextSecurePreferences.setLocalNumber(this, number);
TextSecurePreferences.setPushServerPassword(this, password);
}
private void setState(RegistrationState state) {
this.registrationState = state;
if (registrationStateHandler != null) {
registrationStateHandler.obtainMessage(state.state, state).sendToTarget();
}
}
private void broadcastComplete(boolean success) {
Intent intent = new Intent();
intent.setAction(REGISTRATION_EVENT);
if (success) {
intent.putExtra(NOTIFICATION_TITLE, getString(R.string.RegistrationService_registration_complete));
intent.putExtra(NOTIFICATION_TEXT, getString(R.string.RegistrationService_textsecure_registration_has_successfully_completed));
} else {
intent.putExtra(NOTIFICATION_TITLE, getString(R.string.RegistrationService_registration_error));
intent.putExtra(NOTIFICATION_TEXT, getString(R.string.RegistrationService_textsecure_registration_has_encountered_a_problem));
}
this.sendOrderedBroadcast(intent, null);
}
public void setRegistrationStateHandler(Handler registrationStateHandler) {
this.registrationStateHandler = registrationStateHandler;
}
public class RegistrationServiceBinder extends Binder {
public RegistrationService getService() {
return RegistrationService.this;
}
}
private class GcmRegistrationReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
Log.w("RegistrationService", "Got gcm registration broadcast...");
gcmRegistrationReceived(intent.getStringExtra(GCM_REGISTRATION_ID));
}
}
private class ChallengeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.w("RegistrationService", "Got a challenge broadcast...");
challengeReceived(intent.getStringExtra(CHALLENGE_EXTRA));
}
}
public static class RegistrationState {
public static final int STATE_IDLE = 0;
public static final int STATE_CONNECTING = 1;
public static final int STATE_VERIFYING = 2;
public static final int STATE_TIMER = 3;
public static final int STATE_COMPLETE = 4;
public static final int STATE_TIMEOUT = 5;
public static final int STATE_NETWORK_ERROR = 6;
public static final int STATE_GCM_UNSUPPORTED = 8;
public static final int STATE_GCM_REGISTERING = 9;
public static final int STATE_GCM_TIMEOUT = 10;
public static final int STATE_VOICE_REQUESTED = 12;
public final int state;
public final String number;
public final String password;
public RegistrationState(int state) {
this(state, null);
}
public RegistrationState(int state, String number) {
this(state, number, null);
}
public RegistrationState(int state, String number, String password) {
this.state = state;
this.number = number;
this.password = password;
}
}
}