Support for disappearing messages

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2016-08-15 20:23:56 -07:00
parent f03a086191
commit d7e4928f22
86 changed files with 1635 additions and 261 deletions

View File

@ -72,10 +72,14 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
compile 'org.whispersystems:signal-service-android:2.1.1'
compile 'org.whispersystems:signal-service-android:2.2.0'
compile 'com.h6ah4i.android.compat:mulsellistprefcompat:1.0.0'
compile 'com.google.zxing:core:3.2.1'
compile ('cn.carbswang.android:NumberPickerView:1.0.9') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1'
testCompile 'org.mockito:mockito-core:1.9.5'
@ -126,27 +130,28 @@ dependencyVerification {
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
'org.whispersystems:signal-service-android:1c89623336505f6511e6f68ea126c85eae7f28f6c72beb6b362e5743bc5e5126',
'org.whispersystems:signal-service-android:96a926b0bfd1df8b66be2b574e8b8d6ef1862f715b0f1a5457a2038b28d3ad1b',
'com.h6ah4i.android.compat:mulsellistprefcompat:47167c5cb796de1a854788e9ff318358e36c8fb88123baaa6e38fb78511dfabe',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729',
'com.google.android.gms:play-services-base:ef36e50fa5c0415ed41f74dd399a889efd2fa327c449036e140c7c3786aa0e1f',
'com.android.support:support-annotations:104f353b53d5dd8d64b2f77eece4b37f6b961de9732eb6b706395e91033ec70a',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'org.whispersystems:signal-service-java:48db52056aa3510deb8c4ccd2dfb35033ae115bc4176048820c6dff73290ba6e',
'org.whispersystems:signal-protocol-android:d83cb3d15b667fc2543fa18ce80791c72c053e8ac54fc2941f0429a5944ca691',
'org.whispersystems:signal-service-java:7932363fec666fdc0b4b424eeca4bdca235f6bf2f226fb6a6ff742c49fc37087',
'com.google.android.gms:play-services-basement:e1d29b21e02fd2a63e5a31807415cbb17a59568e27e3254181c01ffae10659bf',
'org.whispersystems:curve25519-android:d6a3ef3a70622af4c728b7fe5f8fdfc9e6cd39b1d39b2c77e7a2add9d876bc23',
'org.whispersystems:signal-protocol-java:d518d52eeb3c44210e0b6c687360848a87afbaee0bdf42e2a8dd9974d54fdb3a',
'com.googlecode.libphonenumber:libphonenumber:9625de9d2270e9a280ff4e6d9ef3106573fb4828773fd32c9b7614f4e17d2811',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af',
'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
'org.whispersystems:curve25519-android:d6a3ef3a70622af4c728b7fe5f8fdfc9e6cd39b1d39b2c77e7a2add9d876bc23',
'org.whispersystems:signal-protocol-java:d518d52eeb3c44210e0b6c687360848a87afbaee0bdf42e2a8dd9974d54fdb3a',
'org.whispersystems:curve25519-java:08cc3be52723e0fc4148e5e7002d51d6d7e495b2130022237f2d47b90af6ae0b',
'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978',
'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
'org.whispersystems:curve25519-java:08cc3be52723e0fc4148e5e7002d51d6d7e495b2130022237f2d47b90af6ae0b',
'com.android.support:support-v4:c62f0d025dafa86f423f48df9185b0d89496adbc5f6a9be5a7c394d84cf91423',
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -46,7 +46,8 @@
android:layout_toRightOf="@id/contact_photo"
android:layout_marginRight="35dp"
android:background="@drawable/received_bubble"
android:orientation="vertical">
android:orientation="vertical"
tools:backgroundTint="@color/blue_900">
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/image_view"
@ -115,14 +116,28 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingTop="2dp"
android:paddingRight="4dp"
android:paddingRight="2dp"
android:paddingEnd="4dp"
android:src="?menu_lock_icon_small"
android:contentDescription="@string/conversation_item__secure_message_description"
android:visibility="gone"
android:tint="?conversation_item_received_text_secondary_color"
android:tintMode="multiply"/>
android:tintMode="multiply"
tools:visibility="visible"/>
<org.thoughtcrime.securesms.components.ExpirationTimerView
android:id="@+id/expiration_indicator"
app:empty="@drawable/ic_hourglass_empty_white_18dp"
app:full="@drawable/ic_hourglass_full_white_18dp"
app:tint="?conversation_item_received_text_secondary_color"
app:percentage="0"
app:offset="0"
android:layout_gravity="center_vertical|end"
android:alpha=".65"
android:layout_width="10dp"
android:layout_height="11dp"
android:visibility="gone"
tools:visibility="visible"/>
<org.thoughtcrime.securesms.components.DeliveryStatusView
android:id="@+id/delivery_status"
@ -141,7 +156,8 @@
android:fontFamily="sans-serif-light"
android:autoLink="none"
android:linksClickable="false"
tools:text="Now"/>
tools:text="Now"
tools:visibility="visible"/>
<TextView android:id="@+id/sim_info"
android:layout_width="wrap_content"

View File

@ -120,7 +120,7 @@
android:minWidth="15sp"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="right"
android:layout_gravity="right|bottom"
android:fontFamily="sans-serif-light"
android:textColor="?conversation_item_sent_text_secondary_color"
android:textSize="@dimen/conversation_item_date_text_size"
@ -135,7 +135,7 @@
android:minWidth="15sp"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="right"
android:layout_gravity="right|bottom"
android:fontFamily="sans-serif-light"
android:textColor="?conversation_item_sent_text_secondary_color"
android:textSize="@dimen/conversation_item_date_text_size"
@ -151,8 +151,24 @@
android:id="@+id/delivery_status"
android:layout_width="20dp"
android:layout_height="wrap_content"
android:alpha=".7"
app:iconColor="?conversation_item_sent_text_secondary_color"/>
<org.thoughtcrime.securesms.components.ExpirationTimerView
android:id="@+id/expiration_indicator"
app:empty="@drawable/ic_hourglass_empty_white_18dp"
app:full="@drawable/ic_hourglass_full_white_18dp"
app:tint="@color/black"
app:percentage="0"
app:offset="0"
android:layout_gravity="center_vertical|end"
android:alpha=".6"
android:layout_marginLeft="3dp"
android:layout_width="10dp"
android:layout_height="11dp"
android:visibility="gone"
tools:visibility="visible"/>
<ImageView android:id="@+id/secure_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -160,10 +176,10 @@
android:visibility="gone"
android:layout_gravity="center_vertical|end"
android:paddingLeft="2dp"
android:paddingBottom="3dp"
android:tint="?conversation_item_sent_text_secondary_color"
android:tintMode="multiply"
android:contentDescription="@string/conversation_item__secure_message_description" />
android:contentDescription="@string/conversation_item__secure_message_description"
tools:visibility="visible"/>
</LinearLayout>
</LinearLayout>

View File

@ -5,6 +5,8 @@
android:id="@+id/conversation_update_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:background="@drawable/conversation_item_background"
android:orientation="horizontal"
android:gravity="center"
android:padding="20dp">

View File

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout android:id="@+id/pending_indicator_stub"
android:layout_width="wrap_content"
android:paddingRight="2dp"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
tools:visibility="gone"/>
<ImageView android:id="@+id/sent_indicator"
android:layout_width="wrap_content"
@ -24,6 +26,7 @@
android:paddingLeft="2dp"
android:paddingBottom="2dp"
android:visibility="gone"
android:contentDescription="@string/conversation_item_sent__delivered_description" />
android:contentDescription="@string/conversation_item_sent__delivered_description"
tools:visibility="visible"/>
</merge>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center">
<cn.carbswang.android.numberpickerview.library.NumberPickerView
android:id="@+id/expiration_number_picker"
android:layout_alignParentTop="true"
app:npv_WrapSelectorWheel="false"
app:npv_DividerColor="#cbc8ea"
app:npv_TextColorSelected="@color/black"
app:npv_ItemPaddingVertical="20dp"
app:npv_TextColorHint="@color/grey_600"
app:npv_TextSizeNormal="16sp"
app:npv_TextSizeSelected="16sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/expiration_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/expiration_number_picker"
android:minLines="2"
android:padding="20dp"/>
</RelativeLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/actionButtonStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:gravity="center">
<ImageView
android:id="@+id/menu_badge_icon"
android:src="@drawable/ic_timer_white_24dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/expiration_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:gravity="center_horizontal|bottom"
android:paddingBottom="3dp"
android:paddingTop="1dp"
android:textColor="@android:color/white"
android:textSize="10sp" />
</FrameLayout>

View File

@ -75,6 +75,26 @@
</TableRow>
<TableRow android:id="@+id/expires_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/message_details_table_row_pad"
android:visibility="gone"
tools:visibility="visible">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Disappears"
android:textStyle="bold"/>
<TextView android:id="@+id/expires_in"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_details_table_row_pad"
tools:text="1 week"/>
</TableRow>
<TableRow android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/message_details_table_row_pad">

View File

@ -4,6 +4,6 @@
<item android:title="@string/conversation_callable_insecure__menu_call"
android:id="@+id/menu_call_insecure"
android:icon="?menu_call_icon"
app:showAsAction="ifRoom" />
app:showAsAction="always" />
</menu>

View File

@ -5,6 +5,6 @@
<item android:title="@string/conversation_callable_secure__menu_call"
android:id="@+id/menu_call_secure"
android:icon="@drawable/ic_call_secure_white_24dp"
app:showAsAction="ifRoom" />
app:showAsAction="always" />
</menu>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/conversation_expiring_off__disappearing_messages"
android:id="@+id/menu_expiring_messages_off"
android:icon="@drawable/ic_timer_off_white_24dp"/>
</menu>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_expiring_messages"
app:actionLayout="@layout/expiration_timer_menu"
app:showAsAction="always"
android:title="@string/menu_conversation_expiring_on__messages_expiring"/>
</menu>

View File

@ -238,4 +238,19 @@
<item>@null</item>
</array>
<integer-array name="expiration_times">
<item>0</item>
<item>5</item>
<item>10</item>
<item>30</item>
<item>60</item>
<item>300</item>
<item>1800</item>
<item>3600</item>
<item>21600</item>
<item>43200</item>
<item>86400</item>
<item>604800</item>
</integer-array>
</resources>

View File

@ -166,4 +166,12 @@
<attr name="camera" format="integer"/>
</declare-styleable>
<declare-styleable name="HourglassView">
<attr name="full" format="reference"/>
<attr name="empty" format="reference"/>
<attr name="tint" format="color"/>
<attr name="percentage" format="integer"/>
<attr name="offset" format="integer"/>
</declare-styleable>
</resources>

View File

@ -379,6 +379,8 @@
<string name="MessageRecord_called_s">Called %s</string>
<string name="MessageRecord_missed_call_from">Missed call from %s</string>
<string name="MessageRecord_s_is_on_signal_say_hey">%s is on Signal, say hey!</string>
<string name="MessageRecord_you">You</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set disappearing message time to %2$s</string>
<!-- PassphraseChangeActivity -->
@ -407,6 +409,11 @@
<string name="DeviceProvisioningActivity_link_a_signal_device">Link a Signal device?</string>
<string name="DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner">It looks like you\'re trying to link a Signal device using a 3rd party scanner. For your protection, please scan the code again from within Signal.</string>
<!-- ExpirationDialog -->
<string name="ExpirationDialog_disappearing_messages">Disappearing messages</string>
<string name="ExpirationDialog_your_messages_will_not_expire">Your messages will not expire.</string>
<string name="ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen">Messages sent and received in this conversation will disappear %s after they have been seen.</string>
<!-- PassphrasePromptActivity -->
<string name="PassphrasePromptActivity_enter_passphrase">Enter passphrase</string>
<string name="PassphrasePromptActivity_watermark_content_description">Signal icon</string>
@ -537,6 +544,7 @@
<string name="ThreadRecord_missed_call">Missed call</string>
<string name="ThreadRecord_media_message">Media message</string>
<string name="ThreadRecord_s_is_on_signal_say_hey">%s is on Signal, say hey!</string>
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
<!-- VerifyIdentityActivity -->
<string name="VerifyIdentityActivity_you_do_not_have_an_identity_key">You do not have an identity key.</string>
@ -711,10 +719,48 @@
<string name="device_list_fragment__no_devices_linked">No devices linked...</string>
<string name="device_list_fragment__link_new_device">Link new device</string>
<!-- experience_upgrade_activity -->
<string name="experience_upgrade_activity__continue">continue</string>
<!-- expiration -->
<string name="expiration_off">Off</string>
<plurals name="expiration_seconds">
<item quantity="one">1 second</item>
<item quantity="other">%d seconds</item>
</plurals>
<string name="expiration_seconds_abbreviated">%ds</string>
<plurals name="expiration_minutes">
<item quantity="one">1 minute</item>
<item quantity="other">%d minutes</item>
</plurals>
<string name="expiration_minutes_abbreviated">%dm</string>
<plurals name="expiration_hours">
<item quantity="one">1 hour</item>
<item quantity="other">%d hours</item>
</plurals>
<string name="expiration_hours_abbreviated">%dh</string>
<plurals name="expiration_days">
<item quantity="one">1 day</item>
<item quantity="other">%d days</item>
</plurals>
<string name="expiration_days_abbreviated">%dd</string>
<plurals name="expiration_weeks">
<item quantity="one">1 week</item>
<item quantity="other">%d weeks</item>
</plurals>
<string name="expiration_weeks_abbreviated">%dw</string>
<!-- log_submit_activity -->
<string name="log_submit_activity__log_fetch_failed">Could not read the log on your device. You can still use ADB to get a debug log instead.</string>
<string name="log_submit_activity__thanks">Thanks for your help!</string>
@ -1078,6 +1124,12 @@
<!-- conversation_context_image -->
<string name="conversation_context_image__save_attachment">Save attachment</string>
<!-- conversation_expiring_off -->
<string name="conversation_expiring_off__disappearing_messages">Disappearing messages</string>
<!-- conversation_expiring_on -->
<string name="menu_conversation_expiring_on__messages_expiring">Messages expiring</string>
<!-- conversation_insecure -->
<string name="conversation_insecure__invite">Invite</string>

View File

@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.jobs.persistence.EncryptingJobSerializer;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirementProvider;
import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirementProvider;
import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirementProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.dependencies.DependencyInjector;
@ -52,6 +53,7 @@ import dagger.ObjectGraph;
*/
public class ApplicationContext extends Application implements DependencyInjector {
private ExpiringMessageManager expiringMessageManager;
private JobManager jobManager;
private ObjectGraph objectGraph;
@ -69,6 +71,7 @@ public class ApplicationContext extends Application implements DependencyInjecto
initializeLogging();
initializeDependencyInjection();
initializeJobManager();
initializeExpiringMessageManager();
initializeGcmCheck();
initializeSignedPreKeyCheck();
}
@ -84,9 +87,12 @@ public class ApplicationContext extends Application implements DependencyInjecto
return jobManager;
}
public ExpiringMessageManager getExpiringMessageManager() {
return expiringMessageManager;
}
private void initializeDeveloperBuild() {
if (BuildConfig.DEV_BUILD) {
// LeakCanary.install(this);
StrictMode.setThreadPolicy(new ThreadPolicy.Builder().detectAll()
.penaltyLog()
.build());
@ -139,4 +145,8 @@ public class ApplicationContext extends Application implements DependencyInjecto
}
}
private void initializeExpiringMessageManager() {
this.expiringMessageManager = new ExpiringMessageManager(this);
}
}

