mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-12 03:47:43 +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:
@@ -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,153 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open 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.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
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;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
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.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class IdentityKeyUtil {
|
||||
|
||||
private static final String MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
|
||||
|
||||
public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
|
||||
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
|
||||
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
|
||||
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
|
||||
public static final String LOKI_SEED = "loki_seed";
|
||||
|
||||
public static boolean hasIdentityKey(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
|
||||
return
|
||||
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
|
||||
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
|
||||
}
|
||||
|
||||
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
|
||||
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
|
||||
|
||||
try {
|
||||
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
|
||||
return new IdentityKey(publicKeyBytes, 0);
|
||||
} catch (IOException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
|
||||
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
|
||||
|
||||
try {
|
||||
IdentityKey publicKey = getIdentityKey(context);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
|
||||
|
||||
return new IdentityKeyPair(publicKey, privateKey);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void generateIdentityKeyPair(@NonNull Context context) {
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
ECPublicKey publicKey = keyPair.getPublicKey();
|
||||
ECPrivateKey privateKey = keyPair.getPrivateKey();
|
||||
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(publicKey.serialize()));
|
||||
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);
|
||||
}
|
||||
|
||||
public static void save(Context context, String key, String value) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
Editor preferencesEditor = preferences.edit();
|
||||
|
||||
preferencesEditor.putString(key, value);
|
||||
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
|
||||
}
|
||||
|
||||
public static void delete(Context context, String key) {
|
||||
context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0).edit().remove(key).commit();
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.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
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
|
||||
object KeyPairUtilities {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
fun generate(): KeyPairGenerationResult {
|
||||
val seed = sodium.randomBytesBuf(16)
|
||||
try {
|
||||
return generate(seed)
|
||||
} catch (exception: Exception) {
|
||||
return generate()
|
||||
}
|
||||
}
|
||||
|
||||
fun generate(seed: ByteArray): KeyPairGenerationResult {
|
||||
val padding = ByteArray(16) { 0 }
|
||||
val ed25519KeyPair = sodium.cryptoSignSeedKeypair(seed + padding)
|
||||
val sodiumX25519KeyPair = sodium.convertKeyPairEd25519ToCurve25519(ed25519KeyPair)
|
||||
val x25519KeyPair = ECKeyPair(DjbECPublicKey(sodiumX25519KeyPair.publicKey.asBytes), DjbECPrivateKey(sodiumX25519KeyPair.secretKey.asBytes))
|
||||
return KeyPairGenerationResult(seed, ed25519KeyPair, x25519KeyPair)
|
||||
}
|
||||
|
||||
fun store(context: Context, seed: ByteArray, ed25519KeyPair: KeyPair, x25519KeyPair: ECKeyPair) {
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.LOKI_SEED, Hex.toStringCondensed(seed))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(x25519KeyPair.publicKey.serialize()))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(x25519KeyPair.privateKey.serialize()))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_PUBLIC_KEY, Base64.encodeBytes(ed25519KeyPair.publicKey.asBytes))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_SECRET_KEY, Base64.encodeBytes(ed25519KeyPair.secretKey.asBytes))
|
||||
}
|
||||
|
||||
fun hasV2KeyPair(context: Context): Boolean {
|
||||
return (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) != null)
|
||||
}
|
||||
|
||||
fun getUserED25519KeyPair(context: Context): KeyPair? {
|
||||
val base64EncodedED25519PublicKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_PUBLIC_KEY) ?: return null
|
||||
val base64EncodedED25519SecretKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) ?: return null
|
||||
val ed25519PublicKey = Key.fromBytes(Base64.decode(base64EncodedED25519PublicKey))
|
||||
val ed25519SecretKey = Key.fromBytes(Base64.decode(base64EncodedED25519SecretKey))
|
||||
return KeyPair(ed25519PublicKey, ed25519SecretKey)
|
||||
}
|
||||
|
||||
data class KeyPairGenerationResult(
|
||||
val seed: ByteArray,
|
||||
val ed25519KeyPair: KeyPair,
|
||||
val x25519KeyPair: ECKeyPair
|
||||
)
|
||||
}
|
@@ -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)
|
||||
|
@@ -1,134 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class IncomingMediaMessage {
|
||||
|
||||
private final Address from;
|
||||
private final Address groupId;
|
||||
private final String body;
|
||||
private final boolean push;
|
||||
private final long sentTimeMillis;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final boolean expirationUpdate;
|
||||
private final QuoteModel quote;
|
||||
private final boolean unidentified;
|
||||
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
public IncomingMediaMessage(Address from,
|
||||
long sentTimeMillis,
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
boolean expirationUpdate,
|
||||
boolean unidentified,
|
||||
Optional<String> body,
|
||||
Optional<SignalServiceGroup> group,
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<Contact>> sharedContacts,
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
{
|
||||
this.push = true;
|
||||
this.from = from;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.body = body.orNull();
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.quote = quote.orNull();
|
||||
this.unidentified = unidentified;
|
||||
|
||||
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
|
||||
else this.groupId = null;
|
||||
|
||||
this.attachments.addAll(PointerAttachment.forPointers(attachments));
|
||||
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
|
||||
this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
|
||||
}
|
||||
|
||||
public static IncomingMediaMessage from(VisibleMessage message,
|
||||
Address from,
|
||||
long expiresIn,
|
||||
Optional<SignalServiceGroup> group,
|
||||
List<SignalServiceAttachment> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
{
|
||||
return new IncomingMediaMessage(from, message.getReceivedTimestamp(), -1, expiresIn, false,
|
||||
false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews);
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public Address getFrom() {
|
||||
return from;
|
||||
}
|
||||
|
||||
public Address getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public boolean isPushMessage() {
|
||||
return push;
|
||||
}
|
||||
|
||||
public boolean isExpirationUpdate() {
|
||||
return expirationUpdate;
|
||||
}
|
||||
|
||||
public long getSentTimeMillis() {
|
||||
return sentTimeMillis;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public boolean isGroupMessage() {
|
||||
return groupId != null;
|
||||
}
|
||||
|
||||
public QuoteModel getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
public List<Contact> getSharedContacts() {
|
||||
return sharedContacts;
|
||||
}
|
||||
|
||||
public List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
public static OutgoingExpirationUpdateMessage from(ExpirationTimerUpdate message,
|
||||
Recipient recipient) {
|
||||
return new OutgoingExpirationUpdateMessage(recipient, message.getSentTimestamp(), message.getDuration() * 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpirationUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@@ -1,89 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
private final GroupContext group;
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull String encodedGroupContext,
|
||||
@NonNull List<Attachment> avatar,
|
||||
long sentTimeMillis,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
throws IOException
|
||||
{
|
||||
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews);
|
||||
|
||||
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
||||
}
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@Nullable final Attachment avatar,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
System.currentTimeMillis(),
|
||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@Nullable final Attachment avatar,
|
||||
long sentTime,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
sentTime,
|
||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isGroupUpdate() {
|
||||
return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
|
||||
}
|
||||
|
||||
public boolean isGroupQuit() {
|
||||
return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
|
||||
}
|
||||
|
||||
public GroupContext getGroupContext() {
|
||||
return group;
|
||||
}
|
||||
}
|
@@ -1,174 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
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;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.NetworkFailure;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingMediaMessage {
|
||||
|
||||
private final Recipient recipient;
|
||||
protected final String body;
|
||||
protected final List<Attachment> attachments;
|
||||
private final long sentTimeMillis;
|
||||
private final int distributionType;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final QuoteModel outgoingQuote;
|
||||
|
||||
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||
private final List<IdentityKeyMismatch> identityKeyMismatches = new LinkedList<>();
|
||||
private final List<Contact> contacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
int subscriptionId, long expiresIn,
|
||||
int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull List<NetworkFailure> networkFailures,
|
||||
@NonNull List<IdentityKeyMismatch> identityKeyMismatches)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.body = message;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.distributionType = distributionType;
|
||||
this.attachments = attachments;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.outgoingQuote = outgoingQuote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
this.linkPreviews.addAll(linkPreviews);
|
||||
this.networkFailures.addAll(networkFailures);
|
||||
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;
|
||||
this.distributionType = that.distributionType;
|
||||
this.attachments = that.attachments;
|
||||
this.sentTimeMillis = that.sentTimeMillis;
|
||||
this.subscriptionId = that.subscriptionId;
|
||||
this.expiresIn = that.expiresIn;
|
||||
this.outgoingQuote = that.outgoingQuote;
|
||||
|
||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||
this.networkFailures.addAll(that.networkFailures);
|
||||
this.contacts.addAll(that.contacts);
|
||||
this.linkPreviews.addAll(that.linkPreviews);
|
||||
}
|
||||
|
||||
public static OutgoingMediaMessage from(VisibleMessage message,
|
||||
Recipient recipient,
|
||||
List<Attachment> attachments,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@Nullable LinkPreview linkPreview)
|
||||
{
|
||||
List<LinkPreview> previews = Collections.emptyList();
|
||||
if (linkPreview != null) {
|
||||
previews = Collections.singletonList(linkPreview);
|
||||
}
|
||||
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
|
||||
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
|
||||
previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public int getDistributionType() {
|
||||
return distributionType;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isGroup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isExpirationUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public long getSentTimeMillis() {
|
||||
return sentTimeMillis;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public @Nullable QuoteModel getOutgoingQuote() {
|
||||
return outgoingQuote;
|
||||
}
|
||||
|
||||
public @NonNull List<Contact> getSharedContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public @NonNull List<LinkPreview> getLinkPreviews() {
|
||||
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,37 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
||||
|
||||
public OutgoingSecureMediaMessage(Recipient recipient, String body,
|
||||
List<Attachment> attachments,
|
||||
long sentTimeMillis,
|
||||
int distributionType,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
super(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user