You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-android/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java

559 lines
22 KiB
Java

/* 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;
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.PowerManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.squareup.phrase.Phrase;
import org.conscrypt.Conscrypt;
import org.session.libsession.database.MessageDataProvider;
import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.groups.GroupManagerV2;
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager;
import org.session.libsession.messaging.notifications.TokenFetcher;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.sending_receiving.pollers.LegacyClosedGroupPollerV2;
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.snode.SnodeClock;
import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.Device;
import org.session.libsession.utilities.Environment;
import org.session.libsession.utilities.NonTranslatableStringConstants;
import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Toaster;
import org.session.libsession.utilities.UsernameUtils;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.WindowDebouncer;
import org.session.libsignal.utilities.HTTP;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.configs.ConfigUploader;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.LastSentTimestampCache;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.Storage;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
import org.thoughtcrime.securesms.debugmenu.DebugActivity;
import org.thoughtcrime.securesms.dependencies.AppComponent;
import org.thoughtcrime.securesms.dependencies.ConfigFactory;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
import org.thoughtcrime.securesms.dependencies.PollerFactory;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.groups.OpenGroupManager;
import org.thoughtcrime.securesms.groups.handler.AdminStateSync;
import org.thoughtcrime.securesms.groups.handler.CleanupInvitationHandler;
import org.thoughtcrime.securesms.groups.handler.DestroyedGroupSync;
import org.thoughtcrime.securesms.groups.handler.RemoveGroupMemberHandler;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.PushRegistrationHandler;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.VersionDataFetcher;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import dagger.Lazy;
import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp;
import kotlin.Deprecated;
import kotlin.Unit;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.R;
/**
* 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.
*
* @author Moxie Marlinspike
*/
@HiltAndroidApp
public class ApplicationContext extends Application implements DefaultLifecycleObserver, Toaster {
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
private static final String TAG = ApplicationContext.class.getSimpleName();
public Poller poller = null;
public Broadcaster broadcaster = null;
private WindowDebouncer conversationListDebouncer;
private HandlerThread conversationListHandlerThread;
private Handler conversationListHandler;
private PersistentLogger persistentLogger;
@Inject LokiAPIDatabase lokiAPIDatabase;
@Inject public Storage storage;
@Inject Device device;
@Inject MessageDataProvider messageDataProvider;
@Inject TextSecurePreferences textSecurePreferences;
@Inject ConfigFactory configFactory;
@Inject PollerFactory pollerFactory;
@Inject LastSentTimestampCache lastSentTimestampCache;
@Inject VersionDataFetcher versionDataFetcher;
@Inject PushRegistrationHandler pushRegistrationHandler;
@Inject TokenFetcher tokenFetcher;
@Inject GroupManagerV2 groupManagerV2;
@Inject SSKEnvironment.ProfileManagerProtocol profileManager;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@Inject ConfigUploader configUploader;
@Inject AdminStateSync adminStateSync;
@Inject DestroyedGroupSync destroyedGroupSync;
@Inject RemoveGroupMemberHandler removeGroupMemberHandler;
@Inject SnodeClock snodeClock;
@Inject ExpiringMessageManager expiringMessageManager;
@Inject TypingStatusRepository typingStatusRepository;
@Inject TypingStatusSender typingStatusSender;
@Inject ReadReceiptManager readReceiptManager;
@Inject Lazy<MessageNotifier> messageNotifierLazy;
@Inject LokiAPIDatabase apiDB;
@Inject EmojiSearchDatabase emojiSearchDb;
@Inject LegacyClosedGroupPollerV2 legacyClosedGroupPollerV2;
@Inject LegacyGroupDeprecationManager legacyGroupDeprecationManager;
@Inject CleanupInvitationHandler cleanupInvitationHandler;
@Inject UsernameUtils usernameUtils;
public volatile boolean isAppVisible;
public String KEYGUARD_LOCK_TAG = NonTranslatableStringConstants.APP_NAME + ":KeyguardLock";
public String WAKELOCK_TAG = NonTranslatableStringConstants.APP_NAME + ":WakeLock";
@Override
public Object getSystemService(String name) {
if (MessagingModuleConfiguration.MESSAGING_MODULE_SERVICE.equals(name)) {
return messagingModuleConfiguration;
}
return super.getSystemService(name);
}
public static ApplicationContext getInstance(Context context) {
return (ApplicationContext) context.getApplicationContext();
}
@Deprecated(message = "Use proper DI to inject this component")
public TextSecurePreferences getPrefs() {
return EntryPoints.get(getApplicationContext(), AppComponent.class).getPrefs();
}
@Deprecated(message = "Use proper DI to inject this component")
public DatabaseComponent getDatabaseComponent() {
return EntryPoints.get(getApplicationContext(), DatabaseComponent.class);
}
@Deprecated(message = "Use proper DI to inject this component")
public MessageNotifier getMessageNotifier() {
return messageNotifierLazy.get();
}
public Handler getConversationListNotificationHandler() {
if (this.conversationListHandlerThread == null) {
conversationListHandlerThread = new HandlerThread("ConversationListHandler");
conversationListHandlerThread.start();
}
if (this.conversationListHandler == null) {
conversationListHandler = new Handler(conversationListHandlerThread.getLooper());
}
return conversationListHandler;
}
public WindowDebouncer getConversationListDebouncer() {
if (conversationListDebouncer == null) {
conversationListDebouncer = new WindowDebouncer(1000, new Timer());
}
return conversationListDebouncer;
}
public PersistentLogger getPersistentLogger() {
return this.persistentLogger;
}
@Override
public void toast(@StringRes int stringRes, int toastLength, @NonNull Map<String, String> parameters) {
Phrase builder = Phrase.from(this, stringRes);
for (Map.Entry<String,String> entry : parameters.entrySet()) {
builder.put(entry.getKey(), entry.getValue());
}
Toast.makeText(this, builder.format(), toastLength).show();
}
@Override
public void toast(@NonNull CharSequence message, int toastLength) {
Toast.makeText(this, message, toastLength).show();
}
@Override
public void onCreate() {
TextSecurePreferences.setPushSuffix(BuildConfig.PUSH_KEY_SUFFIX);
DatabaseModule.init(this);
MessagingModuleConfiguration.configure(this);
super.onCreate();
messagingModuleConfiguration = new MessagingModuleConfiguration(
this,
storage,
device,
messageDataProvider,
configFactory,
lastSentTimestampCache,
this,
tokenFetcher,
groupManagerV2,
snodeClock,
textSecurePreferences,
legacyClosedGroupPollerV2,
legacyGroupDeprecationManager,
usernameUtils
);
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()");
startKovenant();
initializeSecurityProvider();
initializeLogging();
initializeCrashHandling();
NotificationChannels.create(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
AppContext.INSTANCE.configureKovenant();
broadcaster = new Broadcaster(this);
boolean useTestNet = textSecurePreferences.getEnvironment() == Environment.TEST_NET;
SnodeModule.Companion.configure(apiDB, broadcaster, useTestNet);
initializePeriodicTasks();
SSKEnvironment.Companion.configure(typingStatusRepository, readReceiptManager, profileManager, getMessageNotifier(), expiringMessageManager);
initializeWebRtc();
initializeBlobProvider();
resubmitProfilePictureIfNeeded();
loadEmojiSearchIndexIfNeeded();
EmojiSource.refresh();
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
snodeClock.start();
pushRegistrationHandler.run();
configUploader.start();
removeGroupMemberHandler.start();
destroyedGroupSync.start();
adminStateSync.start();
cleanupInvitationHandler.start();
// add our shortcut debug menu if we are not in a release build
if (BuildConfig.BUILD_TYPE != "release") {
// add the config settings shortcut
Intent intent = new Intent(this, DebugActivity.class);
intent.setAction(Intent.ACTION_VIEW);
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(this, "shortcut_debug_menu")
.setShortLabel("Debug Menu")
.setLongLabel("Debug Menu")
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_settings))
.setIntent(intent)
.build();
ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
}
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
isAppVisible = true;
Log.i(TAG, "App is now visible.");
KeyCachingService.onAppForegrounded(this);
// If the user account hasn't been created or onboarding wasn't finished then don't start
// the pollers
if (textSecurePreferences.getLocalNumber() == null) {
return;
}
ThreadUtils.queue(()->{
if (poller != null) {
poller.setCaughtUp(false);
}
startPollingIfNeeded();
OpenGroupManager.INSTANCE.startPolling();
return Unit.INSTANCE;
});
// fetch last version data
versionDataFetcher.startTimedVersionCheck();
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
isAppVisible = false;
Log.i(TAG, "App is no longer visible.");
KeyCachingService.onAppBackgrounded(this);
getMessageNotifier().setVisibleThread(-1);
if (poller != null) {
poller.stopIfNeeded();
}
pollerFactory.stopAll();
legacyClosedGroupPollerV2.stopAll();
versionDataFetcher.stopTimedVersionCheck();
}
@Override
public void onTerminate() {
stopKovenant(); // Loki
OpenGroupManager.INSTANCE.stopPolling();
pollerFactory.stopAll();
versionDataFetcher.stopTimedVersionCheck();
super.onTerminate();
}
@Deprecated(message = "Use proper DI to inject this component")
public ExpiringMessageManager getExpiringMessageManager() {
return expiringMessageManager;
}
@Deprecated(message = "Use proper DI to inject this component")
public TypingStatusRepository getTypingStatusRepository() {
return typingStatusRepository;
}
@Deprecated(message = "Use proper DI to inject this component")
public TypingStatusSender getTypingStatusSender() {
return typingStatusSender;
}
@Deprecated(message = "Use proper DI to inject this component")
public TextSecurePreferences getTextSecurePreferences() {
return textSecurePreferences;
}
@Deprecated(message = "Use proper DI to inject this component")
public ReadReceiptManager getReadReceiptManager() {
return readReceiptManager;
}
public boolean isAppVisible() {
return isAppVisible;
}
// Loki
private void initializeSecurityProvider() {
try {
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
} catch (ClassNotFoundException e) {
Log.e(TAG, "Failed to find AesGcmCipher class");
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.");
}
}
private void initializeLogging() {
if (persistentLogger == null) {
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 initializePeriodicTasks() {
BackgroundPollWorker.schedulePeriodic(this);
}
private void initializeWebRtc() {
try {
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);
});
}
private static class ProviderInitializationException extends RuntimeException { }
private void setUpPollingIfNeeded() {
String userPublicKey = textSecurePreferences.getLocalNumber();
if (userPublicKey == null) return;
poller = new Poller(configFactory, storage, lokiAPIDatabase);
}
public void startPollingIfNeeded() {
setUpPollingIfNeeded();
if (poller != null) {
poller.startIfNeeded();
}
pollerFactory.startAll();
legacyClosedGroupPollerV2.start();
}
public void retrieveUserProfile() {
setUpPollingIfNeeded();
if (poller != null) {
poller.retrieveUserProfile();
}
}
private void resubmitProfilePictureIfNeeded() {
ProfilePictureUtilities.INSTANCE.resubmitProfilePictureIfNeeded(this);
}
private void loadEmojiSearchIndexIfNeeded() {
Executors.newSingleThreadExecutor().execute(() -> {
if (emojiSearchDb.query("face", 1).isEmpty()) {
try (InputStream inputStream = getAssets().open("emoji/emoji_search_index.json")) {
List<EmojiSearchData> searchIndex = Arrays.asList(JsonUtil.fromJson(inputStream, EmojiSearchData[].class));
emojiSearchDb.setSearchIndex(searchIndex);
} catch (IOException e) {
Log.e("Loki", "Failed to load emoji search index");
}
}
});
}
// Method to clear the local data - returns true on success otherwise false
@SuppressLint("ApplySharedPref")
public boolean clearAllData() {
TextSecurePreferences.clearAll(this);
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
Log.d("Loki", "Failed to delete database.");
return false;
}
configFactory.clearAll();
return true;
}
/**
* Clear all local profile data and message history then restart the app after a brief delay.
* @return true on success, false otherwise.
*/
@SuppressLint("ApplySharedPref")
public boolean clearAllDataAndRestart() {
clearAllData();
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
return true;
}
public void restartApplication() {
Intent intent = new Intent(this, HomeActivity.class);
startActivity(Intent.makeRestartActivityTask(intent.getComponent()));
Runtime.getRuntime().exit(0);
}
// endregion
// Method to wake up the screen and dismiss the keyguard
public void wakeUpDeviceAndDismissKeyguardIfRequired() {
// Get the KeyguardManager and PowerManager
KeyguardManager keyguardManager = (KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE);
PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
// Check if the phone is locked & if the screen is awake
boolean isPhoneLocked = keyguardManager.isKeyguardLocked();
boolean isScreenAwake = powerManager.isInteractive();
if (!isScreenAwake) {
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP
| PowerManager.ON_AFTER_RELEASE,
WAKELOCK_TAG);
// Acquire the wake lock to wake up the device
wakeLock.acquire(3000);
}
// Dismiss the keyguard.
// Note: This will not bypass any app-level (Session) lock; only the device-level keyguard.
// TODO: When moving to a minimum Android API of 27, replace these deprecated calls with new APIs.
if (isPhoneLocked) {
KeyguardManager.KeyguardLock keyguardLock = keyguardManager.newKeyguardLock(KEYGUARD_LOCK_TAG);
keyguardLock.disableKeyguard();
}
}
}