Beta support for webrtc video and voice calling

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2016-11-09 09:37:40 -08:00
parent a9651e2e9c
commit ea0945d406
96 changed files with 6098 additions and 130 deletions

View File

@@ -1,46 +0,0 @@
package org.thoughtcrime.redphone;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.TextUtils;
public class RedPhoneShare extends Activity {
private static final String TAG = RedPhone.class.getSimpleName();
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
if (getIntent().getData() != null && "content".equals(getIntent().getData().getScheme())) {
Cursor cursor = null;
try {
cursor = getContentResolver().query(getIntent().getData(), null, null, null, null);
if (cursor != null && cursor.moveToNext()) {
String destination = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1));
if (!TextUtils.isEmpty(destination)) {
Intent serviceIntent = new Intent(this, RedPhoneService.class);
serviceIntent.setAction(RedPhoneService.ACTION_OUTGOING_CALL);
serviceIntent.putExtra(RedPhoneService.EXTRA_REMOTE_NUMBER, destination);
startService(serviceIntent);
Intent activityIntent = new Intent(this, RedPhone.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
}
}
} finally {
if (cursor != null) cursor.close();
}
}
finish();
}
}

View File

@@ -0,0 +1,70 @@
package org.thoughtcrime.redphone;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class VoiceCallShare extends Activity {
private static final String TAG = VoiceCallShare.class.getSimpleName();
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
if (getIntent().getData() != null && "content".equals(getIntent().getData().getScheme())) {
Cursor cursor = null;
try {
cursor = getContentResolver().query(getIntent().getData(), null, null, null, null);
if (cursor != null && cursor.moveToNext()) {
String destination = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1));
if (!TextUtils.isEmpty(destination)) {
Recipients recipients = RecipientFactory.getRecipientsFromString(this, destination, true);
DirectoryHelper.UserCapabilities capabilities = DirectoryHelper.getUserCapabilities(this, recipients);
if (TextSecurePreferences.isWebrtcCallingEnabled(this) &&
capabilities.getVideoCapability() == DirectoryHelper.UserCapabilities.Capability.SUPPORTED)
{
Intent serviceIntent = new Intent(this, WebRtcCallService.class);
serviceIntent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
serviceIntent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, destination);
startService(serviceIntent);
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
} else {
Intent serviceIntent = new Intent(this, RedPhoneService.class);
serviceIntent.setAction(RedPhoneService.ACTION_OUTGOING_CALL);
serviceIntent.putExtra(RedPhoneService.EXTRA_REMOTE_NUMBER, destination);
startService(serviceIntent);
Intent activityIntent = new Intent(this, RedPhone.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
}
}
}
} finally {
if (cursor != null) cursor.close();
}
}
finish();
}
}

View File

@@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.webrtc.PeerConnectionFactory;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.dependencies.DependencyInjector;
import org.whispersystems.jobqueue.requirements.NetworkRequirementProvider;
@@ -85,6 +86,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
initializeSignedPreKeyCheck();
initializePeriodicTasks();
initializeCircumvention();
PeerConnectionFactory.initializeAndroidGlobals(this, true, true, true);
}
@Override

View File

@@ -2,18 +2,14 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.os.AsyncTask;
import android.support.v7.app.AlertDialog;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
@@ -31,6 +27,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
@@ -166,12 +163,14 @@ public class ConfirmIdentityDialog extends AlertDialog {
mismatch.getRecipientId(),
mismatch.getIdentityKey());
boolean legacy = !messageRecord.isContentBundleKeyExchange();
SignalServiceEnvelope envelope = new SignalServiceEnvelope(SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE,
messageRecord.getIndividualRecipient().getNumber(),
messageRecord.getRecipientDeviceId(), "",
messageRecord.getDateSent(),
Base64.decode(messageRecord.getBody().getBody()),
null);
legacy ? Base64.decode(messageRecord.getBody().getBody()) : null,
!legacy ? Base64.decode(messageRecord.getBody().getBody()) : null);
long pushId = pushDatabase.insert(envelope);
@@ -197,22 +196,4 @@ public class ConfirmIdentityDialog extends AlertDialog {
}
}
private static class VerifySpan extends ClickableSpan {
private final Context context;
private final IdentityKeyMismatch mismatch;
private VerifySpan(Context context, IdentityKeyMismatch mismatch) {
this.context = context;
this.mismatch = mismatch;
}
@Override
public void onClick(View widget) {
Intent intent = new Intent(context, VerifyIdentityActivity.class);
intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, mismatch.getRecipientId());
intent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(mismatch.getIdentityKey()));
context.startActivity(intent);
}
}
}

View File

