@ -0,0 +1,3 @@
|
||||
-dontwarn org.webrtc.NetworkMonitorAutoDetect
|
||||
-dontwarn android.net.Network
|
||||
-keep class org.webrtc.** { *; }
|
@ -0,0 +1,3 @@
|
||||
|
||||
all:
|
||||
protoc --java_out=../src/ WebRtcData.proto
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
package signal;
|
||||
|
||||
option java_package = "org.thoughtcrime.securesms.webrtc";
|
||||
option java_outer_classname = "WebRtcDataProtos";
|
||||
|
||||
message Connected {
|
||||
optional uint64 id = 1;
|
||||
}
|
||||
|
||||
message Hangup {
|
||||
optional uint64 id = 1;
|
||||
}
|
||||
|
||||
message VideoStreamingStatus {
|
||||
optional uint64 id = 1;
|
||||
optional bool enabled = 2;
|
||||
}
|
||||
|
||||
message Data {
|
||||
|
||||
optional Connected connected = 1;
|
||||
optional Hangup hangup = 2;
|
||||
optional VideoStreamingStatus videoStreamingStatus = 3;
|
||||
|
||||
}
|
After Width: | Height: | Size: 553 B |
After Width: | Height: | Size: 428 B |
After Width: | Height: | Size: 468 B |
After Width: | Height: | Size: 483 B |
After Width: | Height: | Size: 173 B |
After Width: | Height: | Size: 138 B |
After Width: | Height: | Size: 365 B |
After Width: | Height: | Size: 389 B |
After Width: | Height: | Size: 288 B |
After Width: | Height: | Size: 323 B |
After Width: | Height: | Size: 325 B |
After Width: | Height: | Size: 131 B |
After Width: | Height: | Size: 110 B |
After Width: | Height: | Size: 251 B |
After Width: | Height: | Size: 712 B |
After Width: | Height: | Size: 484 B |
After Width: | Height: | Size: 547 B |
After Width: | Height: | Size: 601 B |
After Width: | Height: | Size: 178 B |
After Width: | Height: | Size: 152 B |
After Width: | Height: | Size: 455 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 713 B |
After Width: | Height: | Size: 830 B |
After Width: | Height: | Size: 882 B |
After Width: | Height: | Size: 234 B |
After Width: | Height: | Size: 185 B |
After Width: | Height: | Size: 654 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 902 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 290 B |
After Width: | Height: | Size: 206 B |
After Width: | Height: | Size: 878 B |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval" >
|
||||
<solid android:color="#22000000" />
|
||||
</shape>
|
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/compoundBackgroundItem" android:drawable="@drawable/webrtc_control_background"/>
|
||||
|
||||
<item android:id="@+id/moreIndicatorItem"
|
||||
android:top="5dp"
|
||||
android:left="5dp"
|
||||
android:right="5dp"
|
||||
android:bottom="5dp">
|
||||
<bitmap android:src="@drawable/redphone_ic_more_indicator_holo_dark"
|
||||
android:gravity="bottom|right" />
|
||||
</item>
|
||||
|
||||
<item android:id="@+id/bluetoothItem"
|
||||
android:top="5dp"
|
||||
android:left="5dp"
|
||||
android:right="5dp"
|
||||
android:bottom="5dp">
|
||||
<bitmap android:src="@drawable/ic_phone_bluetooth_speaker_white_24dp"
|
||||
android:gravity="center" />
|
||||
</item>
|
||||
|
||||
<!-- Handset earpiece is active -->
|
||||
<item android:id="@+id/handsetItem" android:top="5dp"
|
||||
android:left="5dp"
|
||||
android:right="5dp"
|
||||
android:bottom="5dp">
|
||||
<bitmap android:src="@drawable/ic_phone_in_talk_white_24dp"
|
||||
android:gravity="center" />
|
||||
</item>
|
||||
|
||||
<!-- Speakerphone icon showing 'speaker on' state -->
|
||||
<item android:id="@+id/speakerphoneOnItem" android:top="5dp"
|
||||
android:left="5dp"
|
||||
android:right="5dp"
|
||||
android:bottom="5dp">
|
||||
<bitmap android:src="@drawable/ic_volume_up_white_24dp"
|
||||
android:gravity="center" />
|
||||
</item>
|
||||
|
||||
<!--<!– Speakerphone icon showing 'speaker off' state –>-->
|
||||
<!--<item android:id="@+id/speakerphoneOffItem">-->
|
||||
<!--<bitmap android:src="@drawable/ic_volume_mute_white_24dp"-->
|
||||
<!--android:gravity="center" />-->
|
||||
<!--</item>-->
|
||||
|
||||
</layer-list>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/circle_alpha" android:state_checked="true"/>
|
||||
<item android:drawable="@android:color/transparent" />
|
||||
</selector>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/webrtc_control_background"/>
|
||||
<item android:top="5dp"
|
||||
android:left="5dp"
|
||||
android:right="5dp"
|
||||
android:bottom="5dp"
|
||||
android:drawable="@drawable/ic_mic_off_white_24dp"/>
|
||||
</layer-list>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/webrtc_control_background"/>
|
||||
<item android:top="5dp"
|
||||
android:left="5dp"
|
||||
android:right="5dp"
|
||||
android:bottom="5dp"
|
||||
android:drawable="@drawable/ic_videocam_white_24dp"/>
|
||||
</layer-list>
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen android:id="@+id/callScreen"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
|
||||
|
||||
</FrameLayout>
|
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/inCallControls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:background="@color/textsecure_primary">
|
||||
|
||||
<ToggleButton android:id="@+id/audioButton"
|
||||
style="@style/WebRtcCallCompoundButton"
|
||||
android:background="@drawable/webrtc_audio_button"
|
||||
tools:checked="true"
|
||||
android:layout_marginRight="15dp"/>
|
||||
|
||||
|
||||
<ToggleButton android:id="@+id/muteButton"
|
||||
style="@style/WebRtcCallCompoundButton"
|
||||
android:background="@drawable/webrtc_mute_button"
|
||||
android:contentDescription="@string/redphone_call_controls__mute"
|
||||
android:layout_marginRight="15dp"
|
||||
tools:checked="false"
|
||||
/>
|
||||
|
||||
<ToggleButton android:id="@+id/video_mute_button"
|
||||
style="@style/WebRtcCallCompoundButton"
|
||||
android:background="@drawable/webrtc_video_mute_button"/>
|
||||
|
||||
</merge>
|
@ -0,0 +1,249 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2007 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/incall_screen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- "Call info" block #1, for the foreground call. -->
|
||||
<RelativeLayout android:id="@+id/call_info_1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- Contact photo for call_info_1 -->
|
||||
<FrameLayout android:id="@+id/image_container"
|
||||
android:layout_below="@+id/call_banner_1"
|
||||
android:gravity="top|center_horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView android:id="@+id/photo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="visible"
|
||||
tools:src="@drawable/ic_contact_picture_large"
|
||||
/>
|
||||
|
||||
<LinearLayout android:id="@+id/untrusted_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/grey_400"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView android:id="@+id/untrusted_explanation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="16sp"
|
||||
android:maxWidth="270dp"
|
||||
android:lineSpacingExtra="2sp"
|
||||
tools:text="The safety numbers for your conversation with Masha have changed. This could either mean that someone is trying to intercept your communication, or that Masha simply re-installed Signal. You may wish to verify safety numbers for this contact."/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginTop="20dp"
|
||||
android:maxWidth="250dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button android:id="@+id/accept_safety_numbers"
|
||||
android:text="Accept"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="10dp"/>
|
||||
|
||||
<Button android:id="@+id/cancel_safety_numbers"
|
||||
android:text="Cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.PercentFrameLayout
|
||||
android:id="@+id/remote_render_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:visibility="invisible"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.PercentFrameLayout
|
||||
android:id="@+id/local_render_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:visibility="invisible"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- "Call Banner" for call #1, the foregound or ringing call.
|
||||
The "call banner" is a block of info about a single call,
|
||||
including the contact name, phone number, call time counter,
|
||||
and other status info. This info is shown as a "banner"
|
||||
overlaid across the top of contact photo. -->
|
||||
<RelativeLayout android:id="@+id/call_banner_1"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="80dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:background="@color/textsecure_primary"
|
||||
>
|
||||
|
||||
<!-- Name (or the phone number, if we don't have a name to display). -->
|
||||
<TextView android:id="@+id/name"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingRight="50sp"
|
||||
android:textSize="40sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:singleLine="true"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="Ali Connors"
|
||||
/>
|
||||
|
||||
<!-- Label (like "Mobile" or "Work", if present) and phone number, side by side -->
|
||||
<LinearLayout android:id="@+id/labelAndNumber"
|
||||
android:layout_below="@id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingRight="50sp"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
<TextView android:id="@+id/label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#FFFFFF"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="@string/redphone_call_card__signal_call"
|
||||
android:layout_marginRight="10dp"
|
||||
/>
|
||||
<TextView android:id="@+id/phoneNumber"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="#FFFFFF"
|
||||
android:singleLine="true"
|
||||
tools:text="+14152222222"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Elapsed time indication for a call in progress. -->
|
||||
<TextView android:id="@+id/elapsedTime"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="#FFFFFF"
|
||||
android:singleLine="true"
|
||||
/>
|
||||
|
||||
<!-- Call type indication: a special label and/or branding
|
||||
for certain kinds of calls (like "Internet call" for a SIP call.) -->
|
||||
<TextView android:id="@+id/callTypeLabel"
|
||||
android:layout_below="@id/labelAndNumber"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="#FFFFFF"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone"
|
||||
android:text="@string/redphone_call_card__signal_call"
|
||||
/>
|
||||
|
||||
<!-- Social status (currently unused) -->
|
||||
<TextView android:id="@+id/socialStatus"
|
||||
android:layout_below="@id/callTypeLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="#FFFFFF"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls
|
||||
android:id="@+id/inCallControls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_below="@id/labelAndNumber"/>
|
||||
|
||||
|
||||
</RelativeLayout> <!-- End of call_banner for call_info #1. -->
|
||||
|
||||
<!-- The "call state label": In some states, this shows a special
|
||||
indication like "Dialing" or "Incoming call" or "Call ended".
|
||||
It's unused for the normal case of an active ongoing call. -->
|
||||
<!-- This is visually part of the call banner, but it's not actually
|
||||
part of the "call_banner_1" RelativeLayout since it needs a
|
||||
different background color. -->
|
||||
<TextView android:id="@+id/callStateLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/call_banner_1"
|
||||
android:gravity="right"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingRight="24dp"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textAllCaps="true"
|
||||
android:background="#8033b5e5"
|
||||
tools:text="connected"
|
||||
/>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/hangup_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="50dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:src="@drawable/ic_call_end_white_48dp"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="@color/red_500"
|
||||
android:visibility="visible"
|
||||
android:contentDescription="End call"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay
|
||||
android:id="@+id/callControls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.thoughtcrime.redphone.util.multiwaveview.MultiWaveView
|
||||
android:id="@+id/incomingCallWidget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginBottom="-46dp"
|
||||
android:background="@android:color/black"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<TextView android:id="@+id/redphone_banner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@id/incomingCallWidget"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textAllCaps="true"
|
||||
android:background="@color/textsecure_primary"
|
||||
android:text="@string/redphone_call_controls__signal_call"/>
|
||||
|
||||
</RelativeLayout>
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|