Registration with voice verification.

This commit is contained in:
Moxie Marlinspike
2013-07-08 16:29:28 -07:00
parent 6ca029f64a
commit 3634ba0b55
19 changed files with 805 additions and 316 deletions

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import com.actionbarsherlock.app.SherlockActivity;
public class RegistrationProblemsActivity extends SherlockActivity {
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.registration_problems);
setTitle(getString(R.string.RegistrationProblemsActivity_possible_problems));
((Button)findViewById(R.id.close_button)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -7,12 +8,19 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
@@ -21,8 +29,13 @@ import android.widget.TextView;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockActivity;
import org.thoughtcrime.securesms.gcm.PushServiceSocket;
import org.thoughtcrime.securesms.gcm.RateLimitException;
import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.util.PhoneNumberFormatter;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import static org.thoughtcrime.securesms.service.RegistrationService.RegistrationState;
@@ -46,32 +59,33 @@ public class RegistrationProgressActivity extends SherlockActivity {
private ProgressBar connectingProgress;
private ProgressBar verificationProgress;
private ProgressBar gcmRegistrationProgress;
private ProgressBar retrieveDirectoryProgress;
private ImageView connectingCheck;
private ImageView verificationCheck;
private ImageView gcmRegistrationCheck;
private ImageView retrieveDirectoryCheck;
private TextView connectingText;
private TextView verificationText;
private TextView registrationTimerText;
private TextView gcmRegistrationText;
private TextView retrieveDirectoryText;
private Button editButton;
private Button verificationFailureButton;
private Button connectivityFailureButton;
private Button callButton;
private Button verifyButton;
private EditText codeEditText;
private volatile boolean visible;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.getSupportActionBar().setTitle("Verifying number");
this.getSupportActionBar().setTitle(getString(R.string.RegistrationProgressActivity_verifying_number));
setContentView(R.layout.registration_progress_activity);
initializeResources();
initializeLinks();
initializeServiceBinding();
}
@@ -111,27 +125,45 @@ public class RegistrationProgressActivity extends SherlockActivity {
this.connectingProgress = (ProgressBar) findViewById(R.id.connecting_progress);
this.verificationProgress = (ProgressBar) findViewById(R.id.verification_progress);
this.gcmRegistrationProgress = (ProgressBar) findViewById(R.id.gcm_registering_progress);
this.retrieveDirectoryProgress = (ProgressBar) findViewById(R.id.retrieve_directory_progress);
this.connectingCheck = (ImageView) findViewById(R.id.connecting_complete);
this.verificationCheck = (ImageView) findViewById(R.id.verification_complete);
this.gcmRegistrationCheck = (ImageView) findViewById(R.id.gcm_registering_complete);
this.retrieveDirectoryCheck = (ImageView) findViewById(R.id.retrieve_directory_complete);
this.connectingText = (TextView) findViewById(R.id.connecting_text);
this.verificationText = (TextView) findViewById(R.id.verification_text);
this.registrationTimerText = (TextView) findViewById(R.id.registration_timer);
this.gcmRegistrationText = (TextView) findViewById(R.id.gcm_registering_text);
this.retrieveDirectoryText = (TextView) findViewById(R.id.retrieve_directory_text);
this.editButton = (Button) findViewById(R.id.edit_button);
this.verificationFailureButton = (Button) findViewById(R.id.verification_failure_edit_button);
this.connectivityFailureButton = (Button) findViewById(R.id.connectivity_failure_edit_button);
this.callButton = (Button) findViewById(R.id.call_button);
this.verifyButton = (Button) findViewById(R.id.verify_button);
this.codeEditText = (EditText) findViewById(R.id.telephone_code);
this.timeoutProgressLayout = (RelativeLayout) findViewById(R.id.timer_progress_layout);
Button editButton = (Button) findViewById(R.id.edit_button);
this.editButton.setOnClickListener(new EditButtonListener());
editButton.setOnClickListener(new EditButtonListener());
this.verificationFailureButton.setOnClickListener(new EditButtonListener());
this.connectivityFailureButton.setOnClickListener(new EditButtonListener());
}
private void initializeLinks() {
TextView failureText = (TextView) findViewById(R.id.sms_failed_text);
String pretext = getString(R.string.registration_progress_activity__textsecure_timed_out_while_waiting_for_a_verification_sms_message);
String link = getString(R.string.RegistrationProblemsActivity_possible_problems);
SpannableString spannableString = new SpannableString(pretext + " " + link);
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
Intent intent = new Intent(RegistrationProgressActivity.this,
RegistrationProblemsActivity.class);
startActivity(intent);
}
}, pretext.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
failureText.setText(spannableString);
failureText.setMovementMethod(LinkMovementMethod.getInstance());
}
private void handleActivityVisible() {
IntentFilter filter = new IntentFilter(RegistrationService.REGISTRATION_EVENT);
filter.setPriority(1000);
@@ -166,12 +198,9 @@ public class RegistrationProgressActivity extends SherlockActivity {
this.verificationCheck.setVisibility(View.INVISIBLE);
this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
this.retrieveDirectoryCheck.setVisibility(View.INVISIBLE);
this.retrieveDirectoryProgress.setVisibility(View.INVISIBLE);
this.connectingText.setTextColor(FOCUSED_COLOR);
this.verificationText.setTextColor(UNFOCUSED_COLOR);
this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
this.retrieveDirectoryText.setTextColor(UNFOCUSED_COLOR);
this.timeoutProgressLayout.setVisibility(View.VISIBLE);
}
@@ -185,12 +214,9 @@ public class RegistrationProgressActivity extends SherlockActivity {
this.verificationCheck.setVisibility(View.INVISIBLE);
this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
this.retrieveDirectoryCheck.setVisibility(View.INVISIBLE);
this.retrieveDirectoryProgress.setVisibility(View.INVISIBLE);
this.connectingText.setTextColor(UNFOCUSED_COLOR);
this.verificationText.setTextColor(FOCUSED_COLOR);
this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
this.retrieveDirectoryText.setTextColor(UNFOCUSED_COLOR);
this.registrationProgress.setVisibility(View.VISIBLE);
this.timeoutProgressLayout.setVisibility(View.VISIBLE);
}
@@ -205,60 +231,48 @@ public class RegistrationProgressActivity extends SherlockActivity {
this.verificationCheck.setVisibility(View.VISIBLE);
this.gcmRegistrationProgress.setVisibility(View.VISIBLE);
this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
this.retrieveDirectoryCheck.setVisibility(View.INVISIBLE);
this.retrieveDirectoryProgress.setVisibility(View.INVISIBLE);
this.connectingText.setTextColor(UNFOCUSED_COLOR);
this.verificationText.setTextColor(UNFOCUSED_COLOR);
this.gcmRegistrationText.setTextColor(FOCUSED_COLOR);
this.retrieveDirectoryText.setTextColor(UNFOCUSED_COLOR);
this.registrationProgress.setVisibility(View.INVISIBLE);
this.timeoutProgressLayout.setVisibility(View.INVISIBLE);
}
private void handleStateRetrievingDirectory() {
this.registrationLayout.setVisibility(View.VISIBLE);
this.verificationFailureLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.GONE);
this.connectingProgress.setVisibility(View.INVISIBLE);
this.connectingCheck.setVisibility(View.VISIBLE);
this.verificationProgress.setVisibility(View.INVISIBLE);
this.verificationCheck.setVisibility(View.VISIBLE);
this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
this.gcmRegistrationCheck.setVisibility(View.VISIBLE);
this.retrieveDirectoryCheck.setVisibility(View.INVISIBLE);
this.retrieveDirectoryProgress.setVisibility(View.VISIBLE);
this.connectingText.setTextColor(UNFOCUSED_COLOR);
this.verificationText.setTextColor(UNFOCUSED_COLOR);
this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
this.retrieveDirectoryText.setTextColor(FOCUSED_COLOR);
this.registrationProgress.setVisibility(View.INVISIBLE);
this.timeoutProgressLayout.setVisibility(View.INVISIBLE);
private void handleGcmTimeout(RegistrationState state) {
handleConnectivityError(state);
}
private void handleGcmTimeout(String number) {
handleConnectivityError(number);
private void handleVerificationRequestedVoice(RegistrationState state) {
handleVerificationTimeout(state);
verifyButton.setOnClickListener(new VerifyClickListener(state.number, state.password));
verifyButton.setEnabled(true);
codeEditText.setEnabled(true);
}
private void handleVerificationTimeout(String number) {
private void handleVerificationTimeout(RegistrationState state) {
this.callButton.setOnClickListener(new CallClickListener(state.number));
this.verifyButton.setEnabled(false);
this.codeEditText.setEnabled(false);
this.registrationLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.GONE);
this.verificationFailureLayout.setVisibility(View.VISIBLE);
this.verificationFailureButton.setText(String.format("Edit %s",
PhoneNumberFormatter.formatNumberInternational(number)));
this.verificationFailureButton.setText(String.format(getString(R.string.RegistrationProgressActivity_edit_s),
PhoneNumberFormatter.formatNumberInternational(state.number)));
}
private void handleConnectivityError(String number) {
private void handleConnectivityError(RegistrationState state) {
this.registrationLayout.setVisibility(View.GONE);
this.verificationFailureLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.VISIBLE);
this.connectivityFailureButton.setText(String.format("Edit %s",
PhoneNumberFormatter.formatNumberInternational(number)));
this.connectivityFailureButton.setText(String.format(getString(R.string.RegistrationProgressActivity_edit_s),
PhoneNumberFormatter.formatNumberInternational(state.number)));
}
private void handleVerificationComplete() {
if (visible) {
Toast.makeText(this, "Registration complete", Toast.LENGTH_LONG).show();
Toast.makeText(this,
R.string.RegistrationProgressActivity_registration_complete,
Toast.LENGTH_LONG).show();
}
shutdownService();
@@ -316,7 +330,7 @@ public class RegistrationProgressActivity extends SherlockActivity {
registrationService.setRegistrationStateHandler(registrationStateHandler);
RegistrationState state = registrationService.getRegistrationState();
registrationStateHandler.obtainMessage(state.state, state.number).sendToTarget();
registrationStateHandler.obtainMessage(state.state, state).sendToTarget();
handleTimerUpdate();
}
@@ -330,17 +344,19 @@ public class RegistrationProgressActivity extends SherlockActivity {
private class RegistrationStateHandler extends Handler {
@Override
public void handleMessage(Message message) {
RegistrationState state = (RegistrationState)message.obj;
switch (message.what) {
case RegistrationState.STATE_IDLE: handleStateIdle(); break;
case RegistrationState.STATE_CONNECTING: handleStateConnecting(); break;
case RegistrationState.STATE_VERIFYING: handleStateVerifying(); break;
case RegistrationState.STATE_TIMER: handleTimerUpdate(); break;
case RegistrationState.STATE_GCM_REGISTERING: handleStateGcmRegistering(); break;
case RegistrationState.STATE_RETRIEVING_DIRECTORY: handleStateRetrievingDirectory(); break;
case RegistrationState.STATE_TIMEOUT: handleVerificationTimeout((String)message.obj); break;
case RegistrationState.STATE_COMPLETE: handleVerificationComplete(); break;
case RegistrationState.STATE_GCM_TIMEOUT: handleGcmTimeout((String)message.obj); break;
case RegistrationState.STATE_NETWORK_ERROR: handleConnectivityError((String)message.obj); break;
case RegistrationState.STATE_IDLE: handleStateIdle(); break;
case RegistrationState.STATE_CONNECTING: handleStateConnecting(); break;
case RegistrationState.STATE_VERIFYING: handleStateVerifying(); break;
case RegistrationState.STATE_TIMER: handleTimerUpdate(); break;
case RegistrationState.STATE_GCM_REGISTERING: handleStateGcmRegistering(); break;
case RegistrationState.STATE_TIMEOUT: handleVerificationTimeout(state); break;
case RegistrationState.STATE_COMPLETE: handleVerificationComplete(); break;
case RegistrationState.STATE_GCM_TIMEOUT: handleGcmTimeout(state); break;
case RegistrationState.STATE_NETWORK_ERROR: handleConnectivityError(state); break;
case RegistrationState.STATE_VOICE_REQUESTED: handleVerificationRequestedVoice(state); break;
}
}
}
@@ -362,4 +378,177 @@ public class RegistrationProgressActivity extends SherlockActivity {
abortBroadcast();
}
}
private class VerifyClickListener implements View.OnClickListener {
private static final int SUCCESS = 0;
private static final int NETWORK_ERROR = 1;
private static final int RATE_LIMIT_ERROR = 2;
private static final int VERIFICATION_ERROR = 3;
private final String e164number;
private final String password;
private final Context context;
private ProgressDialog progressDialog;
public VerifyClickListener(String e164number, String password) {
this.e164number = e164number;
this.password = password;
this.context = RegistrationProgressActivity.this;
}
@Override
public void onClick(View v) {
final String code = codeEditText.getText().toString();
if (Util.isEmpty(code)) {
Toast.makeText(context,
getString(R.string.RegistrationProgressActivity_you_must_enter_the_code_you_received_first),
Toast.LENGTH_LONG).show();
return;
}
new AsyncTask<Void, Void, Integer>() {
@Override
protected void onPreExecute() {
progressDialog = ProgressDialog.show(context,
getString(R.string.RegistrationProgressActivity_connecting),
getString(R.string.RegistrationProgressActivity_connecting_for_verification),
true, false);
}
@Override
protected void onPostExecute(Integer result) {
if (progressDialog != null) progressDialog.dismiss();
switch (result) {
case SUCCESS:
Intent intent = new Intent(context, RegistrationService.class);
intent.setAction(RegistrationService.VOICE_REGISTER_ACTION);
intent.putExtra("e164number", e164number);
intent.putExtra("password", password);
startService(intent);
break;
case NETWORK_ERROR:
Util.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_network_error),
getString(R.string.RegistrationProgressActivity_unable_to_connect));
break;
case VERIFICATION_ERROR:
Util.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_verification_failed),
getString(R.string.RegistrationProgressActivity_the_verification_code_you_submitted_is_incorrect));
break;
case RATE_LIMIT_ERROR:
Util.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_too_many_attempts),
getString(R.string.RegistrationProgressActivity_youve_submitted_an_incorrect_verification_code_too_many_times));
break;
}
}
@Override
protected Integer doInBackground(Void... params) {
try {
PushServiceSocket socket = new PushServiceSocket(context, e164number, password);
socket.verifyAccount(code);
return SUCCESS;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
return NETWORK_ERROR;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);
return RATE_LIMIT_ERROR;
}
}
}.execute();
}
}
private class CallClickListener implements View.OnClickListener {
private static final int SUCCESS = 0;
private static final int NETWORK_ERROR = 1;
private static final int RATE_LIMIT_EXCEEDED = 2;
private static final int CREATE_ERROR = 3;
private final String e164number;
private final String password;
private final Context context;
public CallClickListener(String e164number) {
this.e164number = e164number;
this.password = Util.getSecret(18);
this.context = RegistrationProgressActivity.this;
}
@Override
public void onClick(View v) {
new AsyncTask<Void, Void, Integer>() {
private ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
progressDialog = ProgressDialog.show(context,
getString(R.string.RegistrationProgressActivity_requesting_call),
getString(R.string.RegistrationProgressActivity_requesting_incoming_call),
true, false);
}
@Override
protected void onPostExecute(Integer result) {
if (progressDialog != null) progressDialog.dismiss();
switch (result) {
case SUCCESS:
Intent intent = new Intent(context, RegistrationService.class);
intent.setAction(RegistrationService.VOICE_REQUESTED_ACTION);
intent.putExtra("e164number", e164number);
intent.putExtra("password", password);
startService(intent);
callButton.setEnabled(false);
new Handler().postDelayed(new Runnable(){
@Override
public void run() {
callButton.setEnabled(true);
}
}, 15000);
break;
case NETWORK_ERROR:
Util.showAlertDialog(context,
getString(R.string.RegistrationProgressActivity_network_error),
getString(R.string.RegistrationProgressActivity_unable_to_connect));
break;
case CREATE_ERROR:
Util.showAlertDialog(context,
getString(R.string.RegistrationProgressActivity_server_error),
getString(R.string.RegistrationProgressActivity_the_server_encountered_an_error));
break;
case RATE_LIMIT_EXCEEDED:
Util.showAlertDialog(context,
getString(R.string.RegistrationProgressActivity_too_many_requests),
getString(R.string.RegistrationProgressActivity_youve_already_requested_a_voice_call));
break;
}
}
@Override
protected Integer doInBackground(Void... params) {
try {
PushServiceSocket socket = new PushServiceSocket(context, e164number, password);
socket.createAccount(true);
return SUCCESS;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
return NETWORK_ERROR;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);
return RATE_LIMIT_EXCEEDED;
}
}
}.execute();
}
}
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms;
public class Release {
public static final String PUSH_SERVICE_URL = "https://gcm.textsecure.whispersystems.org";
// public static final String PUSH_SERVICE_URL = "http://192.168.1.135:8080";
public static final boolean ENFORCE_SSL = true;
}