@@ -123,6 +123,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.recipients.Recipients.RecipientsModifiedListener;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
@@ -224,7 +225,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private int distributionType;
private boolean archived;
private boolean isSecureText;
private boolean isSecureVoice;
private boolean isSecureVideo;
private boolean isDefaultSms = true;
private boolean isMmsEnabled = true;
@@ -437,8 +438,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
if (isSingleConversation()) {
if (isSecureVoice) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
} else if (isGroupConversation()) {
inflater.inflate(R.menu.conversation_group_options, menu);
@@ -749,7 +750,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void handleDial(final Recipient recipient) {
if (recipient == null) return;
if (isSecureVoice) {
if (isSecureVideo && TextSecurePreferences.isWebrtcCallingEnabled(this)) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, recipient.getNumber());
startService(intent);
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
} else if (isSecureText) {
Intent intent = new Intent(this, RedPhoneService.class);
intent.setAction(RedPhoneService.ACTION_OUTGOING_CALL);
intent.putExtra(RedPhoneService.EXTRA_REMOTE_NUMBER, recipient.getNumber());
@@ -806,9 +816,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
startActivity(intent);
}
private void handleSecurityChange(boolean isSecureText, boolean isSecureVoice, boolean isDefaultSms) {
private void handleSecurityChange(boolean isSecureText, boolean isSecureVideo, boolean isDefaultSms) {
this.isSecureText = isSecureText;
this.isSecureVoice = isSecureVoice;
this.isSecureVideo = isSecureVideo;
this.isDefaultSms = isDefaultSms;
boolean isMediaMessage = !recipients.isSingleRecipient() || attachmentManager.isAttachmentPresent();
@@ -889,13 +899,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private ListenableFuture<Boolean> initializeSecurity(final boolean currentSecureText,
final boolean currentSecureVoice,
final boolean currentSecureVideo,
final boolean currentIsDefaultSms)
{
final SettableFuture<Boolean> future = new SettableFuture<>();
handleSecurityChange(currentSecureText || isPushGroupConversation(),
currentSecureVoice && !isGroupConversation(),
currentSecureVideo && !isGroupConversation(),
currentIsDefaultSms);
new AsyncTask<Recipients, Void, boolean[]>() {
@@ -923,7 +933,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected void onPostExecute(boolean[] result) {
if (result[0] != currentSecureText || result[1] != currentSecureVoice || result[2] != currentIsDefaultSms) {
if (result[0] != currentSecureText || result[1] != currentSecureVideo || result[2] != currentIsDefaultSms) {
handleSecurityChange(result[0], result[1], result[2]);
}
future.set(true);
@@ -1120,7 +1130,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
securityUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
initializeSecurity(isSecureText, isSecureVoice, isDefaultSms);
initializeSecurity(isSecureText, isSecureVideo, isDefaultSms);
calculateCharactersRemaining();
}
};
@@ -1768,7 +1778,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onAttachmentChanged() {
handleSecurityChange(isSecureText, isSecureVoice, isDefaultSms);
handleSecurityChange(isSecureText, isSecureVideo, isDefaultSms);
updateToggleButtonState();
}

View File

@@ -520,8 +520,9 @@ public class RegistrationProgressActivity extends BaseActionBarActivity {
try {
SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(context, e164number, password);
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
boolean video = TextSecurePreferences.isWebrtcCallingEnabled(context);
accountManager.verifyAccountWithCode(code, signalingKey, registrationId, true);
accountManager.verifyAccountWithCode(code, signalingKey, registrationId, true, video);
return SUCCESS;
} catch (ExpectationFailedException e) {
@@ -616,10 +617,10 @@ public class RegistrationProgressActivity extends BaseActionBarActivity {
return SUCCESS;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);
Log.w(TAG, e);
return RATE_LIMIT_EXCEEDED;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
Log.w(TAG, e);
return NETWORK_ERROR;
}
}

View File

@@ -0,0 +1,404 @@
/*
* Copyright (C) 2016 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.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import org.thoughtcrime.redphone.util.AudioUtils;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
import org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.events.WebRtcCallEvent;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey;
import de.greenrobot.event.EventBus;
public class WebRtcCallActivity extends Activity {
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
private static final int STANDARD_DELAY_FINISH = 1000;
public static final int BUSY_SIGNAL_DELAY_FINISH = 5500;
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION";
private WebRtcCallScreen callScreen;
private BroadcastReceiver bluetoothStateReceiver;
@Override
public void onCreate(Bundle savedInstanceState) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
initializeResources();
}
@Override
public void onResume() {
super.onResume();
initializeScreenshotSecurity();
EventBus.getDefault().registerSticky(this);
registerBluetoothReceiver();
}
@Override
public void onNewIntent(Intent intent){
if (ANSWER_ACTION.equals(intent.getAction())) {
handleAnswerCall();
} else if (DENY_ACTION.equals(intent.getAction())) {
handleDenyCall();
} else if (END_CALL_ACTION.equals(intent.getAction())) {
handleEndCall();
}
}
@Override
public void onPause() {
super.onPause();
EventBus.getDefault().unregister(this);
unregisterReceiver(bluetoothStateReceiver);
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
}
private void initializeScreenshotSecurity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
TextSecurePreferences.isScreenSecurityEnabled(this))
{
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
private void initializeResources() {
callScreen = ViewUtil.findById(this, R.id.callScreen);
callScreen.setHangupButtonListener(new HangupButtonListener());
callScreen.setIncomingCallActionListener(new IncomingCallActionListener());
callScreen.setAudioMuteButtonListener(new AudioMuteButtonListener());
callScreen.setVideoMuteButtonListener(new VideoMuteButtonListener());
callScreen.setAudioButtonListener(new AudioButtonListener());
}
private void handleSetMuteAudio(boolean enabled) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_SET_MUTE_AUDIO);
intent.putExtra(WebRtcCallService.EXTRA_MUTE, enabled);
startService(intent);
}
private void handleSetMuteVideo(boolean muted) {
callScreen.setLocalVideoEnabled(!muted);
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_SET_MUTE_VIDEO);
intent.putExtra(WebRtcCallService.EXTRA_MUTE, muted);
startService(intent);
}
private void handleAnswerCall() {
WebRtcCallEvent event = EventBus.getDefault().getStickyEvent(WebRtcCallEvent.class);
if (event != null) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering));
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ANSWER_CALL);
startService(intent);
}
}
private void handleDenyCall() {
WebRtcCallEvent event = EventBus.getDefault().getStickyEvent(WebRtcCallEvent.class);
if (event != null) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_DENY_CALL);
startService(intent);
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_ending_call));
delayedFinish();
}
}
private void handleEndCall() {
Log.w(TAG, "Hangup pressed, handling termination now...");
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_LOCAL_HANGUP);
startService(intent);
WebRtcCallEvent event = EventBus.getDefault().getStickyEvent(WebRtcCallEvent.class);
if (event != null) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient());
}
}
private void handleIncomingCall(@NonNull WebRtcCallEvent event) {
callScreen.setIncomingCall(event.getRecipient());
}
private void handleOutgoingCall(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_dialing));
}
private void handleTerminate(@NonNull Recipient recipient /*, int terminationType */) {
Log.w(TAG, "handleTerminate called");
callScreen.setActiveCall(recipient, getString(R.string.RedPhone_ending_call));
EventBus.getDefault().removeStickyEvent(WebRtcCallEvent.class);
delayedFinish();
}
private void handleCallRinging(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_ringing));
}
private void handleCallBusy(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_busy));
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
}
private void handleCallConnected(@NonNull WebRtcCallEvent event) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_connected), "");
}
private void handleConnectingToInitiator(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_connecting));
}
private void handleHandshakeFailed(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_handshake_failed));
delayedFinish();
}
private void handleRecipientUnavailable(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_recipient_unavailable));
delayedFinish();
}
private void handlePerformingHandshake(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_performing_handshake));
}
private void handleServerFailure(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_network_failed));
delayedFinish();
}
private void handleLoginFailed(@NonNull WebRtcCallEvent event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_login_failed));
delayedFinish();
}
private void handleNoSuchUser(final @NonNull WebRtcCallEvent event) {
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.RedPhone_number_not_registered);
dialog.setIconAttribute(R.attr.dialog_alert_icon);
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice);
dialog.setCancelable(true);
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient());
}
});
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient());
}
});
dialog.show();
}
private void handleRemoteVideoDisabled(@NonNull WebRtcCallEvent event) {
callScreen.setRemoteVideoEnabled(false);
}
private void handleRemoteVideoEnabled(@NonNull WebRtcCallEvent event) {
callScreen.setRemoteVideoEnabled(true);
}
private void handleUntrustedIdentity(@NonNull WebRtcCallEvent event) {
final IdentityKey theirIdentity = (IdentityKey)event.getExtra();
final Recipient recipient = event.getRecipient();
callScreen.setUntrustedIdentity(recipient, theirIdentity);
callScreen.setAcceptIdentityListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(WebRtcCallActivity.this);
identityDatabase.saveIdentity(recipient.getRecipientId(), theirIdentity);
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, recipient.getNumber());
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
startService(intent);
}
});
callScreen.setCancelIdentityButton(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleTerminate(recipient);
}
});
}
private void delayedFinish() {
delayedFinish(STANDARD_DELAY_FINISH);
}
private void delayedFinish(int delayMillis) {
callScreen.postDelayed(new Runnable() {
public void run() {
WebRtcCallActivity.this.finish();
}
}, delayMillis);
}
@SuppressWarnings("unused")
public void onEventMainThread(final WebRtcCallEvent event) {
Log.w(TAG, "Got message from service: " + event.getType());
switch (event.getType()) {
case CALL_CONNECTED: handleCallConnected(event); break;
case SERVER_FAILURE: handleServerFailure(event); break;
case PERFORMING_HANDSHAKE: handlePerformingHandshake(event); break;
case HANDSHAKE_FAILED: handleHandshakeFailed(event); break;
case CONNECTING_TO_INITIATOR: handleConnectingToInitiator(event); break;
case CALL_RINGING: handleCallRinging(event); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient()); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case INCOMING_CALL: handleIncomingCall(event); break;
case OUTGOING_CALL: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break;
case LOGIN_FAILED: handleLoginFailed(event); break;
case REMOTE_VIDEO_DISABLED: handleRemoteVideoDisabled(event); break;
case REMOTE_VIDEO_ENABLED: handleRemoteVideoEnabled(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
}
private class HangupButtonListener implements WebRtcCallScreen.HangupButtonListener {
public void onClick() {
handleEndCall();
}
}
private class AudioMuteButtonListener implements WebRtcCallControls.MuteButtonListener {
@Override
public void onToggle(boolean isMuted) {
WebRtcCallActivity.this.handleSetMuteAudio(isMuted);
}
}
private class VideoMuteButtonListener implements WebRtcCallControls.MuteButtonListener {
@Override
public void onToggle(boolean isMuted) {
WebRtcCallActivity.this.handleSetMuteVideo(isMuted);
}
}
private void registerBluetoothReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(AudioUtils.getScoUpdateAction());
bluetoothStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
callScreen.notifyBluetoothChange();
}
};
registerReceiver(bluetoothStateReceiver, filter);
callScreen.notifyBluetoothChange();
}
private class AudioButtonListener implements WebRtcCallControls.AudioButtonListener {
@Override
public void onAudioChange(AudioUtils.AudioMode mode) {
switch(mode) {
case DEFAULT:
AudioUtils.enableDefaultRouting(WebRtcCallActivity.this);
break;
case SPEAKER:
AudioUtils.enableSpeakerphoneRouting(WebRtcCallActivity.this);
break;
case HEADSET:
AudioUtils.enableBluetoothRouting(WebRtcCallActivity.this);
break;
default:
throw new IllegalStateException("Audio mode " + mode + " is not supported.");
}
}
}
private class IncomingCallActionListener implements WebRtcIncomingCallOverlay.IncomingCallActionListener {
@Override
public void onAcceptClick() {
WebRtcCallActivity.this.handleAnswerCall();
}
@Override
public void onDenyClick() {
WebRtcCallActivity.this.handleDenyCall();
}
}
}

View File

@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.Util;
import java.util.List;
import java.util.concurrent.ExecutionException;
class EmojiProvider {
@@ -108,7 +109,7 @@ class EmojiProvider {
});
}
@Override public void onFailure(Throwable error) {
@Override public void onFailure(ExecutionException error) {
Log.w(TAG, error);
}
});

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Xfermode;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.LinkedList;
import java.util.List;
/**
* Simple container that confines the children to a subrectangle specified as percentage values of
* the container size. The children are centered horizontally and vertically inside the confined
* space.
*/
public class PercentFrameLayout extends ViewGroup {
private int xPercent = 0;
private int yPercent = 0;
private int widthPercent = 100;
private int heightPercent = 100;
private boolean square = false;
private boolean hidden = false;
public PercentFrameLayout(Context context) {
super(context);
}
public PercentFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PercentFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setSquare(boolean square) {
this.square = square;
}
public void setHidden(boolean hidden) {
this.hidden = hidden;
}
public void setPosition(int xPercent, int yPercent, int widthPercent, int heightPercent) {
this.xPercent = xPercent;
this.yPercent = yPercent;
this.widthPercent = widthPercent;
this.heightPercent = heightPercent;
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int width = getDefaultSize(Integer.MAX_VALUE, widthMeasureSpec);
final int height = getDefaultSize(Integer.MAX_VALUE, heightMeasureSpec);
setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
int childWidth = width * widthPercent / 100;
int childHeight = height * heightPercent / 100;
if (square) {
if (width > height) childWidth = childHeight;
else childHeight = childWidth;
}
if (hidden) {
childWidth = 1;
childHeight = 1;
}
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST);
for (int i = 0; i < getChildCount(); ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = right - left;
final int height = bottom - top;
// Sub-rectangle specified by percentage values.
final int subWidth = width * widthPercent / 100;
final int subHeight = height * heightPercent / 100;
final int subLeft = left + width * xPercent / 100;
final int subTop = top + height * yPercent / 100;
for (int i = 0; i < getChildCount(); ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
// Center child both vertically and horizontally.
int childLeft = subLeft + (subWidth - childWidth) / 2;
int childTop = subTop + (subHeight - childHeight) / 2;
if (hidden) {
childLeft = 0;
childTop = 0;
}
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
}
}
}

