Support for retrieving stored messages via websocket.

1) When registering with server, indicate that the server should
   store messages and send notifications.

2) Process notification GCM messages, and connect to the server
   to retrieve actual message content.
This commit is contained in:
Moxie Marlinspike
2015-01-25 17:43:24 -08:00
parent 023195dd4b
commit d3271f548c
25 changed files with 3300 additions and 74 deletions

View File

@@ -10,6 +10,7 @@ import android.view.WindowManager;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.MessageRetrievalService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -27,11 +28,13 @@ public class PassphraseRequiredMixin {
initializeNewKeyReceiver(activity);
initializeFromMasterSecret(activity);
KeyCachingService.registerPassphraseActivityStarted(activity);
MessageRetrievalService.registerActivityStarted(activity);
}
public <T extends Activity & PassphraseRequiredActivity> void onPause(T activity) {
removeNewKeyReceiver(activity);
KeyCachingService.registerPassphraseActivityStopped(activity);
MessageRetrievalService.registerActivityStopped(activity);
}
public <T extends Activity & PassphraseRequiredActivity> void onDestroy(T activity) {

View File

@@ -6,7 +6,6 @@ import org.thoughtcrime.securesms.Release;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
import org.thoughtcrime.securesms.jobs.CleanPreKeysJob;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DeliveryReceiptJob;
@@ -19,12 +18,13 @@ import org.thoughtcrime.securesms.push.TextSecurePushTrustStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.service.MessageRetrievalService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.TextSecureAccountManager;
import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.api.util.CredentialsProvider;
import dagger.Module;
import dagger.Provides;
@@ -36,7 +36,8 @@ import dagger.Provides;
PushTextSendJob.class,
PushMediaSendJob.class,
AttachmentDownloadJob.class,
RefreshPreKeysJob.class})
RefreshPreKeysJob.class,
MessageRetrievalService.class})
public class TextSecureCommunicationModule {
private final Context context;
@@ -77,13 +78,36 @@ public class TextSecureCommunicationModule {
@Provides TextSecureMessageReceiver provideTextSecureMessageReceiver() {
return new TextSecureMessageReceiver(Release.PUSH_URL,
new TextSecurePushTrustStore(context),
TextSecurePreferences.getLocalNumber(context),
TextSecurePreferences.getPushServerPassword(context));
new TextSecurePushTrustStore(context),
new DynamicCredentialsProvider(context));
}
public static interface TextSecureMessageSenderFactory {
public TextSecureMessageSender create(MasterSecret masterSecret);
}
private static class DynamicCredentialsProvider implements CredentialsProvider {
private final Context context;
private DynamicCredentialsProvider(Context context) {
this.context = context.getApplicationContext();
}
@Override
public String getUser() {
return TextSecurePreferences.getLocalNumber(context);
}
@Override
public String getPassword() {
return TextSecurePreferences.getPushServerPassword(context);
}
@Override
public String getSignalingKey() {
return TextSecurePreferences.getSignalingKey(context);
}
}
}

View File

@@ -10,6 +10,7 @@ import com.google.android.gms.gcm.GoogleCloudMessaging;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.jobs.PushReceiveJob;
import org.thoughtcrime.securesms.service.MessageRetrievalService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class GcmBroadcastReceiver extends BroadcastReceiver {
@@ -34,6 +35,7 @@ public class GcmBroadcastReceiver extends BroadcastReceiver {
if (!TextUtils.isEmpty(messageData)) handleReceivedMessage(context, messageData);
else if (!TextUtils.isEmpty(receiptData)) handleReceivedMessage(context, receiptData);
else if (intent.hasExtra("notification")) handleReceivedNotification(context);
}
}
@@ -42,4 +44,8 @@ public class GcmBroadcastReceiver extends BroadcastReceiver {
.getJobManager()
.add(new PushReceiveJob(context, data));
}
private void handleReceivedNotification(Context context) {
MessageRetrievalService.registerPushReceived(context);
}
}

View File