View File

@@ -65,15 +65,19 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity {
}
private void routeApplicationState() {
int state = getApplicationState();
switch (state) {
case STATE_CREATE_PASSPHRASE: handleCreatePassphrase(); break;
case STATE_PROMPT_PASSPHRASE: handlePromptPassphrase(); break;
case STATE_IMPORT_DATABASE: handleImportDatabase(); break;
case STATE_CONVERSATION_OR_LIST: handleDisplayConversationOrList(); break;
case STATE_UPGRADE_DATABASE: handleUpgradeDatabase(); break;
}
Intent intent = new Intent(this, RegistrationActivity.class);
startActivity(intent);
return;
//
// int state = getApplicationState();
//
// switch (state) {
// case STATE_CREATE_PASSPHRASE: handleCreatePassphrase(); break;
// case STATE_PROMPT_PASSPHRASE: handlePromptPassphrase(); break;
// case STATE_IMPORT_DATABASE: handleImportDatabase(); break;
// case STATE_CONVERSATION_OR_LIST: handleDisplayConversationOrList(); break;
// case STATE_UPGRADE_DATABASE: handleUpgradeDatabase(); break;
// }
}
private void handleCreatePassphrase() {

View File

@@ -33,6 +33,8 @@ public class GcmIntentService extends GCMBaseIntentService {
getGcmSocket(context).registerGcmId(registrationId);
} catch (IOException e) {
Log.w("GcmIntentService", e);
} catch (RateLimitException e) {
Log.w("GcmIntentService", e);
}
}
}
@@ -43,6 +45,8 @@ public class GcmIntentService extends GCMBaseIntentService {
getGcmSocket(context).unregisterGcmId(registrationId);
} catch (IOException ioe) {
Log.w("GcmIntentService", ioe);
} catch (RateLimitException e) {
Log.w("GcmIntentService", e);
}
}
@@ -71,10 +75,10 @@ public class GcmIntentService extends GCMBaseIntentService {
Log.w("GcmIntentService", "GCM Error: " + s);
}
private GcmSocket getGcmSocket(Context context) {
private PushServiceSocket getGcmSocket(Context context) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String localNumber = preferences.getString(ApplicationPreferencesActivity.LOCAL_NUMBER_PREF, null);
String password = preferences.getString(ApplicationPreferencesActivity.GCM_PASSWORD_PREF, null);
return new GcmSocket(context, localNumber, password);
return new PushServiceSocket(context, localNumber, password);
}
}