View File

@@ -0,0 +1,124 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import org.thoughtcrime.redphone.util.AudioUtils;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class WebRtcCallControls extends LinearLayout {
private CompoundButton audioMuteButton;
private CompoundButton videoMuteButton;
private WebRtcInCallAudioButton audioButton;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
public WebRtcCallControls(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WebRtcCallControls(Context context) {
super(context);
initialize();
}
private void initialize() {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_call_controls, this, true);
this.audioMuteButton = (CompoundButton) findViewById(R.id.muteButton);
this.videoMuteButton = ViewUtil.findById(this, R.id.video_mute_button);
this.audioButton = new WebRtcInCallAudioButton((CompoundButton) findViewById(R.id.audioButton));
updateAudioButton();
}
public void updateAudioButton() {
audioButton.setAudioMode(AudioUtils.getCurrentAudioMode(getContext()));
IntentFilter filter = new IntentFilter();
filter.addAction(AudioUtils.getScoUpdateAction());
handleBluetoothIntent(getContext().registerReceiver(null, filter));
}
private void handleBluetoothIntent(Intent intent) {
if (intent == null) {
return;
}
if (!intent.getAction().equals(AudioUtils.getScoUpdateAction())) {
return;
}
Integer state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
if (state.equals(AudioManager.SCO_AUDIO_STATE_CONNECTED)) {
audioButton.setHeadsetAvailable(true);
} else if (state.equals(AudioManager.SCO_AUDIO_STATE_DISCONNECTED)) {
audioButton.setHeadsetAvailable(false);
}
}
public void setAudioMuteButtonListener(final MuteButtonListener listener) {
audioMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
listener.onToggle(b);
}
});
}
public void setVideoMuteButtonListener(final MuteButtonListener listener) {
videoMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onToggle(!isChecked);
}
});
}
public void setAudioButtonListener(final AudioButtonListener listener) {
audioButton.setListener(listener);
}
public void reset() {
updateAudioButton();
audioMuteButton.setChecked(false);
videoMuteButton.setChecked(false);
}
public static interface MuteButtonListener {
public void onToggle(boolean isMuted);
}
public static interface AudioButtonListener {
public void onAudioChange(AudioUtils.AudioMode mode);
}
}

View File

@@ -0,0 +1,292 @@
/*
* Copyright (C) 2016 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.components.webrtc;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.webrtc.SurfaceViewRenderer;
import org.whispersystems.libsignal.IdentityKey;
/**
* A UI widget that encapsulates the entire in-call screen
* for both initiators and responders.
*
* @author Moxie Marlinspike
*
*/
public class WebRtcCallScreen extends FrameLayout implements Recipient.RecipientModifiedListener {
private static final String TAG = WebRtcCallScreen.class.getSimpleName();
private ImageView photo;
private PercentFrameLayout localRenderLayout;
private PercentFrameLayout remoteRenderLayout;
private TextView name;
private TextView phoneNumber;
private TextView label;
private TextView elapsedTime;
private View untrustedIdentityContainer;
private TextView untrustedIdentityExplanation;
private Button acceptIdentityButton;
private Button cancelIdentityButton;
private TextView status;
private FloatingActionButton endCallButton;
private WebRtcCallControls controls;
private Recipient recipient;
private WebRtcIncomingCallOverlay incomingCallOverlay;
public WebRtcCallScreen(Context context) {
super(context);
initialize();
}
public WebRtcCallScreen(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WebRtcCallScreen(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message, @Nullable String sas) {
setCard(personInfo, message);
setConnected(WebRtcCallService.localRenderer, WebRtcCallService.remoteRenderer);
incomingCallOverlay.setActiveCall(sas);
}
public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message) {
setCard(personInfo, message);
incomingCallOverlay.setActiveCall();
}
public void setIncomingCall(Recipient personInfo) {
setCard(personInfo, getContext().getString(R.string.CallScreen_Incoming_call));
incomingCallOverlay.setIncomingCall();
}
public void setUntrustedIdentity(Recipient personInfo, IdentityKey untrustedIdentity) {
String name = recipient.toShortString();
String introduction = String.format(getContext().getString(R.string.WebRtcCallScreen_new_safety_numbers), name, name);
SpannableString spannableString = new SpannableString(introduction + " " + getContext().getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
spannableString.setSpan(new VerifySpan(getContext(), personInfo.getRecipientId(), untrustedIdentity),
introduction.length()+1, spannableString.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setPersonInfo(personInfo);
this.incomingCallOverlay.setActiveCall();
this.status.setText(R.string.WebRtcCallScreen_new_safety_numbers_title);
this.untrustedIdentityContainer.setVisibility(View.VISIBLE);
this.untrustedIdentityExplanation.setText(spannableString);
this.untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
this.endCallButton.setVisibility(View.INVISIBLE);
}
public void reset() {
setPersonInfo(Recipient.getUnknownRecipient());
this.status.setText("");
this.recipient = null;
this.controls.reset();
this.untrustedIdentityExplanation.setText("");
this.untrustedIdentityContainer.setVisibility(View.GONE);
this.localRenderLayout.removeAllViews();
this.remoteRenderLayout.removeAllViews();
incomingCallOverlay.reset();
}
public void setIncomingCallActionListener(WebRtcIncomingCallOverlay.IncomingCallActionListener listener) {
incomingCallOverlay.setIncomingCallActionListener(listener);
}
public void setAudioMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) {
this.controls.setAudioMuteButtonListener(listener);
}
public void setVideoMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) {
this.controls.setVideoMuteButtonListener(listener);
}
public void setAudioButtonListener(WebRtcCallControls.AudioButtonListener listener) {
this.controls.setAudioButtonListener(listener);
}
public void setHangupButtonListener(final HangupButtonListener listener) {
endCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onClick();
}
});
}
public void setAcceptIdentityListener(OnClickListener listener) {
this.acceptIdentityButton.setOnClickListener(listener);
}
public void setCancelIdentityButton(OnClickListener listener) {
this.cancelIdentityButton.setOnClickListener(listener);
}
public void notifyBluetoothChange() {
this.controls.updateAudioButton();
}
public void setLocalVideoEnabled(boolean enabled) {
if (enabled) {
this.localRenderLayout.setHidden(false);
} else {
this.localRenderLayout.setHidden(true);
}
this.localRenderLayout.requestLayout();
}
public void setRemoteVideoEnabled(boolean enabled) {
if (enabled) {
this.remoteRenderLayout.setHidden(false);
} else {
this.remoteRenderLayout.setHidden(true);
}
this.remoteRenderLayout.requestLayout();
}
private void initialize() {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_call_screen, this, true);
this.elapsedTime = (TextView) findViewById(R.id.elapsedTime);
this.photo = (ImageView) findViewById(R.id.photo);
this.localRenderLayout = (PercentFrameLayout) findViewById(R.id.local_render_layout);
this.remoteRenderLayout = (PercentFrameLayout) findViewById(R.id.remote_render_layout);
this.phoneNumber = (TextView) findViewById(R.id.phoneNumber);
this.name = (TextView) findViewById(R.id.name);
this.label = (TextView) findViewById(R.id.label);
this.status = (TextView) findViewById(R.id.callStateLabel);
this.controls = (WebRtcCallControls) findViewById(R.id.inCallControls);
this.endCallButton = (FloatingActionButton) findViewById(R.id.hangup_fab);
this.incomingCallOverlay = (WebRtcIncomingCallOverlay) findViewById(R.id.callControls);
this.untrustedIdentityContainer = findViewById(R.id.untrusted_layout);
this.untrustedIdentityExplanation = (TextView) findViewById(R.id.untrusted_explanation);
this.acceptIdentityButton = (Button)findViewById(R.id.accept_safety_numbers);
this.cancelIdentityButton = (Button)findViewById(R.id.cancel_safety_numbers);
this.localRenderLayout.setHidden(true);
this.remoteRenderLayout.setHidden(true);
}
private void setConnected(SurfaceViewRenderer localRenderer,
SurfaceViewRenderer remoteRenderer)
{
localRenderLayout.setPosition(7, 7, 25, 25);
localRenderLayout.setSquare(true);
remoteRenderLayout.setPosition(0, 0, 100, 100);
localRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
remoteRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
localRenderer.setMirror(true);
localRenderer.setZOrderMediaOverlay(true);
localRenderLayout.addView(localRenderer);
remoteRenderLayout.addView(remoteRenderer);
}
private void setPersonInfo(final @NonNull Recipient recipient) {
this.recipient = recipient;
this.recipient.addListener(this);
final Context context = getContext();
new AsyncTask<Void, Void, ContactPhoto>() {
@Override
protected ContactPhoto doInBackground(Void... params) {
DisplayMetrics metrics = new DisplayMetrics();
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Uri contentUri = ContactsContract.Contacts.lookupContact(context.getContentResolver(),
recipient.getContactUri());
windowManager.getDefaultDisplay().getMetrics(metrics);
return ContactPhotoFactory.getContactPhoto(context, contentUri, null, metrics.widthPixels);
}
@Override
protected void onPostExecute(final ContactPhoto contactPhoto) {
WebRtcCallScreen.this.photo.setImageDrawable(contactPhoto.asCallCard(context));
}
}.execute();
this.name.setText(recipient.getName());
this.phoneNumber.setText(recipient.getNumber());
}
private void setCard(Recipient recipient, String status) {
setPersonInfo(recipient);
this.status.setText(status);
this.untrustedIdentityContainer.setVisibility(View.GONE);
this.endCallButton.setVisibility(View.VISIBLE);
}
@Override
public void onModified(Recipient recipient) {
if (recipient == this.recipient) {
setPersonInfo(recipient);
}
}
public static interface HangupButtonListener {
public void onClick();
}
}

