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.
This commit is contained in:
Greyson Parrelli 2018-04-26 17:03:54 -07:00
parent 17dbdbd0a9
commit 54dbffaf30
90 changed files with 3628 additions and 195 deletions

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

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

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

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

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

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

View file

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

View file

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

View 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" />

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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);
}
}

View file

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.components.emoji;
public final class EmojiStrings {
public static final String BUST_IN_SILHOUETTE = "\uD83D\uDC64";
}

View file

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

View file

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

View file

@ -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());

View file

@ -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());

View file

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

View 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];
}
};
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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);
}
}

View file

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.contactshare;
public interface Selectable {
void setSelected(boolean selected);
boolean isSelected();
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()),

View file

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

View file

@ -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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 ");

View file

@ -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();

View file

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

View file

@ -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() {

View file

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

View file

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

View file

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

View 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);
}
}