Initial commit of the RingRTC Java interface implementation. The implementation lives in an external .aar with the package org.signal.ringrtc. The package provides two high level objects of interest ======================================================= org.signal.ringrtc.CallConnection -- represents the session of a call, very similar to WebRTC's PeerConnection. org.signal.ringrtc.CallConnectionFactory -- creates CallConnection objects, very similar to WebRTC's PeerConnectionFactory. The implementation interfaces with the Android application in a few places: ================================================================== src/org/thoughtcrime/securesms/ApplicationContext.java -- RingRTC library initialization at application startup. src/org/thoughtcrime/securesms/service/WebRtcCallService.java -- Call creation and state machine. src/org/thoughtcrime/securesms/ringrtc -- this package implements interface classes needed by ringrtc and a CallConnectionWrapper helper class. The two interfaces needed so far are: ringrtc/Logger.java ringrtc/SignalMessageRecipient.java The logger is self-explanatory, but SignalMessageRecipient is a little more involved. SignalMessageRecipient encapsulates the Signal-Android notion of "Recipient" and the mechanism for sending Signal Messages related to audio/video calling. The CallConnectionWrapper class is clone of the original org.thoughtcrime.securesms.webrtc.PeerConnectionWrapper, suitably modified to match the CallConnection interface. This class continues to handle the Camera switching APIs, with that portion of the code remaining unmodified from the original. CallConnectionFactory Details ============================= The primary public methods: initialize() -- initialize the WebRTC library and RingRTC library. The WebRTC initialization is lifted from the original Signal-Android code. createCallConnectionFactory() -- creates a CallConnectionFactory object. Internally it creates a WebRTC PeerConnectionFactory object and a RingRTC CallConnectionFactory object. dispose() -- tears down the CallConnectionFactory object, including the internal PeerConnectionFactory and RingRTC CallConnectionFactory. createCallConnection() -- creates a CallConnection object, connecting that with an application controlled CallConnection.Observer object. This function takes a CallConnection.Configuration object to link the CallConnection object with some application provided services, like sending Signal protocol messages. CallConnection Details ====================== This object is a subclass of WebRTC's PeerConnection class. The primary public methods and objects: CallConnection.Configuration ---------------------------- Configuration object used to parameterize a call. Notable members: - SignalServiceMessageSender messageSender - long callId - org.signal.SignalMessageRecipient recipient The 'accountManager' is used to fetch public information from the Signal service, specifically used here to obtain the public Signal TURN server details. The 'callId' is a 64-bit pseudo-random number generated when the call is initiated, used to identify the call through out its lifetime. The "recipient' is an implementation of the org.signal.SignalMessageRecipient interface, which encapsulates the sending of Signal service messages to a recipient (remote peer) using existing Signal protocol data structures. The native library needs to be able to send Signal messages via the service, but it does not have a native implementation to do so. Instead the native code calls out to the client for sending Signal messages. To accomplish this, the client implements the org.signal.SignalMessageRecipient interface and passes an instance of that in a CallConnection.Configuration object. CallConnection -------------- dispose() -- tears down the CallConnection object, including the internal PeerConnection and RingRTC CallConnection. sendOffer() -- initiates a call to a remote recipient. This is the beginning of an outbound call. validateResponse() -- checks an offer response recipient against the originating call details. handleOfferAnswer() -- handles the receipt of answer, which was a response from an originating offer. acceptOffer() -- accept an offer from a remote participant. This is the begin of an incoming call. answerCall() -- invoked when the call is completely established and online. hangUp() -- hang up the connection and shut things done. This is the end of the call. sendBusy() -- send the remote side an indication that the local side is already in a call and the line is busy. sendVideoStatus() -- send the current state of the local camera video stream to the remote side. CallConnection.Observer ----------------------- Observer object, used by the RingRTC library to notify the client application of important events and status changes. Similar in spirit to WebRTC's PeerConnection.Observer. Observer callbacks come in three flavors: - state change notifications, - on stream notifications - errors conditions For state notifications, the callback contains the callId, the recipient and a CallConnection.CallEvent type. For streams, the callback contains the callId, the recipient and a org.webrtc.MediaStream. For errors, the callback contains the callId, the recipient and an exception type. The currently thrown exceptions include: - UntrustedIdentityException - UnregisteredUserException - IOException Signed-off-by: Curt Brune <curt@signal.org> Updates to support ringrtc-android version 0.1.0. * simplify logging interface It is no longer necessary for the application to specify a Log object as the library can log via the NDK directly. * improve error handling and notification In a number of places where ringrtc errors could occur, no notification was ever sent to the user, nor was the UI cleaned up. It would look like the app was in hung state. This patch updates these situations to send the WebRtcViewModel a NETWORK_FAILURE message. * update handleIncomingCall() for lockManager and notification During the conversion to RingRTC, the implementation of handleIncomingCall() missed a couple of things: -- updating the Phone state with the lockManager -- sending a message to the viewModel * log the callId in various handler methods For debugging purposes it is very handy to have the callId present in the log during the various call handler methods. Signed-off-by: Curt Brune <curt@signal.org>
384 lines
14 KiB
Java
384 lines
14 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 android.annotation.SuppressLint;
|
|
|
|
import androidx.camera.camera2.Camera2AppConfig;
|
|
import androidx.camera.core.CameraX;
|
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
|
import androidx.lifecycle.LifecycleOwner;
|
|
import androidx.lifecycle.ProcessLifecycleOwner;
|
|
import android.content.Context;
|
|
import android.os.AsyncTask;
|
|
import android.os.Build;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.multidex.MultiDexApplication;
|
|
|
|
import com.google.android.gms.security.ProviderInstaller;
|
|
|
|
import org.conscrypt.Conscrypt;
|
|
import org.signal.aesgcmprovider.AesGcmProvider;
|
|
import org.signal.ringrtc.CallConnectionFactory;
|
|
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
|
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
|
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
|
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
|
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
|
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
|
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
|
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
|
|
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
|
import org.thoughtcrime.securesms.logging.Log;
|
|
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
|
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
|
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
|
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
|
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
|
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
|
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
|
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
|
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
|
import org.webrtc.voiceengine.WebRtcAudioManager;
|
|
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
|
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
|
|
|
import java.security.Security;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/**
|
|
* Will be called once when the TextSecure process is created.
|
|
*
|
|
* 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
|
|
*/
|
|
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
|
|
|
|
private static final String TAG = ApplicationContext.class.getSimpleName();
|
|
|
|
private ExpiringMessageManager expiringMessageManager;
|
|
private ViewOnceMessageManager viewOnceMessageManager;
|
|
private TypingStatusRepository typingStatusRepository;
|
|
private TypingStatusSender typingStatusSender;
|
|
private JobManager jobManager;
|
|
private IncomingMessageObserver incomingMessageObserver;
|
|
private PersistentLogger persistentLogger;
|
|
|
|
private volatile boolean isAppVisible;
|
|
|
|
public static ApplicationContext getInstance(Context context) {
|
|
return (ApplicationContext)context.getApplicationContext();
|
|
}
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
Log.i(TAG, "onCreate()");
|
|
initializeSecurityProvider();
|
|
initializeLogging();
|
|
initializeCrashHandling();
|
|
initializeAppDependencies();
|
|
initializeJobManager();
|
|
initializeApplicationMigrations();
|
|
initializeMessageRetrieval();
|
|
initializeExpiringMessageManager();
|
|
initializeRevealableMessageManager();
|
|
initializeTypingStatusRepository();
|
|
initializeTypingStatusSender();
|
|
initializeGcmCheck();
|
|
initializeSignedPreKeyCheck();
|
|
initializePeriodicTasks();
|
|
initializeCircumvention();
|
|
initializeRingRtc();
|
|
initializePendingMessages();
|
|
initializeUnidentifiedDeliveryAbilityRefresh();
|
|
initializeBlobProvider();
|
|
initializeCameraX();
|
|
NotificationChannels.create(this);
|
|
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
|
jobManager.beginJobLoop();
|
|
}
|
|
|
|
@Override
|
|
public void onStart(@NonNull LifecycleOwner owner) {
|
|
isAppVisible = true;
|
|
Log.i(TAG, "App is now visible.");
|
|
executePendingContactSync();
|
|
KeyCachingService.onAppForegrounded(this);
|
|
}
|
|
|
|
@Override
|
|
public void onStop(@NonNull LifecycleOwner owner) {
|
|
isAppVisible = false;
|
|
Log.i(TAG, "App is no longer visible.");
|
|
KeyCachingService.onAppBackgrounded(this);
|
|
MessageNotifier.setVisibleThread(-1);
|
|
}
|
|
|
|
public JobManager getJobManager() {
|
|
return jobManager;
|
|
}
|
|
|
|
public ExpiringMessageManager getExpiringMessageManager() {
|
|
return expiringMessageManager;
|
|
}
|
|
|
|
public ViewOnceMessageManager getViewOnceMessageManager() {
|
|
return viewOnceMessageManager;
|
|
}
|
|
|
|
public TypingStatusRepository getTypingStatusRepository() {
|
|
return typingStatusRepository;
|
|
}
|
|
|
|
public TypingStatusSender getTypingStatusSender() {
|
|
return typingStatusSender;
|
|
}
|
|
|
|
public boolean isAppVisible() {
|
|
return isAppVisible;
|
|
}
|
|
|
|
public PersistentLogger getPersistentLogger() {
|
|
return persistentLogger;
|
|
}
|
|
|
|
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() {
|
|
persistentLogger = new PersistentLogger(this);
|
|
org.thoughtcrime.securesms.logging.Log.initialize(new AndroidLogger(), persistentLogger);
|
|
|
|
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
|
}
|
|
|
|
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)))
|
|
.build());
|
|
}
|
|
|
|
private void initializeApplicationMigrations() {
|
|
ApplicationMigrations.onApplicationCreate(this, jobManager);
|
|
}
|
|
|
|
public void initializeMessageRetrieval() {
|
|
this.incomingMessageObserver = new IncomingMessageObserver(this);
|
|
}
|
|
|
|
private void initializeAppDependencies() {
|
|
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this, new SignalServiceNetworkAccess(this)));
|
|
}
|
|
|
|
private void initializeGcmCheck() {
|
|
if (TextSecurePreferences.isPushRegistered(this)) {
|
|
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
|
|
|
|
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) {
|
|
this.jobManager.add(new FcmRefreshJob());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void initializeSignedPreKeyCheck() {
|
|
if (!TextSecurePreferences.isSignedPreKeyRegistered(this)) {
|
|
jobManager.add(new CreateSignedPreKeyJob(this));
|
|
}
|
|
}
|
|
|
|
private void initializeExpiringMessageManager() {
|
|
this.expiringMessageManager = new ExpiringMessageManager(this);
|
|
}
|
|
|
|
private void initializeRevealableMessageManager() {
|
|
this.viewOnceMessageManager = new ViewOnceMessageManager(this);
|
|
}
|
|
|
|
private void initializeTypingStatusRepository() {
|
|
this.typingStatusRepository = new TypingStatusRepository();
|
|
}
|
|
|
|
private void initializeTypingStatusSender() {
|
|
this.typingStatusSender = new TypingStatusSender(this);
|
|
}
|
|
|
|
private void initializePeriodicTasks() {
|
|
RotateSignedPreKeyListener.schedule(this);
|
|
DirectoryRefreshListener.schedule(this);
|
|
LocalBackupListener.schedule(this);
|
|
RotateSenderCertificateListener.schedule(this);
|
|
|
|
if (BuildConfig.PLAY_STORE_DISABLED) {
|
|
UpdateApkRefreshListener.schedule(this);
|
|
}
|
|
}
|
|
|
|
private void initializeRingRtc() {
|
|
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);
|
|
}
|
|
|
|
CallConnectionFactory.initialize(this);
|
|
} catch (UnsatisfiedLinkError e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
}
|
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
private void initializeCircumvention() {
|
|
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
|
|
@Override
|
|
protected Void doInBackground(Void... params) {
|
|
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
|
|
try {
|
|
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
|
} catch (Throwable t) {
|
|
Log.w(TAG, t);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
}
|
|
|
|
private void executePendingContactSync() {
|
|
if (TextSecurePreferences.needsFullContactSync(this)) {
|
|
ApplicationContext.getInstance(this).getJobManager().add(new MultiDeviceContactUpdateJob(this, true));
|
|
}
|
|
}
|
|
|
|
private void initializePendingMessages() {
|
|
if (TextSecurePreferences.getNeedsMessagePull(this)) {
|
|
Log.i(TAG, "Scheduling a message fetch.");
|
|
if (Build.VERSION.SDK_INT >= 26) {
|
|
FcmJobService.schedule(this);
|
|
} else {
|
|
ApplicationContext.getInstance(this).getJobManager().add(new PushNotificationReceiveJob(this));
|
|
}
|
|
TextSecurePreferences.setNeedsMessagePull(this, false);
|
|
}
|
|
}
|
|
|
|
private void initializeUnidentifiedDeliveryAbilityRefresh() {
|
|
if (TextSecurePreferences.isMultiDevice(this) && !TextSecurePreferences.isUnidentifiedDeliveryEnabled(this)) {
|
|
jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob());
|
|
}
|
|
}
|
|
|
|
private void initializeBlobProvider() {
|
|
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
|
BlobProvider.getInstance().onSessionStart(this);
|
|
});
|
|
}
|
|
|
|
@SuppressLint("RestrictedApi")
|
|
private void initializeCameraX() {
|
|
if (Build.VERSION.SDK_INT >= 21) {
|
|
try {
|
|
CameraX.init(this, Camera2AppConfig.create(this));
|
|
} catch (Throwable t) {
|
|
Log.w(TAG, "Failed to initialize CameraX.");
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void attachBaseContext(Context base) {
|
|
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
|
}
|
|
|
|
private static class ProviderInitializationException extends RuntimeException {
|
|
}
|
|
}
|