View File

@@ -0,0 +1,189 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.graphics.drawable.LayerDrawable;
import android.support.v7.widget.PopupMenu;
import android.util.Log;
import android.view.MenuItem;
import android.widget.CompoundButton;
import org.thoughtcrime.redphone.util.AudioUtils;
import org.thoughtcrime.securesms.R;
import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.DEFAULT;
import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.HEADSET;
import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.SPEAKER;
/**
* Manages the audio button displayed on the in-call screen
*
* The behavior of this button depends on the availability of headset audio, and changes from being a regular
* toggle button (enabling speakerphone) to bringing up a model dialog that includes speakerphone, bluetooth,
* and regular audio options.
*
* Based on com.android.phone.InCallTouchUI
*
* @author Stuart O. Anderson
*/
public class WebRtcInCallAudioButton {
private static final String TAG = WebRtcInCallAudioButton.class.getName();
private final CompoundButton mAudioButton;
private boolean headsetAvailable;
private AudioUtils.AudioMode currentMode;
private Context context;
private WebRtcCallControls.AudioButtonListener listener;
public WebRtcInCallAudioButton(CompoundButton audioButton) {
mAudioButton = audioButton;
currentMode = DEFAULT;
headsetAvailable = false;
updateView();
setListener(new WebRtcCallControls.AudioButtonListener() {
@Override
public void onAudioChange(AudioUtils.AudioMode mode) {
//No Action By Default.
}
});
context = audioButton.getContext();
}
public void setHeadsetAvailable(boolean available) {
headsetAvailable = available;
updateView();
}
public void setAudioMode(AudioUtils.AudioMode newMode) {
currentMode = newMode;
updateView();
}
private void updateView() {
// The various layers of artwork for this button come from
// redphone_btn_compound_audio.xmlaudio.xml. Keep track of which layers we want to be
// visible:
//
// - This selector shows the blue bar below the button icon when
// this button is a toggle *and* it's currently "checked".
boolean showToggleStateIndication = false;
//
// - This is visible if the popup menu is enabled:
boolean showMoreIndicator = false;
//
// - Foreground icons for the button. Exactly one of these is enabled:
boolean showSpeakerOnIcon = false;
// boolean showSpeakerOffIcon = false;
boolean showHandsetIcon = false;
boolean showHeadsetIcon = false;
boolean speakerOn = currentMode == AudioUtils.AudioMode.SPEAKER;
if (headsetAvailable) {
mAudioButton.setEnabled(true);
// The audio button is NOT a toggle in this state. (And its
// setChecked() state is irrelevant since we completely hide the
// redphone_btn_compound_background layer anyway.)
// Update desired layers:
showMoreIndicator = true;
Log.d(TAG, "UI Mode: " + currentMode);
if (currentMode == AudioUtils.AudioMode.HEADSET) {
showHeadsetIcon = true;
} else if (speakerOn) {
showSpeakerOnIcon = true;
} else {
showHandsetIcon = true;
}
} else {
mAudioButton.setEnabled(true);
mAudioButton.setChecked(speakerOn);
showSpeakerOnIcon = true;
// showSpeakerOnIcon = speakerOn;
// showSpeakerOffIcon = !speakerOn;
showToggleStateIndication = true;
}
final int HIDDEN = 0;
final int VISIBLE = 255;
LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
.setAlpha(showToggleStateIndication ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.moreIndicatorItem)
.setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.bluetoothItem)
.setAlpha(showHeadsetIcon ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.handsetItem)
.setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
.setAlpha(showSpeakerOnIcon ? VISIBLE : HIDDEN);
// layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
// .setAlpha(showSpeakerOffIcon ? VISIBLE : HIDDEN);
mAudioButton.invalidate();
}
private void log(String msg) {
Log.d(TAG, msg);
}
public void setListener(final WebRtcCallControls.AudioButtonListener listener) {
this.listener = listener;
mAudioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
if(headsetAvailable) {
displayAudioChoiceDialog();
} else {
currentMode = b ? AudioUtils.AudioMode.SPEAKER : DEFAULT;
listener.onAudioChange(currentMode);
updateView();
}
}
});
}
private void displayAudioChoiceDialog() {
Log.w(TAG, "Displaying popup...");
PopupMenu popupMenu = new PopupMenu(context, mAudioButton);
popupMenu.getMenuInflater().inflate(R.menu.redphone_audio_popup_menu, popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(new AudioRoutingPopupListener());
popupMenu.show();
}
private class AudioRoutingPopupListener implements PopupMenu.OnMenuItemClickListener {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.handset:
currentMode = DEFAULT;
break;
case R.id.headset:
currentMode = HEADSET;
break;
case R.id.speaker:
currentMode = SPEAKER;
break;
default:
Log.w(TAG, "Unknown item selected in audio popup menu: " + item.toString());
}
Log.d(TAG, "Selected: " + currentMode + " -- " + item.getItemId());
listener.onAudioChange(currentMode);
updateView();
return true;
}
}
}

View File

@@ -0,0 +1,116 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.Animation;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.thoughtcrime.redphone.util.multiwaveview.MultiWaveView;
import org.thoughtcrime.securesms.R;
/**
* Displays the controls at the bottom of the in-call screen.
*
* @author Moxie Marlinspike
*
*/
public class WebRtcIncomingCallOverlay extends RelativeLayout {
private MultiWaveView incomingCallWidget;
private TextView redphoneLabel;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message message) {
if (incomingCallWidget.getVisibility() == View.VISIBLE) {
incomingCallWidget.ping();
handler.sendEmptyMessageDelayed(0, 1200);
}
}
};
public WebRtcIncomingCallOverlay(Context context) {
super(context);
initialize();
}
public WebRtcIncomingCallOverlay(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WebRtcIncomingCallOverlay(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public void setIncomingCall() {
Animation animation = incomingCallWidget.getAnimation();
if (animation != null) {
animation.reset();
incomingCallWidget.clearAnimation();
}
incomingCallWidget.reset(false);
incomingCallWidget.setVisibility(View.VISIBLE);
redphoneLabel.setVisibility(View.VISIBLE);
handler.sendEmptyMessageDelayed(0, 500);
}
public void setActiveCall() {
incomingCallWidget.setVisibility(View.GONE);
redphoneLabel.setVisibility(View.GONE);
}
public void setActiveCall(@Nullable String sas) {
setActiveCall();
}
public void reset() {
incomingCallWidget.setVisibility(View.GONE);
redphoneLabel.setVisibility(View.GONE);
}
public void setIncomingCallActionListener(final IncomingCallActionListener listener) {
incomingCallWidget.setOnTriggerListener(new MultiWaveView.OnTriggerListener() {
@Override
public void onTrigger(View v, int target) {
switch (target) {
case 0: listener.onAcceptClick(); break;
case 2: listener.onDenyClick(); break;
}
}
@Override
public void onReleased(View v, int handle) {}
@Override
public void onGrabbedStateChange(View v, int handle) {}
@Override
public void onGrabbed(View v, int handle) {}
});
}
private void initialize() {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_incoming_call_overlay, this, true);
this.incomingCallWidget = (MultiWaveView)findViewById(R.id.incomingCallWidget);
this.redphoneLabel = (TextView)findViewById(R.id.redphone_banner);
}
public static interface IncomingCallActionListener {
public void onAcceptClick();
public void onDenyClick();
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2012 Moxie Marlinspike
*
* 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.components.webrtc;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import org.thoughtcrime.redphone.RedPhone;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
/**
* Manages the state of the RedPhone items in the Android notification bar.
*
* @author Moxie Marlinspike
*
*/
public class WebRtcNotificationBarManager {
private static final int RED_PHONE_NOTIFICATION = 313388;
private static final int MISSED_CALL_NOTIFICATION = 313389;
public static final int TYPE_INCOMING_RINGING = 1;
public static final int TYPE_OUTGOING_RINGING = 2;
public static final int TYPE_ESTABLISHED = 3;
public static void setCallEnded(Context context) {
NotificationManager notificationManager = (NotificationManager)context
.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(RED_PHONE_NOTIFICATION);
}
public static void setCallInProgress(Context context, int type, Recipient recipient) {
NotificationManager notificationManager = (NotificationManager)context
.getSystemService(Context.NOTIFICATION_SERVICE);
Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_call_secure_white_24dp)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setContentTitle(recipient.getName());
if (type == TYPE_INCOMING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call));
builder.addAction(getNotificationAction(context, RedPhone.DENY_ACTION, R.drawable.ic_close_grey600_32dp, R.string.NotificationBarManager__deny_call));
builder.addAction(getNotificationAction(context, RedPhone.ANSWER_ACTION, R.drawable.ic_phone_grey600_32dp, R.string.NotificationBarManager__answer_call));
} else if (type == TYPE_OUTGOING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call));
builder.addAction(getNotificationAction(context, RedPhone.END_CALL_ACTION, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__cancel_call));
} else {
builder.setContentText(context.getString(R.string.NotificationBarManager_signal_call_in_progress));
builder.addAction(getNotificationAction(context, RedPhone.END_CALL_ACTION, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__end_call));
}
notificationManager.notify(RED_PHONE_NOTIFICATION, builder.build());
}
private static NotificationCompat.Action getNotificationAction(Context context, String action, int iconResId, int titleResId) {
Intent intent = new Intent(context, WebRtcCallActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
}
}

