Beta support for webrtc video and voice calling

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

View File

@ -118,6 +118,14 @@
android:launchMode="singleTask">
</activity>
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
android:launchMode="singleTask">
</activity>
<activity android:name=".CountrySelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@ -349,7 +357,7 @@
</intent-filter>
</activity>
<activity android:name="org.thoughtcrime.redphone.RedPhoneShare"
<activity android:name="org.thoughtcrime.redphone.VoiceCallShare"
android:excludeFromRecents="true"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
@ -383,6 +391,7 @@
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
<service android:enabled="true" android:name="org.thoughtcrime.redphone.RedPhoneService"/>
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:name=".service.KeyCachingService"/>

View File

@ -57,7 +57,8 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7'
compile 'org.whispersystems:signal-service-android:2.4.7'
compile 'org.whispersystems:signal-service-android:2.5.0'
compile 'org.whispersystems:webrtc-android:M56'
compile "me.leolin:ShortcutBadger:1.10-WS1"
compile 'se.emilsjolander:stickylistheaders:2.7.0'
@ -123,7 +124,8 @@ dependencyVerification {
'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
'org.whispersystems:signal-service-android:0b5e607c1ffdbc90f8b1117c43ceaba62e3e19c01c8d29b3e1bf57cffce07f2b',
'org.whispersystems:signal-service-android:f207fcf8f17b5a1f04053151cad518f9520f8fbfb2e5563a19828f6b2c2b7b6d',
'org.whispersystems:webrtc-android:1eaaf2c8b48e135834de74733dd5ffcf9585402ad4d568f5167bc3ba6f11d569',
'me.leolin:ShortcutBadger:e8e39df8a59d8211a30f40b1eeab21b3fa57b3f3e0f03abb995f82d66588778c',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa',
@ -157,7 +159,7 @@ dependencyVerification {
'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d',
'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70',
'org.whispersystems:signal-protocol-android:1b4b9d557c8eaf861797ff683990d482d4aa8e9f23d9b17ff0cc67a02f38cb19',
'org.whispersystems:signal-service-java:9738c26c17069a2f1eff47a46da5df62efa875bd66321933bed78f2584b7cc70',
'org.whispersystems:signal-service-java:910ed96e928355d118454e1dff6c11b9f95daa801f3b4022e5c8999bff47a888',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
@ -178,6 +180,7 @@ dependencyVerification {
]
}
android {
compileSdkVersion 25
buildToolsVersion '23.0.3'
@ -239,6 +242,7 @@ android {
'proguard-glide.pro',
'proguard-shortcutbadger.pro',
'proguard-retrofit.pro',
'proguard-webrtc.pro',
'proguard.cfg'
testProguardFiles 'proguard-automation.pro',
'proguard.cfg'

3
proguard-webrtc.pro Normal file
View File

@ -0,0 +1,3 @@
-dontwarn org.webrtc.NetworkMonitorAutoDetect
-dontwarn android.net.Network
-keep class org.webrtc.** { *; }

3
protobuf/Makefile Normal file
View File

@ -0,0 +1,3 @@
all:
protoc --java_out=../src/ WebRtcData.proto

31
protobuf/WebRtcData.proto Normal file
View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

View File

@ -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>

View File

@ -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>
<!--&lt;!&ndash; Speakerphone icon showing 'speaker off' state &ndash;&gt;-->
<!--<item android:id="@+id/speakerphoneOffItem">-->
<!--<bitmap android:src="@drawable/ic_volume_mute_white_24dp"-->
<!--android:gravity="center" />-->
<!--</item>-->
</layer-list>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

Binary file not shown.

Binary file not shown.

View File

@ -641,6 +641,14 @@
<string name="SingleRecipientNotificationBuilder_signal">Signal</string>
<string name="SingleRecipientNotificationBuilder_new_message">New message</string>
<!-- WebRtcCallScreen -->
<string name="WebRtcCallScreen_new_safety_numbers">The safety numbers for your conversation with %1$s have changed. This could either mean that someone is trying to intercept your communication, or that %2$s simply re-installed Signal.</string>
<string name="WebRtcCallScreen_you_may_wish_to_verify_this_contact">You may wish to verify
safety numbers for this contact.
</string>
<string name="WebRtcCallScreen_new_safety_numbers_title">New safety numbers</string>
<!-- attachment_type_selector -->
<string name="attachment_type_selector__image">Image</string>
<string name="attachment_type_selector__image_description">Image</string>
@ -1129,6 +1137,9 @@
<string name="preferences_chats__message_trimming">Message trimming</string>
<string name="preferences_advanced__use_system_emoji">Use system emoji</string>
<string name="preferences_advanced__disable_signal_built_in_emoji_support">Disable Signal\'s built-in emoji support</string>
<string name="preferences_advanced__video_calling_beta">Video calling beta</string>
<string name="preferences_advanced__enable_support_for_next_generation_video_and_voice_calls">Support for next-generation video and voice calls when enabled by both parties. This feature is in beta.</string>
<!-- **************************************** -->
<!-- menus -->

View File

@ -227,6 +227,13 @@
<item name="android:textOff">@null</item>
</style>
<style name="WebRtcCallCompoundButton">
<item name="android:layout_height">31dp</item>
<item name="android:layout_width">31dp</item>
<item name="android:textOn">@null</item>
<item name="android:textOff">@null</item>
</style>
<style name="IdentityKey">
<item name="android:fontFamily">monospace</item>
<item name="android:typeface">monospace</item>

View File

@ -19,6 +19,12 @@
android:title="@string/preferences_advanced__use_system_emoji"
android:summary="@string/preferences_advanced__disable_signal_built_in_emoji_support" />
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_webrtc_calling"
android:title="@string/preferences_advanced__video_calling_beta"
android:summary="@string/preferences_advanced__enable_support_for_next_generation_video_and_voice_calls"/>
<Preference android:key="pref_choose_identity"
android:title="@string/preferences__choose_identity"
android:summary="@string/preferences__choose_your_contact_entry_from_the_contacts_list"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,292 @@
/*
* Copyright (C) 2016 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.webrtc.SurfaceViewRenderer;
import org.whispersystems.libsignal.IdentityKey;
/**
* A UI widget that encapsulates the entire in-call screen
* for both initiators and responders.
*
* @author Moxie Marlinspike
*
*/
public class WebRtcCallScreen extends FrameLayout implements Recipient.RecipientModifiedListener {
private static final String TAG = WebRtcCallScreen.class.getSimpleName();
private ImageView photo;
private PercentFrameLayout localRenderLayout;
private PercentFrameLayout remoteRenderLayout;
private TextView name;
private TextView phoneNumber;
private TextView label;
private TextView elapsedTime;
private View untrustedIdentityContainer;
private TextView untrustedIdentityExplanation;
private Button acceptIdentityButton;
private Button cancelIdentityButton;
private TextView status;
private FloatingActionButton endCallButton;
private WebRtcCallControls controls;
private Recipient recipient;
private WebRtcIncomingCallOverlay incomingCallOverlay;
public WebRtcCallScreen(Context context) {
super(context);
initialize();
}
public WebRtcCallScreen(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WebRtcCallScreen(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message, @Nullable String sas) {
setCard(personInfo, message);
setConnected(WebRtcCallService.localRenderer, WebRtcCallService.remoteRenderer);
incomingCallOverlay.setActiveCall(sas);
}
public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message) {
setCard(personInfo, message);
incomingCallOverlay.setActiveCall();
}
public void setIncomingCall(Recipient personInfo) {
setCard(personInfo, getContext().getString(R.string.CallScreen_Incoming_call));
incomingCallOverlay.setIncomingCall();
}
public void setUntrustedIdentity(Recipient personInfo, IdentityKey untrustedIdentity) {
String name = recipient.toShortString();
String introduction = String.format(getContext().getString(R.string.WebRtcCallScreen_new_safety_numbers), name, name);
SpannableString spannableString = new SpannableString(introduction + " " + getContext().getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
spannableString.setSpan(new VerifySpan(getContext(), personInfo.getRecipientId(), untrustedIdentity),
introduction.length()+1, spannableString.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setPersonInfo(personInfo);
this.incomingCallOverlay.setActiveCall();
this.status.setText(R.string.WebRtcCallScreen_new_safety_numbers_title);
this.untrustedIdentityContainer.setVisibility(View.VISIBLE);
this.untrustedIdentityExplanation.setText(spannableString);
this.untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
this.endCallButton.setVisibility(View.INVISIBLE);
}
public void reset() {
setPersonInfo(Recipient.getUnknownRecipient());
this.status.setText("");
this.recipient = null;
this.controls.reset();
this.untrustedIdentityExplanation.setText("");
this.untrustedIdentityContainer.setVisibility(View.GONE);
this.localRenderLayout.removeAllViews();
this.remoteRenderLayout.removeAllViews();
incomingCallOverlay.reset();
}
public void setIncomingCallActionListener(WebRtcIncomingCallOverlay.IncomingCallActionListener listener) {
incomingCallOverlay.setIncomingCallActionListener(listener);
}
public void setAudioMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) {
this.controls.setAudioMuteButtonListener(listener);
}
public void setVideoMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) {
this.controls.setVideoMuteButtonListener(listener);
}
public void setAudioButtonListener(WebRtcCallControls.AudioButtonListener listener) {
this.controls.setAudioButtonListener(listener);
}
public void setHangupButtonListener(final HangupButtonListener listener) {
endCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onClick();
}
});
}
public void setAcceptIdentityListener(OnClickListener listener) {
this.acceptIdentityButton.setOnClickListener(listener);
}
public void setCancelIdentityButton(OnClickListener listener) {
this.cancelIdentityButton.setOnClickListener(listener);
}
public void notifyBluetoothChange() {
this.controls.updateAudioButton();
}
public void setLocalVideoEnabled(boolean enabled) {
if (enabled) {
this.localRenderLayout.setHidden(false);
} else {
this.localRenderLayout.setHidden(true);
}
this.localRenderLayout.requestLayout();
}
public void setRemoteVideoEnabled(boolean enabled) {
if (enabled) {
this.remoteRenderLayout.setHidden(false);
} else {
this.remoteRenderLayout.setHidden(true);
}
this.remoteRenderLayout.requestLayout();
}
private void initialize() {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_call_screen, this, true);
this.elapsedTime = (TextView) findViewById(R.id.elapsedTime);
this.photo = (ImageView) findViewById(R.id.photo);
this.localRenderLayout = (PercentFrameLayout) findViewById(R.id.local_render_layout);
this.remoteRenderLayout = (PercentFrameLayout) findViewById(R.id.remote_render_layout);
this.phoneNumber = (TextView) findViewById(R.id.phoneNumber);
this.name = (TextView) findViewById(R.id.name);
this.label = (TextView) findViewById(R.id.label);
this.status = (TextView) findViewById(R.id.callStateLabel);
this.controls = (WebRtcCallControls) findViewById(R.id.inCallControls);
this.endCallButton = (FloatingActionButton) findViewById(R.id.hangup_fab);
this.incomingCallOverlay = (WebRtcIncomingCallOverlay) findViewById(R.id.callControls);
this.untrustedIdentityContainer = findViewById(R.id.untrusted_layout);
this.untrustedIdentityExplanation = (TextView) findViewById(R.id.untrusted_explanation);
this.acceptIdentityButton = (Button)findViewById(R.id.accept_safety_numbers);
this.cancelIdentityButton = (Button)findViewById(R.id.cancel_safety_numbers);
this.localRenderLayout.setHidden(true);
this.remoteRenderLayout.setHidden(true);
}
private void setConnected(SurfaceViewRenderer localRenderer,
SurfaceViewRenderer remoteRenderer)
{
localRenderLayout.setPosition(7, 7, 25, 25);
localRenderLayout.setSquare(true);
remoteRenderLayout.setPosition(0, 0, 100, 100);
localRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
remoteRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
localRenderer.setMirror(true);
localRenderer.setZOrderMediaOverlay(true);
localRenderLayout.addView(localRenderer);
remoteRenderLayout.addView(remoteRenderer);
}
private void setPersonInfo(final @NonNull Recipient recipient) {
this.recipient = recipient;
this.recipient.addListener(this);
final Context context = getContext();
new AsyncTask<Void, Void, ContactPhoto>() {
@Override
protected ContactPhoto doInBackground(Void... params) {
DisplayMetrics metrics = new DisplayMetrics();
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Uri contentUri = ContactsContract.Contacts.lookupContact(context.getContentResolver(),
recipient.getContactUri());
windowManager.getDefaultDisplay().getMetrics(metrics);
return ContactPhotoFactory.getContactPhoto(context, contentUri, null, metrics.widthPixels);
}
@Override
protected void onPostExecute(final ContactPhoto contactPhoto) {
WebRtcCallScreen.this.photo.setImageDrawable(contactPhoto.asCallCard(context));
}
}.execute();
this.name.setText(recipient.getName());
this.phoneNumber.setText(recipient.getNumber());
}
private void setCard(Recipient recipient, String status) {
setPersonInfo(recipient);
this.status.setText(status);
this.untrustedIdentityContainer.setVisibility(View.GONE);
this.endCallButton.setVisibility(View.VISIBLE);
}
@Override
public void onModified(Recipient recipient) {
if (recipient == this.recipient) {
setPersonInfo(recipient);
}
}
public static interface HangupButtonListener {
public void onClick();
}
}

