Added ability to share contacts.
The "contact" option in the attachments tray now brings you through an optimized contact sharing flow, allowing you to select specific fields to share. The contact is then presented as a special message type, allowing you to interact with the card to add the contact to your system contacts, invite them to signal, initiate a signal message, etc.
|
@ -403,6 +403,14 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".contactshare.ContactShareEditActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".contactshare.SharedContactDetailsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
|
|
18
build.gradle
|
@ -64,6 +64,7 @@ dependencies {
|
|||
compile 'com.android.support:gridlayout-v7:27.0.2'
|
||||
compile 'com.android.support:multidex:1.0.2'
|
||||
compile "com.android.support:exifinterface:27.0.2"
|
||||
compile "android.arch.lifecycle:extensions:1.1.1"
|
||||
|
||||
compile 'com.google.android.gms:play-services-gcm:9.6.1'
|
||||
compile 'com.google.android.gms:play-services-maps:9.6.1'
|
||||
|
@ -75,7 +76,7 @@ dependencies {
|
|||
compile('org.whispersystems:libpastelog:1.1.2') {
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||
}
|
||||
compile 'org.whispersystems:signal-service-android:2.7.5'
|
||||
compile 'org.whispersystems:signal-service-android:2.7.6'
|
||||
compile 'org.whispersystems:webrtc-android:M64'
|
||||
|
||||
compile "me.leolin:ShortcutBadger:1.1.16"
|
||||
|
@ -158,13 +159,14 @@ dependencyVerification {
|
|||
'com.android.support:gridlayout-v7:227b5fdffa20f53bd562503aab6d2293d52cf64b5a6ab1116d2150f87bff9e88',
|
||||
'com.android.support:multidex:7cd48755c7cfdb6dd2d21cbb02236ec390f6ac91cde87eb62f475b259ab5301d',
|
||||
'com.android.support:exifinterface:0e7cd526c4468895cd8549def46b3d33c8bcfb1ae4830569898d8c7326b15bb2',
|
||||
'android.arch.lifecycle:extensions:429426b2feec2245ffc5e75b3b5309bedb36159cf06dc71843ae43526ac289b6',
|
||||
'com.google.android.gms:play-services-gcm:312e61253a236f2d9b750b9c04fc92fd190d23b0b2755c99de6ce4a28b259dae',
|
||||
'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b',
|
||||
'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e',
|
||||
'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718',
|
||||
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
|
||||
'org.whispersystems:libpastelog:fe56b4db9ec743c8b565e3e4caa9228fafe132dc0bf82000d6e359b97a81177c',
|
||||
'org.whispersystems:signal-service-android:e0a3d55b21c1db483818ed459c500eba96dfb839e70d95dca4d8d4c1a7cd816b',
|
||||
'org.whispersystems:signal-service-android:823eed29e64fb0aa30d2078cb5ec0245e2a0713a4028121329c5c28788ef27f8',
|
||||
'org.whispersystems:webrtc-android:ed297e8b795dad9658cf306c2aa0f7d296c65f0997a2ac4353fd0157910acc12',
|
||||
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
|
||||
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
|
||||
|
@ -200,17 +202,23 @@ dependencyVerification {
|
|||
'com.android.support:support-media-compat:6dd9327ee9aa467cab479aad97df375072b2b6ba61eadffdaa5a88de3843c457',
|
||||
'com.android.support:support-vector-drawable:bf4f4fcbf58b1380616581224e6487c230bfdb3434ec353d4adaa4b1f4865cfa',
|
||||
'com.android.support:support-compat:ed4d25d91a0b13d8b9def1c0de69ed03d7fb89d50fb37eb0e9b63b0cf7a42357',
|
||||
'android.arch.lifecycle:livedata:50ab0490c1ff1a7cfb4e554032998b080888946d0dd424f39900efc4a1bcd750',
|
||||
'android.arch.lifecycle:livedata-core:d6fdd8b985d6178d7ea2f16986a24e83f1bee936b74d43167c69e08d3cc12c50',
|
||||
'android.arch.core:runtime:c3215aa5873311b3f88a6f4e4a3c25ad89971bc127de8c3e1291c57f93a05c39',
|
||||
'android.arch.lifecycle:runtime:c4e4be66c1b2f0abec593571454e1de14013f7e0f96bf2a9f212931a48cae550',
|
||||
'android.arch.core:common:3a616a32f433e9e23f556b38575c31b013613d3ae85206263b7625fe1f4c151a',
|
||||
'android.arch.lifecycle:common:8d378e88ebd5189e09eef623414812c868fd90aa519d6160e2311fb8b81cff56',
|
||||
'android.arch.lifecycle:viewmodel:7de29cfaba77d6b5d5be234c57f6812d0150d087e63941af22ba1d1f8e2bc96a',
|
||||
'com.github.bumptech.glide:gifdecoder:59ccf3bb0cec11dab4b857382cbe0b171111b6fc62bf141adce4e1180889af15',
|
||||
'com.android.support:support-annotations:af05330d997eb92a066534dbe0a3ea24347d26d7001221092113ae02a8f233da',
|
||||
'org.whispersystems:signal-protocol-android:5b8acded7f2a40178eb90ab8e8cbfec89d170d91b3ff5e78487d1098df6185a1',
|
||||
'org.whispersystems:signal-service-java:7b4c34e3a346a236caebd5b81fb2985ed3c91a9974a8a8ddd36b6e1b8ae9350a',
|
||||
'org.whispersystems:signal-service-java:6169643c65dcba8c784744006fc3afd9b6f309041b310a33a624121e3577433a',
|
||||
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
|
||||
'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512',
|
||||
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
|
||||
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
|
||||
'com.klinkerapps:logger:177e325259a8b111ad6745ec10db5861723c99f402222b80629f576f49408541',
|
||||
'com.google.android:flexbox:a9989fd13ae2ee42765dfc515fe362edf4f326e74925d02a10369df8092a4935',
|
||||
'android.arch.lifecycle:runtime:d0b36278878c82b838acc4308595bec61a3b5f6e7f2acc34172d7e071b2cf26d',
|
||||
'org.whispersystems:curve25519-android:82595394422b957d4a5b5f1b27b75ba25cf6dc4db4d312418ca38cd6fff279ca',
|
||||
'org.whispersystems:signal-protocol-java:5152c2b01a25147967d6bf82e540f947901bdfa79260be3eb3e96b03f787d6b5',
|
||||
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
|
||||
|
@ -218,8 +226,6 @@ dependencyVerification {
|
|||
'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
|
||||
'com.squareup.okhttp3:okhttp:7265adbd6f028aade307f58569d814835cd02bc9beffb70c25f72c9de50d61c4',
|
||||
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
|
||||
'android.arch.lifecycle:common:ff0215b54e7cbaaa898f8fd00e265ed6ea198859e10604bc1c5e78477df48b5c',
|
||||
'android.arch.core:common:5192934cd73df32e2c15722ed7fc488dde90baaec9ae030010dd1a80fb4e74e1',
|
||||
'org.whispersystems:curve25519-java:7dd659d8822c06c3aea1a47f18fac9e5761e29cab8100030b877db445005f03e',
|
||||
'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
|
||||
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
|
||||
|
|
BIN
res/drawable-hdpi/baseline_email_white_24.png
Executable file
After Width: | Height: | Size: 268 B |
BIN
res/drawable-hdpi/message_24dp.png
Normal file
After Width: | Height: | Size: 635 B |
BIN
res/drawable-hdpi/phone_24dp.png
Normal file
After Width: | Height: | Size: 709 B |
BIN
res/drawable-hdpi/video_24dp.png
Normal file
After Width: | Height: | Size: 411 B |
BIN
res/drawable-mdpi/baseline_email_white_24.png
Executable file
After Width: | Height: | Size: 209 B |
BIN
res/drawable-mdpi/message_24dp.png
Normal file
After Width: | Height: | Size: 427 B |
BIN
res/drawable-mdpi/phone_24dp.png
Normal file
After Width: | Height: | Size: 476 B |
BIN
res/drawable-mdpi/video_24dp.png
Normal file
After Width: | Height: | Size: 258 B |
BIN
res/drawable-xhdpi/baseline_email_white_24.png
Executable file
After Width: | Height: | Size: 322 B |
BIN
res/drawable-xhdpi/message_24dp.png
Normal file
After Width: | Height: | Size: 895 B |
BIN
res/drawable-xhdpi/phone_24dp.png
Normal file
After Width: | Height: | Size: 955 B |
BIN
res/drawable-xhdpi/video_24dp.png
Normal file
After Width: | Height: | Size: 551 B |
BIN
res/drawable-xxhdpi/baseline_email_white_24.png
Executable file
After Width: | Height: | Size: 419 B |
BIN
res/drawable-xxhdpi/message_24dp.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
res/drawable-xxhdpi/phone_24dp.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
res/drawable-xxhdpi/video_24dp.png
Normal file
After Width: | Height: | Size: 801 B |
BIN
res/drawable-xxxhdpi/baseline_email_white_24.png
Executable file
After Width: | Height: | Size: 532 B |
BIN
res/drawable-xxxhdpi/message_24dp.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
res/drawable-xxxhdpi/phone_24dp.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
res/drawable-xxxhdpi/video_24dp.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
10
res/drawable/baseline_email_24.xml
Executable file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
|
||||
</vector>
|
6
res/drawable/shared_contact_view_background.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/transparent_white_dd"/>
|
||||
<stroke android:color="@color/grey_400_transparent" android:width="@dimen/quote_outline_width"/>
|
||||
<corners android:radius="@dimen/quote_corner_radius" />
|
||||
</shape>
|
20
res/layout/activity_contact_share_edit.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/contact_share_edit_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/contact_share_edit_send"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:src="@drawable/ic_send_push_white_24dp"
|
||||
android:layout_margin="@dimen/floating_action_button_margin"/>
|
||||
|
||||
</FrameLayout>
|
8
res/layout/activity_contact_share_select.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</FrameLayout>
|
159
res/layout/activity_shared_contact_details.xml
Normal file
|
@ -0,0 +1,159 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
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/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<android.support.design.widget.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/shared_contact_details_header_background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/contact_details_avatar"
|
||||
android:layout_width="125dp"
|
||||
android:layout_height="125dp"
|
||||
android:padding="8dp"
|
||||
android:transitionName="avatar"
|
||||
app:layout_collapseMode="parallax"
|
||||
app:layout_collapseParallaxMultiplier="0.7"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/contact_details_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:transitionName="name"
|
||||
android:gravity="center"
|
||||
android:textSize="20sp"
|
||||
tools:text="Peter Parker"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact_details_number"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:transitionName="number"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:gravity="center"
|
||||
tools:text="(610) 555-5555"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/contact_details_add_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_gravity="center"
|
||||
style="@style/Button.Primary"
|
||||
android:text="@string/SharedContactDetailsActivity_add_to_contacts" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:id="@+id/contact_details_invite_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_gravity="center"
|
||||
style="@style/Button.Borderless"
|
||||
android:text="@string/SharedContactDetailsActivity_invite_to_signal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contact_details_engage_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal"
|
||||
style="?attr/buttonBarStyle"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contact_details_message_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="10dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:tint="@color/signal_primary"
|
||||
android:src="@drawable/message_24dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Signal Message"
|
||||
android:textColor="@color/signal_primary"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contact_details_call_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="10dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:tint="@color/signal_primary"
|
||||
android:src="@drawable/phone_24dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Signal Call"
|
||||
android:textColor="@color/signal_primary"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="@style/TextSecure.LightActionBar.DarkText"
|
||||
android:background="@color/transparent"
|
||||
app:layout_collapseMode="pin" />
|
||||
|
||||
</android.support.design.widget.CollapsingToolbarLayout>
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/contact_details_fields"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
|
@ -84,6 +84,13 @@
|
|||
app:message_type="incoming"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/shared_contact_view_stub"
|
||||
android:layout="@layout/conversation_item_shared_contact"
|
||||
android:layout_width="@dimen/media_bubble_max_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/image_view_stub"
|
||||
android:layout="@layout/conversation_item_received_thumbnail"
|
||||
|
|
|
@ -47,6 +47,13 @@
|
|||
app:message_type="outgoing"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/shared_contact_view_stub"
|
||||
android:layout="@layout/conversation_item_shared_contact"
|
||||
android:layout_width="@dimen/media_bubble_max_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/image_view_stub"
|
||||
android:layout_width="@dimen/media_bubble_default_dimens"
|
||||
|
|
6
res/layout/conversation_item_shared_contact.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.SharedContactView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/shared_contact_view"
|
||||
android:layout_width="@dimen/media_bubble_default_dimens"
|
||||
android:layout_height="wrap_content" />
|
43
res/layout/item_editable_contact.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/editable_contact_avatar"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_margin="12dp"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/editable_contact_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="20sp"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
tools:text="Peter Parker"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:src="@color/grey_400"/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/editable_contact_fields"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
50
res/layout/item_selectable_contact_field.xml
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/contact_field_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="16dp"
|
||||
android:tint="@color/grey_600"
|
||||
tools:src="@drawable/ic_call_white_24dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/contact_field_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="(610) 867-5309" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact_field_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="Mobile"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/contact_field_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"/>
|
||||
|
||||
</LinearLayout>
|
84
res/layout/shared_contact_view.xml
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/shared_contact_view_background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="6dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/contact_avatar"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/contact_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textColor="@color/signal_primary"
|
||||
android:textSize="16sp"
|
||||
tools:text="Peter Parker"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact_number"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/grey_600"
|
||||
tools:text="(610) 555-5555"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contact_action_button_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/grey_400_transparent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contact_action_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textColor="@color/signal_primary"
|
||||
tools:text="Add to Contacts"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</merge>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="media_bubble_max_width">220dp</dimen>
|
||||
<dimen name="media_bubble_max_width">250dp</dimen>
|
||||
<dimen name="media_bubble_max_height">300dp</dimen>
|
||||
</resources>
|
5
res/values-sw480dp/dimens.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="media_bubble_max_width">350dp</dimen>
|
||||
<dimen name="media_bubble_max_height">300dp</dimen>
|
||||
</resources>
|
|
@ -131,6 +131,9 @@
|
|||
<attr name="media_overview_document_background" format="color"/>
|
||||
<attr name="media_overview_document_foreground" format="color"/>
|
||||
|
||||
<attr name="shared_contact_details_header_background" format="color"/>
|
||||
<attr name="shared_contact_details_titlebar" format="color"/>
|
||||
|
||||
<attr name="search_toolbar_background" format="color"/>
|
||||
|
||||
<attr name="contact_list_divider" format="reference"/>
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
<color name="transparent_white_60">#60ffffff</color>
|
||||
<color name="transparent_white_70">#70ffffff</color>
|
||||
<color name="transparent_white_aa">#aaffffff</color>
|
||||
<color name="transparent_white_dd">#ddffffff</color>
|
||||
|
||||
<color name="conversation_compose_divider">#32000000</color>
|
||||
|
||||
|
|
|
@ -59,4 +59,7 @@
|
|||
<dimen name="onboarding_subtitle_size">20sp</dimen>
|
||||
|
||||
<dimen name="scribble_stroke_size">3dp</dimen>
|
||||
|
||||
<dimen name="floating_action_button_margin">16dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -97,6 +97,13 @@
|
|||
<string name="ContactsDatabase_message_s">Message %s</string>
|
||||
<string name="ContactsDatabase_signal_call_s">Signal Call %s</string>
|
||||
|
||||
<!-- ContactShareEditActivity -->
|
||||
<string name="ContactShareEditActivity_type_home">Home</string>
|
||||
<string name="ContactShareEditActivity_type_mobile">Mobile</string>
|
||||
<string name="ContactShareEditActivity_type_work">Work</string>
|
||||
<string name="ContactShareEditActivity_type_missing">Other</string>
|
||||
<string name="ContactShareEditActivity_invalid_contact">Selected contact was invalid</string>
|
||||
|
||||
<!-- ConversationItem -->
|
||||
<string name="ConversationItem_error_not_delivered">Not delivered</string>
|
||||
<string name="ConversationItem_received_key_exchange_message_tap_to_process">Received key exchange message, tap to process.</string>
|
||||
|
@ -152,6 +159,8 @@
|
|||
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">Signal needs the Camera permission to take photos or video, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".</string>
|
||||
<string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Signal needs Camera permissions to take photos or video</string>
|
||||
|
||||
<string name="ConversationActivity_quoted_contact_message">%1$s %2$s</string>
|
||||
|
||||
<!-- ConversationAdapter -->
|
||||
<plurals name="ConversationAdapter_n_unread_messages">
|
||||
<item quantity="one">%d unread message</item>
|
||||
|
@ -568,6 +577,23 @@
|
|||
<string name="RingtonePreference_add_ringtone_text">Add ringtone</string>
|
||||
<string name="RingtonePreference_unable_to_add_ringtone">Unable to add custom ringtone</string>
|
||||
|
||||
<!-- SharedContactDetailsActivity -->
|
||||
<string name="SharedContactDetailsActivity_add_to_contacts">Add to Contacts</string>
|
||||
<string name="SharedContactDetailsActivity_invite_to_signal">Invite to Signal</string>
|
||||
<string name="SharedContactDetailsActivity_invite_message">Let\'s switch to Signal: %1$s</string>
|
||||
<string name="SharedContactDetailsActivity_new_contact_success">Contact added.</string>
|
||||
<string name="SharedContactDetailsActivity_new_contact_failure">Failed to retrieve the contact</string>
|
||||
<string name="SharedContactDetailsActivity_updated_contact_success">Contact updated.</string>
|
||||
<string name="SharedContactDetailsActivity_updated_contact_failure">Error while editing contact</string>
|
||||
<string name="SharedContactDetailsActivity_initialization_failure">Error while reading contact</string>
|
||||
<string name="SharedContactDetailsActivity_add_as_new_contact">Add as a new contact</string>
|
||||
<string name="SharedContactDetailsActivity_add_to_existing_contact">Add to an existing contact</string>
|
||||
|
||||
<!-- SharedContactView -->
|
||||
<string name="SharedContactView_add_to_contacts">Add to Contacts</string>
|
||||
<string name="SharedContactView_invite_to_signal">Invite to Signal</string>
|
||||
<string name="SharedContactView_message">Signal Message</string>
|
||||
|
||||
<!-- Slide -->
|
||||
<string name="Slide_image">Image</string>
|
||||
<string name="Slide_audio">Audio</string>
|
||||
|
@ -695,6 +721,8 @@
|
|||
<string name="MessageNotifier_reply">Reply</string>
|
||||
<string name="MessageNotifier_pending_signal_messages">Pending Signal messages</string>
|
||||
<string name="MessageNotifier_you_have_pending_signal_messages">You have pending Signal messages, tap to open and retrieve</string>
|
||||
<string name="MessageNotifier_contact_message">%1$s %2$s</string>
|
||||
<string name="MessageNotifier_unknown_contact_message">Contact</string>
|
||||
|
||||
<!-- MmsPreferencesFragment -->
|
||||
|
||||
|
|
|
@ -61,6 +61,12 @@
|
|||
<item name="android:textColorSecondary">@color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.LightActionBar.DarkText"
|
||||
parent="TextSecure.LightActionBar">
|
||||
<item name="android:textColorPrimary">@color/black</item>
|
||||
<item name="android:textColorSecondary">@color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.FlatLightActionBar"
|
||||
parent="@style/TextSecure.LightActionBar">
|
||||
<item name="elevation">0dp</item>
|
||||
|
@ -188,6 +194,15 @@
|
|||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Button.Primary" parent="Base.Widget.AppCompat.Button.Colored">
|
||||
<item name="colorAccent">@color/signal_primary</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="Button.Borderless" parent="Base.Widget.AppCompat.Button.Borderless">
|
||||
<item name="android:textColor">@color/signal_primary</item>
|
||||
</style>
|
||||
|
||||
<!-- RedPhone -->
|
||||
|
||||
<!-- Buttons in the main "button row" of the in-call onscreen touch UI. -->
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
<item name="media_overview_header_foreground">@color/gray50</item>
|
||||
<item name="media_overview_document_foreground">@color/gray70</item>
|
||||
<item name="media_overview_document_background">@color/white</item>
|
||||
|
||||
<item name="shared_contact_details_header_background">@color/grey_100</item>
|
||||
<item name="shared_contact_details_titlebar">@color/grey_400</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.DarkNoActionBar" parent="@style/TextSecure.DarkTheme">
|
||||
|
@ -47,6 +50,9 @@
|
|||
<item name="media_overview_header_foreground">@color/gray10</item>
|
||||
<item name="media_overview_document_foreground">@color/white</item>
|
||||
<item name="media_overview_document_background">@color/black</item>
|
||||
|
||||
<item name="shared_contact_details_header_background">@color/grey_800</item>
|
||||
<item name="shared_contact_details_titlebar">@color/grey_900</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.HighlightTheme" parent="@style/TextSecure.LightTheme">
|
||||
|
|
|
@ -2,13 +2,15 @@ package org.thoughtcrime.securesms;
|
|||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -26,5 +28,9 @@ public interface BindableConversationItem extends Unbindable {
|
|||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView);
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer;
|
|||
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener;
|
||||
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.DrawerState;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.components.identity.UntrustedSendDialog;
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedSendDialog;
|
||||
|
@ -96,6 +97,9 @@ import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
|||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
|
@ -142,12 +146,12 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
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;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
|
@ -170,6 +174,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
@ -206,16 +211,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
public static final String TIMING_EXTRA = "timing";
|
||||
public static final String LAST_SEEN_EXTRA = "last_seen";
|
||||
|
||||
private static final int PICK_GALLERY = 1;
|
||||
private static final int PICK_DOCUMENT = 2;
|
||||
private static final int PICK_AUDIO = 3;
|
||||
private static final int PICK_CONTACT_INFO = 4;
|
||||
private static final int GROUP_EDIT = 5;
|
||||
private static final int TAKE_PHOTO = 6;
|
||||
private static final int ADD_CONTACT = 7;
|
||||
private static final int PICK_LOCATION = 8;
|
||||
private static final int PICK_GIF = 9;
|
||||
private static final int SMS_DEFAULT = 10;
|
||||
private static final int PICK_GALLERY = 1;
|
||||
private static final int PICK_DOCUMENT = 2;
|
||||
private static final int PICK_AUDIO = 3;
|
||||
private static final int PICK_CONTACT = 4;
|
||||
private static final int GET_CONTACT_DETAILS = 5;
|
||||
private static final int GROUP_EDIT = 6;
|
||||
private static final int TAKE_PHOTO = 7;
|
||||
private static final int ADD_CONTACT = 8;
|
||||
private static final int PICK_LOCATION = 9;
|
||||
private static final int PICK_GIF = 10;
|
||||
private static final int SMS_DEFAULT = 11;
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
protected ComposeText composeText;
|
||||
|
@ -419,8 +425,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
case PICK_AUDIO:
|
||||
setMedia(data.getData(), MediaType.AUDIO);
|
||||
break;
|
||||
case PICK_CONTACT_INFO:
|
||||
addAttachmentContactInfo(data.getData());
|
||||
case PICK_CONTACT:
|
||||
if (isSecureText && !isSmsForced()) {
|
||||
openContactShareEditor(data.getData());
|
||||
} else {
|
||||
addAttachmentContactInfo(data.getData());
|
||||
}
|
||||
break;
|
||||
case GET_CONTACT_DETAILS:
|
||||
sendSharedContact(data.getParcelableArrayListExtra(ContactShareEditActivity.KEY_CONTACTS));
|
||||
break;
|
||||
case GROUP_EDIT:
|
||||
recipient = Recipient.from(this, data.getParcelableExtra(GroupCreateActivity.GROUP_ADDRESS_EXTRA), true);
|
||||
|
@ -770,7 +783,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
.setType(GroupContext.Type.QUIT)
|
||||
.build();
|
||||
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipient(), context, null, System.currentTimeMillis(), 0, null);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipient(), context, null, System.currentTimeMillis(), 0, null, Collections.emptyList());
|
||||
MessageSender.send(self, outgoingMessage, threadId, false, null);
|
||||
DatabaseFactory.getGroupDatabase(self).remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(self)));
|
||||
initializeEnabledCheck();
|
||||
|
@ -826,23 +839,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
if (recipient == null) return;
|
||||
|
||||
if (isSecureText) {
|
||||
Permissions.with(ConversationActivity.this)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera, recipient.toShortString()),
|
||||
R.drawable.ic_mic_white_48dp, R.drawable.ic_videocam_white_48dp)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.toShortString()))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_ADDRESS, recipient.getAddress());
|
||||
startService(intent);
|
||||
|
||||
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
|
||||
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(activityIntent);
|
||||
})
|
||||
.execute();
|
||||
CommunicationActions.startVoiceCall(this, recipient);
|
||||
} else {
|
||||
try {
|
||||
Intent dialIntent = new Intent(Intent.ACTION_DIAL,
|
||||
|
@ -1378,7 +1375,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
case AttachmentTypeSelector.ADD_SOUND:
|
||||
AttachmentManager.selectAudio(this, PICK_AUDIO); break;
|
||||
case AttachmentTypeSelector.ADD_CONTACT_INFO:
|
||||
AttachmentManager.selectContactInfo(this, PICK_CONTACT_INFO); break;
|
||||
AttachmentManager.selectContactInfo(this, PICK_CONTACT); break;
|
||||
case AttachmentTypeSelector.ADD_LOCATION:
|
||||
AttachmentManager.selectLocation(this, PICK_LOCATION); break;
|
||||
case AttachmentTypeSelector.TAKE_PHOTO:
|
||||
|
@ -1397,6 +1394,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height);
|
||||
}
|
||||
|
||||
private void openContactShareEditor(Uri contactUri) {
|
||||
long id = ContactUtil.getContactIdFromUri(contactUri);
|
||||
Intent intent = ContactShareEditActivity.getIntent(this, Collections.singletonList(id));
|
||||
startActivityForResult(intent, GET_CONTACT_DETAILS);
|
||||
}
|
||||
|
||||
private void addAttachmentContactInfo(Uri contactUri) {
|
||||
ContactAccessor contactDataList = ContactAccessor.getInstance();
|
||||
ContactData contactData = contactDataList.getContactData(this, contactUri);
|
||||
|
@ -1405,6 +1408,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
else if (contactData.numbers.size() > 1) selectContactInfo(contactData);
|
||||
}
|
||||
|
||||
private void sendSharedContact(List<Contact> contacts) {
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
boolean initiating = threadId == -1;
|
||||
|
||||
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), contacts, expiresIn, subscriptionId, initiating);
|
||||
}
|
||||
|
||||
private void selectContactInfo(ContactData contactData) {
|
||||
final CharSequence[] numbers = new CharSequence[contactData.numbers.size()];
|
||||
final CharSequence[] numberItems = new CharSequence[contactData.numbers.size()];
|
||||
|
@ -1572,6 +1583,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
return getRecipient() != null && getRecipient().isPushGroupRecipient();
|
||||
}
|
||||
|
||||
private boolean isSmsForced() {
|
||||
return sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
|
||||
}
|
||||
|
||||
protected Recipient getRecipient() {
|
||||
return this.recipient;
|
||||
}
|
||||
|
@ -1682,11 +1697,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
throws InvalidMessageException
|
||||
{
|
||||
Log.w(TAG, "Sending media message...");
|
||||
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), expiresIn, subscriptionId, initiating);
|
||||
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, final long expiresIn, final int subscriptionId, final boolean initiating) {
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, inputPanel.getQuote().orNull());
|
||||
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, List<Contact> contacts, final long expiresIn, final int subscriptionId, final boolean initiating) {
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, inputPanel.getQuote().orNull(), contacts);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = getApplicationContext();
|
||||
|
@ -1871,7 +1886,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
sendMediaMessage(forceSms, "", slideDeck, expiresIn, subscriptionId, initiating).addListener(new AssertedSuccessListener<Void>() {
|
||||
sendMediaMessage(forceSms, "", slideDeck, Collections.emptyList(), expiresIn, subscriptionId, initiating).addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void nothing) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
|
@ -2072,11 +2087,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
author = messageRecord.getIndividualRecipient();
|
||||
}
|
||||
|
||||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
messageRecord.isMms() ? ((MmsMessageRecord)messageRecord).getSlideDeck() : new SlideDeck());
|
||||
if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) {
|
||||
Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0);
|
||||
String displayName = ContactUtil.getDisplayName(contact);
|
||||
String body = getString(R.string.ConversationActivity_quoted_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, displayName);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
|
||||
if (contact.getAvatarAttachment() != null) {
|
||||
slideDeck.addSlide(MediaUtil.getSlideForAttachment(this, contact.getAvatarAttachment()));
|
||||
}
|
||||
|
||||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
author,
|
||||
body,
|
||||
slideDeck);
|
||||
} else {
|
||||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -26,6 +26,8 @@ import android.os.AsyncTask;
|
|||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
|
@ -52,6 +54,9 @@ import android.widget.Toast;
|
|||
|
||||
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
|
@ -59,6 +64,7 @@ import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
|
|||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
|
@ -66,6 +72,7 @@ import org.thoughtcrime.securesms.profiles.UnknownSenderView;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
|
@ -85,7 +92,8 @@ public class ConversationFragment extends Fragment
|
|||
{
|
||||
private static final String TAG = ConversationFragment.class.getSimpleName();
|
||||
|
||||
private static final long PARTIAL_CONVERSATION_LIMIT = 500L;
|
||||
private static final long PARTIAL_CONVERSATION_LIMIT = 500L;
|
||||
private static final int CODE_ADD_EDIT_CONTACT = 77;
|
||||
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
|
||||
|
@ -652,6 +660,60 @@ public class ConversationFragment extends Fragment
|
|||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView) {
|
||||
if (getContext() != null && getActivity() != null) {
|
||||
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), avatarTransitionView, "avatar").toBundle();
|
||||
ActivityCompat.startActivity(getActivity(), SharedContactDetailsActivity.getIntent(getContext(), contact), bundle);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddToContactsClicked(@NonNull Contact contactWithAvatar) {
|
||||
if (getContext() != null) {
|
||||
new AsyncTask<Void, Void, Intent>() {
|
||||
@Override
|
||||
protected Intent doInBackground(Void... voids) {
|
||||
return ContactUtil.buildAddToContactsIntent(getContext(), contactWithAvatar);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Intent intent) {
|
||||
startActivityForResult(intent, CODE_ADD_EDIT_CONTACT);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageSharedContactClicked(@NonNull List<Recipient> choices) {
|
||||
if (getContext() == null) return;
|
||||
|
||||
ContactUtil.selectRecipientThroughDialog(getContext(), choices, locale, recipient -> {
|
||||
CommunicationActions.startConversation(getContext(), recipient, null);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInviteSharedContactClicked(@NonNull List<Recipient> choices) {
|
||||
if (getContext() == null) return;
|
||||
|
||||
ContactUtil.selectRecipientThroughDialog(getContext(), choices, locale, recipient -> {
|
||||
CommunicationActions.composeSmsThroughDefaultApp(getContext(), recipient.getAddress(), getString(R.string.InviteActivity_lets_switch_to_signal, "https://sgnl.link/1KpeYmF"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == CODE_ADD_EDIT_CONTACT && getContext() != null) {
|
||||
ApplicationContext.getInstance(getContext().getApplicationContext())
|
||||
.getJobManager()
|
||||
.add(new DirectoryRefreshJob(getContext().getApplicationContext(), false));
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.content.ActivityNotFoundException;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
|
@ -55,7 +54,9 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
|||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.ExpirationTimerView;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.components.SharedContactView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
|
@ -127,18 +128,21 @@ public class ConversationItem extends LinearLayout
|
|||
private DeliveryStatusView deliveryStatusIndicator;
|
||||
private AlertView alertView;
|
||||
|
||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||
private @NonNull Recipient conversationRecipient;
|
||||
private @NonNull Stub<ThumbnailView> mediaThumbnailStub;
|
||||
private @NonNull Stub<AudioView> audioViewStub;
|
||||
private @NonNull Stub<DocumentView> documentViewStub;
|
||||
private @NonNull ExpirationTimerView expirationTimer;
|
||||
private @Nullable EventListener eventListener;
|
||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||
private @NonNull Recipient conversationRecipient;
|
||||
private @NonNull Stub<ThumbnailView> mediaThumbnailStub;
|
||||
private @NonNull Stub<AudioView> audioViewStub;
|
||||
private @NonNull Stub<DocumentView> documentViewStub;
|
||||
private @NonNull Stub<SharedContactView> sharedContactStub;
|
||||
private @NonNull ExpirationTimerView expirationTimer;
|
||||
private @Nullable EventListener eventListener;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
|
||||
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
|
||||
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
|
||||
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
|
||||
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
|
||||
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
|
||||
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
|
||||
|
||||
private final Context context;
|
||||
|
||||
|
@ -176,6 +180,7 @@ public class ConversationItem extends LinearLayout
|
|||
this.mediaThumbnailStub = new Stub<>(findViewById(R.id.image_view_stub));
|
||||
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
|
||||
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
|
||||
this.sharedContactStub = new Stub<>(findViewById(R.id.shared_contact_view_stub));
|
||||
this.expirationTimer = findViewById(R.id.expiration_indicator);
|
||||
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
|
@ -390,6 +395,10 @@ public class ConversationItem extends LinearLayout
|
|||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getQuote() != null;
|
||||
}
|
||||
|
||||
private boolean hasSharedContact(MessageRecord messageRecord) {
|
||||
return messageRecord.isMms() && !((MmsMessageRecord)messageRecord).getSharedContacts().isEmpty();
|
||||
}
|
||||
|
||||
private void setBodyText(MessageRecord messageRecord) {
|
||||
bodyText.setClickable(false);
|
||||
bodyText.setFocusable(false);
|
||||
|
@ -406,10 +415,23 @@ public class ConversationItem extends LinearLayout
|
|||
private void setMediaAttributes(MessageRecord messageRecord) {
|
||||
boolean showControls = !messageRecord.isFailed() && (!messageRecord.isOutgoing() || messageRecord.isPending());
|
||||
|
||||
if (hasAudio(messageRecord)) {
|
||||
if (hasSharedContact(messageRecord)) {
|
||||
sharedContactStub.get().setVisibility(VISIBLE);
|
||||
if (audioViewStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
|
||||
sharedContactStub.get().setContact(((MediaMmsMessageRecord) messageRecord).getSharedContacts().get(0), glideRequests, locale);
|
||||
sharedContactStub.get().setEventListener(sharedContactEventListener);
|
||||
sharedContactStub.get().setOnClickListener(sharedContactClickListener);
|
||||
sharedContactStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
|
||||
} else if (hasAudio(messageRecord)) {
|
||||
audioViewStub.get().setVisibility(View.VISIBLE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
|
||||
|
@ -421,6 +443,7 @@ public class ConversationItem extends LinearLayout
|
|||
documentViewStub.get().setVisibility(View.VISIBLE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
documentViewStub.get().setDocument(((MediaMmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide(), showControls);
|
||||
|
@ -433,6 +456,7 @@ public class ConversationItem extends LinearLayout
|
|||
mediaThumbnailStub.get().setVisibility(View.VISIBLE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
Slide thumbnailSlide = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide();
|
||||
|
@ -453,6 +477,7 @@ public class ConversationItem extends LinearLayout
|
|||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
}
|
||||
|
@ -652,6 +677,46 @@ public class ConversationItem extends LinearLayout
|
|||
});
|
||||
}
|
||||
|
||||
private class SharedContactEventListener implements SharedContactView.EventListener {
|
||||
@Override
|
||||
public void onAddToContactsClicked(@NonNull Contact contact) {
|
||||
if (eventListener != null && batchSelected.isEmpty()) {
|
||||
eventListener.onAddToContactsClicked(contact);
|
||||
} else {
|
||||
passthroughClickListener.onClick(sharedContactStub.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInviteClicked(@NonNull List<Recipient> choices) {
|
||||
if (eventListener != null && batchSelected.isEmpty()) {
|
||||
eventListener.onInviteSharedContactClicked(choices);
|
||||
} else {
|
||||
passthroughClickListener.onClick(sharedContactStub.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageClicked(@NonNull List<Recipient> choices) {
|
||||
if (eventListener != null && batchSelected.isEmpty()) {
|
||||
eventListener.onMessageSharedContactClicked(choices);
|
||||
} else {
|
||||
passthroughClickListener.onClick(sharedContactStub.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SharedContactClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) {
|
||||
eventListener.onSharedContactDetailsClicked(((MmsMessageRecord) messageRecord).getSharedContacts().get(0), sharedContactStub.get().getAvatarView());
|
||||
} else {
|
||||
passthroughClickListener.onClick(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AttachmentDownloadClickListener implements SlideClickListener {
|
||||
@Override
|
||||
public void onClick(View v, final Slide slide) {
|
||||
|
@ -794,5 +859,4 @@ public class ConversationItem extends LinearLayout
|
|||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package org.thoughtcrime.securesms.attachments;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class AttachmentId {
|
||||
|
||||
@JsonProperty
|
||||
private final long rowId;
|
||||
|
||||
@JsonProperty
|
||||
private final long uniqueId;
|
||||
|
||||
public AttachmentId(long rowId, long uniqueId) {
|
||||
public AttachmentId(@JsonProperty("rowId") long rowId, @JsonProperty("uniqueId") long uniqueId) {
|
||||
this.rowId = rowId;
|
||||
this.uniqueId = uniqueId;
|
||||
}
|
||||
|
|
178
src/org/thoughtcrime/securesms/components/SharedContactView.java
Normal file
|
@ -0,0 +1,178 @@
|
|||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class SharedContactView extends LinearLayout implements RecipientModifiedListener {
|
||||
|
||||
private ImageView avatarView;
|
||||
private TextView nameView;
|
||||
private TextView numberView;
|
||||
private TextView actionButtonView;
|
||||
|
||||
private Contact contact;
|
||||
private Locale locale;
|
||||
private GlideRequests glideRequests;
|
||||
private EventListener eventListener;
|
||||
|
||||
private final Map<String, Recipient> activeRecipients = new HashMap<>();
|
||||
|
||||
public SharedContactView(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public SharedContactView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public SharedContactView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public SharedContactView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
inflate(getContext(), R.layout.shared_contact_view, this);
|
||||
|
||||
avatarView = findViewById(R.id.contact_avatar);
|
||||
nameView = findViewById(R.id.contact_name);
|
||||
numberView = findViewById(R.id.contact_number);
|
||||
actionButtonView = findViewById(R.id.contact_action_button);
|
||||
}
|
||||
|
||||
public void setContact(@NonNull Contact contact, @NonNull GlideRequests glideRequests, @NonNull Locale locale) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.locale = locale;
|
||||
this.contact = contact;
|
||||
|
||||
Stream.of(activeRecipients.values()).forEach(recipient -> recipient.removeListener(this));
|
||||
this.activeRecipients.clear();
|
||||
|
||||
presentContact(contact);
|
||||
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null);
|
||||
presentActionButtons(ContactUtil.getRecipients(getContext(), contact));
|
||||
}
|
||||
|
||||
public void setEventListener(@NonNull EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public @NonNull View getAvatarView() {
|
||||
return avatarView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(Recipient recipient) {
|
||||
Util.runOnMain(() -> presentActionButtons(Collections.singletonList(recipient)));
|
||||
}
|
||||
|
||||
private void presentContact(@Nullable Contact contact) {
|
||||
if (contact != null) {
|
||||
nameView.setText(ContactUtil.getDisplayName(contact));
|
||||
numberView.setText(ContactUtil.getDisplayNumber(contact, locale));
|
||||
} else {
|
||||
nameView.setText("");
|
||||
numberView.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
private void presentAvatar(@Nullable Uri uri) {
|
||||
if (uri != null) {
|
||||
glideRequests.load(new DecryptableUri(uri))
|
||||
.fallback(R.drawable.ic_contact_picture)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.dontAnimate()
|
||||
.into(avatarView);
|
||||
} else {
|
||||
glideRequests.load(R.drawable.ic_contact_picture)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(avatarView);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentActionButtons(@NonNull List<Recipient> recipients) {
|
||||
for (Recipient recipient : recipients) {
|
||||
activeRecipients.put(recipient.getAddress().serialize(), recipient);
|
||||
}
|
||||
|
||||
List<Recipient> pushUsers = new ArrayList<>(recipients.size());
|
||||
List<Recipient> systemUsers = new ArrayList<>(recipients.size());
|
||||
|
||||
for (Recipient recipient : activeRecipients.values()) {
|
||||
recipient.addListener(this);
|
||||
|
||||
if (recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
pushUsers.add(recipient);
|
||||
} else if (recipient.isSystemContact()) {
|
||||
systemUsers.add(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pushUsers.isEmpty()) {
|
||||
actionButtonView.setText(R.string.SharedContactView_message);
|
||||
actionButtonView.setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onMessageClicked(pushUsers);
|
||||
}
|
||||
});
|
||||
} else if (!systemUsers.isEmpty()) {
|
||||
actionButtonView.setText(R.string.SharedContactView_invite_to_signal);
|
||||
actionButtonView.setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onInviteClicked(systemUsers);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButtonView.setText(R.string.SharedContactView_add_to_contacts);
|
||||
actionButtonView.setOnClickListener(v -> {
|
||||
if (eventListener != null && contact != null) {
|
||||
eventListener.onAddToContactsClicked(contact);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onInviteClicked(@NonNull List<Recipient> choices);
|
||||
void onMessageClicked(@NonNull List<Recipient> choices);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
public final class EmojiStrings {
|
||||
public static final String BUST_IN_SILHOUETTE = "\uD83D\uDC64";
|
||||
}
|
|
@ -19,6 +19,8 @@ package org.thoughtcrime.securesms.contacts;
|
|||
import android.accounts.Account;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentProviderResult;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
|
@ -226,6 +228,114 @@ public class ContactsDatabase {
|
|||
|
||||
}
|
||||
|
||||
public @Nullable Cursor getNameDetails(long contactId) {
|
||||
String[] projection = new String[] { ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
|
||||
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
|
||||
ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
|
||||
ContactsContract.CommonDataKinds.StructuredName.PREFIX,
|
||||
ContactsContract.CommonDataKinds.StructuredName.SUFFIX,
|
||||
ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME };
|
||||
String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
|
||||
String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE };
|
||||
|
||||
return context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
args,
|
||||
null);
|
||||
}
|
||||
|
||||
public @Nullable String getOrganizationName(long contactId) {
|
||||
String[] projection = new String[] { ContactsContract.CommonDataKinds.Organization.COMPANY };
|
||||
String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
|
||||
String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE };
|
||||
|
||||
try (Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
args,
|
||||
null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getString(0);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable Cursor getPhoneDetails(long contactId) {
|
||||
String[] projection = new String[] { ContactsContract.CommonDataKinds.Phone.NUMBER,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE,
|
||||
ContactsContract.CommonDataKinds.Phone.LABEL };
|
||||
String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
|
||||
String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE };
|
||||
|
||||
return context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
args,
|
||||
null);
|
||||
}
|
||||
|
||||
public @Nullable Cursor getEmailDetails(long contactId) {
|
||||
String[] projection = new String[] { ContactsContract.CommonDataKinds.Email.ADDRESS,
|
||||
ContactsContract.CommonDataKinds.Email.TYPE,
|
||||
ContactsContract.CommonDataKinds.Email.LABEL };
|
||||
String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
|
||||
String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE };
|
||||
|
||||
return context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
args,
|
||||
null);
|
||||
}
|
||||
|
||||
public @Nullable Cursor getPostalAddressDetails(long contactId) {
|
||||
String[] projection = new String[] { ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.LABEL,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.STREET,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.POBOX,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.CITY,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.REGION,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE,
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY };
|
||||
String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
|
||||
String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE };
|
||||
|
||||
return context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
args,
|
||||
null);
|
||||
}
|
||||
|
||||
public @Nullable Uri getAvatarUri(long contactId) {
|
||||
String[] projection = new String[] { ContactsContract.CommonDataKinds.Photo.PHOTO_URI };
|
||||
String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
|
||||
String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE };
|
||||
|
||||
try (Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
args,
|
||||
null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String uri = cursor.getString(0);
|
||||
if (uri != null) {
|
||||
return Uri.parse(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void addContactVoiceSupport(List<ContentProviderOperation> operations,
|
||||
@NonNull Address address, long rawContactId)
|
||||
{
|
||||
|
|
|
@ -2,6 +2,9 @@ package org.thoughtcrime.securesms.contacts.avatars;
|
|||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
|
||||
import com.bumptech.glide.load.Key;
|
||||
|
@ -13,4 +16,8 @@ public interface ContactPhoto extends Key {
|
|||
|
||||
InputStream openInputStream(Context context) throws IOException;
|
||||
|
||||
@Nullable Uri getUri(@NonNull Context context);
|
||||
|
||||
boolean isProfilePhoto();
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.contacts.avatars;
|
|||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -37,6 +39,16 @@ public class GroupRecordContactPhoto implements ContactPhoto {
|
|||
throw new IOException("Couldn't load avatar for group: " + address.toGroupString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getUri(@NonNull Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProfilePhoto() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(MessageDigest messageDigest) {
|
||||
messageDigest.update(address.serialize().getBytes());
|
||||
|
|
|
@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.contacts.avatars;
|
|||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
|
@ -27,6 +29,16 @@ public class ProfileContactPhoto implements ContactPhoto {
|
|||
return AvatarHelper.getInputStreamFor(context, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getUri(@NonNull Context context) {
|
||||
return Uri.fromFile(AvatarHelper.getAvatarFile(context, address));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProfilePhoto() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(MessageDigest messageDigest) {
|
||||
messageDigest.update(address.serialize().getBytes());
|
||||
|
|
|
@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.contacts.avatars;
|
|||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
@ -27,7 +28,17 @@ public class SystemContactPhoto implements ContactPhoto {
|
|||
@Override
|
||||
public InputStream openInputStream(Context context) throws FileNotFoundException {
|
||||
return context.getContentResolver().openInputStream(contactPhotoUri);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getUri(@NonNull Context context) {
|
||||
return contactPhotoUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProfilePhoto() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
655
src/org/thoughtcrime/securesms/contactshare/Contact.java
Normal file
|
@ -0,0 +1,655 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Contact implements Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final Name name;
|
||||
|
||||
@JsonProperty
|
||||
private final String organization;
|
||||
|
||||
@JsonProperty
|
||||
private final List<Phone> phoneNumbers;
|
||||
|
||||
@JsonProperty
|
||||
private final List<Email> emails;
|
||||
|
||||
@JsonProperty
|
||||
private final List<PostalAddress> postalAddresses;
|
||||
|
||||
@JsonProperty
|
||||
private final Avatar avatar;
|
||||
|
||||
public Contact(@JsonProperty("name") @NonNull Name name,
|
||||
@JsonProperty("organization") @Nullable String organization,
|
||||
@JsonProperty("phoneNumbers") @NonNull List<Phone> phoneNumbers,
|
||||
@JsonProperty("emails") @NonNull List<Email> emails,
|
||||
@JsonProperty("postalAddresses") @NonNull List<PostalAddress> postalAddresses,
|
||||
@JsonProperty("avatar") @Nullable Avatar avatar)
|
||||
{
|
||||
this.name = name;
|
||||
this.organization = organization;
|
||||
this.phoneNumbers = Collections.unmodifiableList(phoneNumbers);
|
||||
this.emails = Collections.unmodifiableList(emails);
|
||||
this.postalAddresses = Collections.unmodifiableList(postalAddresses);
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public Contact(@NonNull Contact contact, @Nullable Avatar avatar) {
|
||||
this(contact.getName(),
|
||||
contact.getOrganization(),
|
||||
contact.getPhoneNumbers(),
|
||||
contact.getEmails(),
|
||||
contact.getPostalAddresses(),
|
||||
avatar);
|
||||
}
|
||||
|
||||
private Contact(Parcel in) {
|
||||
this(in.readParcelable(Name.class.getClassLoader()),
|
||||
in.readString(),
|
||||
in.createTypedArrayList(Phone.CREATOR),
|
||||
in.createTypedArrayList(Email.CREATOR),
|
||||
in.createTypedArrayList(PostalAddress.CREATOR),
|
||||
in.readParcelable(Avatar.class.getClassLoader()));
|
||||
}
|
||||
|
||||
public @NonNull Name getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public @Nullable String getOrganization() {
|
||||
return organization;
|
||||
}
|
||||
|
||||
public @NonNull List<Phone> getPhoneNumbers() {
|
||||
return phoneNumbers;
|
||||
}
|
||||
|
||||
public @NonNull List<Email> getEmails() {
|
||||
return emails;
|
||||
}
|
||||
|
||||
public @NonNull List<PostalAddress> getPostalAddresses() {
|
||||
return postalAddresses;
|
||||
}
|
||||
|
||||
public @Nullable Avatar getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public @Nullable Attachment getAvatarAttachment() {
|
||||
return avatar != null ? avatar.getAttachment() : null;
|
||||
}
|
||||
|
||||
public String serialize() throws IOException {
|
||||
return JsonUtils.toJson(this);
|
||||
}
|
||||
|
||||
public static Contact deserialize(@NonNull String serialized) throws IOException {
|
||||
return JsonUtils.fromJson(serialized, Contact.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(name, flags);
|
||||
dest.writeString(organization);
|
||||
dest.writeTypedList(phoneNumbers);
|
||||
dest.writeTypedList(emails);
|
||||
dest.writeTypedList(postalAddresses);
|
||||
dest.writeParcelable(avatar, flags);
|
||||
}
|
||||
|
||||
public static final Creator<Contact> CREATOR = new Creator<Contact>() {
|
||||
@Override
|
||||
public Contact createFromParcel(Parcel in) {
|
||||
return new Contact(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Contact[] newArray(int size) {
|
||||
return new Contact[size];
|
||||
}
|
||||
};
|
||||
|
||||
public static class Name implements Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final String displayName;
|
||||
|
||||
@JsonProperty
|
||||
private final String givenName;
|
||||
|
||||
@JsonProperty
|
||||
private final String familyName;
|
||||
|
||||
@JsonProperty
|
||||
private final String prefix;
|
||||
|
||||
@JsonProperty
|
||||
private final String suffix;
|
||||
|
||||
@JsonProperty
|
||||
private final String middleName;
|
||||
|
||||
Name(@JsonProperty("displayName") @Nullable String displayName,
|
||||
@JsonProperty("givenName") @Nullable String givenName,
|
||||
@JsonProperty("familyName") @Nullable String familyName,
|
||||
@JsonProperty("prefix") @Nullable String prefix,
|
||||
@JsonProperty("suffix") @Nullable String suffix,
|
||||
@JsonProperty("middleName") @Nullable String middleName)
|
||||
{
|
||||
this.displayName = displayName;
|
||||
this.givenName = givenName;
|
||||
this.familyName = familyName;
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
this.middleName = middleName;
|
||||
}
|
||||
|
||||
private Name(Parcel in) {
|
||||
this(in.readString(), in.readString(), in.readString(), in.readString(), in.readString(), in.readString());
|
||||
}
|
||||
|
||||
public @Nullable String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public @Nullable String getGivenName() {
|
||||
return givenName;
|
||||
}
|
||||
|
||||
public @Nullable String getFamilyName() {
|
||||
return familyName;
|
||||
}
|
||||
|
||||
public @Nullable String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public @Nullable String getSuffix() {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
public @Nullable String getMiddleName() {
|
||||
return middleName;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return TextUtils.isEmpty(displayName) &&
|
||||
TextUtils.isEmpty(givenName) &&
|
||||
TextUtils.isEmpty(familyName) &&
|
||||
TextUtils.isEmpty(prefix) &&
|
||||
TextUtils.isEmpty(suffix) &&
|
||||
TextUtils.isEmpty(middleName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(displayName);
|
||||
dest.writeString(givenName);
|
||||
dest.writeString(familyName);
|
||||
dest.writeString(prefix);
|
||||
dest.writeString(suffix);
|
||||
dest.writeString(middleName);
|
||||
}
|
||||
|
||||
public static final Creator<Name> CREATOR = new Creator<Name>() {
|
||||
@Override
|
||||
public Name createFromParcel(Parcel in) {
|
||||
return new Name(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Name[] newArray(int size) {
|
||||
return new Name[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static class Phone implements Selectable, Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final String number;
|
||||
|
||||
@JsonProperty
|
||||
private final Type type;
|
||||
|
||||
@JsonProperty
|
||||
private final String label;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean selected;
|
||||
|
||||
Phone(@JsonProperty("number") @NonNull String number,
|
||||
@JsonProperty("type") @NonNull Type type,
|
||||
@JsonProperty("label") @Nullable String label)
|
||||
{
|
||||
this.number = number;
|
||||
this.type = type;
|
||||
this.label = label;
|
||||
this.selected = true;
|
||||
}
|
||||
|
||||
private Phone(Parcel in) {
|
||||
this(in.readString(), Type.valueOf(in.readString()), in.readString());
|
||||
}
|
||||
|
||||
public @NonNull String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @Nullable String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(number);
|
||||
dest.writeString(type.name());
|
||||
dest.writeString(label);
|
||||
}
|
||||
|
||||
public static final Creator<Phone> CREATOR = new Creator<Phone>() {
|
||||
@Override
|
||||
public Phone createFromParcel(Parcel in) {
|
||||
return new Phone(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Phone[] newArray(int size) {
|
||||
return new Phone[size];
|
||||
}
|
||||
};
|
||||
|
||||
public enum Type {
|
||||
HOME, MOBILE, WORK, CUSTOM
|
||||
}
|
||||
}
|
||||
|
||||
public static class Email implements Selectable, Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final String email;
|
||||
|
||||
@JsonProperty
|
||||
private final Type type;
|
||||
|
||||
@JsonProperty
|
||||
private final String label;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean selected;
|
||||
|
||||
Email(@JsonProperty("email") @NonNull String email,
|
||||
@JsonProperty("type") @NonNull Type type,
|
||||
@JsonProperty("label") @Nullable String label)
|
||||
{
|
||||
this.email = email;
|
||||
this.type = type;
|
||||
this.label = label;
|
||||
this.selected = true;
|
||||
}
|
||||
|
||||
private Email(Parcel in) {
|
||||
this(in.readString(), Type.valueOf(in.readString()), in.readString());
|
||||
}
|
||||
|
||||
public @NonNull String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @NonNull String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(email);
|
||||
dest.writeString(type.name());
|
||||
dest.writeString(label);
|
||||
}
|
||||
|
||||
public static final Creator<Email> CREATOR = new Creator<Email>() {
|
||||
@Override
|
||||
public Email createFromParcel(Parcel in) {
|
||||
return new Email(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Email[] newArray(int size) {
|
||||
return new Email[size];
|
||||
}
|
||||
};
|
||||
|
||||
public enum Type {
|
||||
HOME, MOBILE, WORK, CUSTOM
|
||||
}
|
||||
}
|
||||
|
||||
public static class PostalAddress implements Selectable, Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final Type type;
|
||||
|
||||
@JsonProperty
|
||||
private final String label;
|
||||
|
||||
@JsonProperty
|
||||
private final String street;
|
||||
|
||||
@JsonProperty
|
||||
private final String poBox;
|
||||
|
||||
@JsonProperty
|
||||
private final String neighborhood;
|
||||
|
||||
@JsonProperty
|
||||
private final String city;
|
||||
|
||||
@JsonProperty
|
||||
private final String region;
|
||||
|
||||
@JsonProperty
|
||||
private final String postalCode;
|
||||
|
||||
@JsonProperty
|
||||
private final String country;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean selected;
|
||||
|
||||
PostalAddress(@JsonProperty("type") @NonNull Type type,
|
||||
@JsonProperty("label") @Nullable String label,
|
||||
@JsonProperty("street") @Nullable String street,
|
||||
@JsonProperty("poBox") @Nullable String poBox,
|
||||
@JsonProperty("neighborhood") @Nullable String neighborhood,
|
||||
@JsonProperty("city") @Nullable String city,
|
||||
@JsonProperty("region") @Nullable String region,
|
||||
@JsonProperty("postalCode") @Nullable String postalCode,
|
||||
@JsonProperty("country") @Nullable String country)
|
||||
{
|
||||
this.type = type;
|
||||
this.label = label;
|
||||
this.street = street;
|
||||
this.poBox = poBox;
|
||||
this.neighborhood = neighborhood;
|
||||
this.city = city;
|
||||
this.region = region;
|
||||
this.postalCode = postalCode;
|
||||
this.country = country;
|
||||
this.selected = true;
|
||||
}
|
||||
|
||||
private PostalAddress(Parcel in) {
|
||||
this(Type.valueOf(in.readString()),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString());
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @Nullable String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public @Nullable String getStreet() {
|
||||
return street;
|
||||
}
|
||||
|
||||
public @Nullable String getPoBox() {
|
||||
return poBox;
|
||||
}
|
||||
|
||||
public @Nullable String getNeighborhood() {
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
public @Nullable String getCity() {
|
||||
return city;
|
||||
}
|
||||
|
||||
public @Nullable String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public @Nullable String getPostalCode() {
|
||||
return postalCode;
|
||||
}
|
||||
|
||||
public @Nullable String getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(type.name());
|
||||
dest.writeString(label);
|
||||
dest.writeString(street);
|
||||
dest.writeString(poBox);
|
||||
dest.writeString(neighborhood);
|
||||
dest.writeString(city);
|
||||
dest.writeString(region);
|
||||
dest.writeString(postalCode);
|
||||
dest.writeString(country);
|
||||
}
|
||||
|
||||
public static final Creator<PostalAddress> CREATOR = new Creator<PostalAddress>() {
|
||||
@Override
|
||||
public PostalAddress createFromParcel(Parcel in) {
|
||||
return new PostalAddress(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PostalAddress[] newArray(int size) {
|
||||
return new PostalAddress[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (!TextUtils.isEmpty(street)) {
|
||||
builder.append(street).append('\n');
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(poBox)) {
|
||||
builder.append(poBox).append('\n');
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(neighborhood)) {
|
||||
builder.append(neighborhood).append('\n');
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(city) && !TextUtils.isEmpty(region)) {
|
||||
builder.append(city).append(", ").append(region);
|
||||
} else if (!TextUtils.isEmpty(city)) {
|
||||
builder.append(city).append(' ');
|
||||
} else if (!TextUtils.isEmpty(region)) {
|
||||
builder.append(region).append(' ');
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(postalCode)) {
|
||||
builder.append(postalCode);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(country)) {
|
||||
builder.append('\n').append(country);
|
||||
}
|
||||
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
HOME, WORK, CUSTOM
|
||||
}
|
||||
}
|
||||
|
||||
public static class Avatar implements Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final AttachmentId attachmentId;
|
||||
|
||||
@JsonProperty
|
||||
private final boolean isProfile;
|
||||
|
||||
@JsonIgnore
|
||||
private final Attachment attachment;
|
||||
|
||||
public Avatar(@Nullable AttachmentId attachmentId, @Nullable Attachment attachment, boolean isProfile) {
|
||||
this.attachmentId = attachmentId;
|
||||
this.attachment = attachment;
|
||||
this.isProfile = isProfile;
|
||||
}
|
||||
|
||||
Avatar(@Nullable Uri attachmentUri, boolean isProfile) {
|
||||
this(null, attachmentFromUri(attachmentUri), isProfile);
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
private Avatar(@JsonProperty("attachmentId") @Nullable AttachmentId attachmentId, @JsonProperty("isProfile") boolean isProfile) {
|
||||
this(attachmentId, null, isProfile);
|
||||
}
|
||||
|
||||
private Avatar(Parcel in) {
|
||||
this((Uri) in.readParcelable(Uri.class.getClassLoader()), in.readByte() != 0);
|
||||
}
|
||||
|
||||
public @Nullable AttachmentId getAttachmentId() {
|
||||
return attachmentId;
|
||||
}
|
||||
|
||||
public @Nullable Attachment getAttachment() {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
public boolean isProfile() {
|
||||
return isProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Attachment attachmentFromUri(@Nullable Uri uri) {
|
||||
if (uri == null) return null;
|
||||
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(attachment != null ? attachment.getDataUri() : null, flags);
|
||||
dest.writeByte((byte) (isProfile ? 1 : 0));
|
||||
}
|
||||
|
||||
public static final Creator<Avatar> CREATOR = new Creator<Avatar>() {
|
||||
@Override
|
||||
public Avatar createFromParcel(Parcel in) {
|
||||
return new Avatar(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Avatar[] newArray(int size) {
|
||||
return new Avatar[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
class ContactFieldAdapter extends RecyclerView.Adapter<ContactFieldAdapter.ContactFieldViewHolder> {
|
||||
|
||||
private final Locale locale;
|
||||
private final boolean selectable;
|
||||
private final List<Field> fields;
|
||||
|
||||
public ContactFieldAdapter(@NonNull Locale locale, boolean selectable) {
|
||||
this.locale = locale;
|
||||
this.selectable = selectable;
|
||||
this.fields = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactFieldViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new ContactFieldViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_selectable_contact_field, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ContactFieldViewHolder holder, int position) {
|
||||
holder.bind(fields.get(position), selectable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(ContactFieldViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return fields.size();
|
||||
}
|
||||
|
||||
void setFields(@NonNull Context context,
|
||||
@NonNull List<Phone> phoneNumbers,
|
||||
@NonNull List<Email> emails,
|
||||
@NonNull List<PostalAddress> postalAddresses)
|
||||
{
|
||||
fields.clear();
|
||||
|
||||
fields.addAll(Stream.of(phoneNumbers).map(phone -> new Field(context, phone, locale)).toList());
|
||||
fields.addAll(Stream.of(emails).map(email -> new Field(context, email)).toList());
|
||||
fields.addAll(Stream.of(postalAddresses).map(address -> new Field(context, address)).toList());
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class ContactFieldViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView value;
|
||||
private final TextView label;
|
||||
private final ImageView icon;
|
||||
private final CheckBox checkBox;
|
||||
|
||||
ContactFieldViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
value = itemView.findViewById(R.id.contact_field_value);
|
||||
label = itemView.findViewById(R.id.contact_field_label);
|
||||
icon = itemView.findViewById(R.id.contact_field_icon);
|
||||
checkBox = itemView.findViewById(R.id.contact_field_checkbox);
|
||||
}
|
||||
|
||||
void bind(@NonNull Field field, boolean selectable) {
|
||||
value.setMaxLines(field.maxLines);
|
||||
value.setText(field.value);
|
||||
label.setText(field.label);
|
||||
icon.setImageResource(field.iconResId);
|
||||
|
||||
if (selectable) {
|
||||
checkBox.setVisibility(View.VISIBLE);
|
||||
checkBox.setOnCheckedChangeListener(null);
|
||||
checkBox.setChecked(field.isSelected());
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> field.setSelected(isChecked));
|
||||
} else {
|
||||
checkBox.setVisibility(View.GONE);
|
||||
checkBox.setOnCheckedChangeListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
checkBox.setOnCheckedChangeListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
static class Field {
|
||||
|
||||
final String value;
|
||||
final String label;
|
||||
final int iconResId;
|
||||
final int maxLines;
|
||||
final Selectable selectable;
|
||||
|
||||
Field(@NonNull Context context, @NonNull Phone phoneNumber, @NonNull Locale locale) {
|
||||
this.value = ContactUtil.getPrettyPhoneNumber(phoneNumber, locale);
|
||||
this.iconResId = R.drawable.ic_call_white_24dp;
|
||||
this.maxLines = 1;
|
||||
this.selectable = phoneNumber;
|
||||
|
||||
switch (phoneNumber.getType()) {
|
||||
case HOME:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_home);
|
||||
break;
|
||||
case MOBILE:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_mobile);
|
||||
break;
|
||||
case WORK:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_work);
|
||||
break;
|
||||
case CUSTOM:
|
||||
label = phoneNumber.getLabel() != null ? phoneNumber.getLabel() : "";
|
||||
break;
|
||||
default:
|
||||
label = "";
|
||||
}
|
||||
}
|
||||
|
||||
Field(@NonNull Context context, @NonNull Email email) {
|
||||
this.value = email.getEmail();
|
||||
this.iconResId = R.drawable.baseline_email_white_24;
|
||||
this.maxLines = 1;
|
||||
this.selectable = email;
|
||||
|
||||
switch (email.getType()) {
|
||||
case HOME:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_home);
|
||||
break;
|
||||
case MOBILE:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_mobile);
|
||||
break;
|
||||
case WORK:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_work);
|
||||
break;
|
||||
case CUSTOM:
|
||||
label = email.getLabel() != null ? email.getLabel() : "";
|
||||
break;
|
||||
default:
|
||||
label = "";
|
||||
}
|
||||
}
|
||||
|
||||
Field(@NonNull Context context, @NonNull PostalAddress postalAddress) {
|
||||
this.value = postalAddress.toString();
|
||||
this.iconResId = R.drawable.ic_location_on_white_24dp;
|
||||
this.maxLines = 3;
|
||||
this.selectable = postalAddress;
|
||||
|
||||
switch (postalAddress.getType()) {
|
||||
case HOME:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_home);
|
||||
break;
|
||||
case WORK:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_work);
|
||||
break;
|
||||
case CUSTOM:
|
||||
label = postalAddress.getLabel() != null ? postalAddress.getLabel() : context.getString(R.string.ContactShareEditActivity_type_missing);
|
||||
break;
|
||||
default:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_missing);
|
||||
}
|
||||
}
|
||||
|
||||
void setSelected(boolean selected) {
|
||||
selectable.setSelected(selected);
|
||||
}
|
||||
|
||||
boolean isSelected() {
|
||||
return selectable.isSelected();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactModelMapper {
|
||||
|
||||
public static SharedContact.Builder localToRemoteBuilder(@NonNull Contact contact) {
|
||||
List<SharedContact.Phone> phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size());
|
||||
List<SharedContact.Email> emails = new ArrayList<>(contact.getEmails().size());
|
||||
List<SharedContact.PostalAddress> postalAddresses = new ArrayList<>(contact.getPostalAddresses().size());
|
||||
|
||||
for (Phone phone : contact.getPhoneNumbers()) {
|
||||
phoneNumbers.add(new SharedContact.Phone.Builder().setValue(phone.getNumber())
|
||||
.setType(localToRemoteType(phone.getType()))
|
||||
.setLabel(phone.getLabel())
|
||||
.build());
|
||||
}
|
||||
|
||||
for (Email email : contact.getEmails()) {
|
||||
emails.add(new SharedContact.Email.Builder().setValue(email.getEmail())
|
||||
.setType(localToRemoteType(email.getType()))
|
||||
.setLabel(email.getLabel())
|
||||
.build());
|
||||
}
|
||||
|
||||
for (PostalAddress postalAddress : contact.getPostalAddresses()) {
|
||||
postalAddresses.add(new SharedContact.PostalAddress.Builder().setType(localToRemoteType(postalAddress.getType()))
|
||||
.setLabel(postalAddress.getLabel())
|
||||
.setStreet(postalAddress.getStreet())
|
||||
.setPobox(postalAddress.getPoBox())
|
||||
.setNeighborhood(postalAddress.getNeighborhood())
|
||||
.setCity(postalAddress.getCity())
|
||||
.setRegion(postalAddress.getRegion())
|
||||
.setPostcode(postalAddress.getPostalCode())
|
||||
.setCountry(postalAddress.getCountry())
|
||||
.build());
|
||||
}
|
||||
|
||||
SharedContact.Name name = new SharedContact.Name.Builder().setDisplay(contact.getName().getDisplayName())
|
||||
.setGiven(contact.getName().getGivenName())
|
||||
.setFamily(contact.getName().getFamilyName())
|
||||
.setPrefix(contact.getName().getPrefix())
|
||||
.setSuffix(contact.getName().getSuffix())
|
||||
.setMiddle(contact.getName().getMiddleName())
|
||||
.build();
|
||||
|
||||
return new SharedContact.Builder().setName(name)
|
||||
.withOrganization(contact.getOrganization())
|
||||
.withPhones(phoneNumbers)
|
||||
.withEmails(emails)
|
||||
.withAddresses(postalAddresses);
|
||||
}
|
||||
|
||||
public static Contact remoteToLocal(@NonNull SharedContact sharedContact) {
|
||||
Name name = new Name(sharedContact.getName().getDisplay().orNull(),
|
||||
sharedContact.getName().getGiven().orNull(),
|
||||
sharedContact.getName().getFamily().orNull(),
|
||||
sharedContact.getName().getPrefix().orNull(),
|
||||
sharedContact.getName().getSuffix().orNull(),
|
||||
sharedContact.getName().getMiddle().orNull());
|
||||
|
||||
List<Phone> phoneNumbers = new LinkedList<>();
|
||||
if (sharedContact.getPhone().isPresent()) {
|
||||
for (SharedContact.Phone phone : sharedContact.getPhone().get()) {
|
||||
phoneNumbers.add(new Phone(phone.getValue(),
|
||||
remoteToLocalType(phone.getType()),
|
||||
phone.getLabel().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
List<Email> emails = new LinkedList<>();
|
||||
if (sharedContact.getEmail().isPresent()) {
|
||||
for (SharedContact.Email email : sharedContact.getEmail().get()) {
|
||||
emails.add(new Email(email.getValue(),
|
||||
remoteToLocalType(email.getType()),
|
||||
email.getLabel().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
List<PostalAddress> postalAddresses = new LinkedList<>();
|
||||
if (sharedContact.getAddress().isPresent()) {
|
||||
for (SharedContact.PostalAddress postalAddress : sharedContact.getAddress().get()) {
|
||||
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()),
|
||||
postalAddress.getLabel().orNull(),
|
||||
postalAddress.getStreet().orNull(),
|
||||
postalAddress.getPobox().orNull(),
|
||||
postalAddress.getNeighborhood().orNull(),
|
||||
postalAddress.getCity().orNull(),
|
||||
postalAddress.getRegion().orNull(),
|
||||
postalAddress.getPostcode().orNull(),
|
||||
postalAddress.getCountry().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
Avatar avatar = null;
|
||||
if (sharedContact.getAvatar().isPresent()) {
|
||||
Attachment attachment = PointerAttachment.forPointer(Optional.of(sharedContact.getAvatar().get().getAttachment().asPointer())).get();
|
||||
boolean isProfile = sharedContact.getAvatar().get().isProfile();
|
||||
|
||||
avatar = new Avatar(null, attachment, isProfile);
|
||||
}
|
||||
|
||||
return new Contact(name, sharedContact.getOrganization().orNull(), phoneNumbers, emails, postalAddresses, avatar);
|
||||
}
|
||||
|
||||
private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Phone.Type.HOME;
|
||||
case MOBILE: return Phone.Type.MOBILE;
|
||||
case WORK: return Phone.Type.WORK;
|
||||
default: return Phone.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static Email.Type remoteToLocalType(SharedContact.Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Email.Type.HOME;
|
||||
case MOBILE: return Email.Type.MOBILE;
|
||||
case WORK: return Email.Type.WORK;
|
||||
default: return Email.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static PostalAddress.Type remoteToLocalType(SharedContact.PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return PostalAddress.Type.HOME;
|
||||
case WORK: return PostalAddress.Type.WORK;
|
||||
default: return PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.Phone.Type localToRemoteType(Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.Phone.Type.HOME;
|
||||
case MOBILE: return SharedContact.Phone.Type.MOBILE;
|
||||
case WORK: return SharedContact.Phone.Type.WORK;
|
||||
default: return SharedContact.Phone.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.Email.Type localToRemoteType(Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.Email.Type.HOME;
|
||||
case MOBILE: return SharedContact.Email.Type.MOBILE;
|
||||
case WORK: return SharedContact.Email.Type.WORK;
|
||||
default: return SharedContact.Email.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.PostalAddress.Type localToRemoteType(PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.PostalAddress.Type.HOME;
|
||||
case WORK: return SharedContact.PostalAddress.Type.WORK;
|
||||
default: return SharedContact.PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Email;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Name;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactRepository {
|
||||
|
||||
private static final String TAG = ContactRepository.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final Executor executor;
|
||||
private final ContactsDatabase contactsDatabase;
|
||||
|
||||
ContactRepository(@NonNull Context context,
|
||||
@NonNull Executor executor,
|
||||
@NonNull ContactsDatabase contactsDatabase)
|
||||
{
|
||||
this.context = context.getApplicationContext();
|
||||
this.executor = executor;
|
||||
this.contactsDatabase = contactsDatabase;
|
||||
}
|
||||
|
||||
void getContacts(@NonNull List<Long> contactIds, @NonNull ValueCallback<List<Contact>> callback) {
|
||||
executor.execute(() -> {
|
||||
List<Contact> contacts = new ArrayList<>(contactIds.size());
|
||||
for (long id : contactIds) {
|
||||
Contact contact = getContact(id);
|
||||
if (contact != null) {
|
||||
contacts.add(contact);
|
||||
}
|
||||
}
|
||||
callback.onComplete(contacts);
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable Contact getContact(long contactId) {
|
||||
Name name = getName(contactId);
|
||||
if (name == null) {
|
||||
Log.w(TAG, "Couldn't find a name associated with the provided contact ID.");
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Phone> phoneNumbers = getPhoneNumbers(contactId);
|
||||
AvatarInfo avatarInfo = getAvatarInfo(contactId, phoneNumbers);
|
||||
Avatar avatar = avatarInfo != null ? new Avatar(avatarInfo.uri, avatarInfo.isProfile) : null;
|
||||
|
||||
return new Contact(name, null, phoneNumbers, getEmails(contactId), getPostalAddresses(contactId), avatar);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable Name getName(long contactId) {
|
||||
try (Cursor cursor = contactsDatabase.getNameDetails(contactId)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String cursorDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME));
|
||||
String cursorGivenName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME));
|
||||
String cursorFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME));
|
||||
String cursorPrefix = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.PREFIX));
|
||||
String cursorSuffix = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.SUFFIX));
|
||||
String cursorMiddleName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME));
|
||||
|
||||
Name name = new Name(cursorDisplayName, cursorGivenName, cursorFamilyName, cursorPrefix, cursorSuffix, cursorMiddleName);
|
||||
if (!name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String org = contactsDatabase.getOrganizationName(contactId);
|
||||
if (!TextUtils.isEmpty(org)) {
|
||||
return new Name(org, org, null, null, null, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Phone> getPhoneNumbers(long contactId) {
|
||||
Map<String, Phone> numberMap = new HashMap<>();
|
||||
try (Cursor cursor = contactsDatabase.getPhoneDetails(contactId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String cursorNumber = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
||||
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
|
||||
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
|
||||
|
||||
String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber);
|
||||
Phone existing = numberMap.get(number);
|
||||
Phone candidate = new Phone(number, phoneTypeFromContactType(cursorType), cursorLabel);
|
||||
|
||||
if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) {
|
||||
numberMap.put(number, candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Phone> numbers = new ArrayList<>(numberMap.size());
|
||||
numbers.addAll(numberMap.values());
|
||||
return numbers;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Email> getEmails(long contactId) {
|
||||
List<Email> emails = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = contactsDatabase.getEmailDetails(contactId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String cursorEmail = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS));
|
||||
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE));
|
||||
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL));
|
||||
|
||||
emails.add(new Email(cursorEmail, emailTypeFromContactType(cursorType), cursorLabel));
|
||||
}
|
||||
}
|
||||
|
||||
return emails;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<PostalAddress> getPostalAddresses(long contactId) {
|
||||
List<PostalAddress> postalAddresses = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = contactsDatabase.getPostalAddressDetails(contactId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.TYPE));
|
||||
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.LABEL));
|
||||
String cursorStreet = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.STREET));
|
||||
String cursorPoBox = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POBOX));
|
||||
String cursorNeighborhood = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD));
|
||||
String cursorCity = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.CITY));
|
||||
String cursorRegion = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.REGION));
|
||||
String cursorPostal = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE));
|
||||
String cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY));
|
||||
|
||||
postalAddresses.add(new PostalAddress(postalAddressTypeFromContactType(cursorType),
|
||||
cursorLabel,
|
||||
cursorStreet,
|
||||
cursorPoBox,
|
||||
cursorNeighborhood,
|
||||
cursorCity,
|
||||
cursorRegion,
|
||||
cursorPostal,
|
||||
cursorCountry));
|
||||
}
|
||||
}
|
||||
|
||||
return postalAddresses;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable AvatarInfo getAvatarInfo(long contactId, List<Phone> phoneNumbers) {
|
||||
AvatarInfo systemAvatar = getSystemAvatarInfo(contactId);
|
||||
|
||||
if (systemAvatar != null) {
|
||||
return systemAvatar;
|
||||
}
|
||||
|
||||
for (Phone phoneNumber : phoneNumbers) {
|
||||
AvatarInfo recipientAvatar = getRecipientAvatarInfo(Address.fromExternal(context, phoneNumber.getNumber()));
|
||||
if (recipientAvatar != null) {
|
||||
return recipientAvatar;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable AvatarInfo getSystemAvatarInfo(long contactId) {
|
||||
Uri uri = contactsDatabase.getAvatarUri(contactId);
|
||||
if (uri != null) {
|
||||
return new AvatarInfo(uri, false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable AvatarInfo getRecipientAvatarInfo(@NonNull Address address) {
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
ContactPhoto contactPhoto = recipient.getContactPhoto();
|
||||
|
||||
if (contactPhoto != null) {
|
||||
Uri avatarUri = contactPhoto.getUri(context);
|
||||
if (avatarUri != null) {
|
||||
return new AvatarInfo(avatarUri, contactPhoto.isProfilePhoto());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Phone.Type phoneTypeFromContactType(int type) {
|
||||
switch (type) {
|
||||
case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
|
||||
return Phone.Type.HOME;
|
||||
case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
|
||||
return Phone.Type.MOBILE;
|
||||
case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
|
||||
return Phone.Type.WORK;
|
||||
}
|
||||
return Phone.Type.CUSTOM;
|
||||
}
|
||||
|
||||
private Email.Type emailTypeFromContactType(int type) {
|
||||
switch (type) {
|
||||
case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
|
||||
return Email.Type.HOME;
|
||||
case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
|
||||
return Email.Type.MOBILE;
|
||||
case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
|
||||
return Email.Type.WORK;
|
||||
}
|
||||
return Email.Type.CUSTOM;
|
||||
}
|
||||
|
||||
private PostalAddress.Type postalAddressTypeFromContactType(int type) {
|
||||
switch (type) {
|
||||
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
|
||||
return PostalAddress.Type.HOME;
|
||||
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
|
||||
return PostalAddress.Type.WORK;
|
||||
}
|
||||
return PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
|
||||
interface ValueCallback<T> {
|
||||
void onComplete(@NonNull T value);
|
||||
}
|
||||
|
||||
private static class AvatarInfo {
|
||||
|
||||
private final Uri uri;
|
||||
private final boolean isProfile;
|
||||
|
||||
private AvatarInfo(Uri uri, boolean isProfile) {
|
||||
this.uri = uri;
|
||||
this.isProfile = isProfile;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public boolean isProfile() {
|
||||
return isProfile;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.ContactShareEditViewModel.*;
|
||||
|
||||
public class ContactShareEditActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
public static final String KEY_CONTACTS = "contacts";
|
||||
private static final String KEY_CONTACT_IDS = "ids";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private ContactShareEditViewModel viewModel;
|
||||
|
||||
public static Intent getIntent(@NonNull Context context, @NonNull List<Long> contactIds) {
|
||||
ArrayList<String> serializedIds = new ArrayList<>(Stream.of(contactIds).map(String::valueOf).toList());
|
||||
|
||||
Intent intent = new Intent(context, ContactShareEditActivity.class);
|
||||
intent.putStringArrayListExtra(KEY_CONTACT_IDS, serializedIds);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState, boolean ready) {
|
||||
setContentView(R.layout.activity_contact_share_edit);
|
||||
|
||||
if (getIntent() == null) {
|
||||
throw new IllegalStateException("You must supply extras to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
List<String> serializedIds = getIntent().getStringArrayListExtra(KEY_CONTACT_IDS);
|
||||
if (serializedIds == null) {
|
||||
throw new IllegalStateException("You must supply contact ID's to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
List<Long> contactIds = Stream.of(serializedIds).map(Long::parseLong).toList();
|
||||
|
||||
View sendButton = findViewById(R.id.contact_share_edit_send);
|
||||
sendButton.setOnClickListener(v -> onSendClicked(viewModel.getFinalizedContacts()));
|
||||
|
||||
RecyclerView contactList = findViewById(R.id.contact_share_edit_list);
|
||||
contactList.setLayoutManager(new LinearLayoutManager(this));
|
||||
contactList.getLayoutManager().setAutoMeasureEnabled(true);
|
||||
|
||||
ContactShareEditAdapter contactAdapter = new ContactShareEditAdapter(GlideApp.with(this), dynamicLanguage.getCurrentLocale());
|
||||
contactList.setAdapter(contactAdapter);
|
||||
|
||||
ContactRepository contactRepository = new ContactRepository(this,
|
||||
AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
DatabaseFactory.getContactsDatabase(this));
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new Factory(contactIds, contactRepository)).get(ContactShareEditViewModel.class);
|
||||
viewModel.getContacts().observe(this, contacts -> {
|
||||
contactAdapter.setContacts(contacts);
|
||||
contactList.post(() -> contactList.scrollToPosition(0));
|
||||
});
|
||||
viewModel.getEvents().observe(this, this::presentEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void presentEvent(@Nullable Event event) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event == Event.BAD_CONTACT) {
|
||||
Toast.makeText(this, R.string.ContactShareEditActivity_invalid_contact, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void onSendClicked(List<Contact> contacts) {
|
||||
Intent intent = new Intent();
|
||||
|
||||
ArrayList<Contact> contactArrayList = new ArrayList<>(contacts.size());
|
||||
contactArrayList.addAll(contacts);
|
||||
intent.putExtra(KEY_CONTACTS, contactArrayList);
|
||||
|
||||
setResult(Activity.RESULT_OK, intent);
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEditAdapter.ContactEditViewHolder> {
|
||||
|
||||
private final Locale locale;
|
||||
private final GlideRequests glideRequests;
|
||||
private final List<Contact> contacts;
|
||||
|
||||
ContactShareEditAdapter(@NonNull GlideRequests glideRequests, @NonNull Locale locale) {
|
||||
this.locale = locale;
|
||||
this.glideRequests = glideRequests;
|
||||
this.contacts = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactEditViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new ContactEditViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_editable_contact, parent, false), locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ContactEditViewHolder holder, int position) {
|
||||
holder.bind(contacts.get(position), glideRequests);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return contacts.size();
|
||||
}
|
||||
|
||||
void setContacts(@Nullable List<Contact> contacts) {
|
||||
this.contacts.clear();
|
||||
|
||||
if (contacts != null) {
|
||||
this.contacts.addAll(contacts);
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class ContactEditViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView name;
|
||||
private final ContactFieldAdapter fieldAdapter;
|
||||
|
||||
ContactEditViewHolder(View itemView, @NonNull Locale locale) {
|
||||
super(itemView);
|
||||
|
||||
this.avatar = itemView.findViewById(R.id.editable_contact_avatar);
|
||||
this.name = itemView.findViewById(R.id.editable_contact_name);
|
||||
this.fieldAdapter = new ContactFieldAdapter(locale, true);
|
||||
|
||||
RecyclerView fields = itemView.findViewById(R.id.editable_contact_fields);
|
||||
fields.setLayoutManager(new LinearLayoutManager(itemView.getContext()));
|
||||
fields.getLayoutManager().setAutoMeasureEnabled(true);
|
||||
fields.setAdapter(fieldAdapter);
|
||||
}
|
||||
|
||||
void bind(@NonNull Contact contact, @NonNull GlideRequests glideRequests) {
|
||||
Context context = itemView.getContext();
|
||||
|
||||
if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getDataUri() != null) {
|
||||
glideRequests.load(contact.getAvatarAttachment().getDataUri())
|
||||
.fallback(R.drawable.ic_contact_picture)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(avatar);
|
||||
} else {
|
||||
glideRequests.load(R.drawable.ic_contact_picture)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(avatar);
|
||||
}
|
||||
|
||||
name.setText(ContactUtil.getDisplayName(contact));
|
||||
fieldAdapter.setFields(context,contact.getPhoneNumbers(), contact.getEmails(), contact.getPostalAddresses());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class ContactShareEditViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<List<Contact>> contacts;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final ContactRepository repo;
|
||||
|
||||
ContactShareEditViewModel(@NonNull List<Long> contactIds,
|
||||
@NonNull ContactRepository contactRepository)
|
||||
{
|
||||
contacts = new MutableLiveData<>();
|
||||
events = new SingleLiveEvent<>();
|
||||
repo = contactRepository;
|
||||
|
||||
repo.getContacts(contactIds, retrieved -> {
|
||||
if (retrieved.isEmpty()) {
|
||||
events.postValue(Event.BAD_CONTACT);
|
||||
} else {
|
||||
contacts.postValue(retrieved);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<Contact>> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
@NonNull List<Contact> getFinalizedContacts() {
|
||||
List<Contact> currentContacts = getCurrentContacts();
|
||||
List<Contact> trimmedContacts = new ArrayList<>(currentContacts.size());
|
||||
|
||||
for (Contact contact : currentContacts) {
|
||||
Contact trimmed = new Contact(contact.getName(),
|
||||
contact.getOrganization(),
|
||||
trimSelectables(contact.getPhoneNumbers()),
|
||||
trimSelectables(contact.getEmails()),
|
||||
trimSelectables(contact.getPostalAddresses()),
|
||||
contact.getAvatar());
|
||||
trimmedContacts.add(trimmed);
|
||||
}
|
||||
|
||||
return trimmedContacts;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
private <E extends Selectable> List<E> trimSelectables(List<E> selectables) {
|
||||
return Stream.of(selectables).filter(Selectable::isSelected).toList();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<Contact> getCurrentContacts() {
|
||||
List<Contact> currentContacts = contacts.getValue();
|
||||
return currentContacts != null ? currentContacts : new ArrayList<>();
|
||||
}
|
||||
|
||||
enum Event {
|
||||
BAD_CONTACT
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final List<Long> contactIds;
|
||||
private final ContactRepository contactRepository;
|
||||
|
||||
Factory(@NonNull List<Long> contactIds, @NonNull ContactRepository contactRepository) {
|
||||
this.contactIds = contactIds;
|
||||
this.contactRepository = contactRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return modelClass.cast(new ContactShareEditViewModel(contactIds, contactRepository));
|
||||
}
|
||||
}
|
||||
}
|
218
src/org/thoughtcrime/securesms/contactshare/ContactUtil.java
Normal file
|
@ -0,0 +1,218 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.annotation.MainThread;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Email;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class ContactUtil {
|
||||
|
||||
private static final String TAG = ContactUtil.class.getSimpleName();
|
||||
|
||||
public static long getContactIdFromUri(@NonNull Uri uri) {
|
||||
try {
|
||||
return Long.parseLong(uri.getLastPathSegment());
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull String getDisplayName(@Nullable Contact contact) {
|
||||
if (contact == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(contact.getName().getDisplayName())) {
|
||||
return contact.getName().getDisplayName();
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(contact.getOrganization())) {
|
||||
return contact.getOrganization();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static @NonNull String getDisplayNumber(@NonNull Contact contact, @NonNull Locale locale) {
|
||||
Phone displayNumber = getPrimaryNumber(contact);
|
||||
|
||||
if (displayNumber != null) {
|
||||
return ContactUtil.getPrettyPhoneNumber(displayNumber, locale);
|
||||
} else if (contact.getEmails().size() > 0) {
|
||||
return contact.getEmails().get(0).getEmail();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Phone getPrimaryNumber(@NonNull Contact contact) {
|
||||
if (contact.getPhoneNumbers().size() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Phone> mobileNumbers = Stream.of(contact.getPhoneNumbers()).filter(number -> number.getType() == Phone.Type.MOBILE).toList();
|
||||
if (mobileNumbers.size() > 0) {
|
||||
return mobileNumbers.get(0);
|
||||
}
|
||||
|
||||
return contact.getPhoneNumbers().get(0);
|
||||
}
|
||||
|
||||
public static @NonNull String getPrettyPhoneNumber(@NonNull Phone phoneNumber, @NonNull Locale fallbackLocale) {
|
||||
return getPrettyPhoneNumber(phoneNumber.getNumber(), fallbackLocale);
|
||||
}
|
||||
|
||||
private static @NonNull String getPrettyPhoneNumber(@NonNull String phoneNumber, @NonNull Locale fallbackLocale) {
|
||||
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||
try {
|
||||
PhoneNumber parsed = util.parse(phoneNumber, fallbackLocale.getISO3Country());
|
||||
return util.format(parsed, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL);
|
||||
} catch (NumberParseException e) {
|
||||
return phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull String getNormalizedPhoneNumber(@NonNull Context context, @NonNull String number) {
|
||||
Address address = Address.fromExternal(context, number);
|
||||
return address.serialize();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public static void selectRecipientThroughDialog(@NonNull Context context, @NonNull List<Recipient> choices, @NonNull Locale locale, @NonNull RecipientSelectedCallback callback) {
|
||||
if (choices.size() > 1) {
|
||||
CharSequence[] values = new CharSequence[choices.size()];
|
||||
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = getPrettyPhoneNumber(choices.get(i).getAddress().toPhoneString(), locale);
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setItems(values, ((dialog, which) -> callback.onSelected(choices.get(which))))
|
||||
.create()
|
||||
.show();
|
||||
} else {
|
||||
callback.onSelected(choices.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Recipient> getRecipients(@NonNull Context context, @NonNull Contact contact) {
|
||||
return Stream.of(contact.getPhoneNumbers()).map(phone -> Recipient.from(context, Address.fromExternal(context, phone.getNumber()), true)).toList();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull Intent buildAddToContactsIntent(@NonNull Context context, @NonNull Contact contact) {
|
||||
Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
|
||||
if (!TextUtils.isEmpty(contact.getOrganization())) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.COMPANY, contact.getOrganization());
|
||||
}
|
||||
|
||||
if (contact.getPhoneNumbers().size() > 0) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE, contact.getPhoneNumbers().get(0).getNumber());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE, getSystemType(contact.getPhoneNumbers().get(0).getType()));
|
||||
}
|
||||
|
||||
if (contact.getPhoneNumbers().size() > 1) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.SECONDARY_PHONE, contact.getPhoneNumbers().get(1).getNumber());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE, getSystemType(contact.getPhoneNumbers().get(1).getType()));
|
||||
}
|
||||
|
||||
if (contact.getPhoneNumbers().size() > 2) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.TERTIARY_PHONE, contact.getPhoneNumbers().get(2).getNumber());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE, getSystemType(contact.getPhoneNumbers().get(2).getType()));
|
||||
}
|
||||
|
||||
if (contact.getEmails().size() > 0) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.EMAIL, contact.getEmails().get(0).getEmail());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.EMAIL_TYPE, getSystemType(contact.getEmails().get(0).getType()));
|
||||
}
|
||||
|
||||
if (contact.getEmails().size() > 1) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.SECONDARY_EMAIL, contact.getEmails().get(1).getEmail());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE, getSystemType(contact.getEmails().get(1).getType()));
|
||||
}
|
||||
|
||||
if (contact.getEmails().size() > 2) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.TERTIARY_EMAIL, contact.getEmails().get(2).getEmail());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE, getSystemType(contact.getEmails().get(2).getType()));
|
||||
}
|
||||
|
||||
if (contact.getPostalAddresses().size() > 0) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.POSTAL, contact.getPostalAddresses().get(0).toString());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, getSystemType(contact.getPostalAddresses().get(0).getType()));
|
||||
}
|
||||
|
||||
if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getDataUri() != null) {
|
||||
try {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
|
||||
values.put(ContactsContract.CommonDataKinds.Photo.PHOTO, Util.readFully(PartAuthority.getAttachmentStream(context, contact.getAvatarAttachment().getDataUri())));
|
||||
|
||||
ArrayList<ContentValues> valuesArray = new ArrayList<>(1);
|
||||
valuesArray.add(values);
|
||||
|
||||
intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, valuesArray);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read avatar into a byte array.", e);
|
||||
}
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
private static int getSystemType(Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return ContactsContract.CommonDataKinds.Phone.TYPE_HOME;
|
||||
case MOBILE: return ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE;
|
||||
case WORK: return ContactsContract.CommonDataKinds.Phone.TYPE_WORK;
|
||||
default: return ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getSystemType(Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return ContactsContract.CommonDataKinds.Email.TYPE_HOME;
|
||||
case MOBILE: return ContactsContract.CommonDataKinds.Email.TYPE_MOBILE;
|
||||
case WORK: return ContactsContract.CommonDataKinds.Email.TYPE_WORK;
|
||||
default: return ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getSystemType(PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME;
|
||||
case WORK: return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK;
|
||||
default: return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
public interface RecipientSelectedCallback {
|
||||
void onSelected(@NonNull Recipient recipient);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
public interface Selectable {
|
||||
void setSelected(boolean selected);
|
||||
boolean isSelected();
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.*;
|
||||
|
||||
public class SharedContactDetailsActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener {
|
||||
|
||||
private static final int CODE_ADD_EDIT_CONTACT = 2323;
|
||||
private static final String KEY_CONTACT = "contact";
|
||||
|
||||
private ContactFieldAdapter contactFieldAdapter;
|
||||
private TextView nameView;
|
||||
private TextView numberView;
|
||||
private ImageView avatarView;
|
||||
private View addButtonView;
|
||||
private View inviteButtonView;
|
||||
private ViewGroup engageContainerView;
|
||||
private View messageButtonView;
|
||||
private View callButtonView;
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
private Contact contact;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private final Map<String, Recipient> activeRecipients = new HashMap<>();
|
||||
|
||||
public static Intent getIntent(@NonNull Context context, @NonNull Contact contact) {
|
||||
Intent intent = new Intent(context, SharedContactDetailsActivity.class);
|
||||
intent.putExtra(KEY_CONTACT, contact);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState, boolean ready) {
|
||||
setContentView(R.layout.activity_shared_contact_details);
|
||||
|
||||
if (getIntent() == null) {
|
||||
throw new IllegalStateException("You must supply arguments to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
contact = getIntent().getParcelableExtra(KEY_CONTACT);
|
||||
if (contact == null) {
|
||||
throw new IllegalStateException("You must supply a contact to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
initToolbar();
|
||||
initViews();
|
||||
|
||||
presentContact(contact);
|
||||
presentActionButtons(ContactUtil.getRecipients(this, contact));
|
||||
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setLogo(null);
|
||||
getSupportActionBar().setTitle("");
|
||||
toolbar.setNavigationOnClickListener(v -> onBackPressed());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
int[] attrs = {R.attr.shared_contact_details_titlebar};
|
||||
TypedArray array = obtainStyledAttributes(attrs);
|
||||
int color = array.getResourceId(0, android.R.color.black);
|
||||
|
||||
array.recycle();
|
||||
|
||||
getWindow().setStatusBarColor(getResources().getColor(color));
|
||||
}
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
nameView = findViewById(R.id.contact_details_name);
|
||||
numberView = findViewById(R.id.contact_details_number);
|
||||
avatarView = findViewById(R.id.contact_details_avatar);
|
||||
addButtonView = findViewById(R.id.contact_details_add_button);
|
||||
inviteButtonView = findViewById(R.id.contact_details_invite_button);
|
||||
engageContainerView = findViewById(R.id.contact_details_engage_container);
|
||||
messageButtonView = findViewById(R.id.contact_details_message_button);
|
||||
callButtonView = findViewById(R.id.contact_details_call_button);
|
||||
|
||||
contactFieldAdapter = new ContactFieldAdapter(dynamicLanguage.getCurrentLocale(), false);
|
||||
|
||||
RecyclerView list = findViewById(R.id.contact_details_fields);
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
list.setAdapter(contactFieldAdapter);
|
||||
|
||||
glideRequests = GlideApp.with(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(Recipient recipient) {
|
||||
Util.runOnMain(() -> presentActionButtons(Collections.singletonList(recipient)));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void presentContact(@Nullable Contact contact) {
|
||||
this.contact = contact;
|
||||
|
||||
if (contact != null) {
|
||||
nameView.setText(ContactUtil.getDisplayName(contact));
|
||||
numberView.setText(ContactUtil.getDisplayNumber(contact, dynamicLanguage.getCurrentLocale()));
|
||||
|
||||
addButtonView.setOnClickListener(v -> {
|
||||
new AsyncTask<Void, Void, Intent>() {
|
||||
@Override
|
||||
protected Intent doInBackground(Void... voids) {
|
||||
return ContactUtil.buildAddToContactsIntent(SharedContactDetailsActivity.this, contact);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Intent intent) {
|
||||
startActivityForResult(intent, CODE_ADD_EDIT_CONTACT);
|
||||
}
|
||||
}.execute();
|
||||
});
|
||||
|
||||
contactFieldAdapter.setFields(this, contact.getPhoneNumbers(), contact.getEmails(), contact.getPostalAddresses());
|
||||
} else {
|
||||
nameView.setText("");
|
||||
numberView.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
public void presentAvatar(@Nullable Uri uri) {
|
||||
if (uri != null) {
|
||||
glideRequests.load(new DecryptableUri(uri))
|
||||
.fallback(R.drawable.ic_contact_picture)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(avatarView);
|
||||
} else {
|
||||
glideRequests.load(R.drawable.ic_contact_picture)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(avatarView);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentActionButtons(@NonNull List<Recipient> recipients) {
|
||||
for (Recipient recipient : recipients) {
|
||||
activeRecipients.put(recipient.getAddress().serialize(), recipient);
|
||||
}
|
||||
|
||||
List<Recipient> pushUsers = new ArrayList<>(recipients.size());
|
||||
List<Recipient> systemUsers = new ArrayList<>(recipients.size());
|
||||
|
||||
for (Recipient recipient : activeRecipients.values()) {
|
||||
recipient.addListener(this);
|
||||
|
||||
if (recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
pushUsers.add(recipient);
|
||||
} else if (recipient.isSystemContact()) {
|
||||
systemUsers.add(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pushUsers.isEmpty()) {
|
||||
engageContainerView.setVisibility(View.VISIBLE);
|
||||
inviteButtonView.setVisibility(View.GONE);
|
||||
|
||||
messageButtonView.setOnClickListener(v -> {
|
||||
ContactUtil.selectRecipientThroughDialog(this, pushUsers, dynamicLanguage.getCurrentLocale(), recipient -> {
|
||||
CommunicationActions.startConversation(this, recipient, null);
|
||||
});
|
||||
});
|
||||
|
||||
callButtonView.setOnClickListener(v -> {
|
||||
ContactUtil.selectRecipientThroughDialog(this, pushUsers, dynamicLanguage.getCurrentLocale(), recipient -> CommunicationActions.startVoiceCall(this, recipient));
|
||||
});
|
||||
} else if (!systemUsers.isEmpty()) {
|
||||
inviteButtonView.setVisibility(View.VISIBLE);
|
||||
engageContainerView.setVisibility(View.GONE);
|
||||
|
||||
inviteButtonView.setOnClickListener(v -> {
|
||||
ContactUtil.selectRecipientThroughDialog(this, systemUsers, dynamicLanguage.getCurrentLocale(), recipient -> {
|
||||
CommunicationActions.composeSmsThroughDefaultApp(this, recipient.getAddress(), getString(R.string.InviteActivity_lets_switch_to_signal, "https://sgnl.link/1KpeYmF"));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
inviteButtonView.setVisibility(View.GONE);
|
||||
engageContainerView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearView() {
|
||||
nameView.setText("");
|
||||
numberView.setText("");
|
||||
inviteButtonView.setVisibility(View.GONE);
|
||||
engageContainerView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == CODE_ADD_EDIT_CONTACT && contact != null) {
|
||||
ApplicationContext.getInstance(getApplicationContext())
|
||||
.getJobManager()
|
||||
.add(new DirectoryRefreshJob(getApplicationContext(), false));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -61,8 +61,10 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
@ -285,7 +287,6 @@ public class AttachmentDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
void deleteAllAttachments() {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
@ -347,19 +348,26 @@ public class AttachmentDatabase extends Database {
|
|||
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
|
||||
}
|
||||
|
||||
void insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments, @NonNull List<Attachment> quoteAttachment)
|
||||
@NonNull Map<Attachment, AttachmentId> insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments, @NonNull List<Attachment> quoteAttachment)
|
||||
throws MmsException
|
||||
{
|
||||
Log.w(TAG, "insertParts(" + attachments.size() + ")");
|
||||
|
||||
Map<Attachment, AttachmentId> insertedAttachments = new HashMap<>();
|
||||
|
||||
for (Attachment attachment : attachments) {
|
||||
AttachmentId attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote());
|
||||
insertedAttachments.put(attachment, attachmentId);
|
||||
Log.w(TAG, "Inserted attachment at ID: " + attachmentId);
|
||||
}
|
||||
|
||||
for (Attachment attachment : quoteAttachment) {
|
||||
insertAttachment(mmsId, attachment, true);
|
||||
AttachmentId attachmentId = insertAttachment(mmsId, attachment, true);
|
||||
insertedAttachments.put(attachment, attachmentId);
|
||||
Log.w(TAG, "Inserted quoted attachment at ID: " + attachmentId);
|
||||
}
|
||||
|
||||
return insertedAttachments;
|
||||
}
|
||||
|
||||
public @NonNull Attachment updateAttachmentData(@NonNull Attachment attachment,
|
||||
|
|
|
@ -32,10 +32,15 @@ import com.google.android.mms.pdu_alt.PduHeaders;
|
|||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
|
@ -65,12 +70,16 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
private static final String TAG = MmsDatabase.class.getSimpleName();
|
||||
|
@ -93,6 +102,8 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
static final String QUOTE_BODY = "quote_body";
|
||||
static final String QUOTE_ATTACHMENT = "quote_attachment";
|
||||
|
||||
static final String SHARED_CONTACTS = "shared_contacts";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
|
||||
READ + " INTEGER DEFAULT 0, " + "m_id" + " TEXT, " + "sub" + " TEXT, " +
|
||||
|
@ -110,7 +121,8 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
||||
EXPIRE_STARTED + " INTEGER DEFAULT 0, " + NOTIFIED + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + QUOTE_ID + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_AUTHOR + " TEXT, " + QUOTE_BODY + " TEXT, " + QUOTE_ATTACHMENT + " INTEGER DEFAULT -1);";
|
||||
QUOTE_AUTHOR + " TEXT, " + QUOTE_BODY + " TEXT, " + QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " +
|
||||
SHARED_CONTACTS + " TEXT);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
|
@ -130,7 +142,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
MESSAGE_SIZE, STATUS, TRANSACTION_ID,
|
||||
BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
|
||||
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, SHARED_CONTACTS,
|
||||
"json_group_array(json_object(" +
|
||||
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
||||
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
|
||||
|
@ -549,20 +561,22 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
if (cursor != null && cursor.moveToNext()) {
|
||||
List<DatabaseAttachment> associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId);
|
||||
|
||||
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
|
||||
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
|
||||
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS));
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
|
||||
int distributionType = DatabaseFactory.getThreadDatabase(context).getDistributionType(threadId);
|
||||
List<Attachment> attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote).map(a -> (Attachment)a).toList();
|
||||
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
|
||||
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
|
||||
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS));
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
|
||||
int distributionType = DatabaseFactory.getThreadDatabase(context).getDistributionType(threadId);
|
||||
|
||||
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID));
|
||||
String quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR));
|
||||
String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY));
|
||||
List<Attachment> quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList();
|
||||
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID));
|
||||
String quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR));
|
||||
String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY));
|
||||
List<Attachment> quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList();
|
||||
List<Contact> contacts = getSharedContacts(cursor, associatedAttachments);
|
||||
Set<Attachment> contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList());
|
||||
List<Attachment> attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote).filterNot(contactAttachments::contains).map(a -> (Attachment)a).toList();
|
||||
|
||||
Recipient recipient = Recipient.from(context, Address.fromSerialized(address), false);
|
||||
QuoteModel quote = null;
|
||||
|
@ -572,12 +586,12 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
}
|
||||
|
||||
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
|
||||
return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote);
|
||||
return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote, contacts);
|
||||
} else if (Types.isExpirationTimerUpdate(outboxType)) {
|
||||
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
||||
}
|
||||
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote, contacts);
|
||||
|
||||
if (Types.isSecureType(outboxType)) {
|
||||
return new OutgoingSecureMediaMessage(message);
|
||||
|
@ -595,6 +609,44 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
}
|
||||
}
|
||||
|
||||
private List<Contact> getSharedContacts(@NonNull Cursor cursor, @NonNull List<DatabaseAttachment> attachments) {
|
||||
String serializedContacts = cursor.getString(cursor.getColumnIndexOrThrow(SHARED_CONTACTS));
|
||||
|
||||
if (TextUtils.isEmpty(serializedContacts)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<AttachmentId, DatabaseAttachment> attachmentIdMap = new HashMap<>();
|
||||
for (DatabaseAttachment attachment : attachments) {
|
||||
attachmentIdMap.put(attachment.getAttachmentId(), attachment);
|
||||
}
|
||||
|
||||
try {
|
||||
List<Contact> contacts = new LinkedList<>();
|
||||
JSONArray jsonContacts = new JSONArray(serializedContacts);
|
||||
|
||||
for (int i = 0; i < jsonContacts.length(); i++) {
|
||||
Contact contact = deserialize(jsonContacts.getJSONObject(i).toString());
|
||||
|
||||
if (contact.getAvatar() != null && contact.getAvatar().getAttachmentId() != null) {
|
||||
DatabaseAttachment attachment = attachmentIdMap.get(contact.getAvatar().getAttachmentId());
|
||||
Avatar updatedAvatar = new Avatar(contact.getAvatar().getAttachmentId(),
|
||||
attachment,
|
||||
contact.getAvatar().isProfile());
|
||||
contacts.add(new Contact(contact, updatedAvatar));
|
||||
} else {
|
||||
contacts.add(contact);
|
||||
}
|
||||
}
|
||||
|
||||
return contacts;
|
||||
} catch (JSONException | IOException e) {
|
||||
Log.w(TAG, "Failed to parse shared contacts.", e);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public long copyMessageInbox(long messageId) throws MmsException {
|
||||
try {
|
||||
OutgoingMediaMessage request = getOutgoingMessage(messageId);
|
||||
|
@ -633,6 +685,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
return insertMediaMessage(request.getBody(),
|
||||
attachments,
|
||||
new LinkedList<>(),
|
||||
request.getSharedContacts(),
|
||||
contentValues,
|
||||
null);
|
||||
} catch (NoSuchMessageException e) {
|
||||
|
@ -690,7 +743,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
return Optional.absent();
|
||||
}
|
||||
|
||||
long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, contentValues, null);
|
||||
long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), contentValues, null);
|
||||
|
||||
if (!Types.isExpirationTimerUpdate(mailbox)) {
|
||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||
|
@ -828,7 +881,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
|
||||
}
|
||||
|
||||
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, contentValues, insertListener);
|
||||
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), contentValues, insertListener);
|
||||
|
||||
if (message.getRecipient().getAddress().isGroup()) {
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().getAddress().toGroupString(), false);
|
||||
|
@ -851,6 +904,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
private long insertMediaMessage(@Nullable String body,
|
||||
@NonNull List<Attachment> attachments,
|
||||
@NonNull List<Attachment> quoteAttachments,
|
||||
@NonNull List<Contact> sharedContacts,
|
||||
@NonNull ContentValues contentValues,
|
||||
@Nullable SmsDatabase.InsertListener insertListener)
|
||||
throws MmsException
|
||||
|
@ -858,14 +912,33 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
|
||||
List<Attachment> allAttachments = new LinkedList<>();
|
||||
List<Attachment> contactAttachments = Stream.of(sharedContacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList();
|
||||
|
||||
allAttachments.addAll(attachments);
|
||||
allAttachments.addAll(contactAttachments);
|
||||
|
||||
contentValues.put(BODY, body);
|
||||
contentValues.put(PART_COUNT, attachments.size());
|
||||
contentValues.put(PART_COUNT, allAttachments.size());
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
long messageId = db.insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
partsDatabase.insertAttachmentsForMessage(messageId, attachments, quoteAttachments);
|
||||
Map<Attachment, AttachmentId> insertedAttachments = partsDatabase.insertAttachmentsForMessage(messageId, allAttachments, quoteAttachments);
|
||||
String serializedContacts = getSerializedSharedContacts(messageId, insertedAttachments, sharedContacts);
|
||||
|
||||
if (!TextUtils.isEmpty(serializedContacts)) {
|
||||
ContentValues contactValues = new ContentValues();
|
||||
contactValues.put(SHARED_CONTACTS, serializedContacts);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
int rows = database.update(TABLE_NAME, contactValues, ID + " = ?", new String[]{ String.valueOf(messageId) });
|
||||
|
||||
if (rows <= 0) {
|
||||
Log.w(TAG, "Failed to update message with shared contact data.");
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
return messageId;
|
||||
|
@ -902,6 +975,32 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
deleteThreads(singleThreadSet);
|
||||
}
|
||||
|
||||
private @Nullable String getSerializedSharedContacts(long mmsId, @NonNull Map<Attachment, AttachmentId> insertedAttachmentIds, @NonNull List<Contact> contacts) {
|
||||
if (contacts.isEmpty()) return null;
|
||||
|
||||
JSONArray sharedContactJson = new JSONArray();
|
||||
|
||||
for (Contact contact : contacts) {
|
||||
try {
|
||||
AttachmentId attachmentId = null;
|
||||
|
||||
if (contact.getAvatarAttachment() != null) {
|
||||
attachmentId = insertedAttachmentIds.get(contact.getAvatarAttachment());
|
||||
}
|
||||
|
||||
Avatar updatedAvatar = new Avatar(attachmentId,
|
||||
contact.getAvatarAttachment(),
|
||||
contact.getAvatar() != null && contact.getAvatar().isProfile());
|
||||
Contact updatedContact = new Contact(contact, updatedAvatar);
|
||||
|
||||
sharedContactJson.put(new JSONObject(updatedContact.serialize()));
|
||||
} catch (JSONException | IOException e) {
|
||||
Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e);
|
||||
}
|
||||
}
|
||||
return sharedContactJson.toString();
|
||||
}
|
||||
|
||||
private boolean isDuplicate(IncomingMediaMessage message, long threadId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
||||
|
@ -1071,7 +1170,8 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
message.getOutgoingQuote().getAuthor(),
|
||||
message.getOutgoingQuote().getText(),
|
||||
new SlideDeck(context, message.getOutgoingQuote().getAttachments())) :
|
||||
null);
|
||||
null,
|
||||
message.getSharedContacts());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1164,17 +1264,20 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
readReceiptCount = 0;
|
||||
}
|
||||
|
||||
Recipient recipient = getRecipientFor(address);
|
||||
List<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument);
|
||||
List<NetworkFailure> networkFailures = getFailures(networkDocument);
|
||||
SlideDeck slideDeck = getSlideDeck(cursor);
|
||||
Quote quote = getQuote(cursor);
|
||||
Recipient recipient = getRecipientFor(address);
|
||||
List<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument);
|
||||
List<NetworkFailure> networkFailures = getFailures(networkDocument);
|
||||
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
|
||||
List<Contact> contacts = getSharedContacts(cursor, attachments);
|
||||
Set<Attachment> contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList());
|
||||
SlideDeck slideDeck = getSlideDeck(Stream.of(attachments).filterNot(contactAttachments::contains).toList());
|
||||
Quote quote = getQuote(cursor);
|
||||
|
||||
return new MediaMmsMessageRecord(context, id, recipient, recipient,
|
||||
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount,
|
||||
threadId, body, slideDeck, partCount, box, mismatches,
|
||||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
readReceiptCount, quote);
|
||||
readReceiptCount, quote, contacts);
|
||||
}
|
||||
|
||||
private Recipient getRecipientFor(String serialized) {
|
||||
|
@ -1213,9 +1316,8 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
private SlideDeck getSlideDeck(@NonNull Cursor cursor) {
|
||||
List<DatabaseAttachment> attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
|
||||
List<? extends Attachment> messageAttachmnets = Stream.of(attachment).filterNot(Attachment::isQuote).toList();
|
||||
private SlideDeck getSlideDeck(@NonNull List<DatabaseAttachment> attachments) {
|
||||
List<? extends Attachment> messageAttachmnets = Stream.of(attachments).filterNot(Attachment::isQuote).toList();
|
||||
return new SlideDeck(context, messageAttachmnets);
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,8 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsDatabase.QUOTE_ID,
|
||||
MmsDatabase.QUOTE_AUTHOR,
|
||||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_ATTACHMENT};
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS};
|
||||
|
||||
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
|
@ -217,7 +218,8 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsDatabase.QUOTE_ID,
|
||||
MmsDatabase.QUOTE_AUTHOR,
|
||||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_ATTACHMENT};
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS};
|
||||
|
||||
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
|
@ -239,7 +241,8 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsDatabase.QUOTE_ID,
|
||||
MmsDatabase.QUOTE_AUTHOR,
|
||||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_ATTACHMENT};
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS};
|
||||
|
||||
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
|
||||
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
|
||||
|
@ -302,6 +305,7 @@ public class MmsSmsDatabase extends Database {
|
|||
mmsColumnsPresent.add(MmsDatabase.QUOTE_AUTHOR);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_BODY);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_ATTACHMENT);
|
||||
mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS);
|
||||
|
||||
Set<String> smsColumnsPresent = new HashSet<>();
|
||||
smsColumnsPresent.add(MmsSmsColumns.ID);
|
||||
|
|
|
@ -44,8 +44,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int NO_MORE_IMAGE_THUMBNAILS_VERSION = 5;
|
||||
private static final int ATTACHMENT_DIMENSIONS = 6;
|
||||
private static final int QUOTED_REPLIES = 7;
|
||||
private static final int SHARED_CONTACTS = 8;
|
||||
|
||||
private static final int DATABASE_VERSION = 7;
|
||||
private static final int DATABASE_VERSION = 8;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -177,6 +178,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
db.execSQL("ALTER TABLE part ADD COLUMN quote INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < SHARED_CONTACTS) {
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN shared_contacts TEXT");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.support.annotation.Nullable;
|
|||
import android.text.SpannableString;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
|
@ -54,11 +55,11 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> failures, int subscriptionId,
|
||||
long expiresIn, long expireStarted, int readReceiptCount,
|
||||
@Nullable Quote quote)
|
||||
@Nullable Quote quote, @Nullable List<Contact> contacts)
|
||||
{
|
||||
super(context, id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount, quote);
|
||||
subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts);
|
||||
|
||||
this.context = context.getApplicationContext();
|
||||
this.partCount = partCount;
|
||||
|
|
|
@ -5,18 +5,21 @@ import android.content.Context;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class MmsMessageRecord extends MessageRecord {
|
||||
|
||||
private final @NonNull SlideDeck slideDeck;
|
||||
private final @Nullable Quote quote;
|
||||
private final @NonNull SlideDeck slideDeck;
|
||||
private final @Nullable Quote quote;
|
||||
private final @NonNull List<Contact> contacts = new LinkedList<>();
|
||||
|
||||
MmsMessageRecord(Context context, long id, String body, Recipient conversationRecipient,
|
||||
Recipient individualRecipient, int recipientDeviceId, long dateSent,
|
||||
|
@ -24,12 +27,14 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
|||
long type, List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> networkFailures, int subscriptionId, long expiresIn,
|
||||
long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount,
|
||||
@Nullable Quote quote)
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts)
|
||||
{
|
||||
super(context, id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount);
|
||||
|
||||
this.slideDeck = slideDeck;
|
||||
this.quote = quote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,4 +65,8 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
|||
public @Nullable Quote getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
public @NonNull List<Contact> getSharedContacts() {
|
||||
return contacts;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
|||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
|
@ -55,7 +56,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
|||
super(context, id, "", conversationRecipient, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId,
|
||||
0, 0, slideDeck, readReceiptCount, null);
|
||||
0, 0, slideDeck, readReceiptCount, null, Collections.emptyList());
|
||||
|
||||
this.contentLocation = contentLocation;
|
||||
this.messageSize = messageSize;
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupC
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -114,7 +115,7 @@ public class GroupManager {
|
|||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false);
|
||||
}
|
||||
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList());
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -212,7 +213,7 @@ public class GroupMessageProcessor {
|
|||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false));
|
||||
Recipient recipient = Recipient.from(context, addres, false);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, envelope.getTimestamp(), 0, null);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, envelope.getTimestamp(), 0, null, Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.jobs;
|
|||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
|
@ -17,6 +16,8 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||
|
@ -28,7 +29,6 @@ import org.thoughtcrime.securesms.database.MessagingDatabase;
|
|||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
|
@ -58,7 +58,6 @@ import org.thoughtcrime.securesms.util.Base64;
|
|||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.jobqueue.JobParameters;
|
||||
import org.whispersystems.libsignal.DuplicateMessageException;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
@ -90,9 +89,12 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
|||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -173,13 +175,14 @@ public class PushDecryptJob extends ContextJob {
|
|||
SignalServiceContent content = cipher.decrypt(envelope);
|
||||
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent();
|
||||
|
||||
if (message.isEndSession()) handleEndSessionMessage(envelope, message, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(envelope, message, smsMessageId);
|
||||
else if (message.isExpirationUpdate()) handleExpirationUpdate(envelope, message, smsMessageId);
|
||||
else if (message.getAttachments().isPresent() || message.getQuote().isPresent()) handleMediaMessage(envelope, message, smsMessageId);
|
||||
else if (message.getBody().isPresent()) handleTextMessage(envelope, message, smsMessageId);
|
||||
if (message.isEndSession()) handleEndSessionMessage(envelope, message, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(envelope, message, smsMessageId);
|
||||
else if (message.isExpirationUpdate()) handleExpirationUpdate(envelope, message, smsMessageId);
|
||||
else if (isMediaMessage) handleMediaMessage(envelope, message, smsMessageId);
|
||||
else if (message.getBody().isPresent()) handleTextMessage(envelope, message, smsMessageId);
|
||||
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
|
||||
handleUnknownGroupMessage(envelope, message.getGroupInfo().get());
|
||||
|
@ -405,7 +408,7 @@ public class PushDecryptJob extends ContextJob {
|
|||
message.getExpiresInSeconds() * 1000L, true,
|
||||
Optional.fromNullable(envelope.getRelay()),
|
||||
Optional.absent(), message.getGroupInfo(),
|
||||
Optional.absent(), Optional.absent());
|
||||
Optional.absent(), Optional.absent(), Optional.absent());
|
||||
|
||||
|
||||
|
||||
|
@ -522,17 +525,19 @@ public class PushDecryptJob extends ContextJob {
|
|||
@NonNull Optional<Long> smsMessageId)
|
||||
throws MmsException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Recipient recipient = getMessageDestination(envelope, message);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, envelope.getSource()),
|
||||
message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000L, false,
|
||||
Optional.fromNullable(envelope.getRelay()),
|
||||
message.getBody(),
|
||||
message.getGroupInfo(),
|
||||
message.getAttachments(),
|
||||
quote);
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Recipient recipient = getMessageDestination(envelope, message);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, envelope.getSource()),
|
||||
message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000L, false,
|
||||
Optional.fromNullable(envelope.getRelay()),
|
||||
message.getBody(),
|
||||
message.getGroupInfo(),
|
||||
message.getAttachments(),
|
||||
quote,
|
||||
sharedContacts);
|
||||
|
||||
if (message.getExpiresInSeconds() != recipient.getExpireMessages()) {
|
||||
handleExpirationUpdate(envelope, message, Optional.absent());
|
||||
|
@ -580,14 +585,16 @@ public class PushDecryptJob extends ContextJob {
|
|||
private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message)
|
||||
throws MmsException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Recipient recipients = getSyncMessageDestination(message);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
|
||||
PointerAttachment.forPointers(message.getMessage().getAttachments()),
|
||||
message.getTimestamp(), -1,
|
||||
message.getMessage().getExpiresInSeconds() * 1000,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull());
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Recipient recipients = getSyncMessageDestination(message);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
|
||||
PointerAttachment.forPointers(message.getMessage().getAttachments()),
|
||||
message.getTimestamp(), -1,
|
||||
message.getMessage().getExpiresInSeconds() * 1000,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
|
||||
sharedContacts.or(Collections.emptyList()));
|
||||
|
||||
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
||||
|
||||
|
@ -674,7 +681,7 @@ public class PushDecryptJob extends ContextJob {
|
|||
long messageId;
|
||||
|
||||
if (recipient.getAddress().isGroup()) {
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT, null);
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList());
|
||||
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
|
||||
|
||||
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null);
|
||||
|
@ -877,7 +884,7 @@ public class PushDecryptJob extends ContextJob {
|
|||
attachments = ((MmsMessageRecord) message).getSlideDeck().asAttachments();
|
||||
}
|
||||
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), attachments));
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, quote.get().getText(), attachments));
|
||||
}
|
||||
|
||||
Log.w(TAG, "Didn't find matching message record...");
|
||||
|
@ -887,6 +894,18 @@ public class PushDecryptJob extends ContextJob {
|
|||
PointerAttachment.forPointers(quote.get().getAttachments())));
|
||||
}
|
||||
|
||||
private Optional<List<Contact>> getContacts(Optional<List<SharedContact>> sharedContacts) {
|
||||
if (!sharedContacts.isPresent()) return Optional.absent();
|
||||
|
||||
List<Contact> contacts = new ArrayList<>(sharedContacts.get().size());
|
||||
|
||||
for (SharedContact sharedContact : sharedContacts.get()) {
|
||||
contacts.add(ContactModelMapper.remoteToLocal(sharedContact));
|
||||
}
|
||||
|
||||
return Optional.of(contacts);
|
||||
}
|
||||
|
||||
private Optional<InsertResult> insertPlaceholder(@NonNull SignalServiceEnvelope envelope) {
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
IncomingTextMessage textMessage = new IncomingTextMessage(Address.fromExternal(context, envelope.getSource()),
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Quote;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
|
||||
|
@ -152,6 +153,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
|||
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
|
||||
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments);
|
||||
Optional<Quote> quote = getQuoteFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
|
||||
List<SignalServiceAddress> addresses;
|
||||
|
||||
|
@ -181,6 +183,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
|||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.build();
|
||||
|
||||
messageSender.sendMessage(addresses, groupMessage);
|
||||
|
|
|
@ -10,7 +10,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
|
@ -18,21 +17,18 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
|||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
@ -117,6 +113,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
|||
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments);
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(attachmentStreams)
|
||||
|
@ -124,6 +121,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
|||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.build();
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ import org.greenrobot.eventbus.EventBus;
|
|||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.TextSecureExpiredException;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
|
@ -27,9 +29,8 @@ import org.whispersystems.jobqueue.JobParameters;
|
|||
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
@ -88,27 +89,35 @@ public abstract class PushSendJob extends SendJob {
|
|||
List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
|
||||
for (final Attachment attachment : parts) {
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
|
||||
attachments.add(SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(is)
|
||||
.withContentType(attachment.getContentType())
|
||||
.withLength(attachment.getSize())
|
||||
.withFileName(attachment.getFileName())
|
||||
.withVoiceNote(attachment.isVoiceNote())
|
||||
.withWidth(attachment.getWidth())
|
||||
.withHeight(attachment.getHeight())
|
||||
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
|
||||
.build());
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Couldn't open attachment", ioe);
|
||||
SignalServiceAttachment converted = getAttachmentFor(attachment);
|
||||
if (converted != null) {
|
||||
attachments.add(converted);
|
||||
}
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
protected SignalServiceAttachment getAttachmentFor(Attachment attachment) {
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
|
||||
return SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(is)
|
||||
.withContentType(attachment.getContentType())
|
||||
.withLength(attachment.getSize())
|
||||
.withFileName(attachment.getFileName())
|
||||
.withVoiceNote(attachment.isVoiceNote())
|
||||
.withWidth(attachment.getWidth())
|
||||
.withHeight(attachment.getHeight())
|
||||
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
|
||||
.build();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Couldn't open attachment", ioe);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
|
||||
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
|
@ -158,6 +167,25 @@ public abstract class PushSendJob extends SendJob {
|
|||
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, new SignalServiceAddress(quoteAuthor.serialize()), quoteBody, quoteAttachments));
|
||||
}
|
||||
|
||||
List<SharedContact> getSharedContactsFor(OutgoingMediaMessage mediaMessage) {
|
||||
List<SharedContact> sharedContacts = new LinkedList<>();
|
||||
|
||||
for (Contact contact : mediaMessage.getSharedContacts()) {
|
||||
SharedContact.Builder builder = ContactModelMapper.localToRemoteBuilder(contact);
|
||||
SharedContact.Avatar avatar = null;
|
||||
|
||||
if (contact.getAvatar() != null && contact.getAvatar().getAttachment() != null) {
|
||||
avatar = SharedContact.Avatar.newBuilder().withAttachment(getAttachmentFor(contact.getAvatarAttachment()))
|
||||
.withProfileFlag(contact.getAvatar().isProfile())
|
||||
.build();
|
||||
}
|
||||
|
||||
builder.setAvatar(avatar);
|
||||
sharedContacts.add(builder.build());
|
||||
}
|
||||
|
||||
return sharedContacts;
|
||||
}
|
||||
|
||||
protected abstract void onPushSend() throws Exception;
|
||||
}
|
||||
|
|
|
@ -2,12 +2,14 @@ package org.thoughtcrime.securesms.mms;
|
|||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -21,9 +23,10 @@ public class IncomingMediaMessage {
|
|||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final boolean expirationUpdate;
|
||||
private final QuoteModel quote;
|
||||
private final QuoteModel quote;
|
||||
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
|
||||
public IncomingMediaMessage(Address from,
|
||||
Optional<Address> groupId,
|
||||
|
@ -56,7 +59,8 @@ public class IncomingMediaMessage {
|
|||
Optional<String> body,
|
||||
Optional<SignalServiceGroup> group,
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
Optional<QuoteModel> quote)
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<Contact>> sharedContacts)
|
||||
{
|
||||
this.push = true;
|
||||
this.from = from;
|
||||
|
@ -71,6 +75,7 @@ public class IncomingMediaMessage {
|
|||
else this.groupId = null;
|
||||
|
||||
this.attachments.addAll(PointerAttachment.forPointers(attachments));
|
||||
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
|
@ -116,4 +121,8 @@ public class IncomingMediaMessage {
|
|||
public QuoteModel getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
public List<Contact> getSharedContacts() {
|
||||
return sharedContacts;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,13 +4,14 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
|||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null);
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.support.annotation.NonNull;
|
|||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
|
@ -22,11 +23,12 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||
@NonNull List<Attachment> avatar,
|
||||
long sentTimeMillis,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote)
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts)
|
||||
throws IOException
|
||||
{
|
||||
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, quote);
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, quote, contacts);
|
||||
|
||||
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
||||
}
|
||||
|
@ -36,12 +38,13 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||
@Nullable final Attachment avatar,
|
||||
long sentTimeMillis,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote)
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
System.currentTimeMillis(),
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote);
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingMediaMessage {
|
||||
|
@ -18,11 +21,13 @@ public class OutgoingMediaMessage {
|
|||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final QuoteModel outgoingQuote;
|
||||
private final List<Contact> contacts = new LinkedList<>();
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
int subscriptionId, long expiresIn,
|
||||
int distributionType, @Nullable QuoteModel outgoingQuote)
|
||||
int distributionType, @Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.body = message;
|
||||
|
@ -32,26 +37,30 @@ public class OutgoingMediaMessage {
|
|||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.outgoingQuote = outgoingQuote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, long expiresIn, int distributionType, @Nullable QuoteModel outgoingQuote)
|
||||
public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, long expiresIn, int distributionType, @Nullable QuoteModel outgoingQuote, @NonNull List<Contact> contacts)
|
||||
{
|
||||
this(recipient,
|
||||
buildMessage(slideDeck, message),
|
||||
slideDeck.asAttachments(),
|
||||
sentTimeMillis, subscriptionId,
|
||||
expiresIn, distributionType, outgoingQuote);
|
||||
expiresIn, distributionType, outgoingQuote, contacts);
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(OutgoingMediaMessage that) {
|
||||
this.recipient = that.getRecipient();
|
||||
this.body = that.body;
|
||||
this.distributionType = that.distributionType;
|
||||
this.attachments = that.attachments;
|
||||
this.sentTimeMillis = that.sentTimeMillis;
|
||||
this.subscriptionId = that.subscriptionId;
|
||||
this.expiresIn = that.expiresIn;
|
||||
this.outgoingQuote = that.outgoingQuote;
|
||||
this.recipient = that.getRecipient();
|
||||
this.body = that.body;
|
||||
this.distributionType = that.distributionType;
|
||||
this.attachments = that.attachments;
|
||||
this.sentTimeMillis = that.sentTimeMillis;
|
||||
this.subscriptionId = that.subscriptionId;
|
||||
this.expiresIn = that.expiresIn;
|
||||
this.outgoingQuote = that.outgoingQuote;
|
||||
|
||||
this.contacts.addAll(that.contacts);
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
|
@ -98,6 +107,10 @@ public class OutgoingMediaMessage {
|
|||
return outgoingQuote;
|
||||
}
|
||||
|
||||
public @NonNull List<Contact> getSharedContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
private static String buildMessage(SlideDeck slideDeck, String message) {
|
||||
if (!TextUtils.isEmpty(message) && !TextUtils.isEmpty(slideDeck.getBody())) {
|
||||
return slideDeck.getBody() + "\n\n" + message;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -14,9 +16,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
|||
long sentTimeMillis,
|
||||
int distributionType,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote)
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote);
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts);
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.net.Uri;
|
|||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.sms.MessageSender;
|
|||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -75,7 +76,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
|
|||
|
||||
if (recipient.isGroupRecipient()) {
|
||||
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null);
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList());
|
||||
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
|
||||
} else {
|
||||
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
|
||||
|
|
|
@ -39,12 +39,16 @@ import android.util.Log;
|
|||
|
||||
import org.thoughtcrime.securesms.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
|
@ -428,6 +432,15 @@ public class MessageNotifier {
|
|||
|
||||
if (KeyCachingService.isLocked(context)) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
|
||||
} else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
|
||||
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
|
||||
String contactName = ContactUtil.getDisplayName(contact);
|
||||
|
||||
if (!TextUtils.isEmpty(contactName)) {
|
||||
body = context.getString(R.string.MessageNotifier_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, contactName);
|
||||
} else {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message));
|
||||
}
|
||||
} else if (record.isMms() && TextUtils.isEmpty(body)) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
|
||||
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -68,7 +69,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
|||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
|
||||
if (recipient.isGroupRecipient()) {
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null);
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList());
|
||||
threadId = MessageSender.send(context, reply, -1, false, null);
|
||||
} else {
|
||||
OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId);
|
||||
|
|
|
@ -90,7 +90,6 @@ public class Recipient implements RecipientModifiedListener {
|
|||
private @Nullable String profileName;
|
||||
private @Nullable String profileAvatar;
|
||||
private boolean profileSharing;
|
||||
private boolean isSystemContact;
|
||||
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
|
@ -140,7 +139,6 @@ public class Recipient implements RecipientModifiedListener {
|
|||
this.profileName = stale.profileName;
|
||||
this.profileAvatar = stale.profileAvatar;
|
||||
this.profileSharing = stale.profileSharing;
|
||||
this.isSystemContact = stale.isSystemContact;
|
||||
this.participants.clear();
|
||||
this.participants.addAll(stale.participants);
|
||||
}
|
||||
|
@ -164,7 +162,6 @@ public class Recipient implements RecipientModifiedListener {
|
|||
this.profileName = details.get().profileName;
|
||||
this.profileAvatar = details.get().profileAvatar;
|
||||
this.profileSharing = details.get().profileSharing;
|
||||
this.isSystemContact = details.get().systemContact;
|
||||
this.participants.clear();
|
||||
this.participants.addAll(details.get().participants);
|
||||
}
|
||||
|
@ -195,7 +192,6 @@ public class Recipient implements RecipientModifiedListener {
|
|||
Recipient.this.profileAvatar = result.profileAvatar;
|
||||
Recipient.this.profileSharing = result.profileSharing;
|
||||
Recipient.this.profileName = result.profileName;
|
||||
Recipient.this.isSystemContact = result.systemContact;
|
||||
|
||||
Recipient.this.participants.clear();
|
||||
Recipient.this.participants.addAll(result.participants);
|
||||
|
@ -241,7 +237,6 @@ public class Recipient implements RecipientModifiedListener {
|
|||
this.profileName = details.profileName;
|
||||
this.profileAvatar = details.profileAvatar;
|
||||
this.profileSharing = details.profileSharing;
|
||||
this.isSystemContact = details.systemContact;
|
||||
this.participants.addAll(details.participants);
|
||||
this.resolving = false;
|
||||
}
|
||||
|
@ -599,7 +594,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||
}
|
||||
|
||||
public synchronized boolean isSystemContact() {
|
||||
return isSystemContact;
|
||||
return contactUri != null;
|
||||
}
|
||||
|
||||
public synchronized Recipient resolve() {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
|
||||
public class CommunicationActions {
|
||||
|
||||
public static void startVoiceCall(@NonNull Activity activity, @NonNull Recipient recipient) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera, recipient.toShortString()),
|
||||
R.drawable.ic_mic_white_48dp,
|
||||
R.drawable.ic_videocam_white_48dp)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.toShortString()))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(activity, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_ADDRESS, recipient.getAddress());
|
||||
activity.startService(intent);
|
||||
|
||||
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
|
||||
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.startActivity(activityIntent);
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void startConversation(@NonNull Context context,
|
||||
@NonNull Recipient recipient,
|
||||
@Nullable String text)
|
||||
{
|
||||
new AsyncTask<Void, Void, Long>() {
|
||||
@Override
|
||||
protected Long doInBackground(Void... voids) {
|
||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Long threadId) {
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis());
|
||||
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, text);
|
||||
}
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
public static void composeSmsThroughDefaultApp(@NonNull Context context, @NonNull Address address, @Nullable String text) {
|
||||
Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:" + address.serialize()));
|
||||
if (text != null) {
|
||||
intent.putExtra("sms_body", text);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util;
|
|||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -17,6 +18,8 @@ public class JsonUtils {
|
|||
|
||||
static {
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
|
||||
objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(byte[] serialized, Class<T> clazz) throws IOException {
|
||||
|
|
|
@ -3,13 +3,11 @@ package org.thoughtcrime.securesms.util;
|
|||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.support.media.ExifInterface;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
|
72
src/org/thoughtcrime/securesms/util/SingleLiveEvent.java
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2017 Google Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.arch.lifecycle.LifecycleOwner;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.Observer;
|
||||
import android.support.annotation.MainThread;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
|
||||
* navigation and Snackbar messages.
|
||||
* <p>
|
||||
* This avoids a common problem with events: on configuration change (like rotation) an update
|
||||
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
|
||||
* explicit call to setValue() or call().
|
||||
* <p>
|
||||
* Note that only one observer is going to be notified of changes.
|
||||
*/
|
||||
public class SingleLiveEvent<T> extends MutableLiveData<T> {
|
||||
|
||||
private static final String TAG = SingleLiveEvent.class.getSimpleName();
|
||||
|
||||
private final AtomicBoolean mPending = new AtomicBoolean(false);
|
||||
|
||||
@MainThread
|
||||
public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer<T> observer) {
|
||||
if (hasActiveObservers()) {
|
||||
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
|
||||
}
|
||||
|
||||
// Observe the internal MutableLiveData
|
||||
super.observe(owner, t -> {
|
||||
if (mPending.compareAndSet(true, false)) {
|
||||
observer.onChanged(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setValue(@Nullable T t) {
|
||||
mPending.set(true);
|
||||
super.setValue(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for cases where T is Void, to make calls cleaner.
|
||||
*/
|
||||
@MainThread
|
||||
public void call() {
|
||||
setValue(null);
|
||||
}
|
||||
}
|