View File

@@ -101,7 +101,7 @@ public class ContactsDatabase {
addedNumbers.add(registeredNumber);
addTextSecureRawContact(operations, account, systemContactInfo.get().number,
systemContactInfo.get().name, systemContactInfo.get().id,
registeredContact.isVoice());
true);
}
}
}
@@ -114,12 +114,9 @@ public class ContactsDatabase {
Log.w(TAG, "Removing number: " + currentContactEntry.getKey());
removeTextSecureRawContact(operations, account, currentContactEntry.getValue().getId());
}
} else if (tokenDetails.isVoice() && !currentContactEntry.getValue().isVoiceSupported()) {
} else if (!currentContactEntry.getValue().isVoiceSupported()) {
Log.w(TAG, "Adding voice support: " + currentContactEntry.getKey());
addContactVoiceSupport(operations, currentContactEntry.getKey(), currentContactEntry.getValue().getId());
} else if (!tokenDetails.isVoice() && currentContactEntry.getValue().isVoiceSupported()) {
Log.w(TAG, "Removing voice support: " + currentContactEntry.getKey());
removeContactVoiceSupport(operations, currentContactEntry.getValue().getId());
} else if (!Util.isStringEquals(currentContactEntry.getValue().getRawDisplayName(),
currentContactEntry.getValue().getAggregateDisplayName()))
{

View File

@@ -56,6 +56,7 @@ public interface MmsSmsColumns {
protected static final long KEY_EXCHANGE_INVALID_VERSION_BIT = 0x800;
protected static final long KEY_EXCHANGE_BUNDLE_BIT = 0x400;
protected static final long KEY_EXCHANGE_IDENTITY_UPDATE_BIT = 0x200;
protected static final long KEY_EXCHANGE_CONTENT_FORMAT = 0x100;
// Secure Message Information
protected static final long SECURE_MESSAGE_BIT = 0x800000;
@@ -161,6 +162,10 @@ public interface MmsSmsColumns {
return (type & KEY_EXCHANGE_BUNDLE_BIT) != 0;
}
public static boolean isContentBundleKeyExchange(long type) {
return (type & KEY_EXCHANGE_CONTENT_FORMAT) != 0;
}
public static boolean isIdentityUpdate(long type) {
return (type & KEY_EXCHANGE_IDENTITY_UPDATE_BIT) != 0;
}

View File

@@ -233,6 +233,10 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (isSecure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0));
}
public void markAsMissedCall(long id) {
updateTypeBitmask(id, Types.TOTAL_MASK, Types.MISSED_CALL_TYPE);
}
public void markExpireStarted(long id) {
markExpireStarted(id, System.currentTimeMillis());
}
@@ -499,8 +503,9 @@ public class SmsDatabase extends MessagingDatabase {
type |= Types.END_SESSION_BIT;
}
if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT;
if (message.isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT;
if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT;
if (message.isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT;
if (message.isContentPreKeyBundle()) type |= Types.KEY_EXCHANGE_CONTENT_FORMAT;
Recipients recipients;

View File

@@ -24,9 +24,10 @@ public class TextSecureDirectory {
private static final int INTRODUCED_CHANGE_FROM_TOKEN_TO_E164_NUMBER = 2;
private static final int INTRODUCED_VOICE_COLUMN = 4;
private static final int INTRODUCED_VIDEO_COLUMN = 5;
private static final String DATABASE_NAME = "whisper_directory.db";
private static final int DATABASE_VERSION = 4;
private static final int DATABASE_VERSION = 5;
private static final String TABLE_NAME = "directory";
private static final String ID = "_id";
@@ -35,13 +36,15 @@ public class TextSecureDirectory {
private static final String RELAY = "relay";
private static final String TIMESTAMP = "timestamp";
private static final String VOICE = "voice";
private static final String VIDEO = "video";
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY, " +
NUMBER + " TEXT UNIQUE, " +
REGISTERED + " INTEGER, " +
RELAY + " TEXT, " +
TIMESTAMP + " INTEGER, " +
VOICE + " INTEGER);";
VOICE + " INTEGER, " +
VIDEO + " INTEGER);";
private static final Object instanceLock = new Object();
private static volatile TextSecureDirectory instance;
@@ -116,6 +119,31 @@ public class TextSecureDirectory {
}
}
public boolean isSecureVideoSupported(String e164number) throws NotInDirectoryException {
if (TextUtils.isEmpty(e164number)) {
return false;
}
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME,
new String[]{VIDEO}, NUMBER + " = ?",
new String[] {e164number}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0) == 1;
} else {
throw new NotInDirectoryException();
}
} finally {
if (cursor != null)
cursor.close();
}
}
public String getRelay(String e164number) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
@@ -151,13 +179,14 @@ public class TextSecureDirectory {
try {
for (ContactTokenDetails token : activeTokens) {
Log.w("Directory", "Adding active token: " + token.getNumber() + ", " + token.getToken());
Log.w("Directory", "Adding active token: " + token.getNumber() + ", " + token.getToken() + ", video: " + token.isVideo());
ContentValues values = new ContentValues();
values.put(NUMBER, token.getNumber());
values.put(REGISTERED, 1);
values.put(TIMESTAMP, timestamp);
values.put(RELAY, token.getRelay());
values.put(VOICE, token.isVoice());
values.put(VIDEO, token.isVideo());
db.replace(TABLE_NAME, null, values);
}
@@ -261,6 +290,10 @@ public class TextSecureDirectory {
if (oldVersion < INTRODUCED_VOICE_COLUMN) {
db.execSQL("ALTER TABLE directory ADD COLUMN voice INTEGER;");
}
if (oldVersion < INTRODUCED_VIDEO_COLUMN) {
db.execSQL("ALTER TABLE directory ADD COLUMN video INTEGER;");
}
}
}

View File

@@ -156,6 +156,10 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isBundleKeyExchange(type);
}
public boolean isContentBundleKeyExchange() {
return SmsDatabase.Types.isContentBundleKeyExchange(type);
}
public boolean isIdentityUpdate() {
return SmsDatabase.Types.isIdentityUpdate(type);
}

View File

