Beta support for webrtc video and voice calling
// FREEBIE
@ -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"/>
|
||||
|
10
build.gradle
@ -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
@ -0,0 +1,3 @@
|
||||
-dontwarn org.webrtc.NetworkMonitorAutoDetect
|
||||
-dontwarn android.net.Network
|
||||
-keep class org.webrtc.** { *; }
|
3
protobuf/Makefile
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
all:
|
||||
protoc --java_out=../src/ WebRtcData.proto
|
31
protobuf/WebRtcData.proto
Normal 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;
|
||||
|
||||
}
|
BIN
res/drawable-hdpi/ic_call_end_white_48dp.png
Normal file
After Width: | Height: | Size: 553 B |
BIN
res/drawable-hdpi/ic_mic_off_white_24dp.png
Normal file
After Width: | Height: | Size: 428 B |
BIN
res/drawable-hdpi/ic_phone_bluetooth_speaker_white_24dp.png
Normal file
After Width: | Height: | Size: 468 B |
BIN
res/drawable-hdpi/ic_phone_in_talk_white_24dp.png
Normal file
After Width: | Height: | Size: 483 B |
BIN
res/drawable-hdpi/ic_videocam_white_24dp.png
Normal file
After Width: | Height: | Size: 173 B |
BIN
res/drawable-hdpi/ic_volume_mute_white_24dp.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
res/drawable-hdpi/ic_volume_up_white_24dp.png
Normal file
After Width: | Height: | Size: 365 B |
BIN
res/drawable-mdpi/ic_call_end_white_48dp.png
Normal file
After Width: | Height: | Size: 389 B |
BIN
res/drawable-mdpi/ic_mic_off_white_24dp.png
Normal file
After Width: | Height: | Size: 288 B |
BIN
res/drawable-mdpi/ic_phone_bluetooth_speaker_white_24dp.png
Normal file
After Width: | Height: | Size: 323 B |
BIN
res/drawable-mdpi/ic_phone_in_talk_white_24dp.png
Normal file
After Width: | Height: | Size: 325 B |
BIN
res/drawable-mdpi/ic_videocam_white_24dp.png
Normal file
After Width: | Height: | Size: 131 B |
BIN
res/drawable-mdpi/ic_volume_mute_white_24dp.png
Normal file
After Width: | Height: | Size: 110 B |
BIN
res/drawable-mdpi/ic_volume_up_white_24dp.png
Normal file
After Width: | Height: | Size: 251 B |
BIN
res/drawable-xhdpi/ic_call_end_white_48dp.png
Normal file
After Width: | Height: | Size: 712 B |
BIN
res/drawable-xhdpi/ic_mic_off_white_24dp.png
Normal file
After Width: | Height: | Size: 484 B |
BIN
res/drawable-xhdpi/ic_phone_bluetooth_speaker_white_24dp.png
Normal file
After Width: | Height: | Size: 547 B |
BIN
res/drawable-xhdpi/ic_phone_in_talk_white_24dp.png
Normal file
After Width: | Height: | Size: 601 B |
BIN
res/drawable-xhdpi/ic_videocam_white_24dp.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
res/drawable-xhdpi/ic_volume_mute_white_24dp.png
Normal file
After Width: | Height: | Size: 152 B |
BIN
res/drawable-xhdpi/ic_volume_up_white_24dp.png
Normal file
After Width: | Height: | Size: 455 B |
BIN
res/drawable-xxhdpi/ic_call_end_white_48dp.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
res/drawable-xxhdpi/ic_mic_off_white_24dp.png
Normal file
After Width: | Height: | Size: 713 B |
BIN
res/drawable-xxhdpi/ic_phone_bluetooth_speaker_white_24dp.png
Normal file
After Width: | Height: | Size: 830 B |
BIN
res/drawable-xxhdpi/ic_phone_in_talk_white_24dp.png
Normal file
After Width: | Height: | Size: 882 B |
BIN
res/drawable-xxhdpi/ic_videocam_white_24dp.png
Normal file
After Width: | Height: | Size: 234 B |
BIN
res/drawable-xxhdpi/ic_volume_mute_white_24dp.png
Normal file
After Width: | Height: | Size: 185 B |
BIN
res/drawable-xxhdpi/ic_volume_up_white_24dp.png
Normal file
After Width: | Height: | Size: 654 B |
BIN
res/drawable-xxxhdpi/ic_call_end_white_48dp.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
res/drawable-xxxhdpi/ic_mic_off_white_24dp.png
Normal file
After Width: | Height: | Size: 902 B |
BIN
res/drawable-xxxhdpi/ic_phone_bluetooth_speaker_white_24dp.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
res/drawable-xxxhdpi/ic_phone_in_talk_white_24dp.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
res/drawable-xxxhdpi/ic_videocam_white_24dp.png
Normal file
After Width: | Height: | Size: 290 B |
BIN
res/drawable-xxxhdpi/ic_volume_mute_white_24dp.png
Normal file
After Width: | Height: | Size: 206 B |
BIN
res/drawable-xxxhdpi/ic_volume_up_white_24dp.png
Normal file
After Width: | Height: | Size: 878 B |
5
res/drawable/circle_alpha.xml
Normal 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>
|
47
res/drawable/webrtc_audio_button.xml
Normal 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>
|
||||
|
||||
<!--<!– Speakerphone icon showing 'speaker off' state –>-->
|
||||
<!--<item android:id="@+id/speakerphoneOffItem">-->
|
||||
<!--<bitmap android:src="@drawable/ic_volume_mute_white_24dp"-->
|
||||
<!--android:gravity="center" />-->
|
||||
<!--</item>-->
|
||||
|
||||
</layer-list>
|
5
res/drawable/webrtc_control_background.xml
Normal 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>
|
9
res/drawable/webrtc_mute_button.xml
Normal 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>
|
9
res/drawable/webrtc_video_mute_button.xml
Normal 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>
|
12
res/layout/webrtc_call_activity.xml
Normal 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>
|
29
res/layout/webrtc_call_controls.xml
Normal 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>
|
249
res/layout/webrtc_call_screen.xml
Normal 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>
|
29
res/layout/webrtc_incoming_call_overlay.xml
Normal 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>
|
BIN
res/raw/webrtc_completed.mp3
Normal file
BIN
res/raw/webrtc_disconnected.mp3
Normal 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 -->
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
70
src/org/thoughtcrime/redphone/VoiceCallShare.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
404
src/org/thoughtcrime/securesms/WebRtcCallActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Xfermode;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Simple container that confines the children to a subrectangle specified as percentage values of
|
||||
* the container size. The children are centered horizontally and vertically inside the confined
|
||||
* space.
|
||||
*/
|
||||
public class PercentFrameLayout extends ViewGroup {
|
||||
private int xPercent = 0;
|
||||
private int yPercent = 0;
|
||||
private int widthPercent = 100;
|
||||
private int heightPercent = 100;
|
||||
|
||||
private boolean square = false;
|
||||
private boolean hidden = false;
|
||||
|
||||
public PercentFrameLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public PercentFrameLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public PercentFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setSquare(boolean square) {
|
||||
this.square = square;
|
||||
}
|
||||
|
||||
public void setHidden(boolean hidden) {
|
||||
this.hidden = hidden;
|
||||
}
|
||||
|
||||
public void setPosition(int xPercent, int yPercent, int widthPercent, int heightPercent) {
|
||||
this.xPercent = xPercent;
|
||||
this.yPercent = yPercent;
|
||||
this.widthPercent = widthPercent;
|
||||
this.heightPercent = heightPercent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDelayChildPressedState() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
final int width = getDefaultSize(Integer.MAX_VALUE, widthMeasureSpec);
|
||||
final int height = getDefaultSize(Integer.MAX_VALUE, heightMeasureSpec);
|
||||
|
||||
setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
|
||||
|
||||
int childWidth = width * widthPercent / 100;
|
||||
int childHeight = height * heightPercent / 100;
|
||||
|
||||
if (square) {
|
||||
if (width > height) childWidth = childHeight;
|
||||
else childHeight = childWidth;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
childWidth = 1;
|
||||
childHeight = 1;
|
||||
}
|
||||
|
||||
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST);
|
||||
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST);
|
||||
|
||||
for (int i = 0; i < getChildCount(); ++i) {
|
||||
final View child = getChildAt(i);
|
||||
if (child.getVisibility() != GONE) {
|
||||
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
final int width = right - left;
|
||||
final int height = bottom - top;
|
||||
// Sub-rectangle specified by percentage values.
|
||||
final int subWidth = width * widthPercent / 100;
|
||||
final int subHeight = height * heightPercent / 100;
|
||||
final int subLeft = left + width * xPercent / 100;
|
||||
final int subTop = top + height * yPercent / 100;
|
||||
|
||||
|
||||
for (int i = 0; i < getChildCount(); ++i) {
|
||||
final View child = getChildAt(i);
|
||||
if (child.getVisibility() != GONE) {
|
||||
final int childWidth = child.getMeasuredWidth();
|
||||
final int childHeight = child.getMeasuredHeight();
|
||||
// Center child both vertically and horizontally.
|
||||
int childLeft = subLeft + (subWidth - childWidth) / 2;
|
||||
int childTop = subTop + (subHeight - childHeight) / 2;
|
||||
|
||||
if (hidden) {
|
||||
childLeft = 0;
|
||||
childTop = 0;
|
||||
}
|
||||
|
||||
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.thoughtcrime.redphone.util.AudioUtils;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class WebRtcCallControls extends LinearLayout {
|
||||
|
||||
private CompoundButton audioMuteButton;
|
||||
private CompoundButton videoMuteButton;
|
||||
private WebRtcInCallAudioButton audioButton;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public WebRtcCallControls(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public WebRtcCallControls(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.webrtc_call_controls, this, true);
|
||||
|
||||
this.audioMuteButton = (CompoundButton) findViewById(R.id.muteButton);
|
||||
this.videoMuteButton = ViewUtil.findById(this, R.id.video_mute_button);
|
||||
this.audioButton = new WebRtcInCallAudioButton((CompoundButton) findViewById(R.id.audioButton));
|
||||
|
||||
updateAudioButton();
|
||||
}
|
||||
|
||||
public void updateAudioButton() {
|
||||
audioButton.setAudioMode(AudioUtils.getCurrentAudioMode(getContext()));
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(AudioUtils.getScoUpdateAction());
|
||||
handleBluetoothIntent(getContext().registerReceiver(null, filter));
|
||||
}
|
||||
|
||||
|
||||
private void handleBluetoothIntent(Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!intent.getAction().equals(AudioUtils.getScoUpdateAction())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
|
||||
if (state.equals(AudioManager.SCO_AUDIO_STATE_CONNECTED)) {
|
||||
audioButton.setHeadsetAvailable(true);
|
||||
} else if (state.equals(AudioManager.SCO_AUDIO_STATE_DISCONNECTED)) {
|
||||
audioButton.setHeadsetAvailable(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAudioMuteButtonListener(final MuteButtonListener listener) {
|
||||
audioMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||
listener.onToggle(b);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setVideoMuteButtonListener(final MuteButtonListener listener) {
|
||||
videoMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
listener.onToggle(!isChecked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setAudioButtonListener(final AudioButtonListener listener) {
|
||||
audioButton.setListener(listener);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
updateAudioButton();
|
||||
audioMuteButton.setChecked(false);
|
||||
videoMuteButton.setChecked(false);
|
||||
}
|
||||
|
||||
public static interface MuteButtonListener {
|
||||
public void onToggle(boolean isMuted);
|
||||
}
|
||||
|
||||
public static interface AudioButtonListener {
|
||||
public void onAudioChange(AudioUtils.AudioMode mode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
/**
|
||||
* A UI widget that encapsulates the entire in-call screen
|
||||
* for both initiators and responders.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class WebRtcCallScreen extends FrameLayout implements Recipient.RecipientModifiedListener {
|
||||
|
||||
private static final String TAG = WebRtcCallScreen.class.getSimpleName();
|
||||
|
||||
private ImageView photo;
|
||||
private PercentFrameLayout localRenderLayout;
|
||||
private PercentFrameLayout remoteRenderLayout;
|
||||
private TextView name;
|
||||
private TextView phoneNumber;
|
||||
private TextView label;
|
||||
private TextView elapsedTime;
|
||||
private View untrustedIdentityContainer;
|
||||
private TextView untrustedIdentityExplanation;
|
||||
private Button acceptIdentityButton;
|
||||
private Button cancelIdentityButton;
|
||||
private TextView status;
|
||||
private FloatingActionButton endCallButton;
|
||||
private WebRtcCallControls controls;
|
||||
|
||||
private Recipient recipient;
|
||||
|
||||
private WebRtcIncomingCallOverlay incomingCallOverlay;
|
||||
|
||||
public WebRtcCallScreen(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public WebRtcCallScreen(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public WebRtcCallScreen(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message, @Nullable String sas) {
|
||||
setCard(personInfo, message);
|
||||
setConnected(WebRtcCallService.localRenderer, WebRtcCallService.remoteRenderer);
|
||||
incomingCallOverlay.setActiveCall(sas);
|
||||
}
|
||||
|
||||
public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message) {
|
||||
setCard(personInfo, message);
|
||||
incomingCallOverlay.setActiveCall();
|
||||
}
|
||||
|
||||
public void setIncomingCall(Recipient personInfo) {
|
||||
setCard(personInfo, getContext().getString(R.string.CallScreen_Incoming_call));
|
||||
incomingCallOverlay.setIncomingCall();
|
||||
}
|
||||
|
||||
public void setUntrustedIdentity(Recipient personInfo, IdentityKey untrustedIdentity) {
|
||||
String name = recipient.toShortString();
|
||||
String introduction = String.format(getContext().getString(R.string.WebRtcCallScreen_new_safety_numbers), name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " + getContext().getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
|
||||
|
||||
spannableString.setSpan(new VerifySpan(getContext(), personInfo.getRecipientId(), untrustedIdentity),
|
||||
introduction.length()+1, spannableString.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
setPersonInfo(personInfo);
|
||||
|
||||
this.incomingCallOverlay.setActiveCall();
|
||||
this.status.setText(R.string.WebRtcCallScreen_new_safety_numbers_title);
|
||||
this.untrustedIdentityContainer.setVisibility(View.VISIBLE);
|
||||
this.untrustedIdentityExplanation.setText(spannableString);
|
||||
this.untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
this.endCallButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
|
||||
public void reset() {
|
||||
setPersonInfo(Recipient.getUnknownRecipient());
|
||||
this.status.setText("");
|
||||
this.recipient = null;
|
||||
this.controls.reset();
|
||||
this.untrustedIdentityExplanation.setText("");
|
||||
this.untrustedIdentityContainer.setVisibility(View.GONE);
|
||||
this.localRenderLayout.removeAllViews();
|
||||
this.remoteRenderLayout.removeAllViews();
|
||||
|
||||
incomingCallOverlay.reset();
|
||||
}
|
||||
|
||||
public void setIncomingCallActionListener(WebRtcIncomingCallOverlay.IncomingCallActionListener listener) {
|
||||
incomingCallOverlay.setIncomingCallActionListener(listener);
|
||||
}
|
||||
|
||||
public void setAudioMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) {
|
||||
this.controls.setAudioMuteButtonListener(listener);
|
||||
}
|
||||
|
||||
public void setVideoMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) {
|
||||
this.controls.setVideoMuteButtonListener(listener);
|
||||
}
|
||||
|
||||
public void setAudioButtonListener(WebRtcCallControls.AudioButtonListener listener) {
|
||||
this.controls.setAudioButtonListener(listener);
|
||||
}
|
||||
|
||||
public void setHangupButtonListener(final HangupButtonListener listener) {
|
||||
endCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onClick();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setAcceptIdentityListener(OnClickListener listener) {
|
||||
this.acceptIdentityButton.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public void setCancelIdentityButton(OnClickListener listener) {
|
||||
this.cancelIdentityButton.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public void notifyBluetoothChange() {
|
||||
this.controls.updateAudioButton();
|
||||
}
|
||||
|
||||
public void setLocalVideoEnabled(boolean enabled) {
|
||||
if (enabled) {
|
||||
this.localRenderLayout.setHidden(false);
|
||||
} else {
|
||||
this.localRenderLayout.setHidden(true);
|
||||
}
|
||||
|
||||
this.localRenderLayout.requestLayout();
|
||||
}
|
||||
|
||||
public void setRemoteVideoEnabled(boolean enabled) {
|
||||
if (enabled) {
|
||||
this.remoteRenderLayout.setHidden(false);
|
||||
} else {
|
||||
this.remoteRenderLayout.setHidden(true);
|
||||
}
|
||||
|
||||
this.remoteRenderLayout.requestLayout();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.webrtc_call_screen, this, true);
|
||||
|
||||
this.elapsedTime = (TextView) findViewById(R.id.elapsedTime);
|
||||
this.photo = (ImageView) findViewById(R.id.photo);
|
||||
this.localRenderLayout = (PercentFrameLayout) findViewById(R.id.local_render_layout);
|
||||
this.remoteRenderLayout = (PercentFrameLayout) findViewById(R.id.remote_render_layout);
|
||||
this.phoneNumber = (TextView) findViewById(R.id.phoneNumber);
|
||||
this.name = (TextView) findViewById(R.id.name);
|
||||
this.label = (TextView) findViewById(R.id.label);
|
||||
this.status = (TextView) findViewById(R.id.callStateLabel);
|
||||
this.controls = (WebRtcCallControls) findViewById(R.id.inCallControls);
|
||||
this.endCallButton = (FloatingActionButton) findViewById(R.id.hangup_fab);
|
||||
this.incomingCallOverlay = (WebRtcIncomingCallOverlay) findViewById(R.id.callControls);
|
||||
this.untrustedIdentityContainer = findViewById(R.id.untrusted_layout);
|
||||
this.untrustedIdentityExplanation = (TextView) findViewById(R.id.untrusted_explanation);
|
||||
this.acceptIdentityButton = (Button)findViewById(R.id.accept_safety_numbers);
|
||||
this.cancelIdentityButton = (Button)findViewById(R.id.cancel_safety_numbers);
|
||||
|
||||
this.localRenderLayout.setHidden(true);
|
||||
this.remoteRenderLayout.setHidden(true);
|
||||
}
|
||||
|
||||
private void setConnected(SurfaceViewRenderer localRenderer,
|
||||
SurfaceViewRenderer remoteRenderer)
|
||||
{
|
||||
localRenderLayout.setPosition(7, 7, 25, 25);
|
||||
localRenderLayout.setSquare(true);
|
||||
remoteRenderLayout.setPosition(0, 0, 100, 100);
|
||||
|
||||
localRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
remoteRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
localRenderer.setMirror(true);
|
||||
localRenderer.setZOrderMediaOverlay(true);
|
||||
|
||||
localRenderLayout.addView(localRenderer);
|
||||
remoteRenderLayout.addView(remoteRenderer);
|
||||
}
|
||||
|
||||
private void setPersonInfo(final @NonNull Recipient recipient) {
|
||||
this.recipient = recipient;
|
||||
this.recipient.addListener(this);
|
||||
|
||||
final Context context = getContext();
|
||||
|
||||
new AsyncTask<Void, Void, ContactPhoto>() {
|
||||
@Override
|
||||
protected ContactPhoto doInBackground(Void... params) {
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
Uri contentUri = ContactsContract.Contacts.lookupContact(context.getContentResolver(),
|
||||
recipient.getContactUri());
|
||||
windowManager.getDefaultDisplay().getMetrics(metrics);
|
||||
return ContactPhotoFactory.getContactPhoto(context, contentUri, null, metrics.widthPixels);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final ContactPhoto contactPhoto) {
|
||||
WebRtcCallScreen.this.photo.setImageDrawable(contactPhoto.asCallCard(context));
|
||||
}
|
||||
}.execute();
|
||||
|
||||
this.name.setText(recipient.getName());
|
||||
this.phoneNumber.setText(recipient.getNumber());
|
||||
}
|
||||
|
||||
private void setCard(Recipient recipient, String status) {
|
||||
setPersonInfo(recipient);
|
||||
this.status.setText(status);
|
||||
this.untrustedIdentityContainer.setVisibility(View.GONE);
|
||||
this.endCallButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(Recipient recipient) {
|
||||
if (recipient == this.recipient) {
|
||||
setPersonInfo(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public static interface HangupButtonListener {
|
||||
public void onClick();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.support.v7.widget.PopupMenu;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.CompoundButton;
|
||||
|
||||
import org.thoughtcrime.redphone.util.AudioUtils;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.DEFAULT;
|
||||
import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.HEADSET;
|
||||
import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.SPEAKER;
|
||||
|
||||
/**
|
||||
* Manages the audio button displayed on the in-call screen
|
||||
*
|
||||
* The behavior of this button depends on the availability of headset audio, and changes from being a regular
|
||||
* toggle button (enabling speakerphone) to bringing up a model dialog that includes speakerphone, bluetooth,
|
||||
* and regular audio options.
|
||||
*
|
||||
* Based on com.android.phone.InCallTouchUI
|
||||
*
|
||||
* @author Stuart O. Anderson
|
||||
*/
|
||||
public class WebRtcInCallAudioButton {
|
||||
|
||||
private static final String TAG = WebRtcInCallAudioButton.class.getName();
|
||||
|
||||
private final CompoundButton mAudioButton;
|
||||
private boolean headsetAvailable;
|
||||
private AudioUtils.AudioMode currentMode;
|
||||
private Context context;
|
||||
private WebRtcCallControls.AudioButtonListener listener;
|
||||
|
||||
public WebRtcInCallAudioButton(CompoundButton audioButton) {
|
||||
mAudioButton = audioButton;
|
||||
|
||||
currentMode = DEFAULT;
|
||||
headsetAvailable = false;
|
||||
|
||||
updateView();
|
||||
setListener(new WebRtcCallControls.AudioButtonListener() {
|
||||
@Override
|
||||
public void onAudioChange(AudioUtils.AudioMode mode) {
|
||||
//No Action By Default.
|
||||
}
|
||||
});
|
||||
context = audioButton.getContext();
|
||||
}
|
||||
|
||||
public void setHeadsetAvailable(boolean available) {
|
||||
headsetAvailable = available;
|
||||
updateView();
|
||||
}
|
||||
|
||||
public void setAudioMode(AudioUtils.AudioMode newMode) {
|
||||
currentMode = newMode;
|
||||
updateView();
|
||||
}
|
||||
|
||||
private void updateView() {
|
||||
// The various layers of artwork for this button come from
|
||||
// redphone_btn_compound_audio.xmlaudio.xml. Keep track of which layers we want to be
|
||||
// visible:
|
||||
//
|
||||
// - This selector shows the blue bar below the button icon when
|
||||
// this button is a toggle *and* it's currently "checked".
|
||||
boolean showToggleStateIndication = false;
|
||||
//
|
||||
// - This is visible if the popup menu is enabled:
|
||||
boolean showMoreIndicator = false;
|
||||
//
|
||||
// - Foreground icons for the button. Exactly one of these is enabled:
|
||||
boolean showSpeakerOnIcon = false;
|
||||
// boolean showSpeakerOffIcon = false;
|
||||
boolean showHandsetIcon = false;
|
||||
boolean showHeadsetIcon = false;
|
||||
|
||||
boolean speakerOn = currentMode == AudioUtils.AudioMode.SPEAKER;
|
||||
|
||||
if (headsetAvailable) {
|
||||
mAudioButton.setEnabled(true);
|
||||
|
||||
// The audio button is NOT a toggle in this state. (And its
|
||||
// setChecked() state is irrelevant since we completely hide the
|
||||
// redphone_btn_compound_background layer anyway.)
|
||||
|
||||
// Update desired layers:
|
||||
showMoreIndicator = true;
|
||||
Log.d(TAG, "UI Mode: " + currentMode);
|
||||
if (currentMode == AudioUtils.AudioMode.HEADSET) {
|
||||
showHeadsetIcon = true;
|
||||
} else if (speakerOn) {
|
||||
showSpeakerOnIcon = true;
|
||||
} else {
|
||||
showHandsetIcon = true;
|
||||
}
|
||||
} else {
|
||||
mAudioButton.setEnabled(true);
|
||||
|
||||
mAudioButton.setChecked(speakerOn);
|
||||
showSpeakerOnIcon = true;
|
||||
// showSpeakerOnIcon = speakerOn;
|
||||
// showSpeakerOffIcon = !speakerOn;
|
||||
|
||||
showToggleStateIndication = true;
|
||||
}
|
||||
|
||||
final int HIDDEN = 0;
|
||||
final int VISIBLE = 255;
|
||||
|
||||
LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
|
||||
|
||||
layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
|
||||
.setAlpha(showToggleStateIndication ? VISIBLE : HIDDEN);
|
||||
|
||||
layers.findDrawableByLayerId(R.id.moreIndicatorItem)
|
||||
.setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
|
||||
|
||||
layers.findDrawableByLayerId(R.id.bluetoothItem)
|
||||
.setAlpha(showHeadsetIcon ? VISIBLE : HIDDEN);
|
||||
|
||||
layers.findDrawableByLayerId(R.id.handsetItem)
|
||||
.setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
|
||||
|
||||
layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
|
||||
.setAlpha(showSpeakerOnIcon ? VISIBLE : HIDDEN);
|
||||
|
||||
// layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
|
||||
// .setAlpha(showSpeakerOffIcon ? VISIBLE : HIDDEN);
|
||||
|
||||
mAudioButton.invalidate();
|
||||
}
|
||||
|
||||
private void log(String msg) {
|
||||
Log.d(TAG, msg);
|
||||
}
|
||||
|
||||
public void setListener(final WebRtcCallControls.AudioButtonListener listener) {
|
||||
this.listener = listener;
|
||||
mAudioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||
if(headsetAvailable) {
|
||||
displayAudioChoiceDialog();
|
||||
} else {
|
||||
currentMode = b ? AudioUtils.AudioMode.SPEAKER : DEFAULT;
|
||||
listener.onAudioChange(currentMode);
|
||||
updateView();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayAudioChoiceDialog() {
|
||||
Log.w(TAG, "Displaying popup...");
|
||||
PopupMenu popupMenu = new PopupMenu(context, mAudioButton);
|
||||
popupMenu.getMenuInflater().inflate(R.menu.redphone_audio_popup_menu, popupMenu.getMenu());
|
||||
popupMenu.setOnMenuItemClickListener(new AudioRoutingPopupListener());
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
private class AudioRoutingPopupListener implements PopupMenu.OnMenuItemClickListener {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.handset:
|
||||
currentMode = DEFAULT;
|
||||
break;
|
||||
case R.id.headset:
|
||||
currentMode = HEADSET;
|
||||
break;
|
||||
case R.id.speaker:
|
||||
currentMode = SPEAKER;
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Unknown item selected in audio popup menu: " + item.toString());
|
||||
}
|
||||
Log.d(TAG, "Selected: " + currentMode + " -- " + item.getItemId());
|
||||
|
||||
listener.onAudioChange(currentMode);
|
||||
updateView();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.redphone.util.multiwaveview.MultiWaveView;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
/**
|
||||
* Displays the controls at the bottom of the in-call screen.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class WebRtcIncomingCallOverlay extends RelativeLayout {
|
||||
|
||||
private MultiWaveView incomingCallWidget;
|
||||
private TextView redphoneLabel;
|
||||
|
||||
private Handler handler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (incomingCallWidget.getVisibility() == View.VISIBLE) {
|
||||
incomingCallWidget.ping();
|
||||
handler.sendEmptyMessageDelayed(0, 1200);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public WebRtcIncomingCallOverlay(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public WebRtcIncomingCallOverlay(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public WebRtcIncomingCallOverlay(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public void setIncomingCall() {
|
||||
Animation animation = incomingCallWidget.getAnimation();
|
||||
|
||||
if (animation != null) {
|
||||
animation.reset();
|
||||
incomingCallWidget.clearAnimation();
|
||||
}
|
||||
|
||||
incomingCallWidget.reset(false);
|
||||
incomingCallWidget.setVisibility(View.VISIBLE);
|
||||
redphoneLabel.setVisibility(View.VISIBLE);
|
||||
|
||||
handler.sendEmptyMessageDelayed(0, 500);
|
||||
}
|
||||
|
||||
public void setActiveCall() {
|
||||
incomingCallWidget.setVisibility(View.GONE);
|
||||
redphoneLabel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setActiveCall(@Nullable String sas) {
|
||||
setActiveCall();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
incomingCallWidget.setVisibility(View.GONE);
|
||||
redphoneLabel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setIncomingCallActionListener(final IncomingCallActionListener listener) {
|
||||
incomingCallWidget.setOnTriggerListener(new MultiWaveView.OnTriggerListener() {
|
||||
@Override
|
||||
public void onTrigger(View v, int target) {
|
||||
switch (target) {
|
||||
case 0: listener.onAcceptClick(); break;
|
||||
case 2: listener.onDenyClick(); break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReleased(View v, int handle) {}
|
||||
|
||||
@Override
|
||||
public void onGrabbedStateChange(View v, int handle) {}
|
||||
|
||||
@Override
|
||||
public void onGrabbed(View v, int handle) {}
|
||||
});
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.webrtc_incoming_call_overlay, this, true);
|
||||
|
||||
this.incomingCallWidget = (MultiWaveView)findViewById(R.id.incomingCallWidget);
|
||||
this.redphoneLabel = (TextView)findViewById(R.id.redphone_banner);
|
||||
}
|
||||
|
||||
public static interface IncomingCallActionListener {
|
||||
public void onAcceptClick();
|
||||
public void onDenyClick();
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Moxie Marlinspike
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
|
||||
import org.thoughtcrime.redphone.RedPhone;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
/**
|
||||
* Manages the state of the RedPhone items in the Android notification bar.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class WebRtcNotificationBarManager {
|
||||
|
||||
private static final int RED_PHONE_NOTIFICATION = 313388;
|
||||
private static final int MISSED_CALL_NOTIFICATION = 313389;
|
||||
|
||||
public static final int TYPE_INCOMING_RINGING = 1;
|
||||
public static final int TYPE_OUTGOING_RINGING = 2;
|
||||
public static final int TYPE_ESTABLISHED = 3;
|
||||
|
||||
public static void setCallEnded(Context context) {
|
||||
NotificationManager notificationManager = (NotificationManager)context
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
notificationManager.cancel(RED_PHONE_NOTIFICATION);
|
||||
}
|
||||
|
||||
public static void setCallInProgress(Context context, int type, Recipient recipient) {
|
||||
NotificationManager notificationManager = (NotificationManager)context
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
|
||||
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_call_secure_white_24dp)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setContentTitle(recipient.getName());
|
||||
|
||||
if (type == TYPE_INCOMING_RINGING) {
|
||||
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call));
|
||||
builder.addAction(getNotificationAction(context, RedPhone.DENY_ACTION, R.drawable.ic_close_grey600_32dp, R.string.NotificationBarManager__deny_call));
|
||||
builder.addAction(getNotificationAction(context, RedPhone.ANSWER_ACTION, R.drawable.ic_phone_grey600_32dp, R.string.NotificationBarManager__answer_call));
|
||||
} else if (type == TYPE_OUTGOING_RINGING) {
|
||||
builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call));
|
||||
builder.addAction(getNotificationAction(context, RedPhone.END_CALL_ACTION, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__cancel_call));
|
||||
} else {
|
||||
builder.setContentText(context.getString(R.string.NotificationBarManager_signal_call_in_progress));
|
||||
builder.addAction(getNotificationAction(context, RedPhone.END_CALL_ACTION, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__end_call));
|
||||
}
|
||||
|
||||
notificationManager.notify(RED_PHONE_NOTIFICATION, builder.build());
|
||||
}
|
||||
|
||||
private static NotificationCompat.Action getNotificationAction(Context context, String action, int iconResId, int titleResId) {
|
||||
Intent intent = new Intent(context, WebRtcCallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.setAction(action);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
|
||||
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
|
||||
}
|
||||
}
|
@ -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()))
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
52
src/org/thoughtcrime/securesms/events/WebRtcCallEvent.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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()) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
|
1039
src/org/thoughtcrime/securesms/service/WebRtcCallService.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
39
src/org/thoughtcrime/securesms/util/VerifySpan.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,86 @@
|
||||
package org.thoughtcrime.securesms.webrtc;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
/**
|
||||
* Manages the state of the WebRtc items in the Android notification bar.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class CallNotificationManager {
|
||||
|
||||
public static final int WEBRTC_NOTIFICATION = 313388;
|
||||
|
||||
public static final int TYPE_INCOMING_RINGING = 1;
|
||||
public static final int TYPE_OUTGOING_RINGING = 2;
|
||||
public static final int TYPE_ESTABLISHED = 3;
|
||||
|
||||
public static void setCallEnded(Context context) {
|
||||
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
|
||||
notificationManager.cancel(WEBRTC_NOTIFICATION);
|
||||
}
|
||||
|
||||
public static void setCallInProgress(Context context, int type, Recipient recipient) {
|
||||
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
|
||||
|
||||
Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
|
||||
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_call_secure_white_24dp)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setContentTitle(recipient.getName());
|
||||
|
||||
if (type == TYPE_INCOMING_RINGING) {
|
||||
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call));
|
||||
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_DENY_CALL, R.drawable.ic_close_grey600_32dp, R.string.NotificationBarManager__deny_call));
|
||||
builder.addAction(getActivityNotificationAction(context, WebRtcCallActivity.ANSWER_ACTION, R.drawable.ic_phone_grey600_32dp, R.string.NotificationBarManager__answer_call));
|
||||
} else if (type == TYPE_OUTGOING_RINGING) {
|
||||
builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call));
|
||||
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_LOCAL_HANGUP, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__cancel_call));
|
||||
} else {
|
||||
builder.setContentText(context.getString(R.string.NotificationBarManager_signal_call_in_progress));
|
||||
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_LOCAL_HANGUP, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__end_call));
|
||||
}
|
||||
|
||||
notificationManager.notify(WEBRTC_NOTIFICATION, builder.build());
|
||||
}
|
||||
|
||||
private static NotificationCompat.Action getServiceNotificationAction(Context context, String action, int iconResId, int titleResId) {
|
||||
Intent intent = new Intent(context, WebRtcCallService.class);
|
||||
intent.setAction(action);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
|
||||
|
||||
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
|
||||
}
|
||||
|
||||
private static NotificationCompat.Action getActivityNotificationAction(@NonNull Context context, @NonNull String action,
|
||||
@DrawableRes int iconResId, @StringRes int titleResId)
|
||||
{
|
||||
Intent intent = new Intent(context, WebRtcCallActivity.class);
|
||||
intent.setAction(action);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
|
||||
|
||||
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.webrtc;
|
||||
|
||||
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
|
||||
public class PeerConnectionFactoryOptions extends PeerConnectionFactory.Options {
|
||||
|
||||
public PeerConnectionFactoryOptions() {
|
||||
this.networkIgnoreMask = 1 << 4;
|
||||
}
|
||||
}
|
314
src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
2248
src/org/thoughtcrime/securesms/webrtc/WebRtcDataProtos.java
Normal file
125
src/org/thoughtcrime/securesms/webrtc/audio/OutgoingRinger.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|