@@ -23,6 +23,7 @@ import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
import java.io.File;
import java.io.IOException;
@@ -99,8 +100,9 @@ public class AvatarDownloadJob extends MasterSecretJob {
private File downloadAttachment(String relay, long contentLocation) throws IOException {
PushServiceSocket socket = new PushServiceSocket(Release.PUSH_URL,
new TextSecurePushTrustStore(context),
TextSecurePreferences.getLocalNumber(context),
TextSecurePreferences.getPushServerPassword(context));
new StaticCredentialsProvider(TextSecurePreferences.getLocalNumber(context),
TextSecurePreferences.getPushServerPassword(context),
null));
File destination = File.createTempFile("avatar", "tmp");

View File

@@ -221,10 +221,12 @@ public class PushDecryptJob extends MasterSecretJob {
}
private void handleDuplicateMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptDuplicate(messageAndThreadId.first);
// Let's start ignoring these now.
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
// Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
// DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptDuplicate(messageAndThreadId.first);
//
// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleUntrustedIdentityMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {

View File

@@ -22,6 +22,11 @@ public class PushReceiveJob extends ContextJob {
private final String data;
public PushReceiveJob(Context context) {
super(context, JobParameters.newBuilder().create());
this.data = null;
}
public PushReceiveJob(Context context, String data) {
super(context, JobParameters.newBuilder()
.withPersistence()
@@ -39,16 +44,7 @@ public class PushReceiveJob extends ContextJob {
String sessionKey = TextSecurePreferences.getSignalingKey(context);
TextSecureEnvelope envelope = new TextSecureEnvelope(data, sessionKey);
if (!isActiveNumber(context, envelope.getSource())) {
TextSecureDirectory directory = TextSecureDirectory.getInstance(context);
ContactTokenDetails contactTokenDetails = new ContactTokenDetails();
contactTokenDetails.setNumber(envelope.getSource());
directory.setNumber(contactTokenDetails, true);
}
if (envelope.isReceipt()) handleReceipt(envelope);
else handleMessage(envelope);
handle(envelope, true);
} catch (IOException | InvalidVersionException e) {
Log.w(TAG, e);
}
@@ -64,13 +60,28 @@ public class PushReceiveJob extends ContextJob {
return false;
}
private void handleMessage(TextSecureEnvelope envelope) {
public void handle(TextSecureEnvelope envelope, boolean sendExplicitReceipt) {
if (!isActiveNumber(context, envelope.getSource())) {
TextSecureDirectory directory = TextSecureDirectory.getInstance(context);
ContactTokenDetails contactTokenDetails = new ContactTokenDetails();
contactTokenDetails.setNumber(envelope.getSource());
directory.setNumber(contactTokenDetails, true);
}
if (envelope.isReceipt()) handleReceipt(envelope);
else handleMessage(envelope, sendExplicitReceipt);
}
private void handleMessage(TextSecureEnvelope envelope, boolean sendExplicitReceipt) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
long messageId = DatabaseFactory.getPushDatabase(context).insert(envelope);
jobManager.add(new DeliveryReceiptJob(context, envelope.getSource(),
envelope.getTimestamp(),
envelope.getRelay()));
if (sendExplicitReceipt) {
jobManager.add(new DeliveryReceiptJob(context, envelope.getSource(),
envelope.getTimestamp(),
envelope.getRelay()));
}
jobManager.add(new PushDecryptJob(context, messageId));
}

View File

@@ -0,0 +1,180 @@
package org.thoughtcrime.securesms.service;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobs.PushReceiveJob;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.jobqueue.requirements.NetworkRequirementProvider;
import org.whispersystems.jobqueue.requirements.RequirementListener;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.textsecure.api.TextSecureMessagePipe;
import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.inject.Inject;
public class MessageRetrievalService extends Service implements Runnable, 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";
private static final long REQUEST_TIMEOUT_MINUTES = 1;
private NetworkRequirement networkRequirement;
private NetworkRequirementProvider networkRequirementProvider;
@Inject
public TextSecureMessageReceiver receiver;
private int activeActivities = 0;
private boolean pushPending = false;
@Override
public void onCreate() {
super.onCreate();
ApplicationContext.getInstance(this).injectDependencies(this);
networkRequirement = new NetworkRequirement(this);
networkRequirementProvider = new NetworkRequirementProvider(this);
networkRequirementProvider.setListener(this);
new Thread(this, "MessageRetrievalService").start();
}
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) return START_STICKY;
if (ACTION_ACTIVITY_STARTED.equals(intent.getAction())) incrementActive();
else if (ACTION_ACTIVITY_FINISHED.equals(intent.getAction())) decrementActive();
else if (ACTION_PUSH_RECEIVED.equals(intent.getAction())) incrementPushReceived();
return START_STICKY;
}
@Override
public void run() {
while (true) {
Log.w(TAG, "Waiting for websocket state change....");
waitForConnectionNecessary();
Log.w(TAG, "Making websocket connection....");
TextSecureMessagePipe pipe = receiver.createMessagePipe();
try {
while (isConnectionNecessary()) {
try {
Log.w(TAG, "Reading message...");
pipe.read(REQUEST_TIMEOUT_MINUTES, TimeUnit.MINUTES,
new TextSecureMessagePipe.MessagePipeCallback() {
@Override
public void onMessage(TextSecureEnvelope envelope) {
Log.w(TAG, "Retrieved envelope! " + envelope.getSource());
PushReceiveJob receiveJob = new PushReceiveJob(MessageRetrievalService.this);
receiveJob.handle(envelope, false);
decrementPushReceived();
}
});
} catch (TimeoutException | 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...");
}
}
@Override
public void onRequirementStatusChanged() {
synchronized (this) {
notifyAll();
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private synchronized void incrementActive() {
activeActivities++;
Log.w(TAG, "Active Count: " + activeActivities);
notifyAll();
}
private synchronized void decrementActive() {
activeActivities--;
Log.w(TAG, "Active Count: " + activeActivities);
notifyAll();
}
private synchronized void incrementPushReceived() {
pushPending = true;
notifyAll();
}
private synchronized void decrementPushReceived() {
pushPending = false;
notifyAll();
}
private synchronized boolean isConnectionNecessary() {
Log.w(TAG, "Network requirement: " + networkRequirement.isPresent());
return TextSecurePreferences.isWebsocketRegistered(this) &&
(activeActivities > 0 || pushPending) &&
networkRequirement.isPresent();
}
private synchronized void waitForConnectionNecessary() {
try {
while (!isConnectionNecessary()) wait();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
private void shutdown(TextSecureMessagePipe pipe) {
try {
pipe.shutdown();
} catch (Throwable t) {
Log.w(TAG, t);
}
}
public static void registerActivityStarted(Context activity) {
Intent intent = new Intent(activity, MessageRetrievalService.class);
intent.setAction(MessageRetrievalService.ACTION_ACTIVITY_STARTED);
activity.startService(intent);
}
public static void registerActivityStopped(Context activity) {
Intent intent = new Intent(activity, MessageRetrievalService.class);
intent.setAction(MessageRetrievalService.ACTION_ACTIVITY_FINISHED);
activity.startService(intent);
}
public static void registerPushReceived(Context context) {
Intent intent = new Intent(context, MessageRetrievalService.class);
intent.setAction(MessageRetrievalService.ACTION_PUSH_RECEIVED);
context.startService(intent);
}
}

View File

@@ -245,9 +245,11 @@ public class RegistrationService extends Service {
setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number));
String gcmRegistrationId = GoogleCloudMessaging.getInstance(this).register(GcmRefreshJob.REGISTRATION_ID);
TextSecurePreferences.setGcmRegistrationId(this, gcmRegistrationId);
accountManager.setGcmId(Optional.of(gcmRegistrationId));
TextSecurePreferences.setGcmRegistrationId(this, gcmRegistrationId);
TextSecurePreferences.setWebsocketRegistered(this, true);
DatabaseFactory.getIdentityDatabase(this).saveIdentity(masterSecret, self.getRecipientId(), identityKey.getPublicKey());
DirectoryHelper.refreshDirectory(this, accountManager, number);

View File

@@ -61,10 +61,19 @@ public class TextSecurePreferences {
private static final String GCM_REGISTRATION_ID_PREF = "pref_gcm_registration_id";
private static final String GCM_REGISTRATION_ID_VERSION_PREF = "pref_gcm_registration_id_version";
private static final String WEBSOCKET_REGISTERED_PREF = "pref_websocket_registered";
private static final String PUSH_REGISTRATION_REMINDER_PREF = "pref_push_registration_reminder";
public static final String REPEAT_ALERTS_PREF = "pref_repeat_alerts";
public static boolean isWebsocketRegistered(Context context) {
return getBooleanPreference(context, WEBSOCKET_REGISTERED_PREF, false);
}
public static void setWebsocketRegistered(Context context, boolean registered) {
setBooleanPreference(context, WEBSOCKET_REGISTERED_PREF, registered);
}
public static boolean isWifiSmsEnabled(Context context) {
return getBooleanPreference(context, WIFI_SMS_PREF, false);
}