@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob;
import org.thoughtcrime.securesms.push.SecurityEventListener;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.service.MessageRetrievalService;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
@@ -57,7 +58,8 @@ import dagger.Provides;
RequestGroupInfoJob.class,
PushGroupUpdateJob.class,
AvatarDownloadJob.class,
RotateSignedPreKeyJob.class})
RotateSignedPreKeyJob.class,
WebRtcCallService.class})
public class SignalCommunicationModule {
private final Context context;

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.events;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.recipients.Recipient;
public class WebRtcCallEvent {
public enum Type {
CALL_CONNECTED,
WAITING_FOR_RESPONDER,
SERVER_FAILURE,
PERFORMING_HANDSHAKE,
HANDSHAKE_FAILED,
CONNECTING_TO_INITIATOR,
CALL_DISCONNECTED,
CALL_RINGING,
RECIPIENT_UNAVAILABLE,
INCOMING_CALL,
OUTGOING_CALL,
CALL_BUSY,
LOGIN_FAILED,
DEBUG_INFO,
NO_SUCH_USER,
REMOTE_VIDEO_ENABLED,
REMOTE_VIDEO_DISABLED,
UNTRUSTED_IDENTITY
}
private final @NonNull Type type;
private final @NonNull Recipient recipient;
private final @Nullable Object extra;
public WebRtcCallEvent(@NonNull Type type, @NonNull Recipient recipient, @Nullable Object extra) {
this.type = type;
this.recipient = recipient;
this.extra = extra;
}
public @NonNull Type getType() {
return type;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
public @Nullable Object getExtra() {
return extra;
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
@@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
@@ -33,6 +35,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
@@ -61,6 +64,11 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
@@ -160,6 +168,16 @@ public class PushDecryptJob extends ContextJob {
else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(masterSecret, syncMessage.getRequest().get());
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(masterSecret, syncMessage.getRead().get(), envelope.getTimestamp());
else Log.w(TAG, "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) {
Log.w(TAG, "Got call message...");
SignalServiceCallMessage message = content.getCallMessage().get();
if (message.getOfferMessage().isPresent()) handleCallOfferMessage(envelope, message.getOfferMessage().get(), smsMessageId);
else if (message.getAnswerMessage().isPresent()) handleCallAnswerMessage(envelope, message.getAnswerMessage().get());
else if (message.getIceUpdateMessages().isPresent()) handleCallIceUpdateMessage(envelope, message.getIceUpdateMessages().get());
else if (message.getHangupMessage().isPresent()) handleCallHangupMessage(envelope, message.getHangupMessage().get(), smsMessageId);
} else {
Log.w(TAG, "Got unrecognized message...");
}
if (envelope.isPreKeySignalMessage()) {
@@ -186,6 +204,70 @@ public class PushDecryptJob extends ContextJob {
}
}
private void handleCallOfferMessage(@NonNull SignalServiceEnvelope envelope,
@NonNull OfferMessage message,
@NonNull Optional<Long> smsMessageId)
{
Log.w(TAG, "handleCallOfferMessage...");
if (smsMessageId.isPresent()) {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
database.markAsMissedCall(smsMessageId.get());
} else {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_INCOMING_CALL);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, envelope.getSource());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_DESCRIPTION, message.getDescription());
intent.putExtra(WebRtcCallService.EXTRA_TIMESTAMP, envelope.getTimestamp());
context.startService(intent);
}
}
private void handleCallAnswerMessage(@NonNull SignalServiceEnvelope envelope,
@NonNull AnswerMessage message)
{
Log.w(TAG, "handleCallAnswerMessage...");
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_RESPONSE_MESSAGE);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, envelope.getSource());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_DESCRIPTION, message.getDescription());
context.startService(intent);
}
private void handleCallIceUpdateMessage(@NonNull SignalServiceEnvelope envelope,
@NonNull List<IceUpdateMessage> messages)
{
Log.w(TAG, "handleCallIceUpdateMessage... " + messages.size());
for (IceUpdateMessage message : messages) {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ICE_MESSAGE);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, envelope.getSource());
intent.putExtra(WebRtcCallService.EXTRA_ICE_SDP, message.getSdp());
intent.putExtra(WebRtcCallService.EXTRA_ICE_SDP_MID, message.getSdpMid());
intent.putExtra(WebRtcCallService.EXTRA_ICE_SDP_LINE_INDEX, message.getSdpMLineIndex());
context.startService(intent);
}
}
private void handleCallHangupMessage(@NonNull SignalServiceEnvelope envelope,
@NonNull HangupMessage message,
@NonNull Optional<Long> smsMessageId)
{
Log.w(TAG, "handleCallHangupMessage");
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).markAsMissedCall(smsMessageId.get());
} else {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_REMOTE_HANGUP);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, envelope.getSource());
context.startService(intent);
}
}
private void handleEndSessionMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope,
@NonNull SignalServiceDataMessage message,
@@ -628,16 +710,17 @@ public class PushDecryptJob extends ContextJob {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
byte[] ciphertext = envelope.hasLegacyMessage() ? envelope.getLegacyMessage() : envelope.getContent();
PreKeySignalMessage whisperMessage = new PreKeySignalMessage(ciphertext);
byte[] serialized = envelope.hasLegacyMessage() ? envelope.getLegacyMessage() : envelope.getContent();
PreKeySignalMessage whisperMessage = new PreKeySignalMessage(serialized);
IdentityKey identityKey = whisperMessage.getIdentityKey();
String encoded = Base64.encodeBytes(ciphertext);
String encoded = Base64.encodeBytes(serialized);
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), encoded,
Optional.<SignalServiceGroup>absent(), 0);
if (!smsMessageId.isPresent()) {
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded, envelope.hasLegacyMessage());
Optional<InsertResult> insertResult = database.insertMessageInbox(masterSecret, bundleMessage);
if (insertResult.isPresent()) {

View File

@@ -22,7 +22,7 @@ public class RefreshAttributesJob extends ContextJob implements InjectableType {
private static final String TAG = RefreshAttributesJob.class.getSimpleName();
@Inject transient SignalServiceAccountManager textSecureAccountManager;
@Inject transient SignalServiceAccountManager signalAccountManager;
@Inject transient RedPhoneAccountManager redPhoneAccountManager;
public RefreshAttributesJob(Context context) {
@@ -30,6 +30,7 @@ public class RefreshAttributesJob extends ContextJob implements InjectableType {
.withPersistence()
.withRequirement(new NetworkRequirement(context))
.withWakeLock(true)
.withGroupId(RefreshAttributesJob.class.getName())
.create());
}
@@ -38,14 +39,15 @@ public class RefreshAttributesJob extends ContextJob implements InjectableType {
@Override
public void onRun() throws IOException {
String signalingKey = TextSecurePreferences.getSignalingKey(context);
String gcmRegistrationId = TextSecurePreferences.getGcmRegistrationId(context);
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
String signalingKey = TextSecurePreferences.getSignalingKey(context);
String gcmRegistrationId = TextSecurePreferences.getGcmRegistrationId(context);
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
boolean video = TextSecurePreferences.isWebrtcCallingEnabled(context);
String token = textSecureAccountManager.getAccountVerificationToken();
String token = signalAccountManager.getAccountVerificationToken();
redPhoneAccountManager.createAccount(token, new RedPhoneAccountAttributes(signalingKey, gcmRegistrationId));
textSecureAccountManager.setAccountAttributes(signalingKey, registrationId, true);
signalAccountManager.setAccountAttributes(signalingKey, registrationId, true, video);
}
@Override

View File

@@ -22,6 +22,7 @@ import com.google.android.gms.gcm.GoogleCloudMessaging;
import org.thoughtcrime.redphone.signaling.RedPhoneAccountManager;
import org.thoughtcrime.redphone.signaling.RedPhoneTrustStore;
import org.thoughtcrime.redphone.signaling.UnauthorizedException;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.LogSubmitActivity;
@@ -30,6 +31,7 @@ import org.thoughtcrime.securesms.RegistrationActivity;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactIdentityManager;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -68,6 +70,7 @@ public class AdvancedPreferenceFragment extends PreferenceFragment {
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__advanced);
initializePushMessagingToggle();
initializeWebrtcCallingToggle();
}
@Override
@@ -94,6 +97,11 @@ public class AdvancedPreferenceFragment extends PreferenceFragment {
preference.setOnPreferenceChangeListener(new PushMessagingClickListener());
}
private void initializeWebrtcCallingToggle() {
this.findPreference(TextSecurePreferences.WEBRTC_CALLING_PREF)
.setOnPreferenceChangeListener(new WebRtcClickListener());
}
private void initializeIdentitySelection() {
ContactIdentityManager identity = ContactIdentityManager.getInstance(getActivity());
@@ -156,6 +164,18 @@ public class AdvancedPreferenceFragment extends PreferenceFragment {
}
}
private class WebRtcClickListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
TextSecurePreferences.setWebrtcCallingEnabled(getContext(), (Boolean)newValue);
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new RefreshAttributesJob(getContext()));
return true;
}
}
private class PushMessagingClickListener implements Preference.OnPreferenceChangeListener {
private static final int SUCCESS = 0;
private static final int NETWORK_ERROR = 1;

View File

@@ -34,6 +34,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
public class Recipient {
@@ -89,7 +90,7 @@ public class Recipient {
}
@Override
public void onFailure(Throwable error) {
public void onFailure(ExecutionException error) {
Log.w(TAG, error);
}
});

View File

@@ -43,6 +43,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
public class Recipients implements Iterable<Recipient>, RecipientModifiedListener {
@@ -112,7 +113,7 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
}
@Override
public void onFailure(Throwable error) {
public void onFailure(ExecutionException error) {
Log.w(TAG, error);
}
});

View File

@@ -203,7 +203,7 @@ public class RegistrationService extends Service {
setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number));
String challenge = waitForChallenge();
accountManager.verifyAccountWithCode(challenge, signalingKey, registrationId, true);
accountManager.verifyAccountWithCode(challenge, signalingKey, registrationId, true, TextSecurePreferences.isWebrtcCallingEnabled(this));
handleCommonRegistration(accountManager, number, password, signalingKey);
markAsVerified(number, password, signalingKey);

File diff suppressed because it is too large Load Diff

View File