View File

@@ -58,8 +58,8 @@ public class OptimizingTransport {
return;
}
GcmSocket gcmSocket = new GcmSocket(context, localNumber, password);
gcmSocket.sendMessage(PhoneNumberFormatter.formatNumber(context, recipient), messageText);
PushServiceSocket pushServiceSocket = new PushServiceSocket(context, localNumber, password);
pushServiceSocket.sendMessage(PhoneNumberFormatter.formatNumber(context, recipient), messageText);
sentIntent.send(Activity.RESULT_OK);
} catch (IOException ioe) {
Log.w("OptimizingTransport", ioe);
@@ -67,6 +67,10 @@ public class OptimizingTransport {
sendSmsTextMessage(recipient, messageText, sentIntent, deliveredIntent);
} catch (PendingIntent.CanceledException e) {
Log.w("OptimizingTransport", e);
} catch (RateLimitException e) {
Log.w("OptimzingTransport", e);
Log.w("OptimzingTransport", "Rate Limit Exceeded, falling back to SMS...");
sendSmsTextMessage(recipient, messageText, sentIntent, deliveredIntent);
}
}

View File

@@ -6,6 +6,7 @@ import android.util.Base64;
import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import org.thoughtcrime.securesms.Release;
import org.thoughtcrime.securesms.directory.DirectoryDescriptor;
import org.thoughtcrime.securesms.directory.NumberFilter;
import org.thoughtcrime.securesms.util.Util;
@@ -19,6 +20,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
@@ -28,46 +30,48 @@ import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.zip.GZIPInputStream;
public class GcmSocket {
public class PushServiceSocket {
private static final String CREATE_ACCOUNT_PATH = "/v1/accounts/%s";
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/%s";
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/%s";
private static final String DIRECTORY_PATH = "/v1/directory/";
private static final String MESSAGE_PATH = "/v1/messages/";
private static final String CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/%s";
private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/%s";
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s";
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
private static final String DIRECTORY_PATH = "/v1/directory/";
private static final String MESSAGE_PATH = "/v1/messages/";
private final String localNumber;
private final String password;
private final TrustManagerFactory trustManagerFactory;
public GcmSocket(Context context, String localNumber, String password) {
public PushServiceSocket(Context context, String localNumber, String password) {
this.localNumber = localNumber;
this.password = password;
this.trustManagerFactory = initializeTrustManagerFactory(context);
}
public void createAccount() throws IOException {
makeRequest(String.format(CREATE_ACCOUNT_PATH, localNumber), "POST", null);
public void createAccount(boolean voice) throws IOException, RateLimitException {
String path = voice ? CREATE_ACCOUNT_VOICE_PATH : CREATE_ACCOUNT_SMS_PATH;
makeRequest(String.format(path, localNumber), "POST", null);
}
public void verifyAccount(String verificationCode, String password)
throws IOException
public void verifyAccount(String verificationCode) throws IOException, RateLimitException {
makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode), "PUT", null);
}
public void registerGcmId(String gcmRegistrationId) throws IOException, RateLimitException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
makeRequest(REGISTER_GCM_PATH, "PUT", new Gson().toJson(registration));
}
public void unregisterGcmId(String gcmRegistrationId) throws IOException, RateLimitException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
makeRequest(REGISTER_GCM_PATH, "DELETE", new Gson().toJson(registration));
}
public void sendMessage(String recipient, String messageText)
throws IOException, RateLimitException
{
Verification verification = new Verification(verificationCode, password);
makeRequest(String.format(VERIFY_ACCOUNT_PATH, localNumber), "PUT", new Gson().toJson(verification));
}
public void registerGcmId(String gcmRegistrationId) throws IOException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
makeRequest(String.format(REGISTER_GCM_PATH, localNumber), "PUT", new Gson().toJson(registration));
}
public void unregisterGcmId(String gcmRegistrationId) throws IOException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
makeRequest(String.format(REGISTER_GCM_PATH, localNumber), "DELETE", new Gson().toJson(registration));
}
public void sendMessage(String recipient, String messageText) throws IOException {
OutgoingGcmMessage message = new OutgoingGcmMessage(recipient, messageText);
String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(message));
GcmMessageResponse response = new Gson().fromJson(responseText, GcmMessageResponse.class);
@@ -89,7 +93,9 @@ public class GcmSocket {
directoryDescriptor.getVersion());
} catch (IOException ioe) {
Log.w("GcmSocket", ioe);
Log.w("PushServiceSocket", ioe);
} catch (RateLimitException e) {
Log.w("PushServiceSocket", e);
}
}
@@ -105,8 +111,8 @@ public class GcmSocket {
OutputStream output = new FileOutputStream(download);
InputStream input = new GZIPInputStream(connection.getInputStream());
int read = 0;
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
@@ -117,8 +123,10 @@ public class GcmSocket {
return download;
}
private String makeRequest(String urlFragment, String method, String body) throws IOException {
HttpsURLConnection connection = getConnection(urlFragment, method);
private String makeRequest(String urlFragment, String method, String body)
throws IOException, RateLimitException
{
HttpURLConnection connection = getConnection(urlFragment, method);
if (body != null) {
connection.setDoOutput(true);
@@ -127,12 +135,16 @@ public class GcmSocket {
connection.connect();
if (body != null) {
Log.w("GcmSocket", method + " -- " + body);
Log.w("PushServiceSocket", method + " -- " + body);
OutputStream out = connection.getOutputStream();
out.write(body.getBytes());
out.close();
}
if (connection.getResponseCode() == 413) {
throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode());
}
if (connection.getResponseCode() != 200) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
}
@@ -140,19 +152,22 @@ public class GcmSocket {
return Util.readFully(connection.getInputStream());
}
private HttpsURLConnection getConnection(String urlFragment, String method) throws IOException {
private HttpURLConnection getConnection(String urlFragment, String method) throws IOException {
try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManagerFactory.getTrustManagers(), null);
URL url = new URL(String.format("https://gcm.textsecure.whispersystems.org%s", urlFragment));
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(context.getSocketFactory());
URL url = new URL(String.format("%s%s", Release.PUSH_SERVICE_URL, urlFragment));
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
if (Release.ENFORCE_SSL) {
((HttpsURLConnection)connection).setSSLSocketFactory(context.getSocketFactory());
}
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/json");
if (password != null) {
System.out.println("Adding authorization header: " + getAuthorizationHeader());
connection.setRequestProperty("Authorization", getAuthorizationHeader());
}
@@ -198,20 +213,16 @@ public class GcmSocket {
}
private class Verification {
private String verificationCode;
private String authenticationToken;
public Verification() {}
public Verification(String verificationCode,
String authenticationToken)
{
this.verificationCode = verificationCode;
this.authenticationToken = authenticationToken;
}
}
// private class Verification {
//
// private String verificationCode;
//
// public Verification() {}
//
// public Verification(String verificationCode) {
// this.verificationCode = verificationCode;
// }
// }
private class GcmRegistrationId {
private String gcmRegistrationId;

View File

@@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.gcm;
public class RateLimitException extends Exception {
public RateLimitException(String s) {
super(s);
}
}

View File

@@ -17,7 +17,8 @@ import com.google.android.gcm.GCMRegistrar;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.gcm.GcmIntentService;
import org.thoughtcrime.securesms.gcm.GcmRegistrationTimeoutException;
import org.thoughtcrime.securesms.gcm.GcmSocket;
import org.thoughtcrime.securesms.gcm.PushServiceSocket;
import org.thoughtcrime.securesms.gcm.RateLimitException;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
@@ -25,8 +26,9 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* The RegisterationService handles the actual process of registration. If it receives an
* intent with a REGISTER_NUMBER_ACTION, it does the following through an executor:
* 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.
@@ -44,9 +46,12 @@ import java.util.concurrent.Executors;
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 REGISTER_NUMBER_ACTION = "org.thoughtcrime.securesms.RegistrationService.REGISTER_NUMBER";
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";
@@ -70,11 +75,13 @@ public class RegistrationService extends Service {
@Override
public int onStartCommand(final Intent intent, int flags, int startId) {
if (intent != null && intent.getAction().equals(REGISTER_NUMBER_ACTION)) {
if (intent != null) {
executor.execute(new Runnable() {
@Override
public void run() {
handleRegistrationIntent(intent);
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);
}
});
}
@@ -141,10 +148,58 @@ public class RegistrationService extends Service {
}
}
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);
GcmSocket socket;
String number = intent.getStringExtra("e164number");
try {
@@ -153,19 +208,18 @@ public class RegistrationService extends Service {
initializeGcmRegistrationListener();
setState(new RegistrationState(RegistrationState.STATE_CONNECTING, number));
socket = new GcmSocket(this, number, password);
socket.createAccount();
PushServiceSocket socket = new PushServiceSocket(this, number, password);
socket.createAccount(false);
setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number));
String challenge = waitForChallenge();
socket.verifyAccount(challenge, password);
socket.verifyAccount(challenge);
setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number));
GCMRegistrar.register(this, GcmIntentService.GCM_SENDER_ID);
String gcmRegistrationId = waitForGcmRegistrationId();
socket.registerGcmId(gcmRegistrationId);
setState(new RegistrationState(RegistrationState.STATE_RETRIEVING_DIRECTORY, number));
socket.registerGcmId(gcmRegistrationId);
socket.retrieveDirectory(this);
markAsVerified(number, password);
@@ -188,6 +242,10 @@ public class RegistrationService extends Service {
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();
@@ -260,7 +318,7 @@ public class RegistrationService extends Service {
this.registrationState = state;
if (registrationStateHandler != null) {
registrationStateHandler.obtainMessage(state.state, state.number).sendToTarget();
registrationStateHandler.obtainMessage(state.state, state).sendToTarget();
}
}
@@ -306,30 +364,36 @@ public class RegistrationService extends Service {
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_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_GCM_UNSUPPORTED = 8;
public static final int STATE_GCM_REGISTERING = 9;
public static final int STATE_GCM_TIMEOUT = 10;
public static final int STATE_RETRIEVING_DIRECTORY = 11;
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 = state;
this.number = number;
this(state, number, null);
}
public RegistrationState(int state, String number, String password) {
this.state = state;
this.number = number;
this.password = password;
}
}
}