mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 12:18:25 +00:00
Merge remote-tracking branch 'upstream/dev' into origin/refactor-sending
# Conflicts: # app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java # app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt # libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt # libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt # libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt # libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt
This commit is contained in:
commit
b685846d7e
@ -1,5 +1,4 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.4.0"
|
||||
ext.kovenant_version = "3.3.0"
|
||||
|
||||
repositories {
|
||||
@ -124,8 +123,8 @@ dependencies {
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
|
||||
implementation "com.github.lelloman:android-identicons:v11"
|
||||
@ -158,8 +157,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 142
|
||||
def canonicalVersionName = "1.7.4"
|
||||
def canonicalVersionCode = 147
|
||||
def canonicalVersionName = "1.9.0"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
@ -236,7 +235,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
minifyEnabled false
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
'proguard/proguard-dagger.pro',
|
||||
|
1
app/proguard/proguard.pro
vendored
1
app/proguard/proguard.pro
vendored
@ -2,6 +2,7 @@
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.whispersystems.** { *; }
|
||||
-keep class org.thoughtcrime.securesms.** { *; }
|
||||
-keep class org.session.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(**);
|
||||
}
|
||||
|
@ -29,8 +29,6 @@ import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.session.libsession.messaging.MessagingConfiguration;
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper;
|
||||
@ -39,6 +37,8 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.snode.SnodeConfiguration;
|
||||
import org.session.libsession.utilities.SSKEnvironment;
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
@ -53,8 +53,22 @@ import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
|
||||
import org.session.libsignal.service.api.util.StreamDetails;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos;
|
||||
import org.session.libsignal.service.loki.api.Poller;
|
||||
import org.session.libsignal.service.loki.api.PushNotificationAPI;
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||
import org.session.libsignal.service.loki.api.SwarmAPI;
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
@ -77,8 +91,8 @@ import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
|
||||
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
|
||||
import org.thoughtcrime.securesms.loki.utilities.FcmUtils;
|
||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
|
||||
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
@ -107,6 +121,7 @@ import java.util.Set;
|
||||
|
||||
import dagger.ObjectGraph;
|
||||
import kotlin.Unit;
|
||||
import kotlinx.coroutines.Job;
|
||||
import network.loki.messenger.BuildConfig;
|
||||
|
||||
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
|
||||
@ -114,7 +129,7 @@ import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
||||
|
||||
/**
|
||||
* Will be called once when the TextSecure process is created.
|
||||
*
|
||||
* <p>
|
||||
* We're using this as an insertion point to patch up the Android PRNG disaster,
|
||||
* to initialize the job manager, and to check for GCM registration freshness.
|
||||
*
|
||||
@ -122,424 +137,463 @@ import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
||||
*/
|
||||
public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver {
|
||||
|
||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private TypingStatusRepository typingStatusRepository;
|
||||
private TypingStatusSender typingStatusSender;
|
||||
private JobManager jobManager;
|
||||
private ReadReceiptManager readReceiptManager;
|
||||
private ProfileManager profileManager;
|
||||
private ObjectGraph objectGraph;
|
||||
private PersistentLogger persistentLogger;
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private TypingStatusRepository typingStatusRepository;
|
||||
private TypingStatusSender typingStatusSender;
|
||||
private JobManager jobManager;
|
||||
private ReadReceiptManager readReceiptManager;
|
||||
private ProfileManager profileManager;
|
||||
private ObjectGraph objectGraph;
|
||||
private PersistentLogger persistentLogger;
|
||||
|
||||
// Loki
|
||||
public MessageNotifier messageNotifier = null;
|
||||
public Poller poller = null;
|
||||
public ClosedGroupPoller closedGroupPoller = null;
|
||||
public PublicChatManager publicChatManager = null;
|
||||
private PublicChatAPI publicChatAPI = null;
|
||||
public Broadcaster broadcaster = null;
|
||||
public SignalCommunicationModule communicationModule;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
public static ApplicationContext getInstance(Context context) {
|
||||
return (ApplicationContext)context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.i(TAG, "onCreate()");
|
||||
startKovenant();
|
||||
initializeSecurityProvider();
|
||||
initializeLogging();
|
||||
initializeCrashHandling();
|
||||
initializeDependencyInjection();
|
||||
NotificationChannels.create(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
// Loki
|
||||
// ========
|
||||
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
||||
broadcaster = new Broadcaster(this);
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
|
||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
MessagingConfiguration.Companion.configure(this,
|
||||
DatabaseFactory.getStorage(this),
|
||||
DatabaseFactory.getAttachmentProvider(this),
|
||||
new SessionProtocolImpl(this));
|
||||
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
|
||||
if (userPublicKey != null) {
|
||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
||||
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
|
||||
}
|
||||
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
|
||||
setUpStorageAPIIfNeeded();
|
||||
resubmitProfilePictureIfNeeded();
|
||||
publicChatManager = new PublicChatManager(this);
|
||||
updateOpenGroupProfilePicturesIfNeeded();
|
||||
if (userPublicKey != null) {
|
||||
registerForFCMIfNeeded(false);
|
||||
}
|
||||
// Set application UI mode (day/night theme) to the user selected one.
|
||||
UiModeUtilities.setupUiModeToUserSelected(this);
|
||||
// ========
|
||||
initializeJobManager();
|
||||
initializeExpiringMessageManager();
|
||||
initializeTypingStatusRepository();
|
||||
initializeTypingStatusSender();
|
||||
initializeReadReceiptManager();
|
||||
initializeProfileManager();
|
||||
initializePeriodicTasks();
|
||||
initializeWebRtc();
|
||||
initializeBlobProvider();
|
||||
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
|
||||
}
|
||||
public MessageNotifier messageNotifier = null;
|
||||
public Poller poller = null;
|
||||
public ClosedGroupPoller closedGroupPoller = null;
|
||||
public PublicChatManager publicChatManager = null;
|
||||
private PublicChatAPI publicChatAPI = null;
|
||||
public Broadcaster broadcaster = null;
|
||||
public SignalCommunicationModule communicationModule;
|
||||
private Job firebaseInstanceIdJob;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
public static ApplicationContext getInstance(Context context) {
|
||||
return (ApplicationContext) context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.i(TAG, "onCreate()");
|
||||
startKovenant();
|
||||
initializeSecurityProvider();
|
||||
initializeLogging();
|
||||
initializeCrashHandling();
|
||||
initializeDependencyInjection();
|
||||
NotificationChannels.create(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
// Loki
|
||||
// ========
|
||||
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
||||
broadcaster = new Broadcaster(this);
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
|
||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
MessagingConfiguration.Companion.configure(this,
|
||||
DatabaseFactory.getStorage(this),
|
||||
DatabaseFactory.getAttachmentProvider(this),
|
||||
new SessionProtocolImpl(this));
|
||||
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
|
||||
if (userPublicKey != null) {
|
||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
||||
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
|
||||
}
|
||||
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
|
||||
setUpStorageAPIIfNeeded();
|
||||
resubmitProfilePictureIfNeeded();
|
||||
publicChatManager = new PublicChatManager(this);
|
||||
updateOpenGroupProfilePicturesIfNeeded();
|
||||
if (userPublicKey != null) {
|
||||
registerForFCMIfNeeded(false);
|
||||
}
|
||||
// Set application UI mode (day/night theme) to the user selected one.
|
||||
UiModeUtilities.setupUiModeToUserSelected(this);
|
||||
// ========
|
||||
initializeJobManager();
|
||||
initializeExpiringMessageManager();
|
||||
initializeTypingStatusRepository();
|
||||
initializeTypingStatusSender();
|
||||
initializeReadReceiptManager();
|
||||
initializeProfileManager();
|
||||
initializePeriodicTasks();
|
||||
initializeWebRtc();
|
||||
initializeBlobProvider();
|
||||
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = true;
|
||||
Log.i(TAG, "App is now visible.");
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
// Loki
|
||||
if (poller != null) {
|
||||
poller.setCaughtUp(false);
|
||||
}
|
||||
startPollingIfNeeded();
|
||||
publicChatManager.markAllAsNotCaughtUp();
|
||||
publicChatManager.startPollersIfNeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = false;
|
||||
Log.i(TAG, "App is no longer visible.");
|
||||
KeyCachingService.onAppBackgrounded(this);
|
||||
messageNotifier.setVisibleThread(-1);
|
||||
// Loki
|
||||
if (poller != null) {
|
||||
poller.stopIfNeeded();
|
||||
}
|
||||
if (closedGroupPoller != null) {
|
||||
closedGroupPoller.stopIfNeeded();
|
||||
}
|
||||
if (publicChatManager != null) {
|
||||
publicChatManager.stopPollers();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
stopKovenant(); // Loki
|
||||
super.onTerminate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectDependencies(Object object) {
|
||||
if (object instanceof InjectableType) {
|
||||
objectGraph.inject(object);
|
||||
}
|
||||
}
|
||||
|
||||
public void initializeLocaleParser() {
|
||||
LocaleParser.Companion.configure(new LocaleParseHelper());
|
||||
}
|
||||
|
||||
public JobManager getJobManager() {
|
||||
return jobManager;
|
||||
}
|
||||
|
||||
public ExpiringMessageManager getExpiringMessageManager() {
|
||||
return expiringMessageManager;
|
||||
}
|
||||
|
||||
public TypingStatusRepository getTypingStatusRepository() {
|
||||
return typingStatusRepository;
|
||||
}
|
||||
|
||||
public TypingStatusSender getTypingStatusSender() {
|
||||
return typingStatusSender;
|
||||
}
|
||||
|
||||
public ReadReceiptManager getReadReceiptManager() {
|
||||
return readReceiptManager;
|
||||
}
|
||||
|
||||
public ProfileManager getProfileManager() {
|
||||
return profileManager;
|
||||
}
|
||||
|
||||
public boolean isAppVisible() {
|
||||
return isAppVisible;
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
return persistentLogger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = true;
|
||||
Log.i(TAG, "App is now visible.");
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
// Loki
|
||||
if (poller != null) { poller.setCaughtUp(false); }
|
||||
startPollingIfNeeded();
|
||||
publicChatManager.markAllAsNotCaughtUp();
|
||||
publicChatManager.startPollersIfNeeded();
|
||||
MultiDeviceProtocol.syncConfigurationIfNeeded(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = false;
|
||||
Log.i(TAG, "App is no longer visible.");
|
||||
KeyCachingService.onAppBackgrounded(this);
|
||||
messageNotifier.setVisibleThread(-1);
|
||||
// Loki
|
||||
if (poller != null) { poller.stopIfNeeded(); }
|
||||
if (closedGroupPoller != null) { closedGroupPoller.stopIfNeeded(); }
|
||||
if (publicChatManager != null) { publicChatManager.stopPollers(); }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
stopKovenant(); // Loki
|
||||
super.onTerminate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectDependencies(Object object) {
|
||||
if (object instanceof InjectableType) {
|
||||
objectGraph.inject(object);
|
||||
}
|
||||
}
|
||||
|
||||
public void initializeLocaleParser() {
|
||||
LocaleParser.Companion.configure(new LocaleParseHelper());
|
||||
}
|
||||
|
||||
public JobManager getJobManager() {
|
||||
return jobManager;
|
||||
}
|
||||
|
||||
public ExpiringMessageManager getExpiringMessageManager() {
|
||||
return expiringMessageManager;
|
||||
}
|
||||
|
||||
public TypingStatusRepository getTypingStatusRepository() {
|
||||
return typingStatusRepository;
|
||||
}
|
||||
|
||||
public TypingStatusSender getTypingStatusSender() {
|
||||
return typingStatusSender;
|
||||
}
|
||||
|
||||
public ReadReceiptManager getReadReceiptManager() { return readReceiptManager; }
|
||||
|
||||
public ProfileManager getProfileManager() { return profileManager; }
|
||||
|
||||
public boolean isAppVisible() {
|
||||
return isAppVisible;
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
return persistentLogger;
|
||||
}
|
||||
|
||||
// Loki
|
||||
public @Nullable PublicChatAPI getPublicChatAPI() {
|
||||
if (publicChatAPI != null || !IdentityKeyUtil.hasIdentityKey(this)) { return publicChatAPI; }
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey== null) { return publicChatAPI; }
|
||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
||||
GroupDatabase groupDB = DatabaseFactory.getGroupDatabase(this);
|
||||
publicChatAPI = new PublicChatAPI(userPublicKey, userPrivateKey, apiDB, userDB, groupDB);
|
||||
return publicChatAPI;
|
||||
}
|
||||
|
||||
private void initializeSecurityProvider() {
|
||||
try {
|
||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.e(TAG, "Failed to find AesGcmCipher class");
|
||||
throw new ProviderInitializationException();
|
||||
public @Nullable
|
||||
PublicChatAPI getPublicChatAPI() {
|
||||
if (publicChatAPI != null || !IdentityKeyUtil.hasIdentityKey(this)) {
|
||||
return publicChatAPI;
|
||||
}
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) {
|
||||
return publicChatAPI;
|
||||
}
|
||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
||||
GroupDatabase groupDB = DatabaseFactory.getGroupDatabase(this);
|
||||
publicChatAPI = new PublicChatAPI(userPublicKey, userPrivateKey, apiDB, userDB, groupDB);
|
||||
return publicChatAPI;
|
||||
}
|
||||
|
||||
int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1);
|
||||
Log.i(TAG, "Installed AesGcmProvider: " + aesPosition);
|
||||
private void initializeSecurityProvider() {
|
||||
try {
|
||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.e(TAG, "Failed to find AesGcmCipher class");
|
||||
throw new ProviderInitializationException();
|
||||
}
|
||||
|
||||
if (aesPosition < 0) {
|
||||
Log.e(TAG, "Failed to install AesGcmProvider()");
|
||||
throw new ProviderInitializationException();
|
||||
int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1);
|
||||
Log.i(TAG, "Installed AesGcmProvider: " + aesPosition);
|
||||
|
||||
if (aesPosition < 0) {
|
||||
Log.e(TAG, "Failed to install AesGcmProvider()");
|
||||
throw new ProviderInitializationException();
|
||||
}
|
||||
|
||||
int conscryptPosition = Security.insertProviderAt(Conscrypt.newProvider(), 2);
|
||||
Log.i(TAG, "Installed Conscrypt provider: " + conscryptPosition);
|
||||
|
||||
if (conscryptPosition < 0) {
|
||||
Log.w(TAG, "Did not install Conscrypt provider. May already be present.");
|
||||
}
|
||||
}
|
||||
|
||||
int conscryptPosition = Security.insertProviderAt(Conscrypt.newProvider(), 2);
|
||||
Log.i(TAG, "Installed Conscrypt provider: " + conscryptPosition);
|
||||
|
||||
if (conscryptPosition < 0) {
|
||||
Log.w(TAG, "Did not install Conscrypt provider. May already be present.");
|
||||
private void initializeLogging() {
|
||||
persistentLogger = new PersistentLogger(this);
|
||||
Log.initialize(new AndroidLogger(), persistentLogger);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeLogging() {
|
||||
persistentLogger = new PersistentLogger(this);
|
||||
Log.initialize(new AndroidLogger(), persistentLogger);
|
||||
}
|
||||
|
||||
private void initializeCrashHandling() {
|
||||
final Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
|
||||
}
|
||||
|
||||
private void initializeJobManager() {
|
||||
this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
|
||||
.setDataSerializer(new JsonDataSerializer())
|
||||
.setJobFactories(JobManagerFactories.getJobFactories(this))
|
||||
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
|
||||
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
|
||||
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
|
||||
.setDependencyInjector(this)
|
||||
.build());
|
||||
}
|
||||
|
||||
private void initializeDependencyInjection() {
|
||||
communicationModule = new SignalCommunicationModule(this);
|
||||
this.objectGraph = ObjectGraph.create(communicationModule);
|
||||
}
|
||||
|
||||
|
||||
private void initializeExpiringMessageManager() {
|
||||
this.expiringMessageManager = new ExpiringMessageManager(this);
|
||||
}
|
||||
|
||||
private void initializeTypingStatusRepository() {
|
||||
this.typingStatusRepository = new TypingStatusRepository();
|
||||
}
|
||||
|
||||
private void initializeReadReceiptManager() {
|
||||
this.readReceiptManager = new ReadReceiptManager();
|
||||
}
|
||||
|
||||
private void initializeProfileManager() {
|
||||
this.profileManager = new ProfileManager();
|
||||
}
|
||||
|
||||
private void initializeTypingStatusSender() {
|
||||
this.typingStatusSender = new TypingStatusSender(this);
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
LocalBackupListener.schedule(this);
|
||||
BackgroundPollWorker.schedulePeriodic(this); // Loki
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
private void initializeCrashHandling() {
|
||||
final Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeWebRtc() {
|
||||
try {
|
||||
Set<String> HARDWARE_AEC_BLACKLIST = new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
add("Moto G5");
|
||||
add("Moto G (5S) Plus");
|
||||
add("Moto G4");
|
||||
add("TA-1053");
|
||||
add("Mi A1");
|
||||
add("E5823"); // Sony z5 compact
|
||||
add("Redmi Note 5");
|
||||
add("FP2"); // Fairphone FP2
|
||||
add("MI 5");
|
||||
}};
|
||||
|
||||
Set<String> OPEN_SL_ES_WHITELIST = new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
}};
|
||||
|
||||
if (HARDWARE_AEC_BLACKLIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
|
||||
}
|
||||
|
||||
if (!OPEN_SL_ES_WHITELIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
|
||||
}
|
||||
|
||||
PeerConnectionFactory.initialize(InitializationOptions.builder(this).createInitializationOptions());
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.w(TAG, e);
|
||||
private void initializeJobManager() {
|
||||
this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
|
||||
.setDataSerializer(new JsonDataSerializer())
|
||||
.setJobFactories(JobManagerFactories.getJobFactories(this))
|
||||
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
|
||||
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
|
||||
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
|
||||
.setDependencyInjector(this)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBlobProvider() {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
BlobProvider.getInstance().onSessionStart(this);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
initializeLocaleParser();
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
}
|
||||
|
||||
private static class ProviderInitializationException extends RuntimeException { }
|
||||
|
||||
// region Loki
|
||||
public boolean setUpStorageAPIIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null || !IdentityKeyUtil.hasIdentityKey(this)) { return false; }
|
||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||
org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void registerForFCMIfNeeded(Boolean force) {
|
||||
Context context = this;
|
||||
FirebaseInstanceId.getInstance().getInstanceId().addOnCompleteListener(task -> {
|
||||
if (!task.isSuccessful()) {
|
||||
Log.w("Loki", "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.getException());
|
||||
return;
|
||||
}
|
||||
String token = task.getResult().getToken();
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
if (userPublicKey == null) return;
|
||||
if (TextSecurePreferences.isUsingFCM(this)) {
|
||||
LokiPushNotificationManager.register(token, userPublicKey, context, force);
|
||||
} else {
|
||||
LokiPushNotificationManager.unregister(token, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpPollingIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return;
|
||||
if (poller != null) {
|
||||
SnodeAPI.shared.setUserPublicKey(userPublicKey);
|
||||
poller.setUserPublicKey(userPublicKey);
|
||||
return;
|
||||
private void initializeDependencyInjection() {
|
||||
communicationModule = new SignalCommunicationModule(this);
|
||||
this.objectGraph = ObjectGraph.create(communicationModule);
|
||||
}
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||
|
||||
|
||||
private void initializeExpiringMessageManager() {
|
||||
this.expiringMessageManager = new ExpiringMessageManager(this);
|
||||
}
|
||||
|
||||
private void initializeTypingStatusRepository() {
|
||||
this.typingStatusRepository = new TypingStatusRepository();
|
||||
}
|
||||
|
||||
private void initializeReadReceiptManager() {
|
||||
this.readReceiptManager = new ReadReceiptManager();
|
||||
}
|
||||
|
||||
private void initializeProfileManager() {
|
||||
this.profileManager = new ProfileManager();
|
||||
}
|
||||
|
||||
private void initializeTypingStatusSender() {
|
||||
this.typingStatusSender = new TypingStatusSender(this);
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
LocalBackupListener.schedule(this);
|
||||
BackgroundPollWorker.schedulePeriodic(this); // Loki
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeWebRtc() {
|
||||
try {
|
||||
Set<String> HARDWARE_AEC_BLACKLIST = new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
add("Moto G5");
|
||||
add("Moto G (5S) Plus");
|
||||
add("Moto G4");
|
||||
add("TA-1053");
|
||||
add("Mi A1");
|
||||
add("E5823"); // Sony z5 compact
|
||||
add("Redmi Note 5");
|
||||
add("FP2"); // Fairphone FP2
|
||||
add("MI 5");
|
||||
}};
|
||||
|
||||
Set<String> OPEN_SL_ES_WHITELIST = new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
}};
|
||||
|
||||
if (HARDWARE_AEC_BLACKLIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
|
||||
}
|
||||
|
||||
if (!OPEN_SL_ES_WHITELIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
|
||||
}
|
||||
|
||||
PeerConnectionFactory.initialize(InitializationOptions.builder(this).createInitializationOptions());
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBlobProvider() {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
BlobProvider.getInstance().onSessionStart(this);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
initializeLocaleParser();
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
}
|
||||
|
||||
private static class ProviderInitializationException extends RuntimeException {
|
||||
}
|
||||
|
||||
// region Loki
|
||||
public boolean setUpStorageAPIIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null || !IdentityKeyUtil.hasIdentityKey(this)) {
|
||||
return false;
|
||||
}
|
||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||
org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void registerForFCMIfNeeded(final Boolean force) {
|
||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
|
||||
if (force && firebaseInstanceIdJob != null) {
|
||||
firebaseInstanceIdJob.cancel(null);
|
||||
}
|
||||
firebaseInstanceIdJob = FcmUtils.getFcmInstanceId(task->{
|
||||
if (!task.isSuccessful()) {
|
||||
Log.w("Loki", "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.getException());
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
String token = task.getResult().getToken();
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return Unit.INSTANCE;
|
||||
if (TextSecurePreferences.isUsingFCM(this)) {
|
||||
LokiPushNotificationManager.register(token, userPublicKey, this, force);
|
||||
} else {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpPollingIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return;
|
||||
if (poller != null) {
|
||||
SnodeAPI.shared.setUserPublicKey(userPublicKey);
|
||||
poller.setUserPublicKey(userPublicKey);
|
||||
return;
|
||||
}
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
||||
poller = new Poller();
|
||||
ClosedGroupPoller.Companion.configureIfNeeded(this);
|
||||
closedGroupPoller = ClosedGroupPoller.Companion.getShared();
|
||||
}
|
||||
ClosedGroupPoller.Companion.configureIfNeeded(this);
|
||||
closedGroupPoller = ClosedGroupPoller.Companion.getShared();
|
||||
}
|
||||
|
||||
public void startPollingIfNeeded() {
|
||||
setUpPollingIfNeeded();
|
||||
if (poller != null) { poller.startIfNeeded(); }
|
||||
if (closedGroupPoller != null) { closedGroupPoller.startIfNeeded(); }
|
||||
}
|
||||
|
||||
public void stopPolling() {
|
||||
if (poller != null) { poller.stopIfNeeded(); }
|
||||
if (closedGroupPoller != null) { closedGroupPoller.stopIfNeeded(); }
|
||||
if (publicChatManager != null) { publicChatManager.stopPollers(); }
|
||||
}
|
||||
|
||||
private void resubmitProfilePictureIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return;
|
||||
long now = new Date().getTime();
|
||||
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
|
||||
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
||||
AsyncTask.execute(() -> {
|
||||
String encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this);
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey);
|
||||
try {
|
||||
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey));
|
||||
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length());
|
||||
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
|
||||
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateOpenGroupProfilePicturesIfNeeded() {
|
||||
AsyncTask.execute(() -> {
|
||||
PublicChatAPI publicChatAPI = null;
|
||||
try {
|
||||
publicChatAPI = getPublicChatAPI();
|
||||
} catch (Exception e) {
|
||||
// Do nothing
|
||||
}
|
||||
if (publicChatAPI == null) { return; }
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
||||
String url = TextSecurePreferences.getProfilePictureURL(this);
|
||||
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
||||
for (String server : servers) {
|
||||
if (profileKey != null) {
|
||||
publicChatAPI.setProfilePicture(server, profileKey, url);
|
||||
public void startPollingIfNeeded() {
|
||||
setUpPollingIfNeeded();
|
||||
if (poller != null) {
|
||||
poller.startIfNeeded();
|
||||
}
|
||||
if (closedGroupPoller != null) {
|
||||
closedGroupPoller.startIfNeeded();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||
String token = TextSecurePreferences.getFCMToken(this);
|
||||
if (token != null && !token.isEmpty()) {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
}
|
||||
String displayName = TextSecurePreferences.getProfileName(this);
|
||||
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
||||
TextSecurePreferences.clearAll(this);
|
||||
if (isMigratingToV2KeyPair) {
|
||||
TextSecurePreferences.setIsMigratingKeyPair(this, true);
|
||||
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
||||
TextSecurePreferences.setProfileName(this, displayName);
|
||||
}
|
||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
if (!deleteDatabase("signal.db")) {
|
||||
Log.d("Loki", "Failed to delete database.");
|
||||
}
|
||||
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
|
||||
}
|
||||
|
||||
public void restartApplication() {
|
||||
Intent intent = new Intent(this, HomeActivity.class);
|
||||
startActivity(Intent.makeRestartActivityTask(intent.getComponent()));
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
public void stopPolling() {
|
||||
if (poller != null) {
|
||||
poller.stopIfNeeded();
|
||||
}
|
||||
if (closedGroupPoller != null) {
|
||||
closedGroupPoller.stopIfNeeded();
|
||||
}
|
||||
if (publicChatManager != null) {
|
||||
publicChatManager.stopPollers();
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
private void resubmitProfilePictureIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userPublicKey == null) return;
|
||||
long now = new Date().getTime();
|
||||
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
|
||||
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
||||
AsyncTask.execute(() -> {
|
||||
String encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this);
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey);
|
||||
try {
|
||||
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey));
|
||||
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length());
|
||||
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
|
||||
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateOpenGroupProfilePicturesIfNeeded() {
|
||||
AsyncTask.execute(() -> {
|
||||
PublicChatAPI publicChatAPI = null;
|
||||
try {
|
||||
publicChatAPI = getPublicChatAPI();
|
||||
} catch (Exception e) {
|
||||
// Do nothing
|
||||
}
|
||||
if (publicChatAPI == null) {
|
||||
return;
|
||||
}
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
||||
String url = TextSecurePreferences.getProfilePictureURL(this);
|
||||
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
||||
for (String server : servers) {
|
||||
if (profileKey != null) {
|
||||
publicChatAPI.setProfilePicture(server, profileKey, url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||
String token = TextSecurePreferences.getFCMToken(this);
|
||||
if (token != null && !token.isEmpty()) {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
}
|
||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
|
||||
firebaseInstanceIdJob.cancel(null);
|
||||
}
|
||||
String displayName = TextSecurePreferences.getProfileName(this);
|
||||
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
|
||||
TextSecurePreferences.clearAll(this);
|
||||
if (isMigratingToV2KeyPair) {
|
||||
TextSecurePreferences.setIsMigratingKeyPair(this, true);
|
||||
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
|
||||
TextSecurePreferences.setProfileName(this, displayName);
|
||||
}
|
||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
if (!deleteDatabase("signal.db")) {
|
||||
Log.d("Loki", "Failed to delete database.");
|
||||
}
|
||||
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
|
||||
}
|
||||
|
||||
public void restartApplication() {
|
||||
Intent intent = new Intent(this, HomeActivity.class);
|
||||
startActivity(Intent.makeRestartActivityTask(intent.getComponent()));
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@ -157,7 +156,7 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
val resized = constraints.getResizedMedia(context, attachment)
|
||||
attachmentDatabase.updateAttachmentData(attachment, resized)
|
||||
} else {
|
||||
throw UndeliverableMessageException("Size constraints could not be met!")
|
||||
throw Exception("Size constraints could not be met!")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
|
@ -18,7 +18,6 @@ import org.session.libsession.utilities.Conversions
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.*
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.database.*
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
@ -91,7 +90,7 @@ object FullBackupExporter {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (preference in IdentityKeyUtil.getBackupRecords(context)) {
|
||||
for (preference in BackupUtil.getBackupRecords(context)) {
|
||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||
outputStream.writePreferenceEntry(preference)
|
||||
}
|
||||
|
@ -158,9 +158,9 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.QuoteId;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
@ -59,6 +59,7 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.Quote;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
@ -79,7 +80,7 @@ import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@ -93,7 +94,6 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
@ -407,7 +407,7 @@ public class ConversationFragment extends Fragment
|
||||
menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser);
|
||||
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
boolean userCanModerate = isPublicChat && PublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
|
||||
boolean userCanModerate = isPublicChat && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
|
||||
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
|
||||
// allow banning if moderating a public chat and only one user's messages are selected
|
||||
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
|
||||
@ -523,7 +523,6 @@ public class ConversationFragment extends Fragment
|
||||
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
||||
ArrayList<Long> failedMessages = new ArrayList<>();
|
||||
boolean isSentByUser = true;
|
||||
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI();
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
isSentByUser = isSentByUser && messageRecord.isOutgoing();
|
||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
|
||||
@ -533,8 +532,8 @@ public class ConversationFragment extends Fragment
|
||||
ignoredMessages.add(messageRecord.getId());
|
||||
}
|
||||
}
|
||||
if (publicChat != null && publicChatAPI != null) {
|
||||
publicChatAPI
|
||||
if (publicChat != null) {
|
||||
OpenGroupAPI
|
||||
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
||||
.success(l -> {
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
@ -604,9 +603,7 @@ public class ConversationFragment extends Fragment
|
||||
protected Void doInBackground(String... userPublicKeyParam) {
|
||||
String userPublicKey = userPublicKeyParam[0];
|
||||
if (publicChat != null) {
|
||||
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI();
|
||||
if (publicChat != null && publicChatAPI != null) {
|
||||
publicChatAPI
|
||||
OpenGroupAPI
|
||||
.ban(userPublicKey, publicChat.getServer())
|
||||
.success(l -> {
|
||||
Log.d("Loki", "User banned");
|
||||
@ -615,7 +612,6 @@ public class ConversationFragment extends Fragment
|
||||
Log.d("Loki", "Couldn't ban user due to error: " + e.toString() + ".");
|
||||
return null;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Tried to ban user from a non-public chat");
|
||||
}
|
||||
|
@ -1,88 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
|
||||
import org.session.libsignal.metadata.SignalProtos;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
|
||||
public class UnidentifiedAccessUtil {
|
||||
|
||||
private static final String TAG = UnidentifiedAccessUtil.class.getSimpleName();
|
||||
|
||||
@WorkerThread
|
||||
public static Optional<UnidentifiedAccess> getAccessFor(@NonNull Context context,
|
||||
@NonNull Recipient recipient)
|
||||
{
|
||||
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
|
||||
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context);
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
|
||||
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
|
||||
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null));
|
||||
|
||||
if (theirUnidentifiedAccessKey != null &&
|
||||
ourUnidentifiedAccessKey != null &&
|
||||
ourUnidentifiedAccessCertificate != null)
|
||||
{
|
||||
return Optional.of(new UnidentifiedAccess(theirUnidentifiedAccessKey));
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static Optional<UnidentifiedAccess> getAccessForSync(@NonNull Context context) {
|
||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
|
||||
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context);
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
|
||||
if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
return Optional.of(new UnidentifiedAccess(ourUnidentifiedAccessKey));
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) {
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context));
|
||||
}
|
||||
|
||||
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
|
||||
byte[] theirProfileKey = recipient.resolve().getProfileKey();
|
||||
|
||||
if (theirProfileKey == null) return Util.getSecretBytes(16);
|
||||
else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
|
||||
|
||||
}
|
||||
|
||||
private static @Nullable byte[] getUnidentifiedAccessCertificate(Context context) {
|
||||
String ourNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
if (ourNumber != null) {
|
||||
SignalProtos.SenderCertificate certificate = SignalProtos.SenderCertificate.newBuilder()
|
||||
.setSender(ourNumber)
|
||||
.setSenderDevice(SignalServiceAddress.DEFAULT_DEVICE_ID)
|
||||
.build();
|
||||
return certificate.toByteArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.session.libsignal.libsignal.IdentityKeyPair;
|
||||
import org.session.libsignal.libsignal.state.IdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
|
||||
public class SignalProtocolStoreImpl implements IdentityKeyStore {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SignalProtocolStoreImpl(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKeyPair getIdentityKeyPair() {
|
||||
return IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
public class ContentValuesBuilder {
|
||||
|
||||
private final ContentValues contentValues;
|
||||
|
||||
public ContentValuesBuilder(ContentValues contentValues) {
|
||||
this.contentValues = contentValues;
|
||||
}
|
||||
|
||||
public void add(String key, String charsetKey, EncodedStringValue value) {
|
||||
if (value != null) {
|
||||
contentValues.put(key, Util.toIsoString(value.getTextString()));
|
||||
contentValues.put(charsetKey, value.getCharacterSet());
|
||||
}
|
||||
}
|
||||
|
||||
public void add(String contentKey, byte[] value) {
|
||||
if (value != null) {
|
||||
contentValues.put(contentKey, Util.toIsoString(value));
|
||||
}
|
||||
}
|
||||
|
||||
public void add(String contentKey, int b) {
|
||||
if (b != 0)
|
||||
contentValues.put(contentKey, b);
|
||||
}
|
||||
|
||||
public void add(String contentKey, long value) {
|
||||
if (value != -1L)
|
||||
contentValues.put(contentKey, value);
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
return contentValues;
|
||||
}
|
||||
}
|
@ -46,12 +46,12 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
|
@ -249,7 +249,14 @@ public class RecipientDatabase extends Database {
|
||||
recipient.resolve().setProfileAvatar(profileAvatar);
|
||||
}
|
||||
|
||||
public void setProfileSharing(@NonNull Recipient recipient, @SuppressWarnings("SameParameterValue") boolean enabled) {
|
||||
public void setProfileName(@NonNull Recipient recipient, @Nullable String profileName) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(SYSTEM_DISPLAY_NAME, profileName);
|
||||
updateOrInsert(recipient.getAddress(), contentValues);
|
||||
recipient.resolve().setProfileName(profileName);
|
||||
}
|
||||
|
||||
public void setProfileSharing(@NonNull Recipient recipient, boolean enabled) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(PROFILE_SHARING, enabled ? 1 : 0);
|
||||
updateOrInsert(recipient.getAddress(), contentValues);
|
||||
|
@ -33,16 +33,16 @@ import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
|
||||
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
|
||||
@ -327,6 +327,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID)
|
||||
}
|
||||
|
||||
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
|
||||
return DatabaseFactory.getLokiMessageDatabase(context).getQuoteServerID(quoteID, publicKey)
|
||||
}
|
||||
|
||||
override fun markAsSent(timestamp: Long, author: String) {
|
||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||
@ -384,7 +388,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
|
||||
}
|
||||
|
||||
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>) {
|
||||
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
|
||||
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
.setType(type0)
|
||||
@ -392,13 +396,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
|
||||
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true)
|
||||
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
|
||||
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
|
||||
val smsDB = DatabaseFactory.getSmsDatabase(context)
|
||||
smsDB.insertMessageInbox(infoMessage)
|
||||
}
|
||||
|
||||
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long) {
|
||||
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
|
||||
val userPublicKey = getUserPublicKey()
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
@ -406,8 +411,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf())
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@ -30,6 +31,17 @@ import com.annimon.stream.Stream;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.GroupRecord;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient.RecipientSettings;
|
||||
import org.session.libsession.utilities.DelimiterUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsignal.libsignal.util.Pair;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
@ -37,32 +49,16 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.threads.GroupRecord;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient.RecipientSettings;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.DelimiterUtil;
|
||||
|
||||
import org.session.libsignal.libsignal.util.Pair;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import nl.komponents.kovenant.combine.Tuple2;
|
||||
|
||||
public class ThreadDatabase extends Database {
|
||||
|
||||
private static final String TAG = ThreadDatabase.class.getSimpleName();
|
||||
@ -507,10 +503,16 @@ public class ThreadDatabase extends Database {
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
public void notifyUpdatedFromConfig() {
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public boolean update(long threadId, boolean unarchive) {
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
long count = mmsSmsDatabase.getConversationCount(threadId);
|
||||
|
||||
|
||||
|
||||
if (count == 0) {
|
||||
deleteThread(threadId);
|
||||
notifyConversationListListeners();
|
||||
|
@ -1,245 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class XmlBackup {
|
||||
|
||||
private static final String PROTOCOL = "protocol";
|
||||
private static final String ADDRESS = "address";
|
||||
private static final String CONTACT_NAME = "contact_name";
|
||||
private static final String DATE = "date";
|
||||
private static final String READABLE_DATE = "readable_date";
|
||||
private static final String TYPE = "type";
|
||||
private static final String SUBJECT = "subject";
|
||||
private static final String BODY = "body";
|
||||
private static final String SERVICE_CENTER = "service_center";
|
||||
private static final String READ = "read";
|
||||
private static final String STATUS = "status";
|
||||
private static final String TOA = "toa";
|
||||
private static final String SC_TOA = "sc_toa";
|
||||
private static final String LOCKED = "locked";
|
||||
|
||||
private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
|
||||
|
||||
private final XmlPullParser parser;
|
||||
|
||||
public XmlBackup(String path) throws XmlPullParserException, FileNotFoundException {
|
||||
this.parser = XmlPullParserFactory.newInstance().newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(new FileInputStream(path), null);
|
||||
}
|
||||
|
||||
public XmlBackupItem getNext() throws IOException, XmlPullParserException {
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = parser.getName();
|
||||
|
||||
if (!name.equalsIgnoreCase("sms")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int attributeCount = parser.getAttributeCount();
|
||||
|
||||
if (attributeCount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XmlBackupItem item = new XmlBackupItem();
|
||||
|
||||
for (int i=0;i<attributeCount;i++) {
|
||||
String attributeName = parser.getAttributeName(i);
|
||||
|
||||
if (attributeName.equals(PROTOCOL )) item.protocol = Integer.parseInt(parser.getAttributeValue(i));
|
||||
else if (attributeName.equals(ADDRESS )) item.address = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(CONTACT_NAME )) item.contactName = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(DATE )) item.date = Long.parseLong(parser.getAttributeValue(i));
|
||||
else if (attributeName.equals(READABLE_DATE )) item.readableDate = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(TYPE )) item.type = Integer.parseInt(parser.getAttributeValue(i));
|
||||
else if (attributeName.equals(SUBJECT )) item.subject = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(BODY )) item.body = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(SERVICE_CENTER)) item.serviceCenter = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(READ )) item.read = Integer.parseInt(parser.getAttributeValue(i));
|
||||
else if (attributeName.equals(STATUS )) item.status = Integer.parseInt(parser.getAttributeValue(i));
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class XmlBackupItem {
|
||||
private int protocol;
|
||||
private String address;
|
||||
private String contactName;
|
||||
private long date;
|
||||
private String readableDate;
|
||||
private int type;
|
||||
private String subject;
|
||||
private String body;
|
||||
private String serviceCenter;
|
||||
private int read;
|
||||
private int status;
|
||||
|
||||
public XmlBackupItem() {}
|
||||
|
||||
public XmlBackupItem(int protocol, String address, String contactName, long date, int type,
|
||||
String subject, String body, String serviceCenter, int read, int status)
|
||||
{
|
||||
this.protocol = protocol;
|
||||
this.address = address;
|
||||
this.contactName = contactName;
|
||||
this.date = date;
|
||||
this.readableDate = dateFormatter.format(date);
|
||||
this.type = type;
|
||||
this.subject = subject;
|
||||
this.body = body;
|
||||
this.serviceCenter = serviceCenter;
|
||||
this.read = read;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public String getContactName() {
|
||||
return contactName;
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public String getReadableDate() {
|
||||
return readableDate;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public String getServiceCenter() {
|
||||
return serviceCenter;
|
||||
}
|
||||
|
||||
public int getRead() {
|
||||
return read;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Writer {
|
||||
|
||||
private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?>";
|
||||
private static final String CREATED_BY = "<!-- File Created By Signal -->";
|
||||
private static final String OPEN_TAG_SMSES = "<smses count=\"%d\">";
|
||||
private static final String CLOSE_TAG_SMSES = "</smses>";
|
||||
private static final String OPEN_TAG_SMS = " <sms ";
|
||||
private static final String CLOSE_EMPTYTAG = "/>";
|
||||
private static final String OPEN_ATTRIBUTE = "=\"";
|
||||
private static final String CLOSE_ATTRIBUTE = "\" ";
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("[^\u0020-\uD7FF]");
|
||||
|
||||
private final BufferedWriter bufferedWriter;
|
||||
|
||||
public Writer(String path, int count) throws IOException {
|
||||
bufferedWriter = new BufferedWriter(new FileWriter(path, false));
|
||||
|
||||
bufferedWriter.write(XML_HEADER);
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(CREATED_BY);
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(String.format(Locale.ROOT, OPEN_TAG_SMSES, count));
|
||||
}
|
||||
|
||||
public void writeItem(XmlBackupItem item) throws IOException {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
stringBuilder.append(OPEN_TAG_SMS);
|
||||
appendAttribute(stringBuilder, PROTOCOL, item.getProtocol());
|
||||
appendAttribute(stringBuilder, ADDRESS, escapeXML(item.getAddress()));
|
||||
appendAttribute(stringBuilder, CONTACT_NAME, escapeXML(item.getContactName()));
|
||||
appendAttribute(stringBuilder, DATE, item.getDate());
|
||||
appendAttribute(stringBuilder, READABLE_DATE, item.getReadableDate());
|
||||
appendAttribute(stringBuilder, TYPE, item.getType());
|
||||
appendAttribute(stringBuilder, SUBJECT, escapeXML(item.getSubject()));
|
||||
appendAttribute(stringBuilder, BODY, escapeXML(item.getBody()));
|
||||
appendAttribute(stringBuilder, TOA, "null");
|
||||
appendAttribute(stringBuilder, SC_TOA, "null");
|
||||
appendAttribute(stringBuilder, SERVICE_CENTER, item.getServiceCenter());
|
||||
appendAttribute(stringBuilder, READ, item.getRead());
|
||||
appendAttribute(stringBuilder, STATUS, item.getStatus());
|
||||
appendAttribute(stringBuilder, LOCKED, 0);
|
||||
stringBuilder.append(CLOSE_EMPTYTAG);
|
||||
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(stringBuilder.toString());
|
||||
}
|
||||
|
||||
private <T> void appendAttribute(StringBuilder stringBuilder, String name, T value) {
|
||||
stringBuilder.append(name).append(OPEN_ATTRIBUTE).append(value).append(CLOSE_ATTRIBUTE);
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(CLOSE_TAG_SMSES);
|
||||
bufferedWriter.close();
|
||||
}
|
||||
|
||||
private String escapeXML(String s) {
|
||||
if (TextUtils.isEmpty(s)) return s;
|
||||
|
||||
Matcher matcher = PATTERN.matcher( s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'"));
|
||||
StringBuffer st = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String escaped="";
|
||||
for (char ch: matcher.group(0).toCharArray()) {
|
||||
escaped += ("&#" + ((int) ch) + ";");
|
||||
}
|
||||
matcher.appendReplacement(st, escaped);
|
||||
}
|
||||
matcher.appendTail(st);
|
||||
return st.toString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -255,8 +255,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
"SmsSentJob",
|
||||
"SmsReceiveJob",
|
||||
"PushGroupUpdateJob",
|
||||
"ResetThreadSessionJob",
|
||||
"SendDeliveryReceiptJob");
|
||||
"ResetThreadSessionJob");
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV22) {
|
||||
@ -268,7 +267,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
"SendReadReceiptJob",
|
||||
"TypingSendJob",
|
||||
"AttachmentUploadJob",
|
||||
"RequestGroupInfoJob");
|
||||
"RequestGroupInfoJob",
|
||||
"ClosedGroupUpdateMessageSendJobV2",
|
||||
"SendDeliveryReceiptJob");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -27,10 +27,10 @@ import android.text.style.StyleSpan;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
@ -116,6 +116,8 @@ public class ThreadRecord extends DisplayRecord {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
} else if (SmsDatabase.Types.isIdentityDefault(type)) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
|
||||
} else if (getCount() == 0) {
|
||||
return new SpannableString(context.getString(R.string.ThreadRecord_empty_message));
|
||||
} else {
|
||||
if (TextUtils.isEmpty(getBody())) {
|
||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
|
||||
|
@ -3,18 +3,12 @@ package org.thoughtcrime.securesms.dependencies;
|
||||
import android.content.Context;
|
||||
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
@ -30,30 +24,12 @@ public class SignalCommunicationModule {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private SignalServiceMessageSender messageSender;
|
||||
private SignalServiceMessageReceiver messageReceiver;
|
||||
|
||||
public SignalCommunicationModule(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public synchronized SignalServiceMessageSender provideSignalMessageSender() {
|
||||
if (this.messageSender == null) {
|
||||
this.messageSender = new SignalServiceMessageSender(new SignalProtocolStoreImpl(context),
|
||||
TextSecurePreferences.getLocalNumber(context),
|
||||
DatabaseFactory.getLokiAPIDatabase(context),
|
||||
DatabaseFactory.getLokiThreadDatabase(context),
|
||||
DatabaseFactory.getLokiMessageDatabase(context),
|
||||
new SessionProtocolImpl(context),
|
||||
DatabaseFactory.getLokiUserDatabase(context),
|
||||
DatabaseFactory.getGroupDatabase(context),
|
||||
((ApplicationContext)context.getApplicationContext()).broadcaster);
|
||||
}
|
||||
|
||||
return this.messageSender;
|
||||
}
|
||||
|
||||
@Provides
|
||||
synchronized SignalServiceMessageReceiver provideSignalMessageReceiver() {
|
||||
if (this.messageReceiver == null) {
|
||||
|
@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJobV2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -32,7 +31,6 @@ public final class JobManagerFactories {
|
||||
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
|
||||
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
||||
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||
put(ClosedGroupUpdateMessageSendJobV2.KEY, new ClosedGroupUpdateMessageSendJobV2.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory());
|
||||
put(PushDecryptJob.KEY, new PushDecryptJob.Factory());
|
||||
|
@ -34,7 +34,7 @@ import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
@ -59,15 +59,14 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceContent;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
|
||||
@ -84,8 +83,6 @@ import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
@ -102,8 +99,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
|
||||
private MessageNotifier messageNotifier;
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
public PushDecryptJob(Context context) {
|
||||
this(context, -1);
|
||||
}
|
||||
@ -458,10 +453,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
||||
String syncTarget = message.getSyncTarget().orNull();
|
||||
|
||||
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
|
||||
handleExpirationUpdate(content, message, Optional.absent());
|
||||
}
|
||||
|
||||
Long threadId = null;
|
||||
|
||||
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
|
||||
|
@ -2,29 +2,30 @@ package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper;
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -110,6 +111,9 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
||||
if (downloadDestination != null) downloadDestination.delete();
|
||||
}
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
|
||||
}
|
||||
database.setProfileAvatar(recipient, profileAvatar);
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ import com.google.android.gms.common.util.IOUtils;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.net.CallRequestController;
|
||||
|
@ -14,6 +14,7 @@ import kotlinx.android.synthetic.main.activity_create_closed_group.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
@ -103,7 +104,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
if (selectedMembers.count() < 1) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (selectedMembers.count() >= MessageSender.groupSizeLimit) { // Minus one because we're going to include self later
|
||||
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
|
||||
|
@ -20,10 +20,10 @@ import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeIn
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
||||
@ -260,7 +260,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
val maxGroupMembers = if (isClosedGroup) MessageSender.groupSizeLimit else legacyGroupSizeLimit
|
||||
val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit
|
||||
if (members.size >= maxGroupMembers) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
@ -22,31 +22,37 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.loki.dialogs.*
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.thoughtcrime.securesms.loki.views.ConversationView
|
||||
import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegate
|
||||
import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.loki.dialogs.*
|
||||
import java.io.IOException
|
||||
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ConversationClickListener,
|
||||
SeedReminderViewDelegate,
|
||||
NewConversationButtonSetViewDelegate {
|
||||
|
||||
private lateinit var glide: GlideRequests
|
||||
private var broadcastReceiver: BroadcastReceiver? = null
|
||||
@ -57,8 +63,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor() : super()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
// Double check that the long poller is up
|
||||
@ -71,9 +75,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
glide = GlideApp.with(this)
|
||||
// Set up toolbar buttons
|
||||
profileButton.glide = glide
|
||||
profileButton.publicKey = publicKey
|
||||
profileButton.displayName = TextSecurePreferences.getProfileName(this)
|
||||
profileButton.update()
|
||||
updateProfileButton()
|
||||
profileButton.setOnClickListener { openSettings() }
|
||||
pathStatusViewContainer.disableClipping()
|
||||
pathStatusViewContainer.setOnClickListener { showPath() }
|
||||
@ -149,11 +151,19 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
}
|
||||
this.broadcastReceiver = broadcastReceiver
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
|
||||
lifecycleScope.launch {
|
||||
// update things based on TextSecurePrefs (profile info etc)
|
||||
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
|
||||
updateProfileButton()
|
||||
}
|
||||
}
|
||||
EventBus.getDefault().register(this@HomeActivity)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared
|
||||
if (TextSecurePreferences.getLocalNumber(this) == null) {
|
||||
return; } // This can be the case after a secondary device is auto-cleared
|
||||
profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||
profileButton.update()
|
||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||
@ -162,16 +172,25 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
}
|
||||
showKeyPairMigrationSheetIfNeeded()
|
||||
showKeyPairMigrationSuccessSheetIfNeeded()
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(this)) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
MultiDeviceProtocol.syncConfigurationIfNeeded(this@HomeActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showKeyPairMigrationSheetIfNeeded() {
|
||||
if (KeyPairUtilities.hasV2KeyPair(this)) { return }
|
||||
if (KeyPairUtilities.hasV2KeyPair(this)) {
|
||||
return
|
||||
}
|
||||
val keyPairMigrationSheet = KeyPairMigrationBottomSheet()
|
||||
keyPairMigrationSheet.show(supportFragmentManager, keyPairMigrationSheet.tag)
|
||||
}
|
||||
|
||||
private fun showKeyPairMigrationSuccessSheetIfNeeded() {
|
||||
if (!KeyPairUtilities.hasV2KeyPair(this) || !TextSecurePreferences.getIsMigratingKeyPair(this)) { return }
|
||||
if (!KeyPairUtilities.hasV2KeyPair(this) || !TextSecurePreferences.getIsMigratingKeyPair(this)) {
|
||||
return
|
||||
}
|
||||
val keyPairMigrationSuccessSheet = KeyPairMigrationSuccessBottomSheet()
|
||||
keyPairMigrationSuccessSheet.show(supportFragmentManager, keyPairMigrationSuccessSheet.tag)
|
||||
TextSecurePreferences.setIsMigratingKeyPair(this, false)
|
||||
@ -190,6 +209,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
super.onDestroy()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -198,6 +218,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
val threadCount = (recyclerView.adapter as HomeAdapter).itemCount
|
||||
emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onUpdateProfileEvent(event: ProfilePictureModifiedEvent) {
|
||||
if (event.recipient.isLocalNumber) {
|
||||
updateProfileButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfileButton() {
|
||||
profileButton.publicKey = publicKey
|
||||
profileButton.displayName = TextSecurePreferences.getProfileName(this)
|
||||
profileButton.recycle()
|
||||
profileButton.update()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
@ -244,34 +278,34 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
|
||||
private fun blockConversation(thread: ThreadRecord) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question)
|
||||
.setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
|
||||
ThreadUtils.queue {
|
||||
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread.recipient, true)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
.setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question)
|
||||
.setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
|
||||
ThreadUtils.queue {
|
||||
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread.recipient, true)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun unblockConversation(thread: ThreadRecord) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
|
||||
.setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
|
||||
ThreadUtils.queue {
|
||||
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread.recipient, false)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
.setTitle(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
|
||||
.setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
|
||||
ThreadUtils.queue {
|
||||
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread.recipient, false)
|
||||
Util.runOnMain {
|
||||
recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun deleteConversation(thread: ThreadRecord) {
|
||||
@ -291,52 +325,54 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
dialog.setMessage(dialogMessage)
|
||||
dialog.setPositiveButton(R.string.yes) { _, _ -> lifecycleScope.launch(Dispatchers.Main) {
|
||||
val context = this@HomeActivity as Context
|
||||
dialog.setPositiveButton(R.string.yes) { _, _ ->
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val context = this@HomeActivity as Context
|
||||
|
||||
// Send a leave group message if this is an active closed group
|
||||
if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
|
||||
var isClosedGroup: Boolean
|
||||
var groupPublicKey: String?
|
||||
try {
|
||||
groupPublicKey = GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
|
||||
isClosedGroup = DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)
|
||||
} catch (e: IOException) {
|
||||
groupPublicKey = null
|
||||
isClosedGroup = false
|
||||
// Send a leave group message if this is an active closed group
|
||||
if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
|
||||
var isClosedGroup: Boolean
|
||||
var groupPublicKey: String?
|
||||
try {
|
||||
groupPublicKey = GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
|
||||
isClosedGroup = DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)
|
||||
} catch (e: IOException) {
|
||||
groupPublicKey = null
|
||||
isClosedGroup = false
|
||||
}
|
||||
if (isClosedGroup) {
|
||||
MessageSender.explicitLeave(groupPublicKey!!)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (isClosedGroup) {
|
||||
MessageSender.explicitLeave(groupPublicKey!!)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
||||
if (publicChat != null) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
||||
|
||||
ApplicationContext.getInstance(context).publicChatAPI!!
|
||||
.leave(publicChat.channel, publicChat.server)
|
||||
|
||||
ApplicationContext.getInstance(context).publicChatManager
|
||||
.removeChat(publicChat.server, publicChat.channel)
|
||||
} else {
|
||||
threadDB.deleteConversation(threadID)
|
||||
}
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
||||
if (publicChat != null) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
||||
|
||||
ApplicationContext.getInstance(context).publicChatAPI!!
|
||||
.leave(publicChat.channel, publicChat.server)
|
||||
|
||||
ApplicationContext.getInstance(context).publicChatManager
|
||||
.removeChat(publicChat.server, publicChat.channel)
|
||||
} else {
|
||||
threadDB.deleteConversation(threadID)
|
||||
}
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
||||
}}
|
||||
}
|
||||
dialog.setNegativeButton(R.string.no) { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
@ -356,7 +392,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
|
||||
private fun openSettings() {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
show(intent)
|
||||
show(intent, isForResult = true)
|
||||
}
|
||||
|
||||
private fun showPath() {
|
||||
|
@ -3,11 +3,9 @@ package org.thoughtcrime.securesms.loki.activities
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.loki.views.FakeChatView
|
||||
@ -23,20 +21,15 @@ class LandingActivity : BaseActionBarActivity() {
|
||||
setContentView(R.layout.activity_landing)
|
||||
setUpActionBarSessionLogo(true)
|
||||
findViewById<FakeChatView>(R.id.fakeChatView).startAnimating()
|
||||
|
||||
findViewById<View>(R.id.registerButton).setOnClickListener { register() }
|
||||
findViewById<View>(R.id.restoreButton).setOnClickListener { restoreFromRecoveryPhrase() }
|
||||
findViewById<View>(R.id.restoreBackupButton).setOnClickListener { restoreFromBackup() }
|
||||
|
||||
// Setup essentials for a new user.
|
||||
findViewById<View>(R.id.restoreButton).setOnClickListener { restore() }
|
||||
findViewById<View>(R.id.linkButton).setOnClickListener { link() }
|
||||
IdentityKeyUtil.generateIdentityKeyPair(this)
|
||||
|
||||
TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode())
|
||||
TextSecurePreferences.setPasswordDisabled(this, true)
|
||||
TextSecurePreferences.setReadReceiptsEnabled(this, true)
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(this, true)
|
||||
|
||||
//AC: This is a temporary workaround to trick the old code that the screen is unlocked.
|
||||
// AC: This is a temporary workaround to trick the old code that the screen is unlocked.
|
||||
KeyCachingService.setMasterSecret(applicationContext, Object())
|
||||
}
|
||||
|
||||
@ -45,21 +38,13 @@ class LandingActivity : BaseActionBarActivity() {
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun restoreFromRecoveryPhrase() {
|
||||
private fun restore() {
|
||||
val intent = Intent(this, RecoveryPhraseRestoreActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun restoreFromBackup() {
|
||||
val intent = Intent(this, BackupRestoreActivity::class.java)
|
||||
private fun link() {
|
||||
val intent = Intent(this, LinkDeviceActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
IdentityKeyUtil.delete(this, IdentityKeyUtil.LOKI_SEED)
|
||||
TextSecurePreferences.removeLocalNumber(this)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.stopPolling()
|
||||
}
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
package org.thoughtcrime.securesms.loki.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.*
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.tabLayout
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.viewPager
|
||||
import kotlinx.android.synthetic.main.activity_link_device.*
|
||||
import kotlinx.android.synthetic.main.conversation_activity.*
|
||||
import kotlinx.android.synthetic.main.fragment_recovery_phrase.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.KeyHelper
|
||||
import org.session.libsignal.service.loki.crypto.MnemonicCodec
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
|
||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private val adapter = LinkDeviceActivityAdapter(this)
|
||||
private var restoreJob: Job? = null
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (restoreJob?.isActive == true) return // don't allow going back with pending job
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
// Set the registration sync variables
|
||||
TextSecurePreferences.apply {
|
||||
setHasViewedSeed(this@LinkDeviceActivity, true)
|
||||
setConfigurationMessageSynced(this@LinkDeviceActivity, false)
|
||||
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
|
||||
}
|
||||
// registration variables are synced
|
||||
setContentView(R.layout.activity_link_device)
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun handleQRCodeScanned(mnemonic: String) {
|
||||
try {
|
||||
val seed = Hex.fromStringCondensed(mnemonic)
|
||||
continueWithSeed(seed)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki","Error getting seed from QR code", e)
|
||||
Toast.makeText(this, "An error occurred.", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun continueWithMnemonic(mnemonic: String) {
|
||||
val loadFileContents: (String) -> String = { fileName ->
|
||||
MnemonicUtilities.loadFileContents(this, fileName)
|
||||
}
|
||||
try {
|
||||
val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic)
|
||||
val seed = Hex.fromStringCondensed(hexEncodedSeed)
|
||||
continueWithSeed(seed)
|
||||
} catch (error: Exception) {
|
||||
val message = if (error is MnemonicCodec.DecodingError) {
|
||||
error.description
|
||||
} else {
|
||||
"An error occurred."
|
||||
}
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueWithSeed(seed: ByteArray) {
|
||||
|
||||
// only have one sync job running at a time (prevent QR from trying to spawn a new job)
|
||||
if (restoreJob?.isActive == true) return
|
||||
|
||||
restoreJob = lifecycleScope.launch {
|
||||
// RestoreActivity handles seed this way
|
||||
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
|
||||
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
||||
KeyPairUtilities.store(this@LinkDeviceActivity, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
|
||||
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(this@LinkDeviceActivity, registrationID)
|
||||
TextSecurePreferences.setLocalNumber(this@LinkDeviceActivity, userHexEncodedPublicKey)
|
||||
TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
||||
TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true)
|
||||
|
||||
loader.isVisible = true
|
||||
val snackBar = Snackbar.make(containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.registration_activity__skip) { register(true) }
|
||||
|
||||
val skipJob = launch {
|
||||
delay(30_000L)
|
||||
snackBar.show()
|
||||
// show a dialog or something saying do you want to skip this bit?
|
||||
}
|
||||
// start polling and wait for updated message
|
||||
ApplicationContext.getInstance(this@LinkDeviceActivity).apply {
|
||||
setUpStorageAPIIfNeeded()
|
||||
startPollingIfNeeded()
|
||||
}
|
||||
TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect {
|
||||
// handle we've synced
|
||||
snackBar.dismiss()
|
||||
skipJob.cancel()
|
||||
register(false)
|
||||
}
|
||||
|
||||
loader.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun register(skipped: Boolean) {
|
||||
restoreJob?.cancel()
|
||||
loader.isVisible = false
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis())
|
||||
val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
push(intent)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Adapter
|
||||
private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
val recoveryPhraseFragment = RecoveryPhraseFragment()
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): Fragment {
|
||||
return when (index) {
|
||||
0 -> recoveryPhraseFragment
|
||||
1 -> {
|
||||
val result = ScanQRCodeWrapperFragment()
|
||||
result.delegate = activity
|
||||
result.message = "Navigate to Settings → Recovery Phrase on your other device to show your QR code."
|
||||
result
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(index: Int): CharSequence {
|
||||
return when (index) {
|
||||
0 -> "Recovery Phrase"
|
||||
1 -> "Scan QR Code"
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Recovery Phrase Fragment
|
||||
class RecoveryPhraseFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_recovery_phrase, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
handleContinueButtonTapped()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
||||
}
|
||||
|
||||
private fun handleContinueButtonTapped() {
|
||||
val mnemonic = mnemonicEditText.text?.trim().toString()
|
||||
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
|
||||
}
|
||||
}
|
||||
// endregion
|
@ -7,15 +7,16 @@ import android.content.Intent
|
||||
import android.graphics.drawable.TransitionDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.DrawableRes
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import kotlinx.android.synthetic.main.activity_display_name.registerButton
|
||||
import kotlinx.android.synthetic.main.activity_pn_mode.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.disableClipping
|
||||
@ -24,7 +25,6 @@ import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.loki.utilities.show
|
||||
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
class PNModeActivity : BaseActionBarActivity() {
|
||||
private var selectedOptionView: PNModeView? = null
|
||||
@ -32,7 +32,8 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
setUpActionBarSessionLogo(true)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
setContentView(R.layout.activity_pn_mode)
|
||||
contentView.disableClipping()
|
||||
fcmOptionView.setOnClickListener { toggleFCM() }
|
||||
@ -150,10 +151,11 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
dialog.create().show()
|
||||
return
|
||||
}
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView))
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
application.startPollingIfNeeded()
|
||||
application.registerForFCMIfNeeded(true)
|
||||
val intent = Intent(this, HomeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
show(intent)
|
||||
|
@ -13,16 +13,16 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.KeyHelper
|
||||
import org.session.libsignal.service.loki.crypto.MnemonicCodec
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
|
||||
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
|
||||
@ -30,6 +30,14 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
// Set the registration sync variables
|
||||
TextSecurePreferences.apply {
|
||||
setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true)
|
||||
setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false)
|
||||
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
||||
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
|
||||
}
|
||||
// registration variables are synced
|
||||
setContentView(R.layout.activity_recovery_phrase_restore)
|
||||
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
restoreButton.setOnClickListener { restore() }
|
||||
@ -69,8 +77,6 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
||||
TextSecurePreferences.setRestorationTime(this, System.currentTimeMillis())
|
||||
TextSecurePreferences.setHasViewedSeed(this, true)
|
||||
val intent = Intent(this, DisplayNameActivity::class.java)
|
||||
push(intent)
|
||||
} catch (e: Exception) {
|
||||
|
@ -18,14 +18,15 @@ import android.widget.Toast
|
||||
import com.goterl.lazycode.lazysodium.utils.KeyPair
|
||||
import kotlinx.android.synthetic.main.activity_register.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.KeyHelper
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import java.util.*
|
||||
|
||||
class RegisterActivity : BaseActionBarActivity() {
|
||||
private var seed: ByteArray? = null
|
||||
@ -38,6 +39,14 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_register)
|
||||
setUpActionBarSessionLogo()
|
||||
// Set the registration sync variables
|
||||
TextSecurePreferences.apply {
|
||||
setHasViewedSeed(this@RegisterActivity, false)
|
||||
setConfigurationMessageSynced(this@RegisterActivity, true)
|
||||
setRestorationTime(this@RegisterActivity, 0)
|
||||
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
|
||||
}
|
||||
// registration variables are synced
|
||||
registerButton.setOnClickListener { register() }
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
||||
|
@ -12,7 +12,7 @@ import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_seed.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
@ -27,29 +27,29 @@ import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.alwaysUi
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsignal.service.api.util.StreamDetails
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.dialogs.ChangeUiModeDialog
|
||||
import org.thoughtcrime.securesms.loki.dialogs.ClearAllDataDialog
|
||||
import org.thoughtcrime.securesms.loki.dialogs.SeedDialog
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.service.api.util.StreamDetails
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
@ -68,6 +68,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
return TextSecurePreferences.getLocalNumber(this)!!
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val updatedProfileResultCode = 1234
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
@ -203,6 +207,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
// updating the profile name or picture
|
||||
if (profilePicture != null || displayName != null) {
|
||||
task {
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
||||
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
|
||||
}
|
||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
||||
}
|
||||
} else {
|
||||
@ -212,15 +222,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
if (displayName != null) {
|
||||
btnGroupNameDisplay.text = displayName
|
||||
}
|
||||
displayNameToBeUploaded = null
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
||||
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
|
||||
profilePictureView.recycle() // clear cached image before update tje profilePictureView
|
||||
profilePictureView.update()
|
||||
}
|
||||
displayNameToBeUploaded = null
|
||||
profilePictureToBeUploaded = null
|
||||
loader.isVisible = false
|
||||
}
|
||||
|
@ -7,12 +7,11 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
|
@ -14,36 +14,12 @@ import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
|
||||
class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
|
||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
||||
|
||||
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
try {
|
||||
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't sign message due to error: $exception.")
|
||||
throw SessionProtocol.Exception.SigningFailed
|
||||
}
|
||||
val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature
|
||||
val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES)
|
||||
try {
|
||||
sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
|
||||
throw SessionProtocol.Exception.EncryptionFailed
|
||||
}
|
||||
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||
|
@ -11,6 +11,7 @@ import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
|
@ -11,7 +11,7 @@ import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
|
||||
class ClearAllDataDialog : DialogFragment() {
|
||||
|
||||
|
@ -13,7 +13,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import kotlinx.android.synthetic.main.dialog_seed.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.session.libsignal.service.loki.crypto.MnemonicCodec
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey
|
||||
|
@ -1,253 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.session.libsession.messaging.jobs.Data
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.loki.utilities.TTLUtilities
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters,
|
||||
private val destination: String,
|
||||
private val kind: Kind,
|
||||
private val sentTime: Long) : BaseJob(parameters) {
|
||||
|
||||
sealed class Kind {
|
||||
class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
|
||||
class Update(val name: String, val members: Collection<ByteArray>) : Kind()
|
||||
object Leave : Kind()
|
||||
class RemoveMembers(val members: Collection<ByteArray>) : Kind()
|
||||
class AddMembers(val members: Collection<ByteArray>) : Kind()
|
||||
class NameChange(val name: String) : Kind()
|
||||
class EncryptionKeyPair(val wrappers: Collection<KeyPairWrapper>, val targetUser: String?) : Kind() // The new encryption key pair encrypted for each member individually
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY = "ClosedGroupUpdateMessageSendJobV2"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class KeyPairWrapper(val publicKey: String, val encryptedKeyPair: ByteArray) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
|
||||
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun toProto(): DataMessage.ClosedGroupControlMessage.KeyPairWrapper {
|
||||
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = ByteString.copyFrom(encryptedKeyPair)
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(20)
|
||||
.build(),
|
||||
destination,
|
||||
kind,
|
||||
sentTime)
|
||||
|
||||
override fun getFactoryKey(): String { return KEY }
|
||||
|
||||
override fun serialize(): Data {
|
||||
val builder = Data.Builder()
|
||||
builder.putString("destination", destination)
|
||||
builder.putLong("sentTime", sentTime)
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
builder.putString("kind", "New")
|
||||
builder.putByteArray("publicKey", kind.publicKey)
|
||||
builder.putString("name", kind.name)
|
||||
builder.putByteArray("encryptionKeyPairPublicKey", kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
builder.putByteArray("encryptionKeyPairPrivateKey", kind.encryptionKeyPair.privateKey.serialize())
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
val admins = kind.admins.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("admins", admins)
|
||||
}
|
||||
is Kind.Update -> {
|
||||
builder.putString("kind", "Update")
|
||||
builder.putString("name", kind.name)
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
is Kind.RemoveMembers -> {
|
||||
builder.putString("kind", "RemoveMembers")
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
Kind.Leave -> {
|
||||
builder.putString("kind", "Leave")
|
||||
}
|
||||
is Kind.AddMembers -> {
|
||||
builder.putString("kind", "AddMembers")
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
builder.putString("kind", "NameChange")
|
||||
builder.putString("name", kind.name)
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
builder.putString("kind", "EncryptionKeyPair")
|
||||
val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) }
|
||||
builder.putString("wrappers", wrappers)
|
||||
builder.putString("targetUser", kind.targetUser)
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<ClosedGroupUpdateMessageSendJobV2> {
|
||||
|
||||
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
|
||||
val destination = data.getString("destination")
|
||||
val rawKind = data.getString("kind")
|
||||
val sentTime = data.getLong("sentTime")
|
||||
val kind: Kind
|
||||
when (rawKind) {
|
||||
"New" -> {
|
||||
val publicKey = data.getByteArray("publicKey")
|
||||
val name = data.getString("name")
|
||||
val encryptionKeyPairPublicKey = data.getByteArray("encryptionKeyPairPublicKey")
|
||||
val encryptionKeyPairPrivateKey = data.getByteArray("encryptionKeyPairPrivateKey")
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairPublicKey), DjbECPrivateKey(encryptionKeyPairPrivateKey))
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, members, admins)
|
||||
}
|
||||
"Update" -> {
|
||||
val name = data.getString("name")
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.Update(name, members)
|
||||
}
|
||||
"EncryptionKeyPair" -> {
|
||||
val wrappers: Collection<KeyPairWrapper> = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) }
|
||||
val targetUser = data.getString("targetUser")
|
||||
kind = Kind.EncryptionKeyPair(wrappers, targetUser)
|
||||
}
|
||||
"RemoveMembers" -> {
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.RemoveMembers(members)
|
||||
}
|
||||
"AddMembers" -> {
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.AddMembers(members)
|
||||
}
|
||||
"NameChange" -> {
|
||||
val name = data.getString("name")
|
||||
kind = Kind.NameChange(name)
|
||||
}
|
||||
"Leave" -> {
|
||||
kind = Kind.Leave
|
||||
}
|
||||
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
|
||||
}
|
||||
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onRun() {
|
||||
val sendDestination = if (kind is Kind.EncryptionKeyPair && kind.targetUser != null) {
|
||||
kind.targetUser
|
||||
} else {
|
||||
destination
|
||||
}
|
||||
val contentMessage = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessage = DataMessage.newBuilder()
|
||||
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(kind.publicKey)
|
||||
closedGroupUpdate.name = kind.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
|
||||
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.Update -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.UPDATE
|
||||
closedGroupUpdate.name = kind.name
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
|
||||
closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() })
|
||||
if (kind.targetUser != null) {
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(destination))
|
||||
}
|
||||
}
|
||||
Kind.Leave -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
|
||||
}
|
||||
is Kind.RemoveMembers -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.AddMembers -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
|
||||
closedGroupUpdate.name = kind.name
|
||||
}
|
||||
}
|
||||
dataMessage.closedGroupControlMessage = closedGroupUpdate.build()
|
||||
contentMessage.dataMessage = dataMessage.build()
|
||||
val serializedContentMessage = contentMessage.build().toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(sendDestination)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(sendDestination), false)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
val ttl = when (kind) {
|
||||
is Kind.EncryptionKeyPair -> 4 * 24 * 60 * 60 * 1000
|
||||
else -> TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate)
|
||||
}
|
||||
try {
|
||||
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
|
||||
messageSender.sendMessage(0, address, udAccess,
|
||||
sentTime, serializedContentMessage, false, ttl,
|
||||
true, false, false, Optional.absent())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send closed group update message to: $sendDestination due to error: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onShouldRetry(e: Exception): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCanceled() { }
|
||||
}
|
@ -3,32 +3,25 @@ package org.thoughtcrime.securesms.loki.protocol
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair
|
||||
import org.session.libsession.messaging.sending_receiving.pendingKeyPair
|
||||
import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair
|
||||
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
@ -37,258 +30,8 @@ import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object ClosedGroupsProtocolV2 {
|
||||
const val groupSizeLimit = 100
|
||||
|
||||
private val pendingKeyPair = ConcurrentHashMap<String,Optional<ECKeyPair>>()
|
||||
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key")
|
||||
object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.")
|
||||
object InvalidUpdate : Error("Invalid group update.")
|
||||
}
|
||||
|
||||
fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Generate the group's public key
|
||||
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
|
||||
val sentTime = System.currentTimeMillis()
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
val encryptionKeyPair = Curve.generateKeyPair()
|
||||
// Create the group
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }), System.currentTimeMillis())
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Send a closed group update message to all members individually
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
apiDB.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
|
||||
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
for (member in members) {
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately to make all of this sync
|
||||
}
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
deferred.resolve(groupID)
|
||||
}
|
||||
// Return
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @param notifyUser Inserts an outgoing info message for the user's leave message, useful to set `false` if
|
||||
* you are exiting asynchronously and deleting the thread from [HomeActivity][org.thoughtcrime.securesms.loki.activities.HomeActivity.deleteConversation]
|
||||
*/
|
||||
@JvmStatic @JvmOverloads
|
||||
fun explicitLeave(context: Context, groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
ThreadUtils.queue {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
val sentTime = System.currentTimeMillis()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return@queue deferred.reject(Error.NoThread)
|
||||
}
|
||||
// Send the update to the group
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.QUIT
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
if (notifyUser) {
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List<String>) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd
|
||||
val membersAsData = updatedMembers.map { Hex.fromStringCondensed(it) }
|
||||
val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() ?: Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)).orNull()
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
throw Error.NoKeyPair
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Save the new group members
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime)
|
||||
ApplicationContext.getInstance(context).jobManager.add(newMemberJob)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List<String>) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove
|
||||
val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
throw Error.NoKeyPair
|
||||
}
|
||||
if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) {
|
||||
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
|
||||
throw Error.InvalidUpdate
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Save the new group members
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitNameChange(context: Context, groupPublicKey: String, newName: String) {
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val members = group.members.map { it.serialize() }.toSet()
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, newName)
|
||||
}
|
||||
|
||||
private fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection<String>) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
return
|
||||
}
|
||||
if (!group.admins.map { it.toString() }.contains(userPublicKey)) {
|
||||
Log.d("Loki", "Can't distribute new encryption key pair as non-admin.")
|
||||
return
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
val newKeyPair = Curve.generateKeyPair()
|
||||
// replace call will not succeed if no value already set
|
||||
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
|
||||
do {
|
||||
// make sure we set the pendingKeyPair or wait until it is not null
|
||||
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Distribute it
|
||||
sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers)
|
||||
// Store it * after * having sent out the message to the group
|
||||
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
}
|
||||
|
||||
private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true) {
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val wrappers = targetMembers.mapNotNull { publicKey ->
|
||||
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
|
||||
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
|
||||
}
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers, targetUser), System.currentTimeMillis())
|
||||
if (force) {
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
} else {
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
|
||||
@ -361,11 +104,11 @@ object ClosedGroupsProtocolV2 {
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user (if we didn't make the group)
|
||||
if (userPublicKey != senderPublicKey) {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
} else if (prevGroup == null) {
|
||||
// only notify if we created this group
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
}
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
@ -410,7 +153,7 @@ object ClosedGroupsProtocolV2 {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, newMembers)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
|
||||
}
|
||||
}
|
||||
val (contextType, signalType) =
|
||||
@ -418,9 +161,9 @@ object ClosedGroupsProtocolV2 {
|
||||
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -448,9 +191,9 @@ object ClosedGroupsProtocolV2 {
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
@ -460,7 +203,7 @@ object ClosedGroupsProtocolV2 {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
for (user in updateMembers) {
|
||||
sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -487,9 +230,9 @@ object ClosedGroupsProtocolV2 {
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -523,15 +266,15 @@ object ClosedGroupsProtocolV2 {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
// Notify user
|
||||
if (userLeft) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -598,59 +341,6 @@ object ClosedGroupsProtocolV2 {
|
||||
Log.d("Loki", "Received a new closed group encryption key pair")
|
||||
}
|
||||
|
||||
private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
.setType(type0)
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
|
||||
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
|
||||
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
|
||||
val smsDB = DatabaseFactory.getSmsDatabase(context)
|
||||
smsDB.insertMessageInbox(infoMessage)
|
||||
}
|
||||
|
||||
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long,
|
||||
sentTimestamp: Long) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
.setType(type)
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTimestamp)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getMessageDestinations(context: Context, groupID: String): List<Address> {
|
||||
return if (GroupUtil.isOpenGroup(groupID)) {
|
||||
listOf(Address.fromSerialized(groupID))
|
||||
} else {
|
||||
var groupPublicKey: String? = null
|
||||
try {
|
||||
groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString() // TODO: The toHexString() here might be unnecessary
|
||||
} catch (exception: Exception) {
|
||||
// Do nothing
|
||||
}
|
||||
if (groupPublicKey != null && DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)) {
|
||||
listOf(Address.fromSerialized(groupPublicKey))
|
||||
} else {
|
||||
DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Deprecated
|
||||
private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Prepare
|
||||
@ -685,7 +375,7 @@ object ClosedGroupsProtocolV2 {
|
||||
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
|
||||
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
|
||||
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
@ -700,9 +390,9 @@ object ClosedGroupsProtocolV2 {
|
||||
val admins = group.admins.map { it.toString() }
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@ -7,11 +7,17 @@ import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
|
||||
@ -42,39 +48,84 @@ object MultiDeviceProtocol {
|
||||
}
|
||||
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return
|
||||
MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)))
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
// TODO: remove this after we migrate to new message receiving pipeline
|
||||
@JvmStatic
|
||||
fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) {
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(context)) return
|
||||
val configurationMessage = ConfigurationMessage.fromProto(content) ?: return
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
if (senderPublicKey != userPublicKey) return
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closedGroup in configurationMessage.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
|
||||
synchronized(this) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, timestamp)) return
|
||||
if (senderPublicKey != userPublicKey) return
|
||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||
TextSecurePreferences.setLastProfileUpdateTime(context, timestamp)
|
||||
|
||||
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey))
|
||||
closedGroupUpdate.name = closedGroup.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.privateKey.serialize())
|
||||
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
val configurationMessage = ConfigurationMessage.fromProto(content) ?: return
|
||||
|
||||
ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
for (openGroup in configurationMessage.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup)) continue
|
||||
OpenGroupUtilities.addGroup(context, openGroup, 1)
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
|
||||
val threadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
|
||||
val ourRecipient = Recipient.from(context, Address.fromSerialized(userPublicKey),false)
|
||||
|
||||
for (closedGroup in configurationMessage.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
|
||||
|
||||
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey))
|
||||
closedGroupUpdate.name = closedGroup.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.privateKey.serialize())
|
||||
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
|
||||
ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
for (openGroup in configurationMessage.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup)) continue
|
||||
OpenGroupUtilities.addGroup(context, openGroup, 1)
|
||||
}
|
||||
if (configurationMessage.displayName.isNotEmpty()) {
|
||||
TextSecurePreferences.setProfileName(context, configurationMessage.displayName)
|
||||
recipientDatabase.setProfileName(ourRecipient, configurationMessage.displayName)
|
||||
}
|
||||
if (configurationMessage.profileKey.isNotEmpty()) {
|
||||
val profileKey = Base64.encodeBytes(configurationMessage.profileKey)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||
recipientDatabase.setProfileKey(ourRecipient, configurationMessage.profileKey)
|
||||
if (!configurationMessage.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != configurationMessage.profilePicture) {
|
||||
TextSecurePreferences.setProfilePictureURL(context, configurationMessage.profilePicture)
|
||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, configurationMessage.profilePicture))
|
||||
}
|
||||
}
|
||||
for (contact in configurationMessage.contacts) {
|
||||
val address = Address.fromSerialized(contact.publicKey)
|
||||
val recipient = Recipient.from(context, address, true)
|
||||
if (!contact.profilePicture.isNullOrEmpty()) {
|
||||
recipientDatabase.setProfileAvatar(recipient, contact.profilePicture)
|
||||
}
|
||||
if (contact.profileKey?.isNotEmpty() == true) {
|
||||
recipientDatabase.setProfileKey(recipient, contact.profileKey)
|
||||
}
|
||||
if (contact.name.isNotEmpty()) {
|
||||
recipientDatabase.setProfileName(recipient, contact.name)
|
||||
}
|
||||
recipientDatabase.setProfileSharing(recipient, true)
|
||||
recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED)
|
||||
// create Thread if needed
|
||||
threadDatabase.getOrCreateThreadIdFor(recipient)
|
||||
}
|
||||
if (configurationMessage.contacts.isNotEmpty()) {
|
||||
threadDatabase.notifyUpdatedFromConfig()
|
||||
}
|
||||
}
|
||||
// TODO: handle new configuration message fields or handle in new pipeline
|
||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
@file:JvmName("FcmUtils")
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import com.google.firebase.iid.InstanceIdResult
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
|
||||
fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().launch(Dispatchers.IO) {
|
||||
val task = FirebaseInstanceId.getInstance().instanceId
|
||||
while (!task.isComplete && isActive) {
|
||||
// wait for task to complete while we are active
|
||||
}
|
||||
if (!isActive) return@launch // don't 'complete' task if we were canceled
|
||||
withContext(Dispatchers.Main) {
|
||||
body(task)
|
||||
}
|
||||
}
|
@ -12,12 +12,12 @@ import kotlinx.android.synthetic.main.view_profile_picture.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.avatars.ProfileContactPhoto
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
// TODO: Look into a better way of handling different sizes. Maybe an enum (with associated values) encapsulating the different modes?
|
||||
|
||||
@ -145,14 +145,14 @@ class ProfilePictureView : RelativeLayout {
|
||||
|
||||
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
|
||||
if (publicKey.isNotEmpty()) {
|
||||
if (imagesCached.contains(publicKey)) return
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||
if (imagesCached.contains(recipient.profileAvatar.orEmpty())) return
|
||||
val signalProfilePicture = recipient.contactPhoto
|
||||
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0"
|
||||
&& (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
|
||||
glide.clear(imageView)
|
||||
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
||||
imagesCached.add(publicKey)
|
||||
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView)
|
||||
imagesCached.add(recipient.profileAvatar.orEmpty())
|
||||
} else {
|
||||
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
|
||||
glide.clear(imageView)
|
||||
@ -162,7 +162,7 @@ class ProfilePictureView : RelativeLayout {
|
||||
publicKey,
|
||||
displayName
|
||||
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
||||
imagesCached.add(publicKey)
|
||||
imagesCached.add(recipient.profileAvatar.orEmpty())
|
||||
}
|
||||
} else {
|
||||
imageView.setImageDrawable(null)
|
||||
|
@ -32,7 +32,7 @@ import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
@ -33,14 +33,20 @@ import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.ServiceUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.service.internal.util.Util;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@ -50,16 +56,11 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.session.libsession.utilities.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.service.internal.util.Util;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -247,8 +248,8 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread();
|
||||
pushCursor = DatabaseFactory.getPushDatabase(context).getPending();
|
||||
|
||||
if ((telcoCursor == null || telcoCursor.isAfterLast()) &&
|
||||
(pushCursor == null || pushCursor.isAfterLast()))
|
||||
if (((telcoCursor == null || telcoCursor.isAfterLast()) &&
|
||||
(pushCursor == null || pushCursor.isAfterLast())) || !TextSecurePreferences.hasSeenWelcomeScreen(context))
|
||||
{
|
||||
cancelActiveNotifications(context);
|
||||
updateBadge(context, 0);
|
||||
|
@ -27,13 +27,14 @@ import androidx.core.app.RemoteInput;
|
||||
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
@ -72,6 +73,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
VisibleMessage message = new VisibleMessage();
|
||||
message.setSentTimestamp(System.currentTimeMillis());
|
||||
message.setText(responseText.toString());
|
||||
|
||||
switch (replyMethod) {
|
||||
@ -79,6 +81,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
||||
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
||||
try {
|
||||
DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(reply, threadId, false, null);
|
||||
MessageSender.send(message, address);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
@ -87,6 +90,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
||||
case SecureMessage: {
|
||||
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
|
||||
DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null);
|
||||
MessageSender.send(message, address);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@ -1,12 +1,7 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -14,34 +9,16 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.backup.BackupDialog;
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProgressPreference;
|
||||
import org.thoughtcrime.securesms.util.BackupDirSelector;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Trimmer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
private static final String TAG = ChatsPreferenceFragment.class.getSimpleName();
|
||||
|
||||
private BackupDirSelector backupDirSelector;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
@ -51,14 +28,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH)
|
||||
.setOnPreferenceChangeListener(new TrimLengthValidationListener());
|
||||
|
||||
findPreference(TextSecurePreferences.BACKUP_ENABLED)
|
||||
.setOnPreferenceClickListener(new BackupClickListener());
|
||||
findPreference(TextSecurePreferences.BACKUP_NOW)
|
||||
.setOnPreferenceClickListener(new BackupCreateListener());
|
||||
|
||||
backupDirSelector = new BackupDirSelector(this);
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,13 +38,11 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setBackupSummary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -83,79 +50,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
backupDirSelector.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(BackupEvent event) {
|
||||
ProgressPreference preference = findPreference(TextSecurePreferences.BACKUP_NOW);
|
||||
|
||||
if (event.getType() == BackupEvent.Type.PROGRESS) {
|
||||
preference.setEnabled(false);
|
||||
preference.setSummary(getString(R.string.ChatsPreferenceFragment_in_progress));
|
||||
preference.setProgress(event.getCount());
|
||||
} else if (event.getType() == BackupEvent.Type.FINISHED) {
|
||||
preference.setEnabled(true);
|
||||
preference.setProgressVisible(false);
|
||||
setBackupSummary();
|
||||
|
||||
if (event.getException() != null) {
|
||||
Toast.makeText(
|
||||
getActivity(),
|
||||
getString(R.string.preferences_chats__backup_export_error),
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setBackupSummary() {
|
||||
findPreference(TextSecurePreferences.BACKUP_NOW)
|
||||
.setSummary(String.format(getString(R.string.ChatsPreferenceFragment_last_backup_s),
|
||||
BackupUtil.getLastBackupTimeString(getContext(), Locale.getDefault())));
|
||||
}
|
||||
|
||||
private CharSequence getSummaryForMediaPreference(Set<String> allowedNetworks) {
|
||||
String[] keys = getResources().getStringArray(R.array.pref_media_download_entries);
|
||||
String[] values = getResources().getStringArray(R.array.pref_media_download_values);
|
||||
List<String> outValues = new ArrayList<>(allowedNetworks.size());
|
||||
|
||||
for (int i=0; i < keys.length; i++) {
|
||||
if (allowedNetworks.contains(keys[i])) outValues.add(values[i]);
|
||||
}
|
||||
|
||||
return outValues.isEmpty() ? getResources().getString(R.string.preferences__none)
|
||||
: TextUtils.join(", ", outValues);
|
||||
}
|
||||
|
||||
private class BackupClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (!((SwitchPreferenceCompat) preference).isChecked()) {
|
||||
BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference, backupDirSelector);
|
||||
} else {
|
||||
BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class BackupCreateListener implements Preference.OnPreferenceClickListener {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Log.i(TAG, "Queuing backup...");
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new LocalBackupJob());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class TrimNowClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
@ -179,19 +73,10 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaDownloadChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
Log.i(TAG, "onPreferenceChange");
|
||||
preference.setSummary(getSummaryForMediaPreference((Set<String>)newValue));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class TrimLengthValidationListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
public TrimLengthValidationListener() {
|
||||
EditTextPreference preference = (EditTextPreference)findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH);
|
||||
EditTextPreference preference = findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH);
|
||||
onPreferenceChange(preference, preference.getText());
|
||||
}
|
||||
|
||||
@ -218,7 +103,4 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
|
||||
import java.util.Comparator;
|
||||
@ -70,6 +70,9 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Address address = Address.fromSerialized(senderPublicKey);
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
|
||||
if (recipient.isBlocked()) return;
|
||||
|
||||
Optional<SignalServiceGroup> groupInfo = Optional.absent();
|
||||
if (content.getDataMessage().hasGroup()) {
|
||||
GroupContext groupContext = content.getDataMessage().getGroup();
|
||||
|
@ -51,6 +51,10 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr
|
||||
return;
|
||||
}
|
||||
|
||||
if (Recipient.from(context, author, false).isBlocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Typist> typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>());
|
||||
Typist typist = new Typist(Recipient.from(context, author, false), device, threadId);
|
||||
|
||||
@ -76,6 +80,10 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr
|
||||
return;
|
||||
}
|
||||
|
||||
if (Recipient.from(context, author, false).isBlocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Typist> typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>());
|
||||
Typist typist = new Typist(Recipient.from(context, author, false), device, threadId);
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
package org.thoughtcrime.securesms.transport;
|
||||
|
||||
public class RetryLaterException extends Exception {
|
||||
public RetryLaterException() {}
|
||||
|
||||
public RetryLaterException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package org.thoughtcrime.securesms.transport;
|
||||
|
||||
public class UndeliverableMessageException extends Exception {
|
||||
|
||||
public UndeliverableMessageException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public UndeliverableMessageException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
@ -7,35 +7,73 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import network.loki.messenger.R
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.ByteUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent
|
||||
import org.thoughtcrime.securesms.backup.BackupPassphrase
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
||||
import org.thoughtcrime.securesms.backup.FullBackupExporter
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.database.BackupFileRecord
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.ByteUtil
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object BackupUtil {
|
||||
private const val MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences"
|
||||
private const val TAG = "BackupUtil"
|
||||
const val BACKUP_FILE_MIME_TYPE = "application/session-backup"
|
||||
const val BACKUP_PASSPHRASE_LENGTH = 30
|
||||
|
||||
fun getBackupRecords(context: Context): List<SharedPreference> {
|
||||
val prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME
|
||||
val preferences = context.getSharedPreferences(prefName, 0)
|
||||
val prefList = LinkedList<SharedPreference>()
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, null))
|
||||
.build())
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, null))
|
||||
.build())
|
||||
if (preferences.contains(IdentityKeyUtil.ED25519_PUBLIC_KEY)) {
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.ED25519_PUBLIC_KEY)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.ED25519_PUBLIC_KEY, null))
|
||||
.build())
|
||||
}
|
||||
if (preferences.contains(IdentityKeyUtil.ED25519_SECRET_KEY)) {
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.ED25519_SECRET_KEY)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.ED25519_SECRET_KEY, null))
|
||||
.build())
|
||||
}
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.LOKI_SEED)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.LOKI_SEED, null))
|
||||
.build())
|
||||
return prefList
|
||||
}
|
||||
|
||||
/**
|
||||
* Set app-wide configuration to enable the backups and schedule them.
|
||||
*
|
||||
@ -91,7 +129,7 @@ object BackupUtil {
|
||||
@JvmStatic
|
||||
fun generateBackupPassphrase(): Array<String> {
|
||||
val random = ByteArray(BACKUP_PASSPHRASE_LENGTH).also { SecureRandom().nextBytes(it) }
|
||||
return Array(6) {i ->
|
||||
return Array(6) { i ->
|
||||
String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000)
|
||||
}
|
||||
}
|
||||
|
@ -49,27 +49,16 @@
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:text="@string/activity_landing_restore_button_title" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/restoreBackupButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:text="@string/activity_landing_restore_backup_button_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/linkButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/onboarding_button_bottom_offset"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:visibility="invisible"
|
||||
android:gravity="center"
|
||||
android:background="@color/transparent"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:text="@string/activity_landing_link_button_title" />
|
||||
android:text="Link a Device" />
|
||||
|
||||
</LinearLayout>
|
63
app/src/main/res/layout-sw400dp/fragment_recovery_phrase.xml
Normal file
63
app/src/main/res/layout-sw400dp/fragment_recovery_phrase.xml
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/very_large_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Recovery Phrase" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="To link your device, enter the recovery phrase that was given to you when you signed up." />
|
||||
|
||||
<EditText
|
||||
style="@style/SessionEditText"
|
||||
android:id="@+id/mnemonicEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:gravity="center_vertical"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="2"
|
||||
android:hint="@string/activity_restore_seed_edit_text_hint" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentFilled"
|
||||
android:id="@+id/continueButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:text="@string/continue_2" />
|
||||
|
||||
</LinearLayout>
|
@ -49,27 +49,16 @@
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:text="@string/activity_landing_restore_button_title" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/restoreBackupButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginTop="@dimen/small_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:text="@string/activity_landing_restore_backup_button_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/linkButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/onboarding_button_bottom_offset"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:visibility="invisible"
|
||||
android:gravity="center"
|
||||
android:background="@color/transparent"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:text="@string/activity_landing_link_button_title" />
|
||||
android:text="Link a Device" />
|
||||
|
||||
</LinearLayout>
|
46
app/src/main/res/layout/activity_link_device.xml
Normal file
46
app/src/main/res/layout/activity_link_device.xml
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/containerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height" />
|
||||
|
||||
</androidx.viewpager.widget.ViewPager>
|
||||
|
||||
<FrameLayout
|
||||
android:animateLayoutChanges="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#A4000000"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_centerInParent="true"
|
||||
app:SpinKit_Color="@android:color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
@ -13,13 +13,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<org.thoughtcrime.securesms.components.location.SignalMapView
|
||||
android:id="@+id/attachment_location"
|
||||
android:layout_width="210dp"
|
||||
android:layout_height="210dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThumbnailView
|
||||
android:id="@+id/attachment_thumbnail"
|
||||
android:layout_width="230dp"
|
||||
|
@ -67,14 +67,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:text="@string/dialog_seed_disclaimer"
|
||||
android:textColor="@color/text"
|
||||
android:alpha="0.6"
|
||||
android:textSize="10sp"
|
||||
android:textAlignment="center" />
|
||||
|
||||
</LinearLayout>
|
63
app/src/main/res/layout/fragment_recovery_phrase.xml
Normal file
63
app/src/main/res/layout/fragment_recovery_phrase.xml
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/large_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Recovery Phrase" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/activity_restore_explanation" />
|
||||
|
||||
<EditText
|
||||
style="@style/SmallSessionEditText"
|
||||
android:id="@+id/mnemonicEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:gravity="center_vertical"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="2"
|
||||
android:hint="@string/activity_restore_seed_edit_text_hint" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentFilled"
|
||||
android:id="@+id/continueButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:text="@string/continue_2" />
|
||||
|
||||
</LinearLayout>
|
@ -707,6 +707,7 @@
|
||||
<string name="ThreadRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
|
||||
<string name="ThreadRecord_you_marked_verified">You marked verified</string>
|
||||
<string name="ThreadRecord_you_marked_unverified">You marked unverified</string>
|
||||
<string name="ThreadRecord_empty_message">This conversation is empty</string>
|
||||
|
||||
<!-- UpdateApkReadyListener -->
|
||||
<string name="UpdateApkReadyListener_Signal_update">Session update</string>
|
||||
@ -1881,5 +1882,7 @@
|
||||
<string name="activity_backup_restore_select_file">Select a file</string>
|
||||
<string name="activity_backup_restore_explanation_1">Select a backup file and enter the passphrase it was created with.</string>
|
||||
<string name="activity_backup_restore_passphrase">30-digit passphrase</string>
|
||||
<!-- LinkDeviceActivity -->
|
||||
<string name="activity_link_device_skip_prompt">This is taking a while, would you like to skip?</string>
|
||||
|
||||
</resources>
|
||||
|
@ -88,7 +88,7 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider" />
|
||||
<!-- <PreferenceCategory android:layout="@layout/preference_divider" />
|
||||
|
||||
<PreferenceCategory android:key="backup_category" android:title="@string/preferences_chats__backups">
|
||||
|
||||
@ -105,6 +105,6 @@
|
||||
android:dependency="pref_backup_enabled_v3"
|
||||
tools:summary="Last backup: 3 days ago" />
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceCategory> -->
|
||||
|
||||
</PreferenceScreen>
|
||||
|
@ -58,6 +58,7 @@ allprojects {
|
||||
}
|
||||
|
||||
project.ext {
|
||||
kotlin_version = "1.4.31"
|
||||
androidBuildToolsVersion = '29.0.3'
|
||||
androidCompileSdkVersion = 29 // This is also our target SDK.
|
||||
androidMinSdkVersion = 21
|
||||
|
@ -38,7 +38,7 @@ dependencies {
|
||||
// Remote:
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
implementation "com.goterl.lazycode:lazysodium-android:4.2.0@aar"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
@ -61,10 +61,10 @@ dependencies {
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation "org.threeten:threetenbp:1.3.6"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
|
||||
testImplementation "junit:junit:3.8.2"
|
||||
|
@ -58,6 +58,8 @@ interface StorageProtocol {
|
||||
fun getThreadID(openGroupID: String): String?
|
||||
fun getAllOpenGroups(): Map<Long, PublicChat>
|
||||
fun addOpenGroup(server: String, channel: Long)
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
||||
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
||||
|
||||
// Open Group Public Keys
|
||||
fun getOpenGroupPublicKey(server: String): String?
|
||||
@ -94,7 +96,6 @@ interface StorageProtocol {
|
||||
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment>
|
||||
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
||||
fun markAsSent(timestamp: Long, author: String)
|
||||
fun markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
@ -112,9 +113,9 @@ interface StorageProtocol {
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>)
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
||||
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long)
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
|
||||
fun isClosedGroup(publicKey: String): Boolean
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
|
@ -1,40 +0,0 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.color.MaterialColor;
|
||||
|
||||
|
||||
/**
|
||||
* Used for migrating legacy colors to modern colors. For normal color generation, use
|
||||
* {@link ContactColors}.
|
||||
*/
|
||||
public class ContactColorsLegacy {
|
||||
|
||||
private static final String[] LEGACY_PALETTE = new String[] {
|
||||
"red",
|
||||
"pink",
|
||||
"purple",
|
||||
"deep_purple",
|
||||
"indigo",
|
||||
"blue",
|
||||
"light_blue",
|
||||
"cyan",
|
||||
"teal",
|
||||
"green",
|
||||
"light_green",
|
||||
"orange",
|
||||
"deep_orange",
|
||||
"amber",
|
||||
"blue_grey"
|
||||
};
|
||||
|
||||
public static MaterialColor generateFor(@NonNull String name) {
|
||||
String serialized = LEGACY_PALETTE[Math.abs(name.hashCode()) % LEGACY_PALETTE.length];
|
||||
try {
|
||||
return MaterialColor.fromSerialized(serialized);
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
return ContactColors.generateFor(name);
|
||||
}
|
||||
}
|
||||
}
|
@ -83,14 +83,14 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
//serialize Message and Destination properties
|
||||
val kryo = Kryo()
|
||||
kryo.isRegistrationRequired = false
|
||||
val serializedMessage = ByteArray(4096)
|
||||
val serializedDestination = ByteArray(4096)
|
||||
var output = Output(serializedMessage)
|
||||
val output = Output(ByteArray(4096), -1) // maxBufferSize '-1' will dynamically grow internally if we run out of room serializing the message
|
||||
kryo.writeClassAndObject(output, message)
|
||||
output.close()
|
||||
output = Output(serializedDestination)
|
||||
val serializedMessage = output.toBytes()
|
||||
output.clear()
|
||||
kryo.writeClassAndObject(output, destination)
|
||||
output.close()
|
||||
val serializedDestination = output.toBytes()
|
||||
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage)
|
||||
.putByteArray(KEY_DESTINATION, serializedDestination)
|
||||
.build();
|
||||
|
@ -7,25 +7,14 @@ import org.session.libsignal.service.loki.utilities.toHexString
|
||||
|
||||
sealed class Destination {
|
||||
|
||||
class Contact() : Destination() {
|
||||
var publicKey: String = ""
|
||||
internal constructor(publicKey: String): this() {
|
||||
this.publicKey = publicKey
|
||||
}
|
||||
class Contact(var publicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class ClosedGroup() : Destination() {
|
||||
var groupPublicKey: String = ""
|
||||
internal constructor(groupPublicKey: String): this() {
|
||||
this.groupPublicKey = groupPublicKey
|
||||
}
|
||||
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class OpenGroup() : Destination() {
|
||||
var channel: Long = 0
|
||||
var server: String = ""
|
||||
internal constructor(channel: Long, server: String): this() {
|
||||
this.channel = channel
|
||||
this.server = server
|
||||
}
|
||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
||||
internal constructor(): this(0, "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
abstract class Message {
|
||||
@ -29,4 +31,12 @@ abstract class Message {
|
||||
|
||||
abstract fun toProto(): SignalServiceProtos.Content?
|
||||
|
||||
fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) {
|
||||
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(recipient!!)
|
||||
groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))
|
||||
groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER
|
||||
dataMessage.group = groupProto.build()
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
@ -26,18 +30,30 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
|
||||
// Kind enum
|
||||
sealed class Kind {
|
||||
class New(val publicKey: ByteString, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<ByteString>, val admins: List<ByteString>) : Kind()
|
||||
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(ByteString.EMPTY, "", null, listOf(), listOf())
|
||||
}
|
||||
/// - Note: Deprecated in favor of more explicit group updates.
|
||||
class Update(val name: String, val members: List<ByteString>) : Kind()
|
||||
class Update(var name: String, var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this("", listOf())
|
||||
}
|
||||
/// An encryption key pair encrypted for each member individually.
|
||||
///
|
||||
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
||||
class EncryptionKeyPair(val publicKey: ByteString?, val wrappers: Collection<KeyPairWrapper>) : Kind()
|
||||
class NameChange(val name: String) : Kind()
|
||||
class MembersAdded(val members: List<ByteString>) : Kind()
|
||||
class MembersRemoved( val members: List<ByteString>) : Kind()
|
||||
class MemberLeft : Kind()
|
||||
class EncryptionKeyPairRequest: Kind()
|
||||
class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() {
|
||||
internal constructor(): this(null, listOf())
|
||||
}
|
||||
class NameChange(var name: String) : Kind() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class MembersAdded(var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(listOf())
|
||||
}
|
||||
class MembersRemoved(var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(listOf())
|
||||
}
|
||||
class MemberLeft() : Kind()
|
||||
class EncryptionKeyPairRequest(): Kind()
|
||||
|
||||
val description: String = run {
|
||||
when(this) {
|
||||
@ -115,8 +131,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val kind = kind ?: return false
|
||||
return when(kind) {
|
||||
is Kind.New -> {
|
||||
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair.publicKey != null
|
||||
&& kind.encryptionKeyPair.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
||||
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair!!.publicKey != null
|
||||
&& kind.encryptionKeyPair!!.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
||||
}
|
||||
is Kind.Update -> kind.name.isNotEmpty()
|
||||
is Kind.EncryptionKeyPair -> true
|
||||
@ -141,16 +157,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupControlMessage.publicKey = kind.publicKey
|
||||
closedGroupControlMessage.name = kind.name
|
||||
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
|
||||
|
||||
try {
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
|
||||
return null
|
||||
}
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
closedGroupControlMessage.addAllAdmins(kind.admins)
|
||||
}
|
||||
@ -187,6 +197,11 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val dataMessageProto = DataMessage.newBuilder()
|
||||
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
// Group context
|
||||
setGroupContext(dataMessageProto)
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
dataMessageProto.expireTimer = Recipient.from(MessagingConfiguration.shared.context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
|
@ -14,11 +14,13 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups: List<String>, val contacts: List<Contact>, val displayName: String, val profilePicture: String?, val profileKey: ByteArray): ControlMessage() {
|
||||
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() {
|
||||
|
||||
class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<String>, val admins: List<String>) {
|
||||
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
|
||||
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||
|
||||
internal constructor(): this("", "", null, listOf(), listOf())
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
@ -30,7 +32,7 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
|
||||
val name = proto.name
|
||||
val encryptionKeyPairAsProto = proto.encryptionKeyPair
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
val members = proto.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
|
||||
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
|
||||
@ -42,8 +44,8 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.name = name
|
||||
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair!!.privateKey.serialize())
|
||||
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
@ -51,7 +53,10 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
|
||||
}
|
||||
}
|
||||
|
||||
class Contact(val publicKey: String, val name: String, val profilePicture: String?, val profileKey: ByteArray?) {
|
||||
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
|
||||
|
||||
internal constructor(): this("", "", null, null)
|
||||
|
||||
companion object {
|
||||
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.Contact): Contact? {
|
||||
if (!proto.hasName() || !proto.hasProfileKey()) return null
|
||||
@ -123,10 +128,13 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
|
||||
val displayName = configurationProto.displayName
|
||||
val profilePicture = configurationProto.profilePicture
|
||||
val profileKey = configurationProto.profileKey
|
||||
return ConfigurationMessage(closedGroups, openGroups, listOf(), displayName, profilePicture, profileKey.toByteArray())
|
||||
val contacts = configurationProto.contactsList.mapNotNull { Contact.fromProto(it) }
|
||||
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(): this(listOf(), listOf(), listOf(), "", null, byteArrayOf())
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder()
|
||||
configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() })
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
class ExpirationTimerUpdate() : ControlMessage() {
|
||||
|
||||
@ -40,6 +42,16 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||
dataMessageProto.expireTimer = duration
|
||||
syncTarget?.let { dataMessageProto.syncTarget = it }
|
||||
// Group context
|
||||
if (MessagingConfiguration.shared.storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessageProto)
|
||||
} catch(e: Exception) {
|
||||
Log.w(VisibleMessage.TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
@ -1,8 +1,7 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
@ -59,20 +58,6 @@ public class OutgoingMediaMessage {
|
||||
this.identityKeyMismatches.addAll(identityKeyMismatches);
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message,
|
||||
long sentTimeMillis, int subscriptionId, long expiresIn,
|
||||
int distributionType, @Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews)
|
||||
{
|
||||
this(recipient,
|
||||
buildMessage(message),
|
||||
slideDeck.asAttachments(),
|
||||
sentTimeMillis, subscriptionId,
|
||||
expiresIn, distributionType, outgoingQuote,
|
||||
contacts, linkPreviews, new LinkedList<>(), new LinkedList<>());
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(OutgoingMediaMessage that) {
|
||||
this.recipient = that.getRecipient();
|
||||
this.body = that.body;
|
||||
@ -116,10 +101,6 @@ public class OutgoingMediaMessage {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public int getDistributionType() {
|
||||
return distributionType;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return true;
|
||||
}
|
||||
@ -156,19 +137,4 @@ public class OutgoingMediaMessage {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
public @NonNull List<NetworkFailure> getNetworkFailures() {
|
||||
return networkFailures;
|
||||
}
|
||||
|
||||
public @NonNull List<IdentityKeyMismatch> getIdentityKeyMismatches() {
|
||||
return identityKeyMismatches;
|
||||
}
|
||||
|
||||
private static String buildMessage(String message) {
|
||||
if (!TextUtils.isEmpty(message)) {
|
||||
return message;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
@ -4,6 +4,9 @@ import com.goterl.lazycode.lazysodium.BuildConfig
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
@ -102,11 +105,28 @@ class VisibleMessage : Message() {
|
||||
}
|
||||
val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
|
||||
dataMessage.addAllAttachments(attachmentPointers)
|
||||
// TODO Contact
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
||||
dataMessage.expireTimer = expiration
|
||||
// Group context
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessage)
|
||||
} catch(e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
}
|
||||
}
|
||||
// Sync target
|
||||
if (syncTarget != null) {
|
||||
dataMessage.syncTarget = syncTarget
|
||||
}
|
||||
// TODO Contact
|
||||
// Build
|
||||
try {
|
||||
proto.dataMessage = dataMessage.build()
|
||||
|
@ -41,6 +41,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
return listOf() // Don't auto-join any open groups right now
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean {
|
||||
if (moderators[server] != null && moderators[server]!![channel] != null) {
|
||||
return moderators[server]!![channel]!!.contains(hexEncodedPublicKey)
|
||||
@ -113,7 +114,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong()
|
||||
val contentType = attachmentAsJSON["contentType"] as String
|
||||
val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt()
|
||||
val fileName = attachmentAsJSON["fileName"] as String
|
||||
val fileName = attachmentAsJSON["fileName"] as? String
|
||||
val flags = 0
|
||||
val url = attachmentAsJSON["url"] as String
|
||||
val caption = attachmentAsJSON["caption"] as? String
|
||||
@ -237,6 +238,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteMessages(messageServerIDs: List<Long>, channel: Long, server: String, isSentByUser: Boolean): Promise<List<Long>, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val isModerationRequest = !isSentByUser
|
||||
@ -337,6 +339,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun ban(publicKey: String, server: String): Promise<Unit,Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then {
|
||||
|
@ -34,10 +34,8 @@ data class OpenGroupMessage(
|
||||
val quote = message.quote
|
||||
if (quote != null && quote.isValid()) {
|
||||
val quotedMessageBody = quote.text ?: quote.timestamp!!.toString()
|
||||
val quotedAttachmentID = quote.attachmentID
|
||||
if (quotedAttachmentID != null) { attachmentIDs.remove(quotedAttachmentID) }
|
||||
// FIXME: For some reason the server always returns a 500 if quotedMessageServerID is set...
|
||||
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, null)
|
||||
val serverID = storage.getQuoteServerID(quote.timestamp!!, quote.publicKey!!)
|
||||
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, serverID)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -51,18 +49,18 @@ data class OpenGroupMessage(
|
||||
linkPreview?.let {
|
||||
if (!linkPreview.isValid()) { return@let }
|
||||
val attachmentID = linkPreview.attachmentID ?: return@let
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getAttachmentPointer(attachmentID) ?: return@let
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID) ?: return@let
|
||||
val openGroupLinkPreview = Attachment(
|
||||
Attachment.Kind.LinkPreview,
|
||||
server,
|
||||
attachment.id,
|
||||
attachment.contentType!!,
|
||||
attachment.size.get(),
|
||||
attachment.fileName.get(),
|
||||
attachment.fileName.orNull(),
|
||||
0,
|
||||
attachment.width,
|
||||
attachment.height,
|
||||
attachment.caption.get(),
|
||||
attachment.caption.orNull(),
|
||||
attachment.url,
|
||||
linkPreview.url,
|
||||
linkPreview.title)
|
||||
@ -77,7 +75,7 @@ data class OpenGroupMessage(
|
||||
attachment.id,
|
||||
attachment.contentType!!,
|
||||
attachment.size.orNull(),
|
||||
attachment.fileName.orNull(),
|
||||
attachment.fileName.orNull() ?: "",
|
||||
0,
|
||||
attachment.width,
|
||||
attachment.height,
|
||||
|
@ -109,7 +109,7 @@ private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMes
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closeGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
|
||||
handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
for (openGroup in message.openGroups) {
|
||||
@ -220,11 +220,11 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
|
||||
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
||||
handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins, message.sentTimestamp!!)
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
// Parameter @sender:String is just for inserting incoming info message
|
||||
private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
|
||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
// Create the group
|
||||
@ -237,7 +237,7 @@ private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: S
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
// Notify the user
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
}
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
@ -295,7 +295,7 @@ private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControl
|
||||
val wasSenderRemoved = !members.contains(senderPublicKey)
|
||||
val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() })
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
|
||||
@ -313,7 +313,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
return
|
||||
}
|
||||
if (!group.members.map { it.toString() }.contains(senderPublicKey)) {
|
||||
android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
|
||||
return
|
||||
}
|
||||
// Find our wrapper and decrypt it if possible
|
||||
@ -354,12 +354,13 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
||||
val name = kind.name
|
||||
storage.updateTitle(groupID, name)
|
||||
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
@ -368,9 +369,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
@ -379,14 +378,25 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
val newMembers = members + updateMembers
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
// Send the latest encryption key pair to the added members if the current user is the admin of the group
|
||||
val isCurrentUserAdmin = admins.contains(storage.getUserPublicKey()!!)
|
||||
if (isCurrentUserAdmin) {
|
||||
for (member in updateMembers) {
|
||||
MessageSender.sendLatestEncryptionKeyPair(member, groupPublicKey)
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
for (user in updateMembers) {
|
||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
|
||||
@ -435,7 +445,7 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
||||
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
|
||||
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
|
||||
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
|
||||
@ -470,7 +480,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) {
|
||||
@ -489,7 +499,13 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: C
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
MessageSender.sendLatestEncryptionKeyPair(senderPublicKey, groupPublicKey)
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(senderPublicKey), targetUser = senderPublicKey, force = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidGroupUpdate(group: GroupRecord,
|
||||
|
@ -21,6 +21,7 @@ import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeConfiguration
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.api.crypto.ProofOfWork
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
@ -32,7 +33,6 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as S
|
||||
|
||||
|
||||
object MessageSender {
|
||||
const val groupSizeLimit = 100
|
||||
|
||||
// Error
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
@ -77,21 +77,21 @@ object MessageSender {
|
||||
// Set the timestamp, sender and recipient
|
||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
|
||||
message.sender = userPublicKey
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
||||
}
|
||||
deferred.reject(error)
|
||||
}
|
||||
try {
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
is Destination.OpenGroup -> throw preconditionFailure
|
||||
}
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
||||
}
|
||||
deferred.reject(error)
|
||||
}
|
||||
// Validate the message
|
||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||
// Stop here if this is a self-send, unless it's:
|
||||
@ -119,7 +119,7 @@ object MessageSender {
|
||||
// Convert it to protobuf
|
||||
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
|
||||
// Serialize the protobuf
|
||||
val plaintext = proto.toByteArray()
|
||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||
// Encrypt the serialized protobuf
|
||||
val ciphertext: ByteArray
|
||||
when (destination) {
|
||||
@ -183,15 +183,14 @@ object MessageSender {
|
||||
errorCount += 1
|
||||
if (errorCount != promiseCount) { return@fail } // Only error out if all promises failed
|
||||
handleFailure(it)
|
||||
deferred.reject(it)
|
||||
}
|
||||
}
|
||||
}.fail {
|
||||
Log.d("Loki", "Couldn't send message due to error: $it.")
|
||||
deferred.reject(it)
|
||||
handleFailure(it)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
deferred.reject(exception)
|
||||
handleFailure(exception)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
@ -24,7 +24,8 @@ import org.session.libsignal.utilities.logging.Log
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
const val groupSizeLimit = 100
|
||||
val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
@ -59,7 +60,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
@ -99,14 +100,16 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) {
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Update the group
|
||||
storage.updateTitle(groupID, newName)
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
|
||||
}
|
||||
|
||||
fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>) {
|
||||
@ -135,18 +138,21 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(member))
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
|
||||
fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<String>) {
|
||||
@ -174,7 +180,9 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (isCurrentUserAdmin) {
|
||||
@ -183,7 +191,7 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
|
||||
fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
|
||||
@ -199,13 +207,14 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
|
||||
closedGroupControlMessage.sentTimestamp = System.currentTimeMillis()
|
||||
val sentTime = System.currentTimeMillis()
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
if (notifyUser) {
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
@ -236,6 +245,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
|
||||
// make sure we set the pendingKeyPair or wait until it is not null
|
||||
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Distribute it
|
||||
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
|
||||
// Store it * after * having sent out the message to the group
|
||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
}
|
||||
}
|
||||
|
||||
fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true): Promise<Unit, Exception>? {
|
||||
val destination = targetUser ?: GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
@ -244,13 +262,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
|
||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
||||
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
}
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(null, wrappers)
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
// Store it * after * having sent out the message to the group
|
||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
}.always {
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
return if (force) {
|
||||
MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination))
|
||||
} else {
|
||||
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(destination))
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,7 +285,9 @@ fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) {
|
||||
val members = group.members.map { it.serialize() }.toSet()
|
||||
if (!members.contains(storage.getUserPublicKey()!!)) return
|
||||
// Send the request to the group
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest())
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,53 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Box
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Sign
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
object MessageSenderEncryption {
|
||||
|
||||
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientPublicKey: String): ByteArray{
|
||||
return MessagingConfiguration.shared.sessionProtocol.encrypt(plaintext, recipientPublicKey)
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
/**
|
||||
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
|
||||
*
|
||||
* @param plaintext the plaintext to encrypt. Must already be padded.
|
||||
* @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group.
|
||||
*
|
||||
* @return the encrypted message.
|
||||
*/
|
||||
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
||||
|
||||
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
try {
|
||||
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't sign message due to error: $exception.")
|
||||
throw Error.SigningFailed
|
||||
}
|
||||
val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature
|
||||
val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES)
|
||||
try {
|
||||
sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
|
||||
throw Error.EncryptionFailed
|
||||
}
|
||||
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
}
|
@ -2,14 +2,17 @@ package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.*
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushNotificationAPI {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val server = "https://live.apns.getsession.org"
|
||||
|
@ -17,19 +17,7 @@
|
||||
package org.session.libsession.messaging.threads.recipients;
|
||||
|
||||
public class RecipientFormattingException extends Exception {
|
||||
public RecipientFormattingException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public RecipientFormattingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RecipientFormattingException(String message, Throwable nested) {
|
||||
super(message, nested);
|
||||
}
|
||||
|
||||
public RecipientFormattingException(Throwable nested) {
|
||||
super(nested);
|
||||
}
|
||||
}
|
||||
|
@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.session.libsession.messaging.threads.recipients;
|
||||
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
public class RecipientsFormatter {
|
||||
|
||||
private static String parseBracketedNumber(String recipient) throws RecipientFormattingException {
|
||||
int begin = recipient.indexOf('<');
|
||||
int end = recipient.indexOf('>');
|
||||
String value = recipient.substring(begin + 1, end);
|
||||
|
||||
if (PhoneNumberUtils.isWellFormedSmsAddress(value))
|
||||
return value;
|
||||
else
|
||||
throw new RecipientFormattingException("Bracketed value: " + value + " is not valid.");
|
||||
}
|
||||
|
||||
private static String parseRecipient(String recipient) throws RecipientFormattingException {
|
||||
recipient = recipient.trim();
|
||||
|
||||
if ((recipient.indexOf('<') != -1) && (recipient.indexOf('>') != -1))
|
||||
return parseBracketedNumber(recipient);
|
||||
|
||||
if (PhoneNumberUtils.isWellFormedSmsAddress(recipient))
|
||||
return recipient;
|
||||
|
||||
throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted.");
|
||||
}
|
||||
|
||||
public static List<String> getRecipients(String rawText) throws RecipientFormattingException {
|
||||
ArrayList<String> results = new ArrayList<String>();
|
||||
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
|
||||
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
results.add(parseRecipient(tokenizer.nextToken()));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static String formatNameAndNumber(String name, String number) {
|
||||
// Format like this: Mike Cleron <(650) 555-1234>
|
||||
// Erick Tseng <(650) 555-1212>
|
||||
// Tutankhamun <tutank1341@gmail.com>
|
||||
// (408) 555-1289
|
||||
String formattedNumber = PhoneNumberUtils.formatNumber(number);
|
||||
if (!TextUtils.isEmpty(name) && !name.equals(number)) {
|
||||
return name + " <" + formattedNumber + ">";
|
||||
} else {
|
||||
return formattedNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import android.content.Context
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.utilities.TextSecurePreferences.isUniversalUnidentifiedAccess
|
||||
import org.session.libsession.utilities.Util.getSecretBytes
|
||||
import org.session.libsignal.metadata.SignalProtos
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
object UnidentifiedAccessUtil {
|
||||
private val TAG = UnidentifiedAccessUtil::class.simpleName
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
fun getAccessFor(recipientPublicKey: String): UnidentifiedAccess? {
|
||||
val theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientPublicKey)
|
||||
val ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey()
|
||||
val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate()
|
||||
|
||||
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
|
||||
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
|
||||
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null))
|
||||
|
||||
return if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
UnidentifiedAccess(theirUnidentifiedAccessKey)
|
||||
} else null
|
||||
}
|
||||
|
||||
fun getAccessForSync(context: Context): UnidentifiedAccess? {
|
||||
var ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey()
|
||||
val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate()
|
||||
if (isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = getSecretBytes(16)
|
||||
}
|
||||
return if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
UnidentifiedAccess(ourUnidentifiedAccessKey)
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? {
|
||||
val theirProfileKey = MessagingConfiguration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16)
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
|
||||
}
|
||||
|
||||
private fun getSelfUnidentifiedAccessKey(): ByteArray? {
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
|
||||
if (userPublicKey != null) {
|
||||
return sodium.randomBytesBuf(16)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getUnidentifiedAccessCertificate(): ByteArray? {
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
|
||||
if (userPublicKey != null) {
|
||||
val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build()
|
||||
return certificate.toByteArray()
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@ -23,7 +23,6 @@ import android.content.SharedPreferences.Editor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.IdentityKeyPair;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
@ -34,8 +33,6 @@ import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility class for working with identity keys.
|
||||
@ -95,45 +92,6 @@ public class IdentityKeyUtil {
|
||||
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(privateKey.serialize()));
|
||||
}
|
||||
|
||||
public static List<BackupProtos.SharedPreference> getBackupRecords(@NonNull Context context) {
|
||||
final String prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME;
|
||||
SharedPreferences preferences = context.getSharedPreferences(prefName, 0);
|
||||
|
||||
LinkedList<BackupProtos.SharedPreference> prefList = new LinkedList<>();
|
||||
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IDENTITY_PUBLIC_KEY_PREF)
|
||||
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
|
||||
.build());
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IDENTITY_PRIVATE_KEY_PREF)
|
||||
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
|
||||
.build());
|
||||
if (preferences.contains(ED25519_PUBLIC_KEY)) {
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(ED25519_PUBLIC_KEY)
|
||||
.setValue(preferences.getString(ED25519_PUBLIC_KEY, null))
|
||||
.build());
|
||||
}
|
||||
if (preferences.contains(ED25519_SECRET_KEY)) {
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(ED25519_SECRET_KEY)
|
||||
.setValue(preferences.getString(ED25519_SECRET_KEY, null))
|
||||
.build());
|
||||
}
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(LOKI_SEED)
|
||||
.setValue(preferences.getString(LOKI_SEED, null))
|
||||
.build());
|
||||
|
||||
return prefList;
|
||||
}
|
||||
|
||||
public static String retrieve(Context context, String key) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
return preferences.getString(key, null);
|
@ -1,11 +1,10 @@
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.utils.Key
|
||||
import com.goterl.lazycode.lazysodium.utils.KeyPair
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
@ -7,6 +7,9 @@ import android.preference.PreferenceManager.getDefaultSharedPreferences
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.ArrayRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import org.session.libsession.R
|
||||
import org.session.libsession.utilities.preferences.NotificationPrivacyPreference
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
@ -16,6 +19,9 @@ import java.util.*
|
||||
object TextSecurePreferences {
|
||||
private val TAG = TextSecurePreferences::class.simpleName
|
||||
|
||||
private val _events = MutableSharedFlow<String>(0, 64, BufferOverflow.DROP_OLDEST)
|
||||
val events get() = _events.asSharedFlow()
|
||||
|
||||
const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase"
|
||||
const val THEME_PREF = "pref_theme"
|
||||
const val LANGUAGE_PREF = "pref_language"
|
||||
@ -101,7 +107,8 @@ object TextSecurePreferences {
|
||||
|
||||
// region Multi Device
|
||||
private const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
|
||||
private const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
||||
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
||||
private const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
|
||||
|
||||
@JvmStatic
|
||||
fun getLastConfigurationSyncTime(context: Context): Long {
|
||||
@ -121,6 +128,7 @@ object TextSecurePreferences {
|
||||
@JvmStatic
|
||||
fun setConfigurationMessageSynced(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, CONFIGURATION_SYNCED, value)
|
||||
_events.tryEmit(CONFIGURATION_SYNCED)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ -321,6 +329,7 @@ object TextSecurePreferences {
|
||||
@JvmStatic
|
||||
fun setProfileName(context: Context, name: String?) {
|
||||
setStringPreference(context, PROFILE_NAME_PREF, name)
|
||||
_events.tryEmit(PROFILE_NAME_PREF)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ -746,5 +755,14 @@ object TextSecurePreferences {
|
||||
fun setIsMigratingKeyPair(context: Context?, newValue: Boolean) {
|
||||
setBooleanPreference(context!!, "is_migrating_key_pair", newValue)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setLastProfileUpdateTime(context: Context, profileUpdateTime: Long) {
|
||||
setLongPreference(context, LAST_PROFILE_UPDATE_TIME, profileUpdateTime)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun shouldUpdateProfile(context: Context, profileUpdateTime: Long) =
|
||||
profileUpdateTime > getLongPreference(context, LAST_PROFILE_UPDATE_TIME, 0)
|
||||
// endregion
|
||||
}
|
@ -1,825 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
package org.session.libsignal.service.api;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.libsignal.state.IdentityKeyStore;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.session.libsignal.service.api.crypto.UntrustedIdentityException;
|
||||
import org.session.libsignal.service.api.messages.SendMessageResult;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
|
||||
import org.session.libsignal.service.api.messages.shared.SharedContact;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
import org.session.libsignal.service.internal.crypto.PaddingInputStream;
|
||||
import org.session.libsignal.service.internal.push.OutgoingPushMessage;
|
||||
import org.session.libsignal.service.internal.push.OutgoingPushMessageList;
|
||||
import org.session.libsignal.service.internal.push.PushAttachmentData;
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Content;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage.LokiProfile;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage;
|
||||
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory;
|
||||
import org.session.libsignal.service.internal.push.http.OutputStreamFactory;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsignal.service.internal.util.Util;
|
||||
import org.session.libsignal.utilities.concurrent.SettableFuture;
|
||||
import org.session.libsignal.service.loki.api.LokiDotNetAPI;
|
||||
import org.session.libsignal.service.loki.api.PushNotificationAPI;
|
||||
import org.session.libsignal.service.loki.api.SignalMessageInfo;
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatMessage;
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiMessageDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.utilities.TTLUtilities;
|
||||
import org.session.libsignal.service.loki.utilities.Broadcaster;
|
||||
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
|
||||
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import nl.komponents.kovenant.Promise;
|
||||
|
||||
/**
|
||||
* The main interface for sending Signal Service messages.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class SignalServiceMessageSender {
|
||||
|
||||
private static final String TAG = SignalServiceMessageSender.class.getSimpleName();
|
||||
|
||||
private final IdentityKeyStore store;
|
||||
// Loki
|
||||
private final String userPublicKey;
|
||||
private final LokiAPIDatabaseProtocol apiDatabase;
|
||||
private final LokiThreadDatabaseProtocol threadDatabase;
|
||||
private final LokiMessageDatabaseProtocol messageDatabase;
|
||||
private final SessionProtocol sessionProtocolImpl;
|
||||
private final LokiUserDatabaseProtocol userDatabase;
|
||||
private final LokiOpenGroupDatabaseProtocol openGroupDatabase;
|
||||
private final Broadcaster broadcaster;
|
||||
|
||||
public SignalServiceMessageSender(IdentityKeyStore store,
|
||||
String userPublicKey,
|
||||
LokiAPIDatabaseProtocol apiDatabase,
|
||||
LokiThreadDatabaseProtocol threadDatabase,
|
||||
LokiMessageDatabaseProtocol messageDatabase,
|
||||
SessionProtocol sessionProtocolImpl,
|
||||
LokiUserDatabaseProtocol userDatabase,
|
||||
LokiOpenGroupDatabaseProtocol openGroupDatabase,
|
||||
Broadcaster broadcaster)
|
||||
{
|
||||
this.store = store;
|
||||
this.userPublicKey = userPublicKey;
|
||||
this.apiDatabase = apiDatabase;
|
||||
this.threadDatabase = threadDatabase;
|
||||
this.messageDatabase = messageDatabase;
|
||||
this.sessionProtocolImpl = sessionProtocolImpl;
|
||||
this.userDatabase = userDatabase;
|
||||
this.openGroupDatabase = openGroupDatabase;
|
||||
this.broadcaster = broadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a read receipt for a received message.
|
||||
*
|
||||
* @param recipient The sender of the received message you're acknowledging.
|
||||
* @param message The read receipt to deliver.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void sendReceipt(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceReceiptMessage message)
|
||||
throws IOException {
|
||||
byte[] content = createReceiptContent(message);
|
||||
boolean useFallbackEncryption = true;
|
||||
sendMessage(recipient, unidentifiedAccess, message.getWhen(), content, false, message.getTTL(), useFallbackEncryption);
|
||||
}
|
||||
|
||||
public void sendTyping(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
|
||||
SignalServiceTypingMessage message)
|
||||
throws IOException
|
||||
{
|
||||
byte[] content = createTypingContent(message);
|
||||
sendMessage(0, recipients, unidentifiedAccess, message.getTimestamp(), content, true, message.getTTL(), false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a single recipient.
|
||||
*
|
||||
* @param recipient The message's destination.
|
||||
* @param message The message.
|
||||
* @throws IOException
|
||||
*/
|
||||
public SendMessageResult sendMessage(long messageID,
|
||||
SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceDataMessage message,
|
||||
boolean isSelfSend)
|
||||
throws IOException
|
||||
{
|
||||
byte[] content = createMessageContent(message, recipient);
|
||||
long timestamp = message.getTimestamp();
|
||||
boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL;
|
||||
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccess, timestamp, content, false, message.getTTL(), true, isClosedGroup, message.hasVisibleContent() && !isSelfSend, message.getSyncTarget());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a group.
|
||||
*
|
||||
* @param recipients The group members.
|
||||
* @param message The group message.
|
||||
* @throws IOException
|
||||
*/
|
||||
public List<SendMessageResult> sendMessage(long messageID,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
|
||||
SignalServiceDataMessage message)
|
||||
throws IOException {
|
||||
// Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine
|
||||
// whether an attachment is being sent to an open group or not.
|
||||
byte[] content = createMessageContent(message, recipients.get(0));
|
||||
long timestamp = message.getTimestamp();
|
||||
boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL;
|
||||
|
||||
return sendMessage(messageID, recipients, unidentifiedAccess, timestamp, content, false, message.getTTL(), isClosedGroup, message.hasVisibleContent());
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentPointer uploadAttachment(SignalServiceAttachmentStream attachment, boolean usePadding, @Nullable SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
boolean shouldEncrypt = true;
|
||||
String server = FileServerAPI.shared.getServer();
|
||||
|
||||
// Loki - Check if we are sending to an open group
|
||||
if (recipient != null) {
|
||||
long threadID = threadDatabase.getThreadID(recipient.getNumber());
|
||||
PublicChat publicChat = threadDatabase.getPublicChat(threadID);
|
||||
if (publicChat != null) {
|
||||
shouldEncrypt = false;
|
||||
server = publicChat.getServer();
|
||||
}
|
||||
}
|
||||
|
||||
byte[] attachmentKey = Util.getSecretBytes(64);
|
||||
long paddedLength = usePadding ? PaddingInputStream.getPaddedSize(attachment.getLength()) : attachment.getLength();
|
||||
InputStream dataStream = usePadding ? new PaddingInputStream(attachment.getInputStream(), attachment.getLength()) : attachment.getInputStream();
|
||||
long ciphertextLength = shouldEncrypt ? AttachmentCipherOutputStream.getCiphertextLength(paddedLength) : attachment.getLength();
|
||||
|
||||
OutputStreamFactory outputStreamFactory = shouldEncrypt ? new AttachmentCipherOutputStreamFactory(attachmentKey) : new PlaintextOutputStreamFactory();
|
||||
PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(), dataStream, ciphertextLength, outputStreamFactory, attachment.getListener());
|
||||
|
||||
// Loki - Upload attachment
|
||||
LokiDotNetAPI.UploadResult result = FileServerAPI.shared.uploadAttachment(server, attachmentData);
|
||||
return new SignalServiceAttachmentPointer(result.getId(),
|
||||
attachment.getContentType(),
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachment.getLength())),
|
||||
attachment.getPreview(),
|
||||
attachment.getWidth(), attachment.getHeight(),
|
||||
Optional.fromNullable(result.getDigest()),
|
||||
attachment.getFileName(),
|
||||
attachment.getVoiceNote(),
|
||||
attachment.getCaption(),
|
||||
result.getUrl());
|
||||
}
|
||||
|
||||
private byte[] createTypingContent(SignalServiceTypingMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
TypingMessage.Builder builder = TypingMessage.newBuilder();
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
if (message.isTypingStarted()) builder.setAction(TypingMessage.Action.STARTED);
|
||||
else if (message.isTypingStopped()) builder.setAction(TypingMessage.Action.STOPPED);
|
||||
else throw new IllegalArgumentException("Unknown typing indicator");
|
||||
|
||||
return container.setTypingMessage(builder).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createReceiptContent(SignalServiceReceiptMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
ReceiptMessage.Builder builder = ReceiptMessage.newBuilder();
|
||||
|
||||
for (long timestamp : message.getTimestamps()) {
|
||||
builder.addTimestamp(timestamp);
|
||||
}
|
||||
|
||||
if (message.isDeliveryReceipt()) builder.setType(ReceiptMessage.Type.DELIVERY);
|
||||
else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ);
|
||||
|
||||
return container.setReceiptMessage(builder).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMessageContent(SignalServiceDataMessage message, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
Content.Builder container = Content.newBuilder();
|
||||
|
||||
DataMessage.Builder builder = DataMessage.newBuilder();
|
||||
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments(), recipient);
|
||||
|
||||
if (!pointers.isEmpty()) {
|
||||
builder.addAllAttachments(pointers);
|
||||
}
|
||||
|
||||
if (message.getBody().isPresent()) {
|
||||
builder.setBody(message.getBody().get());
|
||||
}
|
||||
|
||||
if (message.getGroupInfo().isPresent()) {
|
||||
builder.setGroup(createGroupContent(message.getGroupInfo().get(), recipient));
|
||||
}
|
||||
|
||||
if (message.isExpirationUpdate()) {
|
||||
builder.setFlags(DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE);
|
||||
}
|
||||
|
||||
if (message.getExpiresInSeconds() > 0) {
|
||||
builder.setExpireTimer(message.getExpiresInSeconds());
|
||||
}
|
||||
|
||||
if (message.getProfileKey().isPresent()) {
|
||||
builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get()));
|
||||
}
|
||||
|
||||
if (message.getSyncTarget().isPresent()) {
|
||||
builder.setSyncTarget(message.getSyncTarget().get());
|
||||
}
|
||||
|
||||
if (message.getQuote().isPresent()) {
|
||||
DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder()
|
||||
.setId(message.getQuote().get().getId())
|
||||
.setAuthor(message.getQuote().get().getAuthor().getNumber())
|
||||
.setText(message.getQuote().get().getText());
|
||||
|
||||
for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : message.getQuote().get().getAttachments()) {
|
||||
DataMessage.Quote.QuotedAttachment.Builder quotedAttachment = DataMessage.Quote.QuotedAttachment.newBuilder();
|
||||
|
||||
quotedAttachment.setContentType(attachment.getContentType());
|
||||
|
||||
if (attachment.getFileName() != null) {
|
||||
quotedAttachment.setFileName(attachment.getFileName());
|
||||
}
|
||||
|
||||
if (attachment.getThumbnail() != null) {
|
||||
quotedAttachment.setThumbnail(createAttachmentPointer(attachment.getThumbnail().asStream(), recipient));
|
||||
}
|
||||
|
||||
quoteBuilder.addAttachments(quotedAttachment);
|
||||
}
|
||||
|
||||
builder.setQuote(quoteBuilder);
|
||||
}
|
||||
|
||||
if (message.getSharedContacts().isPresent()) {
|
||||
builder.addAllContact(createSharedContactContent(message.getSharedContacts().get(), recipient));
|
||||
}
|
||||
|
||||
if (message.getPreviews().isPresent()) {
|
||||
for (SignalServiceDataMessage.Preview preview : message.getPreviews().get()) {
|
||||
DataMessage.Preview.Builder previewBuilder = DataMessage.Preview.newBuilder();
|
||||
previewBuilder.setTitle(preview.getTitle());
|
||||
previewBuilder.setUrl(preview.getUrl());
|
||||
|
||||
if (preview.getImage().isPresent()) {
|
||||
if (preview.getImage().get().isStream()) {
|
||||
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asStream(), recipient));
|
||||
} else {
|
||||
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asPointer()));
|
||||
}
|
||||
}
|
||||
|
||||
builder.addPreview(previewBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
LokiProfile.Builder lokiUserProfileBuilder = LokiProfile.newBuilder();
|
||||
String displayName = userDatabase.getDisplayName(userPublicKey);
|
||||
if (displayName != null) { lokiUserProfileBuilder.setDisplayName(displayName); }
|
||||
String profilePictureURL = userDatabase.getProfilePictureURL(userPublicKey);
|
||||
if (profilePictureURL != null) { lokiUserProfileBuilder.setProfilePicture(profilePictureURL); }
|
||||
builder.setProfile(lokiUserProfileBuilder.build());
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
container.setDataMessage(builder);
|
||||
|
||||
return container.build().toByteArray();
|
||||
}
|
||||
|
||||
private GroupContext createGroupContent(SignalServiceGroup group, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
GroupContext.Builder builder = GroupContext.newBuilder();
|
||||
builder.setId(ByteString.copyFrom(group.getGroupId()));
|
||||
|
||||
if (group.getType() != SignalServiceGroup.Type.DELIVER) {
|
||||
if (group.getType() == SignalServiceGroup.Type.UPDATE) builder.setType(GroupContext.Type.UPDATE);
|
||||
else if (group.getType() == SignalServiceGroup.Type.QUIT) builder.setType(GroupContext.Type.QUIT);
|
||||
else if (group.getType() == SignalServiceGroup.Type.REQUEST_INFO) builder.setType(GroupContext.Type.REQUEST_INFO);
|
||||
else throw new AssertionError("Unknown type: " + group.getType());
|
||||
|
||||
if (group.getName().isPresent()) builder.setName(group.getName().get());
|
||||
if (group.getMembers().isPresent()) builder.addAllMembers(group.getMembers().get());
|
||||
if (group.getAdmins().isPresent()) builder.addAllAdmins(group.getAdmins().get());
|
||||
|
||||
if (group.getAvatar().isPresent()) {
|
||||
if (group.getAvatar().get().isStream()) {
|
||||
builder.setAvatar(createAttachmentPointer(group.getAvatar().get().asStream(), recipient));
|
||||
} else {
|
||||
builder.setAvatar(createAttachmentPointer(group.getAvatar().get().asPointer()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
builder.setType(GroupContext.Type.DELIVER);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private List<DataMessage.Contact> createSharedContactContent(List<SharedContact> contacts, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
List<DataMessage.Contact> results = new LinkedList<>();
|
||||
|
||||
for (SharedContact contact : contacts) {
|
||||
DataMessage.Contact.Name.Builder nameBuilder = DataMessage.Contact.Name.newBuilder();
|
||||
|
||||
if (contact.getName().getFamily().isPresent()) nameBuilder.setFamilyName(contact.getName().getFamily().get());
|
||||
if (contact.getName().getGiven().isPresent()) nameBuilder.setGivenName(contact.getName().getGiven().get());
|
||||
if (contact.getName().getMiddle().isPresent()) nameBuilder.setMiddleName(contact.getName().getMiddle().get());
|
||||
if (contact.getName().getPrefix().isPresent()) nameBuilder.setPrefix(contact.getName().getPrefix().get());
|
||||
if (contact.getName().getSuffix().isPresent()) nameBuilder.setSuffix(contact.getName().getSuffix().get());
|
||||
if (contact.getName().getDisplay().isPresent()) nameBuilder.setDisplayName(contact.getName().getDisplay().get());
|
||||
|
||||
DataMessage.Contact.Builder contactBuilder = DataMessage.Contact.newBuilder()
|
||||
.setName(nameBuilder);
|
||||
|
||||
if (contact.getAddress().isPresent()) {
|
||||
for (SharedContact.PostalAddress address : contact.getAddress().get()) {
|
||||
DataMessage.Contact.PostalAddress.Builder addressBuilder = DataMessage.Contact.PostalAddress.newBuilder();
|
||||
|
||||
switch (address.getType()) {
|
||||
case HOME: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.HOME); break;
|
||||
case WORK: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.WORK); break;
|
||||
case CUSTOM: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.CUSTOM); break;
|
||||
default: throw new AssertionError("Unknown type: " + address.getType());
|
||||
}
|
||||
|
||||
if (address.getCity().isPresent()) addressBuilder.setCity(address.getCity().get());
|
||||
if (address.getCountry().isPresent()) addressBuilder.setCountry(address.getCountry().get());
|
||||
if (address.getLabel().isPresent()) addressBuilder.setLabel(address.getLabel().get());
|
||||
if (address.getNeighborhood().isPresent()) addressBuilder.setNeighborhood(address.getNeighborhood().get());
|
||||
if (address.getPobox().isPresent()) addressBuilder.setPobox(address.getPobox().get());
|
||||
if (address.getPostcode().isPresent()) addressBuilder.setPostcode(address.getPostcode().get());
|
||||
if (address.getRegion().isPresent()) addressBuilder.setRegion(address.getRegion().get());
|
||||
if (address.getStreet().isPresent()) addressBuilder.setStreet(address.getStreet().get());
|
||||
|
||||
contactBuilder.addAddress(addressBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.getEmail().isPresent()) {
|
||||
for (SharedContact.Email email : contact.getEmail().get()) {
|
||||
DataMessage.Contact.Email.Builder emailBuilder = DataMessage.Contact.Email.newBuilder()
|
||||
.setValue(email.getValue());
|
||||
|
||||
switch (email.getType()) {
|
||||
case HOME: emailBuilder.setType(DataMessage.Contact.Email.Type.HOME); break;
|
||||
case WORK: emailBuilder.setType(DataMessage.Contact.Email.Type.WORK); break;
|
||||
case MOBILE: emailBuilder.setType(DataMessage.Contact.Email.Type.MOBILE); break;
|
||||
case CUSTOM: emailBuilder.setType(DataMessage.Contact.Email.Type.CUSTOM); break;
|
||||
default: throw new AssertionError("Unknown type: " + email.getType());
|
||||
}
|
||||
|
||||
if (email.getLabel().isPresent()) emailBuilder.setLabel(email.getLabel().get());
|
||||
|
||||
contactBuilder.addEmail(emailBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.getPhone().isPresent()) {
|
||||
for (SharedContact.Phone phone : contact.getPhone().get()) {
|
||||
DataMessage.Contact.Phone.Builder phoneBuilder = DataMessage.Contact.Phone.newBuilder()
|
||||
.setValue(phone.getValue());
|
||||
|
||||
switch (phone.getType()) {
|
||||
case HOME: phoneBuilder.setType(DataMessage.Contact.Phone.Type.HOME); break;
|
||||
case WORK: phoneBuilder.setType(DataMessage.Contact.Phone.Type.WORK); break;
|
||||
case MOBILE: phoneBuilder.setType(DataMessage.Contact.Phone.Type.MOBILE); break;
|
||||
case CUSTOM: phoneBuilder.setType(DataMessage.Contact.Phone.Type.CUSTOM); break;
|
||||
default: throw new AssertionError("Unknown type: " + phone.getType());
|
||||
}
|
||||
|
||||
if (phone.getLabel().isPresent()) phoneBuilder.setLabel(phone.getLabel().get());
|
||||
|
||||
contactBuilder.addNumber(phoneBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.getAvatar().isPresent()) {
|
||||
AttachmentPointer pointer = contact.getAvatar().get().getAttachment().isStream() ? createAttachmentPointer(contact.getAvatar().get().getAttachment().asStream(), recipient)
|
||||
: createAttachmentPointer(contact.getAvatar().get().getAttachment().asPointer());
|
||||
contactBuilder.setAvatar(DataMessage.Contact.Avatar.newBuilder()
|
||||
.setAvatar(pointer)
|
||||
.setIsProfile(contact.getAvatar().get().isProfile()));
|
||||
}
|
||||
|
||||
if (contact.getOrganization().isPresent()) {
|
||||
contactBuilder.setOrganization(contact.getOrganization().get());
|
||||
}
|
||||
|
||||
results.add(contactBuilder.build());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<SendMessageResult> sendMessage(long messageID,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean isClosedGroup,
|
||||
boolean notifyPNServer)
|
||||
{
|
||||
List<SendMessageResult> results = new LinkedList<>();
|
||||
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
|
||||
Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator();
|
||||
|
||||
while (recipientIterator.hasNext()) {
|
||||
SignalServiceAddress recipient = recipientIterator.next();
|
||||
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, true, isClosedGroup, notifyPNServer, Optional.absent());
|
||||
results.add(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private SendMessageResult sendMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean useFallbackEncryption)
|
||||
throws IOException
|
||||
{
|
||||
// Loki - This method is only invoked for various types of control messages
|
||||
return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, useFallbackEncryption, false,Optional.absent());
|
||||
}
|
||||
|
||||
public SendMessageResult sendMessage(final long messageID,
|
||||
final SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup,
|
||||
boolean notifyPNServer,
|
||||
Optional<String> syncTarget)
|
||||
{
|
||||
boolean isSelfSend = syncTarget.isPresent() && !syncTarget.get().isEmpty();
|
||||
long threadID;
|
||||
if (isSelfSend) {
|
||||
threadID = threadDatabase.getThreadID(syncTarget.get());
|
||||
} else {
|
||||
threadID = threadDatabase.getThreadID(recipient.getNumber());
|
||||
}
|
||||
PublicChat publicChat = threadDatabase.getPublicChat(threadID);
|
||||
if (publicChat != null) {
|
||||
return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat);
|
||||
} else {
|
||||
return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer, syncTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private SendMessageResult sendMessageToPublicChat(final long messageID,
|
||||
final SignalServiceAddress recipient,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
PublicChat publicChat) {
|
||||
if (messageID == 0) {
|
||||
Log.d("Loki", "Missing message ID.");
|
||||
}
|
||||
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
|
||||
try {
|
||||
DataMessage data = Content.parseFrom(content).getDataMessage();
|
||||
String body = (data.getBody() != null && data.getBody().length() > 0) ? data.getBody() : Long.toString(data.getTimestamp());
|
||||
PublicChatMessage.Quote quote = null;
|
||||
if (data.hasQuote()) {
|
||||
long quoteID = data.getQuote().getId();
|
||||
String quoteePublicKey = data.getQuote().getAuthor();
|
||||
long serverID = messageDatabase.getQuoteServerID(quoteID, quoteePublicKey);
|
||||
quote = new PublicChatMessage.Quote(quoteID, quoteePublicKey, data.getQuote().getText(), serverID);
|
||||
}
|
||||
DataMessage.Preview linkPreview = (data.getPreviewList().size() > 0) ? data.getPreviewList().get(0) : null;
|
||||
ArrayList<PublicChatMessage.Attachment> attachments = new ArrayList<>();
|
||||
if (linkPreview != null && linkPreview.hasImage()) {
|
||||
AttachmentPointer attachmentPointer = linkPreview.getImage();
|
||||
String caption = attachmentPointer.hasCaption() ? attachmentPointer.getCaption() : null;
|
||||
attachments.add(new PublicChatMessage.Attachment(
|
||||
PublicChatMessage.Attachment.Kind.LinkPreview,
|
||||
publicChat.getServer(),
|
||||
attachmentPointer.getId(),
|
||||
attachmentPointer.getContentType(),
|
||||
attachmentPointer.getSize(),
|
||||
attachmentPointer.getFileName(),
|
||||
attachmentPointer.getFlags(),
|
||||
attachmentPointer.getWidth(),
|
||||
attachmentPointer.getHeight(),
|
||||
caption,
|
||||
attachmentPointer.getUrl(),
|
||||
linkPreview.getUrl(),
|
||||
linkPreview.getTitle()
|
||||
));
|
||||
}
|
||||
for (AttachmentPointer attachmentPointer : data.getAttachmentsList()) {
|
||||
String caption = attachmentPointer.hasCaption() ? attachmentPointer.getCaption() : null;
|
||||
attachments.add(new PublicChatMessage.Attachment(
|
||||
PublicChatMessage.Attachment.Kind.Attachment,
|
||||
publicChat.getServer(),
|
||||
attachmentPointer.getId(),
|
||||
attachmentPointer.getContentType(),
|
||||
attachmentPointer.getSize(),
|
||||
attachmentPointer.getFileName(),
|
||||
attachmentPointer.getFlags(),
|
||||
attachmentPointer.getWidth(),
|
||||
attachmentPointer.getHeight(),
|
||||
caption,
|
||||
attachmentPointer.getUrl(),
|
||||
null,
|
||||
null
|
||||
));
|
||||
}
|
||||
PublicChatMessage message = new PublicChatMessage(userPublicKey, "", body, timestamp, PublicChatAPI.getPublicChatMessageType(), quote, attachments);
|
||||
byte[] privateKey = store.getIdentityKeyPair().getPrivateKey().serialize();
|
||||
new PublicChatAPI(userPublicKey, privateKey, apiDatabase, userDatabase, openGroupDatabase).sendMessage(message, publicChat.getChannel(), publicChat.getServer()).success(new Function1<PublicChatMessage, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(PublicChatMessage message) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
messageDatabase.setServerID(messageID, message.getServerID());
|
||||
f.set(Unit.INSTANCE);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
try {
|
||||
f.get(1, TimeUnit.MINUTES);
|
||||
return SendMessageResult.success(recipient, false, false);
|
||||
} catch (Exception exception) {
|
||||
return SendMessageResult.networkFailure(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
private SendMessageResult sendMessageToPrivateChat(final long messageID,
|
||||
final SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
final long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup,
|
||||
final boolean notifyPNServer,
|
||||
Optional<String> syncTarget)
|
||||
{
|
||||
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
|
||||
OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content);
|
||||
// Loki - Remove this when we have shared sender keys
|
||||
// ========
|
||||
if (messages.getMessages().isEmpty()) {
|
||||
return SendMessageResult.success(recipient, false, false);
|
||||
}
|
||||
// ========
|
||||
OutgoingPushMessage message = messages.getMessages().get(0);
|
||||
final SignalServiceProtos.Envelope.Type type = SignalServiceProtos.Envelope.Type.valueOf(message.type);
|
||||
final String senderID;
|
||||
if (type == SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT) {
|
||||
senderID = recipient.getNumber();
|
||||
} else if (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) {
|
||||
senderID = "";
|
||||
} else {
|
||||
senderID = userPublicKey;
|
||||
}
|
||||
final int senderDeviceID = (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) ? 0 : SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
// Make sure we have a valid ttl; otherwise default to 2 days
|
||||
if (ttl <= 0) { ttl = TTLUtilities.INSTANCE.getFallbackMessageTTL(); }
|
||||
final int regularMessageTTL = TTLUtilities.getTTL(TTLUtilities.MessageType.Regular);
|
||||
final int __ttl = ttl;
|
||||
final SignalMessageInfo messageInfo = new SignalMessageInfo(type, timestamp, senderID, senderDeviceID, message.content, recipient.getNumber(), ttl, false);
|
||||
SnodeAPI.shared.sendSignalMessage(messageInfo).success(new Function1<Set<Promise<Map<?, ?>, Exception>>, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Set<Promise<Map<?, ?>, Exception>> promises) {
|
||||
final boolean[] isSuccess = { false };
|
||||
final int[] promiseCount = {promises.size()};
|
||||
final int[] errorCount = { 0 };
|
||||
for (Promise<Map<?, ?>, Exception> promise : promises) {
|
||||
promise.success(new Function1<Map<?, ?>, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Map<?, ?> map) {
|
||||
if (isSuccess[0]) { return Unit.INSTANCE; } // Succeed as soon as the first promise succeeds
|
||||
if (__ttl == regularMessageTTL) {
|
||||
broadcaster.broadcast("messageSent", timestamp);
|
||||
}
|
||||
isSuccess[0] = true;
|
||||
if (notifyPNServer) {
|
||||
PushNotificationAPI.shared.notify(messageInfo);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.set(Unit.INSTANCE);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
errorCount[0] += 1;
|
||||
if (errorCount[0] != promiseCount[0]) { return Unit.INSTANCE; } // Only error out if all promises failed
|
||||
if (__ttl == regularMessageTTL) {
|
||||
broadcaster.broadcast("messageFailed", timestamp);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
});
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(exception -> {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
try {
|
||||
f.get(1, TimeUnit.MINUTES);
|
||||
return SendMessageResult.success(recipient, false, true);
|
||||
} catch (Exception exception) {
|
||||
Throwable underlyingError = exception.getCause();
|
||||
if (underlyingError instanceof SnodeAPI.Error) {
|
||||
return SendMessageResult.lokiAPIError(recipient, (SnodeAPI.Error)underlyingError);
|
||||
} else {
|
||||
return SendMessageResult.networkFailure(recipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<AttachmentPointer> createAttachmentPointers(Optional<List<SignalServiceAttachment>> attachments, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
List<AttachmentPointer> pointers = new LinkedList<>();
|
||||
|
||||
if (!attachments.isPresent() || attachments.get().isEmpty()) {
|
||||
Log.w(TAG, "No attachments present...");
|
||||
return pointers;
|
||||
}
|
||||
|
||||
for (SignalServiceAttachment attachment : attachments.get()) {
|
||||
if (attachment.isStream()) {
|
||||
Log.w(TAG, "Found attachment, creating pointer...");
|
||||
pointers.add(createAttachmentPointer(attachment.asStream(), recipient));
|
||||
} else if (attachment.isPointer()) {
|
||||
Log.w(TAG, "Including existing attachment pointer...");
|
||||
pointers.add(createAttachmentPointer(attachment.asPointer()));
|
||||
}
|
||||
}
|
||||
|
||||
return pointers;
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) {
|
||||
AttachmentPointer.Builder builder = AttachmentPointer.newBuilder()
|
||||
.setContentType(attachment.getContentType())
|
||||
.setId(attachment.getId())
|
||||
.setKey(ByteString.copyFrom(attachment.getKey()))
|
||||
.setDigest(ByteString.copyFrom(attachment.getDigest().get()))
|
||||
.setSize(attachment.getSize().get())
|
||||
.setUrl(attachment.getUrl());
|
||||
|
||||
if (attachment.getFileName().isPresent()) {
|
||||
builder.setFileName(attachment.getFileName().get());
|
||||
}
|
||||
|
||||
if (attachment.getPreview().isPresent()) {
|
||||
builder.setThumbnail(ByteString.copyFrom(attachment.getPreview().get()));
|
||||
}
|
||||
|
||||
if (attachment.getWidth() > 0) {
|
||||
builder.setWidth(attachment.getWidth());
|
||||
}
|
||||
|
||||
if (attachment.getHeight() > 0) {
|
||||
builder.setHeight(attachment.getHeight());
|
||||
}
|
||||
|
||||
if (attachment.getVoiceNote()) {
|
||||
builder.setFlags(AttachmentPointer.Flags.VOICE_MESSAGE_VALUE);
|
||||
}
|
||||
|
||||
if (attachment.getCaption().isPresent()) {
|
||||
builder.setCaption(attachment.getCaption().get());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
return createAttachmentPointer(attachment, false, recipient);
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, boolean usePadding, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
SignalServiceAttachmentPointer pointer = uploadAttachment(attachment, usePadding, recipient);
|
||||
return createAttachmentPointer(pointer);
|
||||
}
|
||||
|
||||
private OutgoingPushMessageList getSessionProtocolEncryptedMessage(SignalServiceAddress recipient, long timestamp, byte[] plaintext)
|
||||
{
|
||||
List<OutgoingPushMessage> messages = new LinkedList<>();
|
||||
|
||||
String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group
|
||||
boolean isClosedGroup = apiDatabase.isClosedGroup(publicKey);
|
||||
String encryptionPublicKey;
|
||||
if (isClosedGroup) {
|
||||
ECKeyPair encryptionKeyPair = apiDatabase.getLatestClosedGroupEncryptionKeyPair(publicKey);
|
||||
encryptionPublicKey = HexEncodingKt.getHexEncodedPublicKey(encryptionKeyPair);
|
||||
} else {
|
||||
encryptionPublicKey = publicKey;
|
||||
}
|
||||
byte[] ciphertext = sessionProtocolImpl.encrypt(PushTransportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey);
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
|
||||
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(type, body);
|
||||
messages.add(message);
|
||||
|
||||
return new OutgoingPushMessageList(publicKey, timestamp, messages, false);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package org.session.libsignal.service.api.crypto;
|
||||
|
||||
public class InvalidCiphertextException extends Exception {
|
||||
public InvalidCiphertextException(Exception nested) {
|
||||
super(nested);
|
||||
}
|
||||
|
||||
public InvalidCiphertextException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package org.session.libsignal.service.api.crypto;
|
||||
|
||||
|
||||
import org.session.libsignal.libsignal.util.ByteUtil;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class UnidentifiedAccess {
|
||||
|
||||
private final byte[] unidentifiedAccessKey;
|
||||
|
||||
public UnidentifiedAccess(byte[] unidentifiedAccessKey)
|
||||
{
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
}
|
||||
|
||||
public byte[] getUnidentifiedAccessKey() {
|
||||
return unidentifiedAccessKey;
|
||||
}
|
||||
|
||||
public static byte[] deriveAccessKeyFrom(byte[] profileKey) {
|
||||
try {
|
||||
byte[] nonce = new byte[12];
|
||||
byte[] input = new byte[16];
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce));
|
||||
|
||||
byte[] ciphertext = cipher.doFinal(input);
|
||||
|
||||
return ByteUtil.trim(ciphertext, 16);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
package org.session.libsignal.service.api.crypto;
|
||||
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
|
||||
public class UntrustedIdentityException extends Exception {
|
||||
|
||||
private final IdentityKey identityKey;
|
||||
private final String e164number;
|
||||
|
||||
public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) {
|
||||
super(s);
|
||||
this.e164number = e164number;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public String getE164Number() {
|
||||
return e164number;
|
||||
}
|
||||
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package org.session.libsignal.service.api.messages;
|
||||
|
||||
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||
|
||||
public class SendMessageResult {
|
||||
|
||||
private final SignalServiceAddress address;
|
||||
private final Success success;
|
||||
private final boolean networkFailure;
|
||||
private final boolean unregisteredFailure;
|
||||
private final IdentityFailure identityFailure;
|
||||
private final SnodeAPI.Error lokiAPIError;
|
||||
|
||||
public static SendMessageResult success(SignalServiceAddress address, boolean unidentified, boolean needsSync) {
|
||||
return new SendMessageResult(address, new Success(unidentified, needsSync), false, false, null, null);
|
||||
}
|
||||
|
||||
public static SendMessageResult lokiAPIError(SignalServiceAddress address, SnodeAPI.Error lokiAPIError) {
|
||||
return new SendMessageResult(address, null, false, false, null, lokiAPIError);
|
||||
}
|
||||
|
||||
public static SendMessageResult networkFailure(SignalServiceAddress address) {
|
||||
return new SendMessageResult(address, null, true, false, null, null);
|
||||
}
|
||||
|
||||
public SignalServiceAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public Success getSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public boolean isNetworkFailure() {
|
||||
return networkFailure;
|
||||
}
|
||||
|
||||
public IdentityFailure getIdentityFailure() {
|
||||
return identityFailure;
|
||||
}
|
||||
|
||||
public SnodeAPI.Error getLokiAPIError() { return lokiAPIError; }
|
||||
|
||||
private SendMessageResult(SignalServiceAddress address, Success success, boolean networkFailure, boolean unregisteredFailure, IdentityFailure identityFailure, SnodeAPI.Error lokiAPIError) {
|
||||
this.address = address;
|
||||
this.success = success;
|
||||
this.networkFailure = networkFailure;
|
||||
this.unregisteredFailure = unregisteredFailure;
|
||||
this.identityFailure = identityFailure;
|
||||
this.lokiAPIError = lokiAPIError;
|
||||
}
|
||||
|
||||
public static class Success {
|
||||
private final boolean unidentified;
|
||||
private final boolean needsSync;
|
||||
|
||||
private Success(boolean unidentified, boolean needsSync) {
|
||||
this.unidentified = unidentified;
|
||||
this.needsSync = needsSync;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
|
||||
public boolean isNeedsSync() {
|
||||
return needsSync;
|
||||
}
|
||||
}
|
||||
|
||||
public static class IdentityFailure {
|
||||
private final IdentityKey identityKey;
|
||||
|
||||
private IdentityFailure(IdentityKey identityKey) {
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
package org.session.libsignal.service.internal.push;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class OutgoingPushMessage {
|
||||
|
||||
@JsonProperty
|
||||
public int type;
|
||||
@JsonProperty
|
||||
public String content;
|
||||
|
||||
public OutgoingPushMessage(int type, String content)
|
||||
{
|
||||
this.type = type;
|
||||
this.content = content;
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
package org.session.libsignal.service.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingPushMessageList {
|
||||
|
||||
@JsonProperty
|
||||
private String destination;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
@JsonProperty
|
||||
private List<OutgoingPushMessage> messages;
|
||||
|
||||
@JsonProperty
|
||||
private boolean online;
|
||||
|
||||
public OutgoingPushMessageList(String destination,
|
||||
long timestamp,
|
||||
List<OutgoingPushMessage> messages,
|
||||
boolean online)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.destination = destination;
|
||||
this.messages = messages;
|
||||
this.online = online;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public List<OutgoingPushMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public boolean isOnline() {
|
||||
return online;
|
||||
}
|
||||
}
|
@ -17,17 +17,6 @@ interface SessionProtocol {
|
||||
object DecryptionFailed : Exception("Couldn't decrypt message.")
|
||||
object InvalidSignature : Exception("Invalid message signature.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
|
||||
*
|
||||
* @param plaintext the plaintext to encrypt. Must already be padded.
|
||||
* @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group.
|
||||
*
|
||||
* @return the encrypted message.
|
||||
*/
|
||||
fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray
|
||||
|
||||
/**
|
||||
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user