@@ -2,18 +2,26 @@ package org.thoughtcrime.securesms.sms;
public class IncomingPreKeyBundleMessage extends IncomingTextMessage {
public IncomingPreKeyBundleMessage(IncomingTextMessage base, String newBody) {
private final boolean legacy;
public IncomingPreKeyBundleMessage(IncomingTextMessage base, String newBody, boolean legacy) {
super(base, newBody);
this.legacy = legacy;
}
@Override
public IncomingPreKeyBundleMessage withMessageBody(String messageBody) {
return new IncomingPreKeyBundleMessage(this, messageBody);
return new IncomingPreKeyBundleMessage(this, messageBody, legacy);
}
@Override
public boolean isPreKeyBundle() {
return true;
public boolean isLegacyPreKeyBundle() {
return legacy;
}
@Override
public boolean isContentPreKeyBundle() {
return !legacy;
}
}

View File

@@ -192,6 +192,14 @@ public class IncomingTextMessage implements Parcelable {
}
public boolean isPreKeyBundle() {
return isLegacyPreKeyBundle() || isContentPreKeyBundle();
}
public boolean isLegacyPreKeyBundle() {
return false;
}
public boolean isContentPreKeyBundle() {
return false;
}

View File

@@ -40,8 +40,8 @@ public class DirectoryHelper {
public static class UserCapabilities {
public static final UserCapabilities UNKNOWN = new UserCapabilities(Capability.UNKNOWN, Capability.UNKNOWN);
public static final UserCapabilities UNSUPPORTED = new UserCapabilities(Capability.UNSUPPORTED, Capability.UNSUPPORTED);
public static final UserCapabilities UNKNOWN = new UserCapabilities(Capability.UNKNOWN, Capability.UNKNOWN, Capability.UNKNOWN);
public static final UserCapabilities UNSUPPORTED = new UserCapabilities(Capability.UNSUPPORTED, Capability.UNSUPPORTED, Capability.UNSUPPORTED);
public enum Capability {
UNKNOWN, SUPPORTED, UNSUPPORTED
@@ -49,10 +49,12 @@ public class DirectoryHelper {
private final Capability text;
private final Capability voice;
private final Capability video;
public UserCapabilities(Capability text, Capability voice) {
public UserCapabilities(Capability text, Capability voice, Capability video) {
this.text = text;
this.voice = voice;
this.video = video;
}
public Capability getTextCapability() {
@@ -62,6 +64,10 @@ public class DirectoryHelper {
public Capability getVoiceCapability() {
return voice;
}
public Capability getVideoCapability() {
return video;
}
}
private static final String TAG = DirectoryHelper.class.getSimpleName();
@@ -131,7 +137,9 @@ public class DirectoryHelper {
notifyNewUsers(context, masterSecret, result.getNewUsers());
}
return new UserCapabilities(Capability.SUPPORTED, details.get().isVoice() ? Capability.SUPPORTED : Capability.UNSUPPORTED);
return new UserCapabilities(Capability.SUPPORTED,
details.get().isVoice() ? Capability.SUPPORTED : Capability.UNSUPPORTED,
details.get().isVideo() ? Capability.SUPPORTED : Capability.UNSUPPORTED);
} else {
ContactTokenDetails absent = new ContactTokenDetails();
absent.setNumber(number);
@@ -161,7 +169,7 @@ public class DirectoryHelper {
}
if (recipients.isGroupRecipient()) {
return new UserCapabilities(Capability.SUPPORTED, Capability.UNSUPPORTED);
return new UserCapabilities(Capability.SUPPORTED, Capability.UNSUPPORTED, Capability.UNSUPPORTED);
}
final String number = recipients.getPrimaryRecipient().getNumber();
@@ -173,9 +181,11 @@ public class DirectoryHelper {
String e164number = Util.canonicalizeNumber(context, number);
boolean secureText = TextSecureDirectory.getInstance(context).isSecureTextSupported(e164number);
boolean secureVoice = TextSecureDirectory.getInstance(context).isSecureVoiceSupported(e164number);
boolean secureVideo = TextSecureDirectory.getInstance(context).isSecureVideoSupported(e164number);
return new UserCapabilities(secureText ? Capability.SUPPORTED : Capability.UNSUPPORTED,
secureVoice ? Capability.SUPPORTED : Capability.UNSUPPORTED);
secureVoice ? Capability.SUPPORTED : Capability.UNSUPPORTED,
secureVideo ? Capability.SUPPORTED : Capability.UNSUPPORTED);
} catch (InvalidNumberException e) {
Log.w(TAG, e);

View File

@@ -16,7 +16,9 @@
*/
package org.thoughtcrime.securesms.util;
import java.util.concurrent.ExecutionException;
public interface FutureTaskListener<V> {
public void onSuccess(V result);
public void onFailure(Throwable error);
public void onFailure(ExecutionException exception);
}

View File

@@ -22,6 +22,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
public class ListenableFutureTask<V> extends FutureTask<V> {
@@ -31,15 +32,24 @@ public class ListenableFutureTask<V> extends FutureTask<V> {
@Nullable
private final Object identifier;
@Nullable
private final Executor callbackExecutor;
public ListenableFutureTask(Callable<V> callable) {
this(callable, null);
}
public ListenableFutureTask(Callable<V> callable, @Nullable Object identifier) {
super(callable);
this.identifier = identifier;
this(callable, identifier, null);
}
public ListenableFutureTask(Callable<V> callable, @Nullable Object identifier, @Nullable Executor callbackExecutor) {
super(callable);
this.identifier = identifier;
this.callbackExecutor = callbackExecutor;
}
public ListenableFutureTask(final V result) {
this(result, null);
}
@@ -51,7 +61,8 @@ public class ListenableFutureTask<V> extends FutureTask<V> {
return result;
}
});
this.identifier = identifier;
this.identifier = identifier;
this.callbackExecutor = null;
this.run();
}
@@ -73,9 +84,17 @@ public class ListenableFutureTask<V> extends FutureTask<V> {
}
private void callback() {
for (FutureTaskListener<V> listener : listeners) {
callback(listener);
}
Runnable callbackRunnable = new Runnable() {
@Override
public void run() {
for (FutureTaskListener<V> listener : listeners) {
callback(listener);
}
}
};
if (callbackExecutor == null) callbackRunnable.run();
else callbackExecutor.execute(callbackRunnable);
}
private void callback(FutureTaskListener<V> listener) {

View File

@@ -90,6 +90,7 @@ public class TextSecurePreferences {
public static final String REPEAT_ALERTS_PREF = "pref_repeat_alerts";
public static final String NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy";
public static final String NEW_CONTACTS_NOTIFICATIONS = "pref_enable_new_contacts_notifications";
public static final String WEBRTC_CALLING_PREF = "pref_webrtc_calling";
public static final String MEDIA_DOWNLOAD_MOBILE_PREF = "pref_media_download_mobile";
public static final String MEDIA_DOWNLOAD_WIFI_PREF = "pref_media_download_wifi";
@@ -99,6 +100,14 @@ public class TextSecurePreferences {
private static final String MULTI_DEVICE_PROVISIONED_PREF = "pref_multi_device";
public static final String DIRECT_CAPTURE_CAMERA_ID = "pref_direct_capture_camera_id";
public static boolean isWebrtcCallingEnabled(Context context) {
return getBooleanPreference(context, WEBRTC_CALLING_PREF, false);
}
public static void setWebrtcCallingEnabled(Context context, boolean enabled) {
setBooleanPreference(context, WEBRTC_CALLING_PREF, enabled);
}
public static void setDirectCaptureCameraId(Context context, int value) {
setIntegerPrefrence(context, DIRECT_CAPTURE_CAMERA_ID, value);
}

View File

@@ -459,4 +459,8 @@ public class Util {
if (first == null) return second == null;
return first.equals(second);
}
public static boolean isEquals(@Nullable Long first, long second) {
return first != null && first == second;
}
}

View File

@@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.text.style.ClickableSpan;
import android.view.View;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.whispersystems.libsignal.IdentityKey;
public class VerifySpan extends ClickableSpan {
private final Context context;
private final long recipientId;
private final IdentityKey identityKey;
public VerifySpan(@NonNull Context context, @NonNull IdentityKeyMismatch mismatch) {
this.context = context;
this.recipientId = mismatch.getRecipientId();
this.identityKey = mismatch.getIdentityKey();
}
public VerifySpan(@NonNull Context context, long recipientId, @NonNull IdentityKey identityKey) {
this.context = context;
this.recipientId = recipientId;
this.identityKey = identityKey;
}
@Override
public void onClick(View widget) {
Intent intent = new Intent(context, VerifyIdentityActivity.class);
intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, recipientId);
intent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(identityKey));
context.startActivity(intent);
}
}

View File

@@ -42,6 +42,7 @@ public class SettableFuture<T> implements ListenableFuture<T> {
this.result = result;
this.completed = true;
notifyAll();
}
@@ -55,6 +56,7 @@ public class SettableFuture<T> implements ListenableFuture<T> {
this.exception = throwable;
this.completed = true;
notifyAll();
}

View File

@@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.webrtc;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.ServiceUtil;
/**
* Manages the state of the WebRtc items in the Android notification bar.
*
* @author Moxie Marlinspike
*
*/
public class CallNotificationManager {
public static final int WEBRTC_NOTIFICATION = 313388;
public static final int TYPE_INCOMING_RINGING = 1;
public static final int TYPE_OUTGOING_RINGING = 2;
public static final int TYPE_ESTABLISHED = 3;
public static void setCallEnded(Context context) {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
notificationManager.cancel(WEBRTC_NOTIFICATION);
}
public static void setCallInProgress(Context context, int type, Recipient recipient) {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_call_secure_white_24dp)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setContentTitle(recipient.getName());
if (type == TYPE_INCOMING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call));
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_DENY_CALL, R.drawable.ic_close_grey600_32dp, R.string.NotificationBarManager__deny_call));
builder.addAction(getActivityNotificationAction(context, WebRtcCallActivity.ANSWER_ACTION, R.drawable.ic_phone_grey600_32dp, R.string.NotificationBarManager__answer_call));
} else if (type == TYPE_OUTGOING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call));
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_LOCAL_HANGUP, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__cancel_call));
} else {
builder.setContentText(context.getString(R.string.NotificationBarManager_signal_call_in_progress));
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_LOCAL_HANGUP, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__end_call));
}
notificationManager.notify(WEBRTC_NOTIFICATION, builder.build());
}
private static NotificationCompat.Action getServiceNotificationAction(Context context, String action, int iconResId, int titleResId) {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
}
private static NotificationCompat.Action getActivityNotificationAction(@NonNull Context context, @NonNull String action,
@DrawableRes int iconResId, @StringRes int titleResId)
{
Intent intent = new Intent(context, WebRtcCallActivity.class);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
}
}

View File

@@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.webrtc;
import org.webrtc.PeerConnectionFactory;
public class PeerConnectionFactoryOptions extends PeerConnectionFactory.Options {
public PeerConnectionFactoryOptions() {
this.networkIgnoreMask = 1 << 4;
}
}

View File

