mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 08:02:24 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user