View File

@ -15,4 +15,6 @@ public interface BindableConversationItem extends Unbindable {
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipients recipients);
MessageRecord getMessageRecord();
}

View File

@ -36,6 +36,7 @@ import android.os.Vibrator;
import android.provider.Browser;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.WindowCompat;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
@ -91,7 +92,6 @@ import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences;
@ -103,6 +103,7 @@ import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.LocationSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
@ -127,6 +128,7 @@ import org.thoughtcrime.securesms.util.DirectoryHelper.UserCapabilities;
import org.thoughtcrime.securesms.util.DirectoryHelper.UserCapabilities.Capability;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -137,7 +139,6 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
@ -200,7 +201,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private AttachmentManager attachmentManager;
private AudioRecorder audioRecorder;
private BroadcastReceiver securityUpdateReceiver;
private BroadcastReceiver groupUpdateReceiver;
private BroadcastReceiver recipientsStaleReceiver;
private EmojiDrawer emojiDrawer;
protected HidingLinearLayout quickAttachmentToggle;
private QuickAttachmentDrawer quickAttachmentDrawer;
@ -320,7 +321,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
saveDraft();
if (recipients != null) recipients.removeListener(this);
if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver);
if (groupUpdateReceiver != null) unregisterReceiver(groupUpdateReceiver);
if (recipientsStaleReceiver != null) unregisterReceiver(recipientsStaleReceiver);
super.onDestroy();
}
@ -382,6 +383,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MenuInflater inflater = this.getMenuInflater();
menu.clear();
if (isSecureText) {
if (recipients.getExpireMessages() > 0) {
inflater.inflate(R.menu.conversation_expiring_on, menu);
final MenuItem item = menu.findItem(R.id.menu_expiring_messages);
final View actionView = MenuItemCompat.getActionView(item);
final TextView badgeView = (TextView)actionView.findViewById(R.id.expiration_badge);
badgeView.setText(ExpirationUtil.getExpirationAbbreviatedDisplayValue(this, recipients.getExpireMessages()));
actionView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onOptionsItemSelected(item);
}
});
} else {
inflater.inflate(R.menu.conversation_expiring_off, menu);
}
}
if (isSingleConversation()) {
if (isSecureVoice) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
@ -438,6 +459,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_mute_notifications: handleMuteNotifications(); return true;
case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true;
case R.id.menu_conversation_settings: handleConversationSettings(); return true;
case R.id.menu_expiring_messages_off:
case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true;
case android.R.id.home: handleReturnToConversationList(); return true;
}
@ -465,6 +488,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
finish();
}
private void handleSelectMessageExpiration() {
ExpirationDialog.show(this, recipients.getExpireMessages(), new ExpirationDialog.OnClickListener() {
@Override
public void onClick(final int expirationTime) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this)
.setExpireMessages(recipients, expirationTime);
recipients.setExpireMessages(expirationTime);
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipients(), System.currentTimeMillis(), expirationTime * 1000);
MessageSender.send(ConversationActivity.this, masterSecret, outgoingMessage, threadId, false);
invalidateOptionsMenu();
return null;
}
}.execute();
}
});
}
private void handleMuteNotifications() {
MuteDialog.show(this, new MuteDialog.MuteSelectionListener() {
@Override
@ -547,7 +592,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final Context context = getApplicationContext();
OutgoingEndSessionMessage endSessionMessage =
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE", -1));
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE", 0, -1));
new AsyncTask<OutgoingEndSessionMessage, Void, Long>() {
@Override
@ -599,7 +644,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.setType(GroupContext.Type.QUIT)
.build();
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipients(), context, null, System.currentTimeMillis());
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipients(), context, null, System.currentTimeMillis(), 0);
MessageSender.send(self, masterSecret, outgoingMessage, threadId, false);
DatabaseFactory.getGroupDatabase(self).remove(groupId, TextSecurePreferences.getLocalNumber(self));
initializeEnabledCheck();
@ -1002,6 +1047,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
titleView.setTitle(recipients);
setBlockedUserState(recipients);
setActionBarColor(recipients.getColor());
invalidateOptionsMenu();
updateRecipientPreferences();
}
});
@ -1016,26 +1062,30 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
};
groupUpdateReceiver = new BroadcastReceiver() {
recipientsStaleReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.w("ConversationActivity", "Group update received...");
Log.w(TAG, "Group update received...");
if (recipients != null) {
long[] ids = recipients.getIds();
Log.w("ConversationActivity", "Looking up new recipients...");
Log.w(TAG, "Looking up new recipients...");
recipients = RecipientFactory.getRecipientsForIds(context, ids, true);
recipients.addListener(ConversationActivity.this);
titleView.setTitle(recipients);
onModified(recipients);
fragment.reloadList();
}
}
};
IntentFilter staleFilter = new IntentFilter();
staleFilter.addAction(GroupDatabase.DATABASE_UPDATE_ACTION);
staleFilter.addAction(RecipientFactory.RECIPIENT_CLEAR_ACTION);
registerReceiver(securityUpdateReceiver,
new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT),
KeyCachingService.KEY_PERMISSION, null);
registerReceiver(groupUpdateReceiver,
new IntentFilter(GroupDatabase.DATABASE_UPDATE_ACTION));
registerReceiver(recipientsStaleReceiver, staleFilter);
}
//////// Helper Methods
@ -1281,6 +1331,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Recipients recipients = getRecipients();
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
long expiresIn = recipients.getExpireMessages() * 1000;
Log.w(TAG, "isManual Selection: " + sendButton.isManualSelection());
Log.w(TAG, "forceSms: " + forceSms);
@ -1292,9 +1343,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
handleManualMmsRequired();
} else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
sendMediaMessage(forceSms, subscriptionId);
sendMediaMessage(forceSms, expiresIn, subscriptionId);
} else {
sendTextMessage(forceSms, subscriptionId);
sendTextMessage(forceSms, expiresIn, subscriptionId);
}
} catch (RecipientFormattingException ex) {
Toast.makeText(ConversationActivity.this,
@ -1308,13 +1359,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void sendMediaMessage(final boolean forceSms, final int subscriptionId)
private void sendMediaMessage(final boolean forceSms, final long expiresIn, final int subscriptionId)
throws InvalidMessageException
{
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), subscriptionId);
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), expiresIn, subscriptionId);
}
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, final int subscriptionId)
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, final long expiresIn, final int subscriptionId)
throws InvalidMessageException
{
final SettableFuture<Void> future = new SettableFuture<>();
@ -1324,6 +1375,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
body,
System.currentTimeMillis(),
subscriptionId,
expiresIn,
distributionType);
if (isSecureText && !forceSms) {
@ -1349,16 +1401,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return future;
}
private void sendTextMessage(final boolean forceSms, final int subscriptionId)
private void sendTextMessage(final boolean forceSms, final long expiresIn, final int subscriptionId)
throws InvalidMessageException
{
final Context context = getApplicationContext();
OutgoingTextMessage message;
if (isSecureText && !forceSms) {
message = new OutgoingEncryptedMessage(recipients, getMessage());
message = new OutgoingEncryptedMessage(recipients, getMessage(), expiresIn);
} else {
message = new OutgoingTextMessage(recipients, getMessage(), subscriptionId);
message = new OutgoingTextMessage(recipients, getMessage(), expiresIn, subscriptionId);
}
this.composeText.setText("");
@ -1451,11 +1503,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
try {
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
long expiresIn = recipients.getExpireMessages() * 1000;
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, ContentType.AUDIO_AAC);
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
sendMediaMessage(forceSms, "", slideDeck, subscriptionId).addListener(new AssertedSuccessListener<Void>() {
sendMediaMessage(forceSms, "", slideDeck, expiresIn, subscriptionId).addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask<Void, Void, Void>() {

View File

@ -22,7 +22,6 @@ import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@ -39,6 +38,8 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.VisibleForTesting;
import java.lang.ref.SoftReference;
import java.security.MessageDigest;
@ -49,9 +50,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.VisibleForTesting;
/**
* A cursor adapter for a conversation thread. Ultimately
* used by ComposeMessageActivity to display a conversation
@ -94,8 +92,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
}
public interface ItemClickListener {
void onItemClick(ConversationItem item);
void onItemLongClick(ConversationItem item);
void onItemClick(MessageRecord item);
void onItemLongClick(MessageRecord item);
}
@SuppressWarnings("ConstantConditions")
@ -156,21 +154,23 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@Override
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
if (viewType == MESSAGE_TYPE_INCOMING || viewType == MESSAGE_TYPE_OUTGOING) {
itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (clickListener != null) clickListener.onItemClick((ConversationItem)itemView);
if (clickListener != null) {
clickListener.onItemClick(itemView.getMessageRecord());
}
}
});
itemView.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (clickListener != null) clickListener.onItemLongClick((ConversationItem)itemView);
if (clickListener != null) {
clickListener.onItemLongClick(itemView.getMessageRecord());
}
return true;
}
});
}
return new ViewHolder(itemView);
}
@ -195,7 +195,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined()) {
if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate()) {
return MESSAGE_TYPE_UPDATE;
} else if (messageRecord.isOutgoing()) {
return MESSAGE_TYPE_OUTGOING;

View File

@ -27,8 +27,8 @@ import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -57,10 +57,10 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import java.util.Collections;
import java.util.Comparator;
@ -165,24 +165,34 @@ public class ConversationFragment extends Fragment
if (this.recipients != null && this.threadId != -1) {
list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients));
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
list.getItemAnimator().setSupportsChangeAnimations(false);
list.getItemAnimator().setMoveDuration(120);
}
}
private void setCorrectMenuVisibility(Menu menu) {
Set<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
boolean actionMessage = false;
if (actionMode != null && messageRecords.size() == 0) {
actionMode.finish();
return;
}
for (MessageRecord messageRecord : messageRecords) {
if (messageRecord.isGroupAction() || messageRecord.isCallLog() ||
messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate())
{
actionMessage = true;
break;
}
}
if (messageRecords.size() > 1) {
menu.findItem(R.id.menu_context_forward).setVisible(false);
menu.findItem(R.id.menu_context_details).setVisible(false);
menu.findItem(R.id.menu_context_save_attachment).setVisible(false);
menu.findItem(R.id.menu_context_resend).setVisible(false);
menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage);
} else {
MessageRecord messageRecord = messageRecords.iterator().next();
@ -191,9 +201,9 @@ public class ConversationFragment extends Fragment
!messageRecord.isMmsNotification() &&
((MediaMmsMessageRecord)messageRecord).containsMediaSlide());
menu.findItem(R.id.menu_context_forward).setVisible(true);
menu.findItem(R.id.menu_context_details).setVisible(true);
menu.findItem(R.id.menu_context_copy).setVisible(true);
menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_details).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage);
}
}
@ -386,9 +396,8 @@ public class ConversationFragment extends Fragment
private class ConversationFragmentItemClickListener implements ItemClickListener {
@Override
public void onItemClick(ConversationItem item) {
public void onItemClick(MessageRecord messageRecord) {
if (actionMode != null) {
MessageRecord messageRecord = item.getMessageRecord();
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged();
@ -397,9 +406,9 @@ public class ConversationFragment extends Fragment
}
@Override
public void onItemLongClick(ConversationItem item) {
public void onItemLongClick(MessageRecord messageRecord) {
if (actionMode == null) {
((ConversationAdapter) list.getAdapter()).toggleSelection(item.getMessageRecord());
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged();
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);

View File

@ -23,6 +23,7 @@ import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
@ -42,6 +43,7 @@ import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.ExpirationTimerView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@ -61,6 +63,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -110,6 +113,7 @@ public class ConversationItem extends LinearLayout
private @NonNull AudioView audioView;
private @NonNull Button mmsDownloadButton;
private @NonNull TextView mmsDownloadingLabel;
private @NonNull ExpirationTimerView expirationTimer;
private int defaultBubbleColor;
@ -151,6 +155,7 @@ public class ConversationItem extends LinearLayout
this.bodyBubble = findViewById(R.id.body_bubble);
this.mediaThumbnail = (ThumbnailView) findViewById(R.id.image_view);
this.audioView = (AudioView) findViewById(R.id.audio_view);
this.expirationTimer = (ExpirationTimerView) findViewById(R.id.expiration_indicator);
setOnClickListener(new ClickListener(null));
PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
@ -194,6 +199,7 @@ public class ConversationItem extends LinearLayout
setMinimumWidth();
setMediaAttributes(messageRecord);
setSimInfo(messageRecord);
setExpiration(messageRecord);
}
private void initializeAttributes() {
@ -211,6 +217,8 @@ public class ConversationItem extends LinearLayout
if (recipient != null) {
recipient.removeListener(this);
}
this.expirationTimer.stopAnimation();
}
public MessageRecord getMessageRecord() {
@ -353,6 +361,36 @@ public class ConversationItem extends LinearLayout
}
}
private void setExpiration(final MessageRecord messageRecord) {
if (messageRecord.getExpiresIn() > 0) {
this.expirationTimer.setVisibility(View.VISIBLE);
this.expirationTimer.setPercentage(0);
if (messageRecord.getExpireStarted() > 0) {
this.expirationTimer.setExpirationTime(messageRecord.getExpireStarted(),
messageRecord.getExpiresIn());
this.expirationTimer.startAnimation();
} else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
long id = messageRecord.getId();
boolean mms = messageRecord.isMms();
if (mms) DatabaseFactory.getMmsDatabase(context).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(context).markExpireStarted(id);
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
return null;
}
}.execute();
}
} else {
this.expirationTimer.setVisibility(View.GONE);
}
}
private void setFailedStatusIcons() {
alertView.setFailed();
deliveryStatusIndicator.setNone();

View File

@ -221,7 +221,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
public void onChange(boolean selfChange) {
super.onChange(selfChange);
Log.w(TAG, "Detected android contact data changed, refreshing cache");
RecipientFactory.clearCache();
RecipientFactory.clearCache(ConversationListActivity.this);
ConversationListActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {

View File

@ -2,6 +2,9 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
@ -59,6 +62,17 @@ public class ConversationUpdateItem extends LinearLayout
@NonNull Recipients conversationRecipients)
{
bind(messageRecord, locale);
if (batchSelected.contains(messageRecord)) {
setSelected(true);
} else {
setSelected(false);
}
}
@Override
public MessageRecord getMessageRecord() {
return messageRecord;
}
private void bind(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
@ -71,6 +85,7 @@ public class ConversationUpdateItem extends LinearLayout
if (messageRecord.isGroupAction()) setGroupRecord(messageRecord);
else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
else throw new AssertionError("Neither group nor log nor joined.");
}
@ -84,8 +99,22 @@ public class ConversationUpdateItem extends LinearLayout
date.setVisibility(View.VISIBLE);
}
private void setTimerRecord(final MessageRecord messageRecord) {
if (messageRecord.getExpiresIn() > 0) {
icon.setImageResource(R.drawable.ic_timer_white_24dp);
icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
} else {
icon.setImageResource(R.drawable.ic_timer_off_white_24dp);
icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
}
body.setText(messageRecord.getDisplayBody());
date.setVisibility(View.GONE);
}
private void setGroupRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_group_grey600_24dp);
icon.clearColorFilter();
GroupUtil.getDescription(getContext(), messageRecord.getBody().getBody()).addListener(this);
body.setText(messageRecord.getDisplayBody());
@ -95,6 +124,7 @@ public class ConversationUpdateItem extends LinearLayout
private void setJoinedRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_favorite_grey600_24dp);
icon.clearColorFilter();
body.setText(messageRecord.getDisplayBody());
date.setVisibility(View.GONE);
}

View File

@ -0,0 +1,94 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
public class ExpirationDialog extends AlertDialog {
protected ExpirationDialog(Context context) {
super(context);
}
protected ExpirationDialog(Context context, int theme) {
super(context, theme);
}
protected ExpirationDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
}
public static void show(final Context context,
final int currentExpiration,
final @NonNull OnClickListener listener)
{
final View view = createNumberPickerView(context, currentExpiration);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private static View createNumberPickerView(final Context context, final int currentExpiration) {
final LayoutInflater inflater = LayoutInflater.from(context);
final View view = inflater.inflate(R.layout.expiration_dialog, null);
final NumberPickerView numberPickerView = (NumberPickerView)view.findViewById(R.id.expiration_number_picker);
final TextView textView = (TextView)view.findViewById(R.id.expiration_details);
final int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
final String[] expirationDisplayValues = new String[expirationTimes.length];
int selectedIndex = expirationTimes.length - 1;
for (int i=0;i<expirationTimes.length;i++) {
expirationDisplayValues[i] = ExpirationUtil.getExpirationDisplayValue(context, expirationTimes[i]);
if ((currentExpiration >= expirationTimes[i]) &&
(i == expirationTimes.length -1 || currentExpiration < expirationTimes[i+1])) {
selectedIndex = i;
}
}
numberPickerView.setDisplayedValues(expirationDisplayValues);
numberPickerView.setMinValue(0);
numberPickerView.setMaxValue(expirationTimes.length-1);
NumberPickerView.OnValueChangeListener listener = new NumberPickerView.OnValueChangeListener() {
@Override
public void onValueChange(NumberPickerView picker, int oldVal, int newVal) {
if (newVal == 0) {
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
} else {
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
}
}
};
numberPickerView.setOnValueChangedListener(listener);
numberPickerView.setValue(selectedIndex);
listener.onValueChange(numberPickerView, selectedIndex, selectedIndex);
return view;
}
public interface OnClickListener {
public void onClick(int expirationTime);
}
}

View File

@ -23,6 +23,7 @@ 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.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.Log;
@ -49,6 +50,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util;
@ -79,9 +81,11 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private ConversationItem conversationItem;
private ViewGroup itemParent;
private View metadataContainer;
private View expiresContainer;
private TextView errorText;
private TextView sentDate;
private TextView receivedDate;
private TextView expiresInText;
private View receivedContainer;
private TextView transport;
private TextView toFrom;
@ -91,6 +95,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private DynamicTheme dynamicTheme = new DynamicTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
private boolean running;
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
@ -100,6 +106,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
@Override
public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
setContentView(R.layout.message_details_activity);
running = true;
initializeResources();
initializeActionBar();
@ -122,6 +129,12 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(-1L);
}
@Override
protected void onDestroy() {
super.onDestroy();
running = false;
}
private void initializeActionBar() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -165,6 +178,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
receivedDate = (TextView ) header.findViewById(R.id.received_time);
transport = (TextView ) header.findViewById(R.id.transport);
toFrom = (TextView ) header.findViewById(R.id.tofrom);
expiresContainer = header.findViewById(R.id.expires_container);
expiresInText = (TextView) header.findViewById(R.id.expires_in);
recipientsList.setHeaderDividersEnabled(false);
recipientsList.addHeaderView(header, null, false);
}
@ -204,6 +219,29 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}
}
private void updateExpirationTime(final MessageRecord messageRecord) {
if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
expiresContainer.setVisibility(View.GONE);
return;
}
expiresContainer.setVisibility(View.VISIBLE);
expiresInText.post(new Runnable() {
@Override
public void run() {
long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
long remaining = messageRecord.getExpiresIn() - elapsed;
String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1));
expiresInText.setText(duration);
if (running) {
expiresInText.postDelayed(this, 500);
}
}
});
}
private void updateRecipients(MessageRecord messageRecord, Recipients recipients) {
final int toFromRes;
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
@ -233,7 +271,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}
}
private MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT:
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
@ -257,9 +295,14 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
final MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
if (messageRecord == null) {
finish();
} else {
new MessageRecipientAsyncTask(this, messageRecord).execute();
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
@ -281,7 +324,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private WeakReference<Context> weakContext;
private MessageRecord messageRecord;
public MessageRecipientAsyncTask(Context context, MessageRecord messageRecord) {
public MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
this.weakContext = new WeakReference<>(context);
this.messageRecord = messageRecord;
}
@ -340,6 +383,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
} else {
updateTransport(messageRecord);
updateTime(messageRecord);
updateExpirationTime(messageRecord);
errorText.setVisibility(View.GONE);
metadataContainer.setVisibility(View.VISIBLE);
}

View File

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
@ -28,6 +31,7 @@ import org.thoughtcrime.securesms.color.MaterialColors;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.preferences.AdvancedRingtonePreference;
import org.thoughtcrime.securesms.preferences.ColorPreference;
@ -57,6 +61,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private Toolbar toolbar;
private TextView title;
private TextView blockedIndicator;
private BroadcastReceiver staleReceiver;
@Override
public void onPreCreate() {
@ -72,6 +77,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
Recipients recipients = RecipientFactory.getRecipientsForIds(this, recipientIds, true);
initializeToolbar();
initializeReceivers();
setHeader(recipients);
recipients.addListener(this);
@ -87,6 +93,12 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
dynamicLanguage.onResume(this);
}
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(staleReceiver);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@ -118,6 +130,23 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
this.blockedIndicator = (TextView) toolbar.findViewById(R.id.blocked_indicator);
}
private void initializeReceivers() {
this.staleReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Recipients recipients = RecipientFactory.getRecipientsForIds(context, getIntent().getLongArrayExtra(RECIPIENTS_EXTRA), true);
recipients.addListener(RecipientPreferenceActivity.this);
onModified(recipients);
}
};
IntentFilter staleFilter = new IntentFilter();
staleFilter.addAction(GroupDatabase.DATABASE_UPDATE_ACTION);
staleFilter.addAction(RecipientFactory.RECIPIENT_CLEAR_ACTION);
registerReceiver(staleReceiver, staleFilter);
}
private void setHeader(Recipients recipients) {
this.avatar.setAvatar(recipients, true);
this.title.setText(recipients.toShortString());
@ -149,18 +178,15 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private final Handler handler = new Handler();
private Recipients recipients;
private BroadcastReceiver staleReceiver;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.recipient_preferences);
initializeRecipients();
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(),
getArguments().getLongArray(RECIPIENTS_EXTRA),
true);
this.recipients.addListener(this);
this.findPreference(PREFERENCE_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener());
this.findPreference(PREFERENCE_VIBRATE)
@ -185,6 +211,30 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
public void onDestroy() {
super.onDestroy();
this.recipients.removeListener(this);
getActivity().unregisterReceiver(staleReceiver);
}
private void initializeRecipients() {
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(),
getArguments().getLongArray(RECIPIENTS_EXTRA),
true);
this.recipients.addListener(this);
this.staleReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
recipients.removeListener(RecipientPreferenceFragment.this);
recipients = RecipientFactory.getRecipientsForIds(getActivity(), getArguments().getLongArray(RECIPIENTS_EXTRA), true);
onModified(recipients);
}
};
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(GroupDatabase.DATABASE_UPDATE_ACTION);
intentFilter.addAction(RecipientFactory.RECIPIENT_CLEAR_ACTION);
getActivity().registerReceiver(staleReceiver, intentFilter);
}
private void setSummaries(Recipients recipients) {

View File

@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import java.util.concurrent.TimeUnit;
public class ExpirationTimerView extends HourglassView {
private final Handler handler = new Handler();
private long startedAt;
private long expiresIn;
private boolean visible = false;
private boolean stopped = true;
public ExpirationTimerView(Context context) {
super(context);
}
public ExpirationTimerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ExpirationTimerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setExpirationTime(long startedAt, long expiresIn) {
this.startedAt = startedAt;
this.expiresIn = expiresIn;
setPercentage(calculateProgress(this.startedAt, this.expiresIn));
}
public void startAnimation() {
synchronized (this) {
visible = true;
if (stopped == false) return;
else stopped = false;
}
handler.postDelayed(new Runnable() {
@Override
public void run() {
setPercentage(calculateProgress(startedAt, expiresIn));
synchronized (ExpirationTimerView.this) {
if (!visible) {
stopped = true;
return;
}
}
handler.postDelayed(this, calculateAnimationDelay(startedAt, expiresIn));
}
}, calculateAnimationDelay(this.startedAt, this.expiresIn));
}
public void stopAnimation() {
synchronized (this) {
visible = false;
}
}
private float calculateProgress(long startedAt, long expiresIn) {
long progressed = System.currentTimeMillis() - startedAt;
float percentComplete = (float)progressed / (float)expiresIn;
return percentComplete * 100;
}
private long calculateAnimationDelay(long startedAt, long expiresIn) {
long progressed = System.currentTimeMillis() - startedAt;
long remaining = expiresIn - progressed;
if (remaining < TimeUnit.SECONDS.toMillis(30)) {
return 50;
} else {
return 1000;
}
}
}

View File

@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import org.thoughtcrime.securesms.R;
public class HourglassView extends View {
private final Paint foregroundPaint;
private final Paint backgroundPaint;
private final Paint progressPaint;
private Bitmap empty;
private Bitmap full;
private int tint;
private float percentage;
private int offset;
public HourglassView(Context context) {
this(context, null);
}
public HourglassView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HourglassView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.HourglassView, 0, 0);
this.empty = BitmapFactory.decodeResource(getResources(), typedArray.getResourceId(R.styleable.HourglassView_empty, 0));
this.full = BitmapFactory.decodeResource(getResources(), typedArray.getResourceId(R.styleable.HourglassView_full, 0));
this.tint = typedArray.getColor(R.styleable.HourglassView_tint, 0);
this.percentage = typedArray.getInt(R.styleable.HourglassView_percentage, 50);
this.offset = typedArray.getInt(R.styleable.HourglassView_offset, 0);
typedArray.recycle();
}
this.backgroundPaint = new Paint();
this.foregroundPaint = new Paint();
this.progressPaint = new Paint();
this.backgroundPaint.setColorFilter(new PorterDuffColorFilter(tint, PorterDuff.Mode.MULTIPLY));
this.foregroundPaint.setColorFilter(new PorterDuffColorFilter(tint, PorterDuff.Mode.MULTIPLY));
this.progressPaint.setColor(getResources().getColor(R.color.black));
this.progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
if (android.os.Build.VERSION.SDK_INT >= 11)
{
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
}
@Override
public void onDraw(Canvas canvas) {
float progressHeight = (full.getHeight() - (offset*2)) * (percentage / 100);
canvas.drawBitmap(full, 0, 0, backgroundPaint);
canvas.drawRect(0, 0, full.getWidth(), offset + progressHeight, progressPaint);
canvas.drawBitmap(empty, 0, 0, foregroundPaint);
}
public void setPercentage(float percentage) {
this.percentage = percentage;
invalidate();
}
}

View File

@ -72,7 +72,8 @@ public class DatabaseFactory {
private static final int INTRODUCED_CONVERSATION_LIST_STATUS_VERSION = 25;
private static final int MIGRATED_CONVERSATION_LIST_STATUS_VERSION = 26;
private static final int INTRODUCED_SUBSCRIPTION_ID_VERSION = 27;
private static final int DATABASE_VERSION = 27;
private static final int INTRODUCED_EXPIRE_MESSAGES_VERSION = 28;
private static final int DATABASE_VERSION = 28;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@ -820,6 +821,15 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE mms ADD COLUMN subscription_id INTEGER DEFAULT -1");
}
if (oldVersion < INTRODUCED_EXPIRE_MESSAGES_VERSION) {
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN expire_messages INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE sms ADD COLUMN expires_in INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE mms ADD COLUMN expires_in INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE sms ADD COLUMN expire_started INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE mms ADD COLUMN expire_started INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE thread ADD COLUMN expires_in INTEGER DEFAULT 0");
}
db.setTransactionSuccessful();
db.endTransaction();
}

View File

@ -11,11 +11,9 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
@ -147,7 +145,7 @@ public class GroupDatabase extends Database {
GROUP_ID + " = ?",
new String[] {GroupUtil.getEncodedId(groupId)});
RecipientFactory.clearCache();
RecipientFactory.clearCache(context);
notifyDatabaseListeners();
}
@ -157,7 +155,7 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
new String[] {GroupUtil.getEncodedId(groupId)});
RecipientFactory.clearCache();
RecipientFactory.clearCache(context);
notifyDatabaseListeners();
}
@ -172,7 +170,7 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
new String[] {GroupUtil.getEncodedId(groupId)});
RecipientFactory.clearCache();
RecipientFactory.clearCache(context);
notifyDatabaseListeners();
}

View File

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
@ -110,7 +111,8 @@ public class MmsDatabase extends MessagingDatabase {
"ct_cls" + " INTEGER, " + "resp_txt" + " TEXT, " + "d_tm" + " INTEGER, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " +
NETWORK_FAILURE + " TEXT DEFAULT NULL," + "d_rpt" + " INTEGER, " +
SUBSCRIPTION_ID + " INTEGER DEFAULT -1);";
SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
EXPIRE_STARTED + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@ -130,6 +132,7 @@ public class MmsDatabase extends MessagingDatabase {
MESSAGE_SIZE, STATUS, TRANSACTION_ID,
BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
EXPIRES_IN, EXPIRE_STARTED,
AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS,
AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID,
@ -340,6 +343,11 @@ public class MmsDatabase extends MessagingDatabase {
return cursor;
}
public Reader getExpireStartedMessages(@Nullable MasterSecret masterSecret) {
String where = EXPIRE_STARTED + " > 0";
return readerFor(masterSecret, rawQuery(where, null));
}
public Reader getDecryptInProgressMessages(MasterSecret masterSecret) {
String where = MESSAGE_BOX + " & " + (Types.ENCRYPTION_ASYMMETRIC_BIT) + " != 0";
return readerFor(masterSecret, rawQuery(where, null));
@ -432,6 +440,21 @@ public class MmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
}
public void markExpireStarted(long messageId) {
markExpireStarted(messageId, System.currentTimeMillis());
}
public void markExpireStarted(long messageId, long startedTimestamp) {
ContentValues contentValues = new ContentValues();
contentValues.put(EXPIRE_STARTED, startedTimestamp);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
long threadId = getThreadIdForMessage(messageId);
notifyConversationListeners(threadId);
}
public List<SyncMessageId> setMessagesRead(long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String where = THREAD_ID + " = ? AND " + READ + " = 0";
@ -579,6 +602,7 @@ public class MmsDatabase extends MessagingDatabase {
String messageText = 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));
List<Attachment> attachments = new LinkedList<Attachment>(attachmentDatabase.getAttachmentsForMessage(messageId));
MmsAddresses addresses = addr.getAddressesForId(messageId);
List<String> destinations = new LinkedList<>();
@ -591,10 +615,12 @@ public class MmsDatabase extends MessagingDatabase {
Recipients recipients = RecipientFactory.getRecipientsFromStrings(context, destinations, false);
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
return new OutgoingGroupMediaMessage(recipients, body, attachments, timestamp);
return new OutgoingGroupMediaMessage(recipients, body, attachments, timestamp, 0);
} else if (Types.isExpirationTimerUpdate(outboxType)) {
return new OutgoingExpirationUpdateMessage(recipients, timestamp, expiresIn);
}
OutgoingMediaMessage message = new OutgoingMediaMessage(recipients, body, attachments, timestamp, subscriptionId,
OutgoingMediaMessage message = new OutgoingMediaMessage(recipients, body, attachments, timestamp, subscriptionId, expiresIn,
!addresses.getBcc().isEmpty() ? ThreadDatabase.DistributionTypes.BROADCAST :
ThreadDatabase.DistributionTypes.DEFAULT);
if (Types.isSecureType(outboxType)) {
@ -623,6 +649,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(THREAD_ID, getThreadIdForMessage(messageId));
contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
contentValues.put(EXPIRES_IN, request.getExpiresIn());
List<Attachment> attachments = new LinkedList<>();
@ -678,6 +705,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(DATE_RECEIVED, generatePduCompatTimestamp());
contentValues.put(PART_COUNT, retrieved.getAttachments().size());
contentValues.put(SUBSCRIPTION_ID, retrieved.getSubscriptionId());
contentValues.put(EXPIRES_IN, retrieved.getExpiresIn());
contentValues.put(READ, 0);
if (!contentValues.containsKey(DATE_SENT)) {
@ -688,8 +716,11 @@ public class MmsDatabase extends MessagingDatabase {
retrieved.getBody(), retrieved.getAttachments(),
contentValues);
if (!Types.isExpirationTimerUpdate(mailbox)) {
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
}
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
@ -713,6 +744,10 @@ public class MmsDatabase extends MessagingDatabase {
type |= Types.PUSH_MESSAGE_BIT;
}
if (retrieved.isExpirationUpdate()) {
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
}
return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId, type);
}
@ -733,6 +768,10 @@ public class MmsDatabase extends MessagingDatabase {
type |= Types.PUSH_MESSAGE_BIT;
}
if (retrieved.isExpirationUpdate()) {
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
}
return insertMessageInbox(masterSecret, retrieved, "", threadId, type);
}
@ -805,6 +844,10 @@ public class MmsDatabase extends MessagingDatabase {
else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT;
}
if (message.isExpirationUpdate()) {
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
}
List<String> recipientNumbers = message.getRecipients().toNumberStringList(true);
MmsAddresses addresses;
@ -826,6 +869,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
contentValues.put(EXPIRES_IN, message.getExpiresIn());
if (message.getRecipients().isSingleRecipient()) {
try {
@ -1118,6 +1162,8 @@ public class MmsDatabase extends MessagingDatabase {
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.SUBSCRIPTION_ID));
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRES_IN));
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED));
Recipients recipients = getRecipientsFor(address);
List<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument);
@ -1127,7 +1173,7 @@ public class MmsDatabase extends MessagingDatabase {
return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
addressDeviceId, dateSent, dateReceived, receiptCount,
threadId, body, slideDeck, partCount, box, mismatches,
networkFailures, subscriptionId);
networkFailures, subscriptionId, expiresIn, expireStarted);
}
private Recipients getRecipientsFor(String address) {

View File

@ -15,6 +15,8 @@ public interface MmsSmsColumns {
public static final String MISMATCHED_IDENTITIES = "mismatched_identities";
public static final String UNIQUE_ROW_ID = "unique_row_id";
public static final String SUBSCRIPTION_ID = "subscription_id";
public static final String EXPIRES_IN = "expires_in";
public static final String EXPIRE_STARTED = "expire_started";
public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF;
@ -63,6 +65,7 @@ public interface MmsSmsColumns {
// Group Message Information
protected static final long GROUP_UPDATE_BIT = 0x10000;
protected static final long GROUP_QUIT_BIT = 0x20000;
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
// Encrypted Storage Information
protected static final long ENCRYPTION_MASK = 0xFF000000;
@ -166,6 +169,10 @@ public interface MmsSmsColumns {
return type == INCOMING_CALL_TYPE || type == OUTGOING_CALL_TYPE || type == MISSED_CALL_TYPE;
}
public static boolean isExpirationTimerUpdate(long type) {
return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0;
}
public static boolean isIncomingCall(long type) {
return type == INCOMING_CALL_TYPE;
}

View File

@ -33,8 +33,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.HashSet;
import java.util.Set;
import ws.com.google.android.mms.pdu.PduHeaders;
public class MmsSmsDatabase extends Database {
private static final String TAG = MmsSmsDatabase.class.getSimpleName();
@ -56,7 +54,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE,
MmsSmsColumns.SUBSCRIPTION_ID, TRANSPORT,
MmsSmsColumns.SUBSCRIPTION_ID,
MmsSmsColumns.EXPIRES_IN,
MmsSmsColumns.EXPIRE_STARTED, TRANSPORT,
AttachmentDatabase.ATTACHMENT_ID_ALIAS,
AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID,
@ -147,7 +147,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsSmsColumns.SUBSCRIPTION_ID, MmsDatabase.NETWORK_FAILURE, TRANSPORT,
MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED,
MmsDatabase.NETWORK_FAILURE, TRANSPORT,
AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID,
AttachmentDatabase.SIZE,
@ -171,7 +172,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsSmsColumns.SUBSCRIPTION_ID,
MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED,
MmsDatabase.NETWORK_FAILURE, TRANSPORT,
AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID,
@ -209,6 +210,8 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
mmsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
mmsColumnsPresent.add(MmsSmsColumns.SUBSCRIPTION_ID);
mmsColumnsPresent.add(MmsSmsColumns.EXPIRES_IN);
mmsColumnsPresent.add(MmsSmsColumns.EXPIRE_STARTED);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX);
mmsColumnsPresent.add(MmsDatabase.DATE_SENT);
@ -240,6 +243,8 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
smsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
smsColumnsPresent.add(MmsSmsColumns.SUBSCRIPTION_ID);
smsColumnsPresent.add(MmsSmsColumns.EXPIRES_IN);
smsColumnsPresent.add(MmsSmsColumns.EXPIRE_STARTED);
smsColumnsPresent.add(SmsDatabase.TYPE);
smsColumnsPresent.add(SmsDatabase.SUBJECT);
smsColumnsPresent.add(SmsDatabase.DATE_SENT);

View File

@ -33,6 +33,7 @@ public class RecipientPreferenceDatabase extends Database {
private static final String COLOR = "color";
private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder";
private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id";
private static final String EXPIRE_MESSAGES = "expire_messages";
public enum VibrateState {
DEFAULT(0), ENABLED(1), DISABLED(2);
@ -62,7 +63,8 @@ public class RecipientPreferenceDatabase extends Database {
MUTE_UNTIL + " INTEGER DEFAULT 0, " +
COLOR + " TEXT DEFAULT NULL, " +
SEEN_INVITE_REMINDER + " INTEGER DEFAULT 0, " +
DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1);";
DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
EXPIRE_MESSAGES + " INTEGER DEFAULT 0);";
public RecipientPreferenceDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
@ -98,6 +100,7 @@ public class RecipientPreferenceDatabase extends Database {
Uri notificationUri = notification == null ? null : Uri.parse(notification);
boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1;
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES));
MaterialColor color;
@ -113,7 +116,7 @@ public class RecipientPreferenceDatabase extends Database {
return Optional.of(new RecipientsPreferences(blocked, muteUntil,
VibrateState.fromId(vibrateState),
notificationUri, color, seenInviteReminder,
defaultSubscriptionId));
defaultSubscriptionId, expireMessages));
}
return Optional.absent();
@ -134,7 +137,6 @@ public class RecipientPreferenceDatabase extends Database {
updateOrInsert(recipients, values);
}
public void setBlocked(Recipients recipients, boolean blocked) {
ContentValues values = new ContentValues();
values.put(BLOCK, blocked ? 1 : 0);
@ -166,6 +168,14 @@ public class RecipientPreferenceDatabase extends Database {
updateOrInsert(recipients, values);
}
public void setExpireMessages(Recipients recipients, int expiration) {
recipients.setExpireMessages(expiration);
ContentValues values = new ContentValues(1);
values.put(EXPIRE_MESSAGES, expiration);
updateOrInsert(recipients, values);
}
private void updateOrInsert(Recipients recipients, ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
@ -193,13 +203,15 @@ public class RecipientPreferenceDatabase extends Database {
private final MaterialColor color;
private final boolean seenInviteReminder;
private final int defaultSubscriptionId;
private final int expireMessages;
public RecipientsPreferences(boolean blocked, long muteUntil,
@NonNull VibrateState vibrateState,
@Nullable Uri notification,
@Nullable MaterialColor color,
boolean seenInviteReminder,
int defaultSubscriptionId)
int defaultSubscriptionId,
int expireMessages)
{
this.blocked = blocked;
this.muteUntil = muteUntil;
@ -208,6 +220,7 @@ public class RecipientPreferenceDatabase extends Database {
this.color = color;
this.seenInviteReminder = seenInviteReminder;
this.defaultSubscriptionId = defaultSubscriptionId;
this.expireMessages = expireMessages;
}
public @Nullable MaterialColor getColor() {
@ -237,5 +250,9 @@ public class RecipientPreferenceDatabase extends Database {
public Optional<Integer> getDefaultSubscriptionId() {
return defaultSubscriptionId != -1 ? Optional.of(defaultSubscriptionId) : Optional.<Integer>absent();
}
public int getExpireMessages() {
return expireMessages;
}
}
}

View File

@ -77,7 +77,8 @@ public class SmsDatabase extends MessagingDatabase {
DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0," + SUBJECT + " TEXT, " + BODY + " TEXT, " +
MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1);";
MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
EXPIRES_IN + " INTEGER DEFAULT 0, " + EXPIRE_STARTED + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@ -94,7 +95,7 @@ public class SmsDatabase extends MessagingDatabase {
DATE_SENT + " AS " + NORMALIZED_DATE_SENT,
PROTOCOL, READ, STATUS, TYPE,
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, RECEIPT_COUNT,
MISMATCHED_IDENTITIES, SUBSCRIPTION_ID
MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED
};
private static final EarlyReceiptCache earlyReceiptCache = new EarlyReceiptCache();
@ -235,6 +236,23 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE);
}
public void markExpireStarted(long id) {
markExpireStarted(id, System.currentTimeMillis());
}
public void markExpireStarted(long id, long startedAtTimestamp) {
ContentValues contentValues = new ContentValues();
contentValues.put(EXPIRE_STARTED, startedAtTimestamp);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(id)});
long threadId = getThreadIdForMessage(id);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId);
}
public void markStatus(long id, int status) {
Log.w("MessageDatabase", "Updating ID: " + id + " to status: " + status);
ContentValues contentValues = new ContentValues();
@ -402,6 +420,7 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(READ, 0);
contentValues.put(BODY, record.getBody().getBody());
contentValues.put(THREAD_ID, record.getThreadId());
contentValues.put(EXPIRES_IN, record.getExpiresIn());
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long newMessageId = db.insert(TABLE_NAME, null, contentValues);
@ -505,6 +524,7 @@ public class SmsDatabase extends MessagingDatabase {
values.put(PROTOCOL, message.getProtocol());
values.put(READ, unread ? 0 : 1);
values.put(SUBSCRIPTION_ID, message.getSubscriptionId());
values.put(EXPIRES_IN, message.getExpiresIn());
if (!TextUtils.isEmpty(message.getPseudoSubject()))
values.put(SUBJECT, message.getPseudoSubject());
@ -552,6 +572,7 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(READ, 1);
contentValues.put(TYPE, type);
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
contentValues.put(EXPIRES_IN, message.getExpiresIn());
try {
contentValues.put(RECEIPT_COUNT, earlyReceiptCache.remove(date, canonicalizeNumber(context, address)));
@ -594,6 +615,12 @@ public class SmsDatabase extends MessagingDatabase {
return db.query(TABLE_NAME, MESSAGE_PROJECTION, selection, args, null, null, null);
}
public Cursor getExpirationStartedMessages() {
String where = EXPIRE_STARTED + " > 0";
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, MESSAGE_PROJECTION, where, null, null, null, null);
}
public Cursor getMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""},
@ -719,6 +746,8 @@ public class SmsDatabase extends MessagingDatabase {
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.RECEIPT_COUNT));
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.MISMATCHED_IDENTITIES));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.SUBSCRIPTION_ID));
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRES_IN));
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED));
List<IdentityKeyMismatch> mismatches = getMismatches(mismatchDocument);
Recipients recipients = getRecipientsFor(address);
@ -728,7 +757,8 @@ public class SmsDatabase extends MessagingDatabase {
recipients.getPrimaryRecipient(),
addressDeviceId,
dateSent, dateReceived, receiptCount, type,
threadId, status, mismatches, subscriptionId);
threadId, status, mismatches, subscriptionId,
expiresIn, expireStarted);
}
private Recipients getRecipientsFor(String address) {

View File

@ -67,6 +67,7 @@ public class ThreadDatabase extends Database {
public static final String ARCHIVED = "archived";
public static final String STATUS = "status";
public static final String RECEIPT_COUNT = "delivery_receipt_count";
private static final String EXPIRES_IN = "expires_in";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " +
@ -75,7 +76,7 @@ public class ThreadDatabase extends Database {
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0);";
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");",
@ -133,7 +134,8 @@ public class ThreadDatabase extends Database {
}
private void updateThread(long threadId, long count, String body, @Nullable Uri attachment,
long date, int status, int receiptCount, long type, boolean unarchive)
long date, int status, int receiptCount, long type, boolean unarchive,
long expiresIn)
{
ContentValues contentValues = new ContentValues(7);
contentValues.put(DATE, date - date % 1000);
@ -143,6 +145,7 @@ public class ThreadDatabase extends Database {
contentValues.put(SNIPPET_TYPE, type);
contentValues.put(STATUS, status);
contentValues.put(RECEIPT_COUNT, receiptCount);
contentValues.put(EXPIRES_IN, expiresIn);
if (unarchive) {
contentValues.put(ARCHIVED, 0);
@ -503,7 +506,7 @@ public class ThreadDatabase extends Database {
if (reader != null && (record = reader.getNext()) != null) {
updateThread(threadId, count, record.getBody().getBody(), getAttachmentUriFor(record),
record.getTimestamp(), record.getDeliveryStatus(), record.getReceiptCount(),
record.getType(), unarchive);
record.getType(), unarchive, record.getExpiresIn());
notifyConversationListListeners();
return false;
} else {
@ -572,10 +575,12 @@ public class ThreadDatabase extends Database {
boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0;
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS));
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.RECEIPT_COUNT));
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
Uri snippetUri = getSnippetUri(cursor);
return new ThreadRecord(context, body, snippetUri, recipients, date, count, read == 1,
threadId, receiptCount, status, type, distributionType, archived);
threadId, receiptCount, status, type, distributionType, archived,
expiresIn);
}
private DisplayRecord.Body getPlaintextBody(Cursor cursor) {

View File

@ -115,6 +115,10 @@ public abstract class DisplayRecord {
return isGroupUpdate() || isGroupQuit();
}
public boolean isExpirationTimerUpdate() {
return SmsDatabase.Types.isExpirationTimerUpdate(type);
}
public boolean isCallLog() {
return SmsDatabase.Types.isCallLog(type);
}

View File

@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
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 org.thoughtcrime.securesms.recipients.Recipients;
@ -53,10 +54,12 @@ public class MediaMmsMessageRecord extends MessageRecord {
@NonNull SlideDeck slideDeck,
int partCount, long mailbox,
List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> failures, int subscriptionId)
List<NetworkFailure> failures, int subscriptionId,
long expiresIn, long expireStarted)
{
super(context, id, body, recipients, individualRecipient, recipientDeviceId, dateSent,
dateReceived, threadId, Status.STATUS_NONE, receiptCount, mailbox, mismatches, failures, subscriptionId);
dateReceived, threadId, Status.STATUS_NONE, receiptCount, mailbox, mismatches, failures,
subscriptionId, expiresIn, expireStarted);
this.context = context.getApplicationContext();
this.partCount = partCount;
@ -85,6 +88,17 @@ public class MediaMmsMessageRecord extends MessageRecord {
return false;
}
@Override
public boolean isMediaPending() {
for (Slide slide : getSlideDeck().getSlides()) {
if (slide.isInProgress() || slide.isPendingDownload()) {
return true;
}
}
return false;
}
@Override
public SpannableString getDisplayBody() {
if (MmsDatabase.Types.isDecryptInProgressType(type)) {

View File

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import java.util.List;
@ -51,6 +52,8 @@ public abstract class MessageRecord extends DisplayRecord {
private final List<IdentityKeyMismatch> mismatches;
private final List<NetworkFailure> networkFailures;
private final int subscriptionId;
private final long expiresIn;
private final long expireStarted;
MessageRecord(Context context, long id, Body body, Recipients recipients,
Recipient individualRecipient, int recipientDeviceId,
@ -58,7 +61,7 @@ public abstract class MessageRecord extends DisplayRecord {
int deliveryStatus, int receiptCount, long type,
List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> networkFailures,
int subscriptionId)
int subscriptionId, long expiresIn, long expireStarted)
{
super(context, body, recipients, dateSent, dateReceived, threadId, deliveryStatus, receiptCount,
type);
@ -68,6 +71,8 @@ public abstract class MessageRecord extends DisplayRecord {
this.mismatches = mismatches;
this.networkFailures = networkFailures;
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expireStarted = expireStarted;
}
public abstract boolean isMms();
@ -103,6 +108,10 @@ public abstract class MessageRecord extends DisplayRecord {
return emphasisAdded(context.getString(R.string.MessageRecord_missed_call_from, getIndividualRecipient().toShortString()));
} else if (isJoined()) {
return emphasisAdded(context.getString(R.string.MessageRecord_s_is_on_signal_say_hey, getIndividualRecipient().toShortString()));
} else if (isExpirationTimerUpdate()) {
String sender = isOutgoing() ? context.getString(R.string.MessageRecord_you) : getIndividualRecipient().toShortString();
String time = ExpirationUtil.getExpirationDisplayValue(context, (int)(getExpiresIn() / 1000));
return emphasisAdded(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, sender, time));
} else if (getBody().getBody().length() > MAX_DISPLAY_LENGTH) {
return new SpannableString(getBody().getBody().substring(0, MAX_DISPLAY_LENGTH));
}
@ -155,6 +164,10 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
}
public boolean isMediaPending() {
return false;
}
public Recipient getIndividualRecipient() {
return individualRecipient;
}
@ -201,4 +214,12 @@ public abstract class MessageRecord extends DisplayRecord {
public int getSubscriptionId() {
return subscriptionId;
}
public long getExpiresIn() {
return expiresIn;
}
public long getExpireStarted() {
return expireStarted;
}
}

View File

@ -54,7 +54,8 @@ public class NotificationMmsMessageRecord extends MessageRecord {
{
super(context, id, new Body("", true), recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, Status.STATUS_NONE, receiptCount, mailbox,
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId);
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId,
0, 0);
this.contentLocation = contentLocation;
this.messageSize = messageSize;
@ -113,6 +114,11 @@ public class NotificationMmsMessageRecord extends MessageRecord {
return true;
}
@Override
public boolean isMediaPending() {
return true;
}
@Override
public SpannableString getDisplayBody() {
return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_multimedia_message));

View File

@ -48,11 +48,12 @@ public class SmsMessageRecord extends MessageRecord {
int receiptCount,
long type, long threadId,
int status, List<IdentityKeyMismatch> mismatches,
int subscriptionId)
int subscriptionId, long expiresIn, long expireStarted)
{
super(context, id, body, recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, status, receiptCount, type,
mismatches, new LinkedList<NetworkFailure>(), subscriptionId);
mismatches, new LinkedList<NetworkFailure>(), subscriptionId,
expiresIn, expireStarted);
}
public long getType() {

View File

@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
/**
@ -46,11 +47,12 @@ public class ThreadRecord extends DisplayRecord {
private final boolean read;
private final int distributionType;
private final boolean archived;
private final long expiresIn;
public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri,
@NonNull Recipients recipients, long date, long count, boolean read,
long threadId, int receiptCount, int status, long snippetType,
int distributionType, boolean archived)
int distributionType, boolean archived, long expiresIn)
{
super(context, body, recipients, date, date, threadId, status, receiptCount, snippetType);
this.context = context.getApplicationContext();
@ -59,6 +61,7 @@ public class ThreadRecord extends DisplayRecord {
this.read = read;
this.distributionType = distributionType;
this.archived = archived;
this.expiresIn = expiresIn;
}
public @Nullable Uri getSnippetUri() {
@ -96,6 +99,9 @@ public class ThreadRecord extends DisplayRecord {
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_missed_call));
} else if (SmsDatabase.Types.isJoinedType(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal_say_hey, getRecipients().getPrimaryRecipient().toShortString()));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
String time = ExpirationUtil.getExpirationDisplayValue(context, (int)(getExpiresIn() / 1000));
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
} else {
if (TextUtils.isEmpty(getBody().getBody())) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
@ -135,4 +141,8 @@ public class ThreadRecord extends DisplayRecord {
public int getDistributionType() {
return distributionType;
}
public long getExpiresIn() {
return expiresIn;
}
}

View File

@ -107,7 +107,7 @@ public class GroupManager {
avatarAttachment = new UriAttachment(avatarUri, ContentType.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length);
}
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis());
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0);
long threadId = MessageSender.send(context, masterSecret, outgoingMessage, -1, false);
return new GroupActionResult(groupRecipient, threadId);

View File

@ -185,7 +185,7 @@ public class GroupMessageProcessor {
if (outgoing) {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(group.getGroupId()), false);
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipients, storage, null, envelope.getTimestamp());
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipients, storage, null, envelope.getTimestamp(), 0);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = mmsDatabase.insertMessageOutbox(masterSecret, outgoingMessage, threadId, false);
@ -195,7 +195,7 @@ public class GroupMessageProcessor {
} else {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = Base64.encodeBytes(storage.toByteArray());
IncomingTextMessage incoming = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), body, Optional.of(group));
IncomingTextMessage incoming = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), body, Optional.of(group), 0);
IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, storage, body);
Pair<Long, Long> messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, groupMessage);

View File

@ -194,7 +194,7 @@ public class MmsDownloadJob extends MasterSecretJob {
IncomingMediaMessage message = new IncomingMediaMessage(from, to, cc, body, retrieved.getDate() * 1000L, attachments, subscriptionId);
IncomingMediaMessage message = new IncomingMediaMessage(from, to, cc, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(new MasterSecretUnion(masterSecret),
message, contentLocation, threadId);

View File

@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
@ -49,10 +50,11 @@ import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
@ -141,6 +143,7 @@ public class PushDecryptJob extends ContextJob {
if (message.isEndSession()) handleEndSessionMessage(masterSecret, envelope, message, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(masterSecret, envelope, message, smsMessageId);
else if (message.isExpirationUpdate()) handleExpirationUpdate(masterSecret, envelope, message, smsMessageId);
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message, smsMessageId);
else handleTextMessage(masterSecret, envelope, message, smsMessageId);
} else if (content.getSyncMessage().isPresent()) {
@ -185,7 +188,7 @@ public class PushDecryptJob extends ContextJob {
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(),
"", Optional.<SignalServiceGroup>absent());
"", Optional.<SignalServiceGroup>absent(), 0);
long threadId;
@ -218,6 +221,33 @@ public class PushDecryptJob extends ContextJob {
}
}
private void handleExpirationUpdate(@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId)
throws MmsException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
String localNumber = TextSecurePreferences.getLocalNumber(context);
Recipients recipients = getMessageDestination(envelope, message);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, envelope.getSource(),
localNumber, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000, true,
Optional.fromNullable(envelope.getRelay()),
Optional.<String>absent(), message.getGroupInfo(),
Optional.<List<SignalServiceAttachment>>absent());
database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
DatabaseFactory.getRecipientPreferenceDatabase(context).setExpireMessages(recipients, message.getExpiresInSeconds());
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
}
private void handleSynchronizeSentMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope,
@NonNull SentTranscriptMessage message,
@ -228,6 +258,8 @@ public class PushDecryptJob extends ContextJob {
if (message.getMessage().isGroupUpdate()) {
threadId = GroupMessageProcessor.process(context, masterSecret, envelope, message.getMessage(), true);
} else if (message.getMessage().isExpirationUpdate()) {
threadId = handleSynchronizeSentExpirationUpdate(masterSecret, message, smsMessageId);
} else if (message.getMessage().getAttachments().isPresent()) {
threadId = handleSynchronizeSentMediaMessage(masterSecret, message, smsMessageId);
} else {
@ -275,13 +307,19 @@ public class PushDecryptJob extends ContextJob {
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
String localNumber = TextSecurePreferences.getLocalNumber(context);
Recipients recipients = getMessageDestination(envelope, message);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, envelope.getSource(),
localNumber, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000, false,
Optional.fromNullable(envelope.getRelay()),
message.getBody(),
message.getGroupInfo(),
message.getAttachments());
if (message.getExpiresInSeconds() != recipients.getExpireMessages()) {
handleExpirationUpdate(masterSecret, envelope, message, Optional.<Long>absent());
}
Pair<Long, Long> messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageAndThreadId.first);
@ -299,6 +337,40 @@ public class PushDecryptJob extends ContextJob {
MessageNotifier.updateNotification(context, masterSecret.getMasterSecret().orNull(), messageAndThreadId.second);
}
private long handleSynchronizeSentExpirationUpdate(@NonNull MasterSecretUnion masterSecret,
@NonNull SentTranscriptMessage message,
@NonNull Optional<Long> smsMessageId)
throws MmsException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipients recipients = getSyncMessageDestination(message);
OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipients,
message.getTimestamp(),
message.getMessage().getExpiresInSeconds() * 1000);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = database.insertMessageOutbox(masterSecret, expirationUpdateMessage, threadId, false);
database.markAsSent(messageId);
database.markAsPush(messageId);
DatabaseFactory.getRecipientPreferenceDatabase(context).setExpireMessages(recipients, message.getMessage().getExpiresInSeconds());
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
ApplicationContext.getInstance(context)
.getExpiringMessageManager()
.scheduleDeletion(messageId, true,
message.getExpirationStartTimestamp(),
message.getMessage().getExpiresInSeconds());
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
return threadId;
}
private long handleSynchronizeSentMediaMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SentTranscriptMessage message,
@NonNull Optional<Long> smsMessageId)
@ -308,10 +380,16 @@ public class PushDecryptJob extends ContextJob {
Recipients recipients = getSyncMessageDestination(message);
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
PointerAttachment.forPointers(masterSecret, message.getMessage().getAttachments()),
message.getTimestamp(), -1, ThreadDatabase.DistributionTypes.DEFAULT);
message.getTimestamp(), -1,
message.getMessage().getExpiresInSeconds() * 1000,
ThreadDatabase.DistributionTypes.DEFAULT);
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
if (recipients.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
handleSynchronizeSentExpirationUpdate(masterSecret, message, Optional.<Long>absent());
}
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = database.insertMessageOutbox(masterSecret, mediaMessage, threadId, false);
@ -328,6 +406,15 @@ public class PushDecryptJob extends ContextJob {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
if (message.getMessage().getExpiresInSeconds() > 0) {
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
ApplicationContext.getInstance(context)
.getExpiringMessageManager()
.scheduleDeletion(messageId, true,
message.getExpirationStartTimestamp(),
message.getMessage().getExpiresInSeconds());
}
return threadId;
}
@ -335,9 +422,15 @@ public class PushDecryptJob extends ContextJob {
@NonNull SignalServiceEnvelope envelope,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId)
throws MmsException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : "";
Recipients recipients = getMessageDestination(envelope, message);
if (message.getExpiresInSeconds() != recipients.getExpireMessages()) {
handleExpirationUpdate(masterSecret, envelope, message, Optional.<Long>absent());
}
Pair<Long, Long> messageAndThreadId;
@ -347,7 +440,8 @@ public class PushDecryptJob extends ContextJob {
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(), body,
message.getGroupInfo());
message.getGroupInfo(),
message.getExpiresInSeconds() * 1000);
textMessage = new IncomingEncryptedMessage(textMessage, body);
messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
@ -361,11 +455,17 @@ public class PushDecryptJob extends ContextJob {
private long handleSynchronizeSentTextMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SentTranscriptMessage message,
@NonNull Optional<Long> smsMessageId)
throws MmsException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Recipients recipients = getSyncMessageDestination(message);
String body = message.getMessage().getBody().or("");
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipients, body, -1);
long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000;
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipients, body, expiresInMillis, -1);
if (recipients.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
handleSynchronizeSentExpirationUpdate(masterSecret, message, Optional.<Long>absent());
}
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = database.insertMessageOutbox(masterSecret, threadId, outgoingTextMessage, false, message.getTimestamp());
@ -378,6 +478,13 @@ public class PushDecryptJob extends ContextJob {
database.deleteMessage(smsMessageId.get());
}
if (expiresInMillis > 0) {
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
ApplicationContext.getInstance(context)
.getExpiringMessageManager()
.scheduleDeletion(messageId, false, message.getExpirationStartTimestamp(), expiresInMillis);
}
return threadId;
}
@ -470,7 +577,7 @@ public class PushDecryptJob extends ContextJob {
String encoded = Base64.encodeBytes(envelope.getLegacyMessage());
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), encoded,
Optional.<SignalServiceGroup>absent());
Optional.<SignalServiceGroup>absent(), 0);
if (!smsMessageId.isPresent()) {
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
@ -492,7 +599,7 @@ public class PushDecryptJob extends ContextJob {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), "",
Optional.<SignalServiceGroup>absent());
Optional.<SignalServiceGroup>absent(), 0);
textMessage = new IncomingEncryptedMessage(textMessage, "");
return database.insertMessageInbox(textMessage);
@ -505,4 +612,12 @@ public class PushDecryptJob extends ContextJob {
return RecipientFactory.getRecipientsFromString(context, message.getDestination().get(), false);
}
}
private Recipients getMessageDestination(SignalServiceEnvelope envelope, SignalServiceDataMessage message) {
if (message.getGroupInfo().isPresent()) {
return RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId()), false);
} else {
return RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
}
}
}

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.jobqueue.JobParameters;
@ -85,6 +87,13 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId);
database.markAsSent(messageId);
markAttachmentsUploaded(messageId, message.getAttachments());
if (message.getExpiresIn() > 0) {
database.markExpireStarted(messageId);
ApplicationContext.getInstance(context)
.getExpiringMessageManager()
.scheduleDeletion(messageId, true, message.getExpiresIn());
}
} catch (InvalidNumberException | RecipientFormattingException | UndeliverableMessageException e) {
Log.w(TAG, e);
database.markAsSentFailed(messageId);
@ -152,7 +161,10 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
messageSender.sendMessage(addresses, groupDataMessage);
} else {
SignalServiceGroup group = new SignalServiceGroup(groupId);
SignalServiceDataMessage groupMessage = new SignalServiceDataMessage(message.getSentTimeMillis(), group, attachments, message.getBody());
SignalServiceDataMessage groupMessage = new SignalServiceDataMessage(message.getSentTimeMillis(), group,
attachments, message.getBody(), false,
(int)(message.getExpiresIn() / 1000),
message.isExpirationUpdate());
messageSender.sendMessage(addresses, groupMessage);
}

View File

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
@ -62,6 +63,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
throws RetryLaterException, MmsException, NoSuchMessageException,
UndeliverableMessageException
{
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId);
@ -71,6 +73,12 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId);
database.markAsSent(messageId);
markAttachmentsUploaded(messageId, message.getAttachments());
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
}
} catch (InsecureFallbackApprovalException ifae) {
Log.w(TAG, ifae);
database.markAsPendingInsecureSmsFallback(messageId);
@ -122,6 +130,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.withBody(message.getBody())
.withAttachments(attachmentStreams)
.withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000))
.asExpirationUpdate(message.isExpirationUpdate())
.build();
messageSender.sendMessage(address, mediaMessage);

View File

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
@ -53,6 +54,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
@Override
public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
@ -64,6 +66,11 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId);
database.markAsSent(messageId);
if (record.getExpiresIn() > 0) {
database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn());
}
} catch (InsecureFallbackApprovalException e) {
Log.w(TAG, e);
database.markAsPendingInsecureSmsFallback(record.getId());
@ -108,6 +115,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent())
.withBody(message.getBody().getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.asEndSessionMessage(message.isEndSession())
.build();

View File

@ -20,6 +20,8 @@ public class IncomingMediaMessage {
private final boolean push;
private final long sentTimeMillis;
private final int subscriptionId;
private final long expiresIn;
private final boolean expirationUpdate;
private final List<String> to = new LinkedList<>();
private final List<String> cc = new LinkedList<>();
@ -27,7 +29,8 @@ public class IncomingMediaMessage {
public IncomingMediaMessage(String from, List<String> to, List<String> cc,
String body, long sentTimeMillis,
List<Attachment> attachments, int subscriptionId)
List<Attachment> attachments, int subscriptionId,
long expiresIn, boolean expirationUpdate)
{
this.from = from;
this.sentTimeMillis = sentTimeMillis;
@ -35,6 +38,8 @@ public class IncomingMediaMessage {
this.groupId = null;
this.push = false;
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate;
this.to.addAll(to);
this.cc.addAll(cc);
@ -46,6 +51,8 @@ public class IncomingMediaMessage {
String to,
long sentTimeMillis,
int subscriptionId,
long expiresIn,
boolean expirationUpdate,
Optional<String> relay,
Optional<String> body,
Optional<SignalServiceGroup> group,
@ -56,6 +63,8 @@ public class IncomingMediaMessage {
this.sentTimeMillis = sentTimeMillis;
this.body = body.orNull();
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate;
if (group.isPresent()) this.groupId = GroupUtil.getEncodedId(group.get().getGroupId());
else this.groupId = null;
@ -88,10 +97,18 @@ public class IncomingMediaMessage {
return push;
}
public boolean isExpirationUpdate() {
return expirationUpdate;
}
public long getSentTimeMillis() {
return sentTimeMillis;
}
public long getExpiresIn() {
return expiresIn;
}
public boolean isGroupMessage() {
return groupId != null || to.size() > 1 || cc.size() > 0;
}

View File

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.mms;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.LinkedList;
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
public OutgoingExpirationUpdateMessage(Recipients recipients, long sentTimeMillis, long expiresIn) {
super(recipients, "", new LinkedList<Attachment>(), sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn);
}
@Override
public boolean isExpirationUpdate() {
return true;
}
}

View File

@ -20,11 +20,12 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
public OutgoingGroupMediaMessage(@NonNull Recipients recipients,
@NonNull String encodedGroupContext,
@NonNull List<Attachment> avatar,
long sentTimeMillis)
long sentTimeMillis,
long expiresIn)
throws IOException
{
super(recipients, encodedGroupContext, avatar, sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION);
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn);
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
}
@ -32,12 +33,13 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
public OutgoingGroupMediaMessage(@NonNull Recipients recipients,
@NonNull GroupContext group,
@Nullable final Attachment avatar,
long sentTimeMillis)
long sentTimeMillis,
long expireIn)
{
super(recipients, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(),
ThreadDatabase.DistributionTypes.CONVERSATION);
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn);
this.group = group;
}

View File

@ -15,10 +15,11 @@ public class OutgoingMediaMessage {
private final long sentTimeMillis;
private final int distributionType;
private final int subscriptionId;
private final long expiresIn;
public OutgoingMediaMessage(Recipients recipients, String message,
List<Attachment> attachments, long sentTimeMillis,
int subscriptionId,
int subscriptionId, long expiresIn,
int distributionType)
{
this.recipients = recipients;
@ -27,15 +28,16 @@ public class OutgoingMediaMessage {
this.distributionType = distributionType;
this.attachments = attachments;
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
}
public OutgoingMediaMessage(Recipients recipients, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, int distributionType)
public OutgoingMediaMessage(Recipients recipients, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, long expiresIn, int distributionType)
{
this(recipients,
buildMessage(slideDeck, message),
slideDeck.asAttachments(),
sentTimeMillis, subscriptionId,
distributionType);
expiresIn, distributionType);
}
public OutgoingMediaMessage(OutgoingMediaMessage that) {
@ -45,6 +47,7 @@ public class OutgoingMediaMessage {
this.attachments = that.attachments;
this.sentTimeMillis = that.sentTimeMillis;
this.subscriptionId = that.subscriptionId;
this.expiresIn = that.expiresIn;
}
public Recipients getRecipients() {
@ -71,6 +74,10 @@ public class OutgoingMediaMessage {
return false;
}
public boolean isExpirationUpdate() {
return false;
}
public long getSentTimeMillis() {
return sentTimeMillis;
}
@ -79,6 +86,10 @@ public class OutgoingMediaMessage {
return subscriptionId;
}
public long getExpiresIn() {
return expiresIn;
}
private static String buildMessage(SlideDeck slideDeck, String message) {
if (!TextUtils.isEmpty(message) && !TextUtils.isEmpty(slideDeck.getBody())) {
return slideDeck.getBody() + "\n\n" + message;

View File

@ -14,9 +14,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
public OutgoingSecureMediaMessage(Recipients recipients, String body,
List<Attachment> attachments,
long sentTimeMillis,
int distributionType)
int distributionType,
long expiresIn)
{
super(recipients, body, attachments, sentTimeMillis, -1, distributionType);
super(recipients, body, attachments, sentTimeMillis, -1, expiresIn, distributionType);
}
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {

View File

@ -73,12 +73,13 @@ public class WearReplyReceiver extends MasterSecretBroadcastReceiver {
Optional<RecipientsPreferences> preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(recipientIds);
int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1;
long expiresIn = preferences.isPresent() ? preferences.get().getExpireMessages() * 1000 : 0;
if (recipients.isGroupRecipient()) {
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipients, responseText.toString(), new LinkedList<Attachment>(), System.currentTimeMillis(), subscriptionId, 0);
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipients, responseText.toString(), new LinkedList<Attachment>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0);
threadId = MessageSender.send(context, masterSecret, reply, -1, false);
} else {
OutgoingTextMessage reply = new OutgoingTextMessage(recipients, responseText.toString(), subscriptionId);
OutgoingTextMessage reply = new OutgoingTextMessage(recipients, responseText.toString(), expiresIn, subscriptionId);
threadId = MessageSender.send(context, masterSecret, reply, -1, false);
}

View File

@ -17,6 +17,7 @@
package org.thoughtcrime.securesms.recipients;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.text.TextUtils;
@ -31,6 +32,8 @@ import java.util.StringTokenizer;
public class RecipientFactory {
public static final String RECIPIENT_CLEAR_ACTION = "org.thoughtcrime.securesms.database.RecipientFactory.CLEAR";
private static final RecipientProvider provider = new RecipientProvider();
public static Recipients getRecipientsForIds(Context context, String recipientIds, boolean asynchronous) {
@ -133,8 +136,9 @@ public class RecipientFactory {
return value;
}
public static void clearCache() {
public static void clearCache(Context context) {
provider.clearCache();
context.sendBroadcast(new Intent(RECIPIENT_CLEAR_ACTION));
}
}

View File

@ -55,6 +55,7 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
private long mutedUntil = 0;
private boolean blocked = false;
private VibrateState vibrate = VibrateState.DEFAULT;
private int expireMessages = 0;
private boolean stale = false;
Recipients() {
@ -69,6 +70,7 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
mutedUntil = preferences.getMuteUntil();
vibrate = preferences.getVibrateState();
blocked = preferences.isBlocked();
expireMessages = preferences.getExpireMessages();
}
}
@ -83,6 +85,7 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
mutedUntil = stale.mutedUntil;
vibrate = stale.vibrate;
blocked = stale.blocked;
expireMessages = stale.expireMessages;
}
preferences.addListener(new FutureTaskListener<RecipientsPreferences>() {
@ -97,6 +100,7 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
mutedUntil = result.getMuteUntil();
vibrate = result.getVibrateState();
blocked = result.isBlocked();
expireMessages = result.getExpireMessages();
localListeners = new HashSet<>(listeners);
}
@ -178,6 +182,18 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
else if (!isEmpty()) recipients.get(0).setColor(color);
}
public synchronized int getExpireMessages() {
return expireMessages;
}
public void setExpireMessages(int expireMessages) {
synchronized (this) {
this.expireMessages = expireMessages;
}
notifyListeners();
}
public synchronized void addListener(RecipientsModifiedListener listener) {
if (listeners.isEmpty()) {
for (Recipient recipient : recipients) {

View File

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.service;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.thoughtcrime.securesms.ApplicationContext;
public class ExpirationListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ApplicationContext.getInstance(context).getExpiringMessageManager().checkSchedule();
}
public static void setAlarm(Context context, long waitTimeMillis) {
Intent intent = new Intent(context, ExpirationListener.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(pendingIntent);
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + waitTimeMillis, pendingIntent);
}
}

View File

@ -0,0 +1,148 @@
package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import java.util.Comparator;
import java.util.TreeSet;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class ExpiringMessageManager {
private static final String TAG = ExpiringMessageManager.class.getSimpleName();
private final TreeSet<ExpiringMessageReference> expiringMessageReferences = new TreeSet<>(new ExpiringMessageComparator());
private final Executor executor = Executors.newSingleThreadExecutor();
private final SmsDatabase smsDatabase;
private final MmsDatabase mmsDatabase;
private final Context context;
public ExpiringMessageManager(Context context) {
this.context = context.getApplicationContext();
this.smsDatabase = DatabaseFactory.getSmsDatabase(context);
this.mmsDatabase = DatabaseFactory.getMmsDatabase(context);
executor.execute(new LoadTask());
executor.execute(new ProcessTask());
}
public void scheduleDeletion(long id, boolean mms, long expiresInMillis) {
scheduleDeletion(id, mms, System.currentTimeMillis(), expiresInMillis);
}
public void scheduleDeletion(long id, boolean mms, long startedAtTimestamp, long expiresInMillis) {
long expiresAtMillis = startedAtTimestamp + expiresInMillis;
synchronized (expiringMessageReferences) {
expiringMessageReferences.add(new ExpiringMessageReference(id, mms, expiresAtMillis));
expiringMessageReferences.notifyAll();
}
}
public void checkSchedule() {
synchronized (expiringMessageReferences) {
expiringMessageReferences.notifyAll();
}
}
private class LoadTask implements Runnable {
public void run() {
SmsDatabase.Reader smsReader = smsDatabase.readerFor(smsDatabase.getExpirationStartedMessages());
MmsDatabase.Reader mmsReader = mmsDatabase.getExpireStartedMessages(null);
MessageRecord messageRecord = null;
while ((messageRecord = smsReader.getNext()) != null) {
expiringMessageReferences.add(new ExpiringMessageReference(messageRecord.getId(),
messageRecord.isMms(),
messageRecord.getExpireStarted() + messageRecord.getExpiresIn()));
}
while ((messageRecord = mmsReader.getNext()) != null) {
expiringMessageReferences.add(new ExpiringMessageReference(messageRecord.getId(),
messageRecord.isMms(),
messageRecord.getExpireStarted() + messageRecord.getExpiresIn()));
}
}
}
private class ProcessTask implements Runnable {
public void run() {
while (true) {
ExpiringMessageReference expiredMessage = null;
synchronized (expiringMessageReferences) {
try {
while (expiringMessageReferences.isEmpty()) expiringMessageReferences.wait();
ExpiringMessageReference nextReference = expiringMessageReferences.first();
long waitTime = nextReference.expiresAtMillis - System.currentTimeMillis();
if (waitTime > 0) {
ExpirationListener.setAlarm(context, waitTime);
expiringMessageReferences.wait(waitTime);
} else {
expiredMessage = nextReference;
expiringMessageReferences.remove(nextReference);
}
} catch (InterruptedException e) {
Log.w(TAG, e);
}
}
if (expiredMessage != null) {
if (expiredMessage.mms) mmsDatabase.delete(expiredMessage.id);
else smsDatabase.deleteMessage(expiredMessage.id);
}
}
}
}
private static class ExpiringMessageReference {
private final long id;
private final boolean mms;
private final long expiresAtMillis;
private ExpiringMessageReference(long id, boolean mms, long expiresAtMillis) {
this.id = id;
this.mms = mms;
this.expiresAtMillis = expiresAtMillis;
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (!(other instanceof ExpiringMessageReference)) return false;
ExpiringMessageReference that = (ExpiringMessageReference)other;
return this.id == that.id && this.mms == that.mms && this.expiresAtMillis == that.expiresAtMillis;
}
@Override
public int hashCode() {
return (int)this.id ^ (mms ? 1 : 0) ^ (int)expiresAtMillis;
}
}
private static class ExpiringMessageComparator implements Comparator<ExpiringMessageReference> {
@Override
public int compare(ExpiringMessageReference lhs, ExpiringMessageReference rhs) {
if (lhs.expiresAtMillis < rhs.expiresAtMillis) return -1;
else if (lhs.expiresAtMillis > rhs.expiresAtMillis) return 1;
else if (lhs.id < rhs.id) return -1;
else if (lhs.id > rhs.id) return 1;
else if (!lhs.mms && rhs.mms) return -1;
else if (lhs.mms && !rhs.mms) return 1;
else return 0;
}
}
}

View File

@ -56,13 +56,14 @@ public class QuickResponseService extends MasterSecretIntentService {
Recipients recipients = RecipientFactory.getRecipientsFromString(this, numbers, false);
Optional<RecipientsPreferences> preferences = DatabaseFactory.getRecipientPreferenceDatabase(this).getRecipientsPreferences(recipients.getIds());
int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1;
long expiresIn = preferences.isPresent() ? preferences.get().getExpireMessages() * 1000 : 0;
if (!TextUtils.isEmpty(content)) {
if (recipients.isSingleRecipient()) {
MessageSender.send(this, masterSecret, new OutgoingTextMessage(recipients, content, subscriptionId), -1, false);
MessageSender.send(this, masterSecret, new OutgoingTextMessage(recipients, content, expiresIn, subscriptionId), -1, false);
} else {
MessageSender.send(this, masterSecret, new OutgoingMediaMessage(recipients, new SlideDeck(), content, System.currentTimeMillis(),
subscriptionId, ThreadDatabase.DistributionTypes.DEFAULT), -1, false);
subscriptionId, expiresIn, ThreadDatabase.DistributionTypes.DEFAULT), -1, false);
}
}
} catch (URISyntaxException e) {

View File

@ -6,7 +6,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
public class IncomingJoinedMessage extends IncomingTextMessage {
public IncomingJoinedMessage(String sender) {
super(sender, 1, System.currentTimeMillis(), null, Optional.<SignalServiceGroup>absent());
super(sender, 1, System.currentTimeMillis(), null, Optional.<SignalServiceGroup>absent(), 0);
}
@Override

View File

@ -36,6 +36,7 @@ public class IncomingTextMessage implements Parcelable {
private final String groupId;
private final boolean push;
private final int subscriptionId;
private final long expiresInMillis;
public IncomingTextMessage(SmsMessage message, int subscriptionId) {
this.message = message.getDisplayMessageBody();
@ -47,12 +48,14 @@ public class IncomingTextMessage implements Parcelable {
this.pseudoSubject = message.getPseudoSubject();
this.sentTimestampMillis = message.getTimestampMillis();
this.subscriptionId = subscriptionId;
this.expiresInMillis = 0;
this.groupId = null;
this.push = false;
}
public IncomingTextMessage(String sender, int senderDeviceId, long sentTimestampMillis,
String encodedBody, Optional<SignalServiceGroup> group)
String encodedBody, Optional<SignalServiceGroup> group,
long expiresInMillis)
{
this.message = encodedBody;
this.sender = sender;
@ -64,6 +67,7 @@ public class IncomingTextMessage implements Parcelable {
this.sentTimestampMillis = sentTimestampMillis;
this.push = true;
this.subscriptionId = -1;
this.expiresInMillis = expiresInMillis;
if (group.isPresent()) {
this.groupId = GroupUtil.getEncodedId(group.get().getGroupId());
@ -84,6 +88,7 @@ public class IncomingTextMessage implements Parcelable {
this.groupId = in.readString();
this.push = (in.readInt() == 1);
this.subscriptionId = in.readInt();
this.expiresInMillis = in.readLong();
}
public IncomingTextMessage(IncomingTextMessage base, String newBody) {
@ -98,6 +103,7 @@ public class IncomingTextMessage implements Parcelable {
this.groupId = base.getGroupId();
this.push = base.isPush();
this.subscriptionId = base.getSubscriptionId();
this.expiresInMillis = base.getExpiresIn();
}
public IncomingTextMessage(List<IncomingTextMessage> fragments) {
@ -118,6 +124,7 @@ public class IncomingTextMessage implements Parcelable {
this.groupId = fragments.get(0).getGroupId();
this.push = fragments.get(0).isPush();
this.subscriptionId = fragments.get(0).getSubscriptionId();
this.expiresInMillis = fragments.get(0).getExpiresIn();
}
protected IncomingTextMessage(String sender, String groupId)
@ -133,12 +140,17 @@ public class IncomingTextMessage implements Parcelable {
this.groupId = groupId;
this.push = true;
this.subscriptionId = -1;
this.expiresInMillis = 0;
}
public int getSubscriptionId() {
return subscriptionId;
}
public long getExpiresIn() {
return expiresInMillis;
}
public long getSentTimestampMillis() {
return sentTimestampMillis;
}

View File

@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@ -77,7 +78,7 @@ public class MessageSender {
long messageId = database.insertMessageOutbox(new MasterSecretUnion(masterSecret), allocatedThreadId,
message, forceSms, System.currentTimeMillis());
sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
sendTextMessage(context, recipients, forceSms, keyExchange, messageId, message.getExpiresIn());
return allocatedThreadId;
}
@ -103,7 +104,7 @@ public class MessageSender {
Recipients recipients = message.getRecipients();
long messageId = database.insertMessageOutbox(new MasterSecretUnion(masterSecret), message, allocatedThreadId, forceSms);
sendMediaMessage(context, masterSecret, recipients, forceSms, messageId);
sendMediaMessage(context, masterSecret, recipients, forceSms, messageId, message.getExpiresIn());
return allocatedThreadId;
} catch (MmsException e) {
@ -124,13 +125,14 @@ public class MessageSender {
long messageId = messageRecord.getId();
boolean forceSms = messageRecord.isForcedSms();
boolean keyExchange = messageRecord.isKeyExchange();
long expiresIn = messageRecord.getExpiresIn();
if (messageRecord.isMms()) {
Recipients recipients = DatabaseFactory.getMmsAddressDatabase(context).getRecipientsForId(messageId);
sendMediaMessage(context, masterSecret, recipients, forceSms, messageId);
sendMediaMessage(context, masterSecret, recipients, forceSms, messageId, expiresIn);
} else {
Recipients recipients = messageRecord.getRecipients();
sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
sendTextMessage(context, recipients, forceSms, keyExchange, messageId, expiresIn);
}
} catch (MmsException e) {
Log.w(TAG, e);
@ -138,11 +140,12 @@ public class MessageSender {
}
private static void sendMediaMessage(Context context, MasterSecret masterSecret,
Recipients recipients, boolean forceSms, long messageId)
Recipients recipients, boolean forceSms,
long messageId, long expiresIn)
throws MmsException
{
if (!forceSms && isSelfSend(context, recipients)) {
sendMediaSelf(context, masterSecret, messageId);
sendMediaSelf(context, masterSecret, messageId, expiresIn);
} else if (isGroupPushSend(recipients)) {
sendGroupPush(context, recipients, messageId, -1);
} else if (!forceSms && isPushMediaSend(context, recipients)) {
@ -153,10 +156,11 @@ public class MessageSender {
}
private static void sendTextMessage(Context context, Recipients recipients,
boolean forceSms, boolean keyExchange, long messageId)
boolean forceSms, boolean keyExchange,
long messageId, long expiresIn)
{
if (!forceSms && isSelfSend(context, recipients)) {
sendTextSelf(context, messageId);
sendTextSelf(context, messageId, expiresIn);
} else if (!forceSms && isPushTextSend(context, recipients, keyExchange)) {
sendTextPush(context, recipients, messageId);
} else {
@ -164,7 +168,7 @@ public class MessageSender {
}
}
private static void sendTextSelf(Context context, long messageId) {
private static void sendTextSelf(Context context, long messageId, long expiresIn) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
database.markAsSent(messageId);
@ -172,17 +176,32 @@ public class MessageSender {
Pair<Long, Long> messageAndThreadId = database.copyMessageInbox(messageId);
database.markAsPush(messageAndThreadId.first);
if (expiresIn > 0) {
ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
database.markExpireStarted(messageId);
expiringMessageManager.scheduleDeletion(messageId, false, expiresIn);
}
}
private static void sendMediaSelf(Context context, MasterSecret masterSecret, long messageId)
private static void sendMediaSelf(Context context, MasterSecret masterSecret,
long messageId, long expiresIn)
throws MmsException
{
ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.markAsSent(messageId);
database.markAsPush(messageId);
long newMessageId = database.copyMessageInbox(masterSecret, messageId);
database.markAsPush(newMessageId);
if (expiresIn > 0) {
database.markExpireStarted(messageId);
expiringMessageManager.scheduleDeletion(messageId, true, expiresIn);
}
}
private static void sendTextPush(Context context, Recipients recipients, long messageId) {

View File

@ -1,12 +1,11 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class OutgoingEncryptedMessage extends OutgoingTextMessage {
public OutgoingEncryptedMessage(Recipients recipients, String body) {
super(recipients, body, -1);
public OutgoingEncryptedMessage(Recipients recipients, String body, long expiresIn) {
super(recipients, body, expiresIn, -1);
}
private OutgoingEncryptedMessage(OutgoingEncryptedMessage base, String body) {

View File

@ -8,19 +8,30 @@ public class OutgoingTextMessage {
private final Recipients recipients;
private final String message;
private final int subscriptionId;
private final long expiresIn;
public OutgoingTextMessage(Recipients recipients, String message, int subscriptionId) {
this(recipients, message, 0, subscriptionId);
}
public OutgoingTextMessage(Recipients recipients, String message, long expiresIn, int subscriptionId) {
this.recipients = recipients;
this.message = message;
this.expiresIn = expiresIn;
this.subscriptionId = subscriptionId;
}
protected OutgoingTextMessage(OutgoingTextMessage base, String body) {
this.recipients = base.getRecipients();
this.subscriptionId = base.getSubscriptionId();
this.expiresIn = base.getExpiresIn();
this.message = body;
}
public long getExpiresIn() {
return expiresIn;
}
public int getSubscriptionId() {
return subscriptionId;
}
@ -51,13 +62,13 @@ public class OutgoingTextMessage {
public static OutgoingTextMessage from(SmsMessageRecord record) {
if (record.isSecure()) {
return new OutgoingEncryptedMessage(record.getRecipients(), record.getBody().getBody());
return new OutgoingEncryptedMessage(record.getRecipients(), record.getBody().getBody(), record.getExpiresIn());
} else if (record.isKeyExchange()) {
return new OutgoingKeyExchangeMessage(record.getRecipients(), record.getBody().getBody());
} else if (record.isEndSession()) {
return new OutgoingEndSessionMessage(new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody(), -1));
return new OutgoingEndSessionMessage(new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody(), 0, -1));
} else {
return new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody(), record.getSubscriptionId());
return new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody(), record.getExpiresIn(), record.getSubscriptionId());
}
}

View File

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import org.thoughtcrime.securesms.R;
import java.util.concurrent.TimeUnit;
public class ExpirationUtil {
public static String getExpirationDisplayValue(Context context, int expirationTime) {
if (expirationTime <= 0) {
return context.getString(R.string.expiration_off);
} else if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
return context.getResources().getQuantityString(R.plurals.expiration_seconds, expirationTime, expirationTime);
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1);
return context.getResources().getQuantityString(R.plurals.expiration_minutes, minutes, minutes);
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1);
return context.getResources().getQuantityString(R.plurals.expiration_hours, hours, hours);
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1);
return context.getResources().getQuantityString(R.plurals.expiration_days, days, days);
} else {
int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7);
return context.getResources().getQuantityString(R.plurals.expiration_weeks, weeks, weeks);
}
}
public static String getExpirationAbbreviatedDisplayValue(Context context, int expirationTime) {
if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
return context.getResources().getString(R.string.expiration_seconds_abbreviated, expirationTime);
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1);
return context.getResources().getString(R.string.expiration_minutes_abbreviated, minutes);
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1);
return context.getResources().getString(R.string.expiration_hours_abbreviated, hours);
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1);
return context.getResources().getString(R.string.expiration_days_abbreviated, days);
} else {
int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7);
return context.getResources().getString(R.string.expiration_weeks_abbreviated, weeks);
}
}
}