@@ -0,0 +1,314 @@
package org.thoughtcrime.securesms.webrtc;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
public class PeerConnectionWrapper {
private static final String TAG = PeerConnectionWrapper.class.getSimpleName();
private static final PeerConnection.IceServer STUN_SERVER = new PeerConnection.IceServer("stun:stun1.l.google.com:19302");
@NonNull private final PeerConnection peerConnection;
@NonNull private final AudioTrack audioTrack;
@NonNull private final AudioSource audioSource;
@Nullable private final VideoCapturer videoCapturer;
@Nullable private final VideoSource videoSource;
@Nullable private final VideoTrack videoTrack;
public PeerConnectionWrapper(@NonNull Context context,
@NonNull PeerConnectionFactory factory,
@NonNull PeerConnection.Observer observer,
@NonNull VideoRenderer.Callbacks localRenderer,
@NonNull List<PeerConnection.IceServer> turnServers)
{
List<PeerConnection.IceServer> iceServers = new LinkedList<>();
iceServers.add(STUN_SERVER);
iceServers.addAll(turnServers);
MediaConstraints constraints = new MediaConstraints();
MediaConstraints audioConstraints = new MediaConstraints();
PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(iceServers);
configuration.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
configuration.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
constraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
audioConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
this.peerConnection = factory.createPeerConnection(configuration, constraints, observer);
this.videoCapturer = createVideoCapturer(context);
MediaStream mediaStream = factory.createLocalMediaStream("ARDAMS");
this.audioSource = factory.createAudioSource(audioConstraints);
this.audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource);
this.audioTrack.setEnabled(false);
mediaStream.addTrack(audioTrack);
if (videoCapturer != null) {
this.videoSource = factory.createVideoSource(videoCapturer);
this.videoCapturer.startCapture(1280, 720, 30);
this.videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource);
this.videoTrack.addRenderer(new VideoRenderer(localRenderer));
this.videoTrack.setEnabled(false);
mediaStream.addTrack(videoTrack);
} else {
this.videoSource = null;
this.videoTrack = null;
}
this.peerConnection.addStream(mediaStream);
}
public void setVideoEnabled(boolean enabled) {
if (this.videoTrack != null) {
this.videoTrack.setEnabled(enabled);
}
}
public void setAudioEnabled(boolean enabled) {
this.audioTrack.setEnabled(enabled);
}
public DataChannel createDataChannel(String name) {
return this.peerConnection.createDataChannel(name, new DataChannel.Init());
}
public SessionDescription createOffer(MediaConstraints mediaConstraints) throws PeerConnectionException {
final SettableFuture<SessionDescription> future = new SettableFuture<>();
peerConnection.createOffer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {
future.set(sdp);
}
@Override
public void onCreateFailure(String error) {
future.setException(new PeerConnectionException(error));
}
@Override
public void onSetSuccess() {
throw new AssertionError();
}
@Override
public void onSetFailure(String error) {
throw new AssertionError();
}
}, mediaConstraints);
try {
return correctSessionDescription(future.get());
} catch (InterruptedException e) {
throw new AssertionError(e);
} catch (ExecutionException e) {
throw new PeerConnectionException(e);
}
}
public SessionDescription createAnswer(MediaConstraints mediaConstraints) throws PeerConnectionException {
final SettableFuture<SessionDescription> future = new SettableFuture<>();
peerConnection.createAnswer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {
future.set(sdp);
}
@Override
public void onCreateFailure(String error) {
future.setException(new PeerConnectionException(error));
}
@Override
public void onSetSuccess() {
throw new AssertionError();
}
@Override
public void onSetFailure(String error) {
throw new AssertionError();
}
}, mediaConstraints);
try {
return correctSessionDescription(future.get());
} catch (InterruptedException e) {
throw new AssertionError(e);
} catch (ExecutionException e) {
throw new PeerConnectionException(e);
}
}
public void setRemoteDescription(SessionDescription sdp) throws PeerConnectionException {
final SettableFuture<Boolean> future = new SettableFuture<>();
peerConnection.setRemoteDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {}
@Override
public void onCreateFailure(String error) {}
@Override
public void onSetSuccess() {
future.set(true);
}
@Override
public void onSetFailure(String error) {
future.setException(new PeerConnectionException(error));
}
}, sdp);
try {
future.get();
} catch (InterruptedException e) {
throw new AssertionError(e);
} catch (ExecutionException e) {
throw new PeerConnectionException(e);
}
}
public void setLocalDescription(SessionDescription sdp) throws PeerConnectionException {
final SettableFuture<Boolean> future = new SettableFuture<>();
peerConnection.setLocalDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {
throw new AssertionError();
}
@Override
public void onCreateFailure(String error) {
throw new AssertionError();
}
@Override
public void onSetSuccess() {
future.set(true);
}
@Override
public void onSetFailure(String error) {
future.setException(new PeerConnectionException(error));
}
}, sdp);
try {
future.get();
} catch (InterruptedException e) {
throw new AssertionError(e);
} catch (ExecutionException e) {
throw new PeerConnectionException(e);
}
}
public void dispose() {
if (this.videoCapturer != null) {
try {
this.videoCapturer.stopCapture();
} catch (InterruptedException e) {
Log.w(TAG, e);
}
this.videoCapturer.dispose();
}
if (this.videoSource != null) {
this.videoSource.dispose();
}
this.audioSource.dispose();
this.peerConnection.close();
this.peerConnection.dispose();
}
public boolean addIceCandidate(IceCandidate candidate) {
return this.peerConnection.addIceCandidate(candidate);
}
private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull Context context) {
Log.w(TAG, "Camera2 enumerator supported: " + Camera2Enumerator.isSupported(context));
CameraEnumerator enumerator;
if (Camera2Enumerator.isSupported(context)) enumerator = new Camera2Enumerator(context);
else enumerator = new Camera1Enumerator(true);
String[] deviceNames = enumerator.getDeviceNames();
for (String deviceName : deviceNames) {
if (enumerator.isFrontFacing(deviceName)) {
Log.w(TAG, "Creating front facing camera capturer.");
final CameraVideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
Log.w(TAG, "Found front facing capturer: " + deviceName);
return videoCapturer;
}
}
}
for (String deviceName : deviceNames) {
if (!enumerator.isFrontFacing(deviceName)) {
Log.w(TAG, "Creating other camera capturer.");
final CameraVideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
Log.w(TAG, "Found other facing capturer: " + deviceName);
return videoCapturer;
}
}
}
Log.w(TAG, "Video capture not supported!");
return null;
}
private SessionDescription correctSessionDescription(SessionDescription sessionDescription) {
String updatedSdp = sessionDescription.description.replaceAll("(a=fmtp:111 ((?!cbr=).)*)\r?\n", "$1;cbr=1\r\n");
updatedSdp = updatedSdp.replaceAll(".+urn:ietf:params:rtp-hdrext:ssrc-audio-level.*\r?\n", "");
return new SessionDescription(sessionDescription.type, updatedSdp);
}
public static class PeerConnectionException extends Exception {
public PeerConnectionException(String error) {
super(error);
}
public PeerConnectionException(Throwable throwable) {
super(throwable);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
package org.thoughtcrime.securesms.webrtc.audio;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.io.IOException;
/**
* Handles loading and playing the sequence of sounds we use to indicate call initialization.
*
* @author Stuart O. Anderson
*/
public class OutgoingRinger implements MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener {
private static final String TAG = OutgoingRinger.class.getSimpleName();
private MediaPlayer mediaPlayer;
private int currentSoundID;
private boolean loopEnabled;
private Context context;
public OutgoingRinger(Context context) {
this.context = context;
loopEnabled = true;
currentSoundID = -1;
}
public void playSonar() {
start(R.raw.redphone_sonarping);
}
public void playRing() {
start(R.raw.redphone_outring);
}
public void playComplete() {
stop(R.raw.webrtc_completed);
}
public void playDisconnected() {
stop(R.raw.webrtc_disconnected);
}
public void playBusy() {
start(R.raw.redphone_busy);
}
private void setSound( int soundID ) {
currentSoundID = soundID;
loopEnabled = true;
}
private void start( int soundID ) {
if( soundID == currentSoundID ) return;
setSound( soundID );
start();
}
private void start() {
if( mediaPlayer != null ) mediaPlayer.release();
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
mediaPlayer.setOnCompletionListener(this);
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.setLooping(loopEnabled);
String packageName = context.getPackageName();
Uri dataUri = Uri.parse("android.resource://" + packageName + "/" + currentSoundID);
try {
mediaPlayer.setDataSource(context, dataUri);
mediaPlayer.prepareAsync();
} catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) {
Log.w(TAG, e);
// TODO Auto-generated catch block
return;
}
}
public void stop() {
if (mediaPlayer == null) return;
mediaPlayer.release();
mediaPlayer = null;
currentSoundID = -1;
}
private void stop( int soundID ) {
setSound( soundID );
loopEnabled = false;
start();
}
public void onCompletion(MediaPlayer mp) {
//mediaPlayer.release();
//mediaPlayer = null;
}
public void onPrepared(MediaPlayer mp) {
AudioManager am = ServiceUtil.getAudioManager(context);
if (am.isBluetoothScoAvailableOffCall()) {
Log.d(TAG, "bluetooth sco is available");
try {
am.startBluetoothSco();
} catch (NullPointerException e) {
// Lollipop bug (https://stackoverflow.com/questions/26642218/audiomanager-startbluetoothsco-crashes-on-android-lollipop)
}
}
try {
mp.start();
} catch (IllegalStateException e) {
Log.w(TAG, e);
}
}
}