View File

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

View File

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

View File

@ -0,0 +1,90 @@
/*
* Copyright (C) 2012 Moxie Marlinspike
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.components.webrtc;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import org.thoughtcrime.redphone.RedPhone;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
/**
* Manages the state of the RedPhone items in the Android notification bar.
*
* @author Moxie Marlinspike
*
*/
public class WebRtcNotificationBarManager {
private static final int RED_PHONE_NOTIFICATION = 313388;
private static final int MISSED_CALL_NOTIFICATION = 313389;
public static final int TYPE_INCOMING_RINGING = 1;
public static final int TYPE_OUTGOING_RINGING = 2;
public static final int TYPE_ESTABLISHED = 3;
public static void setCallEnded(Context context) {
NotificationManager notificationManager = (NotificationManager)context
.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(RED_PHONE_NOTIFICATION);
}
public static void setCallInProgress(Context context, int type, Recipient recipient) {
NotificationManager notificationManager = (NotificationManager)context
.getSystemService(Context.NOTIFICATION_SERVICE);
Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_call_secure_white_24dp)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setContentTitle(recipient.getName());
if (type == TYPE_INCOMING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call));
builder.addAction(getNotificationAction(context, RedPhone.DENY_ACTION, R.drawable.ic_close_grey600_32dp, R.string.NotificationBarManager__deny_call));
builder.addAction(getNotificationAction(context, RedPhone.ANSWER_ACTION, R.drawable.ic_phone_grey600_32dp, R.string.NotificationBarManager__answer_call));
} else if (type == TYPE_OUTGOING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call));
builder.addAction(getNotificationAction(context, RedPhone.END_CALL_ACTION, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__cancel_call));
} else {
builder.setContentText(context.getString(R.string.NotificationBarManager_signal_call_in_progress));
builder.addAction(getNotificationAction(context, RedPhone.END_CALL_ACTION, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__end_call));
}
notificationManager.notify(RED_PHONE_NOTIFICATION, builder.build());
}
private static NotificationCompat.Action getNotificationAction(Context context, String action, int iconResId, int titleResId) {
Intent intent = new Intent(context, WebRtcCallActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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