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:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7' compile 'org.whispersystems:libpastelog:1.0.7'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' 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.h6ah4i.android.compat:mulsellistprefcompat:1.0.0'
compile 'com.google.zxing:core:3.2.1' 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 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'org.assertj:assertj-core:1.7.1'
testCompile 'org.mockito:mockito-core:1.9.5' testCompile 'org.mockito:mockito-core:1.9.5'
@ -97,57 +101,58 @@ dependencies {
dependencyVerification { dependencyVerification {
verify = [ verify = [
'me.leolin:ShortcutBadger:3142d017234bfa0cdd69ccded7cc5ea63f13b97574803c8c616c9bbeaad33ad9', 'me.leolin:ShortcutBadger:3142d017234bfa0cdd69ccded7cc5ea63f13b97574803c8c616c9bbeaad33ad9',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
'com.google.android.gms:play-services-gcm:757ecd2c837ac81c98f4cc7dc783e7454c6d0506f6cc66b10417126b675248c9', 'com.google.android.gms:play-services-gcm:757ecd2c837ac81c98f4cc7dc783e7454c6d0506f6cc66b10417126b675248c9',
'com.google.android.gms:play-services-maps:c58a9d98a98889fb0b27f78100f2d9341ed7722db24ccf832df62b6e8ce1b42e', 'com.google.android.gms:play-services-maps:c58a9d98a98889fb0b27f78100f2d9341ed7722db24ccf832df62b6e8ce1b42e',
'com.google.android.gms:play-services-location:8226f778aa86bd15b9143f62425262cc53d64021990f62eb1aaec108d4e25f35', 'com.google.android.gms:play-services-location:8226f778aa86bd15b9143f62425262cc53d64021990f62eb1aaec108d4e25f35',
'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa', 'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa',
'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b', 'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b',
'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1', 'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc', 'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc',
'com.github.bumptech.glide:glide:76ef123957b5fbaebb05fcbe6606dd58c3bc3fcdadb257f99811d0ac9ea9b88b', 'com.github.bumptech.glide:glide:76ef123957b5fbaebb05fcbe6606dd58c3bc3fcdadb257f99811d0ac9ea9b88b',
'com.makeramen:roundedimageview:1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1', 'com.makeramen:roundedimageview:1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1',
'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54', 'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54',
'de.greenrobot:eventbus:61d743a748156a372024d083de763b9e91ac2dcb3f6a1cbc74995c7ddab6e968', 'de.greenrobot:eventbus:61d743a748156a372024d083de763b9e91ac2dcb3f6a1cbc74995c7ddab6e968',
'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c', 'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c',
'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177', 'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
'com.android.support:appcompat-v7:4b5ccba8c4557ef04f99aa0a80f8aa7d50f05f926a709010a54afd5c878d3618', 'com.android.support:appcompat-v7:4b5ccba8c4557ef04f99aa0a80f8aa7d50f05f926a709010a54afd5c878d3618',
'com.android.support:recyclerview-v7:b0f530a5b14334d56ce0de85527ffe93ac419bc928e2884287ce1dddfedfb505', 'com.android.support:recyclerview-v7:b0f530a5b14334d56ce0de85527ffe93ac419bc928e2884287ce1dddfedfb505',
'com.android.support:design:58be3ca6a73789615f7ece0937d2f683b98b594bb90aa10565fa760fb10b07ee', 'com.android.support:design:58be3ca6a73789615f7ece0937d2f683b98b594bb90aa10565fa760fb10b07ee',
'com.android.support:cardview-v7:2c2354761a4e20ba451ae903ab808f15c9acc8343b1e74001869c2d0a672c1fc', 'com.android.support:cardview-v7:2c2354761a4e20ba451ae903ab808f15c9acc8343b1e74001869c2d0a672c1fc',
'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263', 'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4', 'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad', 'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
'com.android.support:gridlayout-v7:a9b770cffca2c7c5cd83cba4dd12503365de5e8d9c79c479165adf18ab3bc25b', 'com.android.support:gridlayout-v7:a9b770cffca2c7c5cd83cba4dd12503365de5e8d9c79c479165adf18ab3bc25b',
'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883', 'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d', 'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a', 'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181', 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88', 'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb', '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.h6ah4i.android.compat:mulsellistprefcompat:47167c5cb796de1a854788e9ff318358e36c8fb88123baaa6e38fb78511dfabe',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259', 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'com.google.android.gms:play-services-base:ef36e50fa5c0415ed41f74dd399a889efd2fa327c449036e140c7c3786aa0e1f', 'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729',
'com.android.support:support-annotations:104f353b53d5dd8d64b2f77eece4b37f6b961de9732eb6b706395e91033ec70a', 'com.google.android.gms:play-services-base:ef36e50fa5c0415ed41f74dd399a889efd2fa327c449036e140c7c3786aa0e1f',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', 'com.android.support:support-annotations:104f353b53d5dd8d64b2f77eece4b37f6b961de9732eb6b706395e91033ec70a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'org.whispersystems:signal-service-java:48db52056aa3510deb8c4ccd2dfb35033ae115bc4176048820c6dff73290ba6e', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'org.whispersystems:signal-protocol-android:d83cb3d15b667fc2543fa18ce80791c72c053e8ac54fc2941f0429a5944ca691', 'org.whispersystems:signal-protocol-android:d83cb3d15b667fc2543fa18ce80791c72c053e8ac54fc2941f0429a5944ca691',
'com.google.android.gms:play-services-basement:e1d29b21e02fd2a63e5a31807415cbb17a59568e27e3254181c01ffae10659bf', 'org.whispersystems:signal-service-java:7932363fec666fdc0b4b424eeca4bdca235f6bf2f226fb6a6ff742c49fc37087',
'com.googlecode.libphonenumber:libphonenumber:9625de9d2270e9a280ff4e6d9ef3106573fb4828773fd32c9b7614f4e17d2811', 'com.google.android.gms:play-services-basement:e1d29b21e02fd2a63e5a31807415cbb17a59568e27e3254181c01ffae10659bf',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74', 'org.whispersystems:curve25519-android:d6a3ef3a70622af4c728b7fe5f8fdfc9e6cd39b1d39b2c77e7a2add9d876bc23',
'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af', 'org.whispersystems:signal-protocol-java:d518d52eeb3c44210e0b6c687360848a87afbaee0bdf42e2a8dd9974d54fdb3a',
'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d', 'com.googlecode.libphonenumber:libphonenumber:9625de9d2270e9a280ff4e6d9ef3106573fb4828773fd32c9b7614f4e17d2811',
'org.whispersystems:curve25519-android:d6a3ef3a70622af4c728b7fe5f8fdfc9e6cd39b1d39b2c77e7a2add9d876bc23', 'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'org.whispersystems:signal-protocol-java:d518d52eeb3c44210e0b6c687360848a87afbaee0bdf42e2a8dd9974d54fdb3a', 'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af',
'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978', 'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94', 'org.whispersystems:curve25519-java:08cc3be52723e0fc4148e5e7002d51d6d7e495b2130022237f2d47b90af6ae0b',
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0', 'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978',
'org.whispersystems:curve25519-java:08cc3be52723e0fc4148e5e7002d51d6d7e495b2130022237f2d47b90af6ae0b', 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
'com.android.support:support-v4:c62f0d025dafa86f423f48df9185b0d89496adbc5f6a9be5a7c394d84cf91423', 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
'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_toRightOf="@id/contact_photo"
android:layout_marginRight="35dp" android:layout_marginRight="35dp"
android:background="@drawable/received_bubble" android:background="@drawable/received_bubble"
android:orientation="vertical"> android:orientation="vertical"
tools:backgroundTint="@color/blue_900">
<org.thoughtcrime.securesms.components.ThumbnailView <org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/image_view" android:id="@+id/image_view"
@ -115,14 +116,28 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:paddingTop="2dp" android:paddingRight="2dp"
android:paddingRight="4dp"
android:paddingEnd="4dp" android:paddingEnd="4dp"
android:src="?menu_lock_icon_small" android:src="?menu_lock_icon_small"
android:contentDescription="@string/conversation_item__secure_message_description" android:contentDescription="@string/conversation_item__secure_message_description"
android:visibility="gone" android:visibility="gone"
android:tint="?conversation_item_received_text_secondary_color" 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 <org.thoughtcrime.securesms.components.DeliveryStatusView
android:id="@+id/delivery_status" android:id="@+id/delivery_status"
@ -141,7 +156,8 @@
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:autoLink="none" android:autoLink="none"
android:linksClickable="false" android:linksClickable="false"
tools:text="Now"/> tools:text="Now"
tools:visibility="visible"/>
<TextView android:id="@+id/sim_info" <TextView android:id="@+id/sim_info"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -120,7 +120,7 @@
android:minWidth="15sp" android:minWidth="15sp"
android:linksClickable="false" android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="right" android:layout_gravity="right|bottom"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:textColor="?conversation_item_sent_text_secondary_color" android:textColor="?conversation_item_sent_text_secondary_color"
android:textSize="@dimen/conversation_item_date_text_size" android:textSize="@dimen/conversation_item_date_text_size"
@ -135,7 +135,7 @@
android:minWidth="15sp" android:minWidth="15sp"
android:linksClickable="false" android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="right" android:layout_gravity="right|bottom"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:textColor="?conversation_item_sent_text_secondary_color" android:textColor="?conversation_item_sent_text_secondary_color"
android:textSize="@dimen/conversation_item_date_text_size" android:textSize="@dimen/conversation_item_date_text_size"
@ -151,8 +151,24 @@
android:id="@+id/delivery_status" android:id="@+id/delivery_status"
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alpha=".7"
app:iconColor="?conversation_item_sent_text_secondary_color"/> 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" <ImageView android:id="@+id/secure_indicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -160,10 +176,10 @@
android:visibility="gone" android:visibility="gone"
android:layout_gravity="center_vertical|end" android:layout_gravity="center_vertical|end"
android:paddingLeft="2dp" android:paddingLeft="2dp"
android:paddingBottom="3dp"
android:tint="?conversation_item_sent_text_secondary_color" android:tint="?conversation_item_sent_text_secondary_color"
android:tintMode="multiply" android:tintMode="multiply"
android:contentDescription="@string/conversation_item__secure_message_description" /> android:contentDescription="@string/conversation_item__secure_message_description"
tools:visibility="visible"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

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

View File

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?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" <FrameLayout android:id="@+id/pending_indicator_stub"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:paddingRight="2dp" android:paddingRight="2dp"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
tools:visibility="gone"/>
<ImageView android:id="@+id/sent_indicator" <ImageView android:id="@+id/sent_indicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -24,6 +26,7 @@
android:paddingLeft="2dp" android:paddingLeft="2dp"
android:paddingBottom="2dp" android:paddingBottom="2dp"
android:visibility="gone" android:visibility="gone"
android:contentDescription="@string/conversation_item_sent__delivered_description" /> android:contentDescription="@string/conversation_item_sent__delivered_description"
tools:visibility="visible"/>
</merge> </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>
<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" <TableRow android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/message_details_table_row_pad"> android:padding="@dimen/message_details_table_row_pad">

View File

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

View File

@ -5,6 +5,6 @@
<item android:title="@string/conversation_callable_secure__menu_call" <item android:title="@string/conversation_callable_secure__menu_call"
android:id="@+id/menu_call_secure" android:id="@+id/menu_call_secure"
android:icon="@drawable/ic_call_secure_white_24dp" android:icon="@drawable/ic_call_secure_white_24dp"
app:showAsAction="ifRoom" /> app:showAsAction="always" />
</menu> </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> <item>@null</item>
</array> </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> </resources>

View File

@ -166,4 +166,12 @@
<attr name="camera" format="integer"/> <attr name="camera" format="integer"/>
</declare-styleable> </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> </resources>

View File

@ -379,6 +379,8 @@
<string name="MessageRecord_called_s">Called %s</string> <string name="MessageRecord_called_s">Called %s</string>
<string name="MessageRecord_missed_call_from">Missed call from %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_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 --> <!-- PassphraseChangeActivity -->
@ -407,6 +409,11 @@
<string name="DeviceProvisioningActivity_link_a_signal_device">Link a Signal device?</string> <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> <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 --> <!-- PassphrasePromptActivity -->
<string name="PassphrasePromptActivity_enter_passphrase">Enter passphrase</string> <string name="PassphrasePromptActivity_enter_passphrase">Enter passphrase</string>
<string name="PassphrasePromptActivity_watermark_content_description">Signal icon</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_missed_call">Missed call</string>
<string name="ThreadRecord_media_message">Media message</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_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 --> <!-- VerifyIdentityActivity -->
<string name="VerifyIdentityActivity_you_do_not_have_an_identity_key">You do not have an identity key.</string> <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__no_devices_linked">No devices linked...</string>
<string name="device_list_fragment__link_new_device">Link new device</string> <string name="device_list_fragment__link_new_device">Link new device</string>
<!-- experience_upgrade_activity --> <!-- experience_upgrade_activity -->
<string name="experience_upgrade_activity__continue">continue</string> <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 --> <!-- 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__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> <string name="log_submit_activity__thanks">Thanks for your help!</string>
@ -1078,6 +1124,12 @@
<!-- conversation_context_image --> <!-- conversation_context_image -->
<string name="conversation_context_image__save_attachment">Save attachment</string> <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 --> <!-- conversation_insecure -->
<string name="conversation_insecure__invite">Invite</string> <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.MasterSecretRequirementProvider;
import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirementProvider; import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirementProvider;
import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirementProvider; import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirementProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobManager; import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.dependencies.DependencyInjector; import org.whispersystems.jobqueue.dependencies.DependencyInjector;
@ -52,8 +53,9 @@ import dagger.ObjectGraph;
*/ */
public class ApplicationContext extends Application implements DependencyInjector { public class ApplicationContext extends Application implements DependencyInjector {
private JobManager jobManager; private ExpiringMessageManager expiringMessageManager;
private ObjectGraph objectGraph; private JobManager jobManager;
private ObjectGraph objectGraph;
private MediaNetworkRequirementProvider mediaNetworkRequirementProvider = new MediaNetworkRequirementProvider(); private MediaNetworkRequirementProvider mediaNetworkRequirementProvider = new MediaNetworkRequirementProvider();
@ -69,6 +71,7 @@ public class ApplicationContext extends Application implements DependencyInjecto
initializeLogging(); initializeLogging();
initializeDependencyInjection(); initializeDependencyInjection();
initializeJobManager(); initializeJobManager();
initializeExpiringMessageManager();
initializeGcmCheck(); initializeGcmCheck();
initializeSignedPreKeyCheck(); initializeSignedPreKeyCheck();
} }
@ -84,9 +87,12 @@ public class ApplicationContext extends Application implements DependencyInjecto
return jobManager; return jobManager;
} }
public ExpiringMessageManager getExpiringMessageManager() {
return expiringMessageManager;
}
private void initializeDeveloperBuild() { private void initializeDeveloperBuild() {
if (BuildConfig.DEV_BUILD) { if (BuildConfig.DEV_BUILD) {
// LeakCanary.install(this);
StrictMode.setThreadPolicy(new ThreadPolicy.Builder().detectAll() StrictMode.setThreadPolicy(new ThreadPolicy.Builder().detectAll()
.penaltyLog() .penaltyLog()
.build()); .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 Locale locale,
@NonNull Set<MessageRecord> batchSelected, @NonNull Set<MessageRecord> batchSelected,
@NonNull Recipients recipients); @NonNull Recipients recipients);
MessageRecord getMessageRecord();
} }

View File

@ -36,6 +36,7 @@ import android.os.Vibrator;
import android.provider.Browser; import android.provider.Browser;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.WindowCompat; import android.support.v4.view.WindowCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.Editable; 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.Draft;
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts; import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types; import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; 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.AudioSlide;
import org.thoughtcrime.securesms.mms.LocationSlide; import org.thoughtcrime.securesms.mms.LocationSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; 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.DirectoryHelper.UserCapabilities.Capability;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; 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.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException; import java.io.IOException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -200,7 +201,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private AttachmentManager attachmentManager; private AttachmentManager attachmentManager;
private AudioRecorder audioRecorder; private AudioRecorder audioRecorder;
private BroadcastReceiver securityUpdateReceiver; private BroadcastReceiver securityUpdateReceiver;
private BroadcastReceiver groupUpdateReceiver; private BroadcastReceiver recipientsStaleReceiver;
private EmojiDrawer emojiDrawer; private EmojiDrawer emojiDrawer;
protected HidingLinearLayout quickAttachmentToggle; protected HidingLinearLayout quickAttachmentToggle;
private QuickAttachmentDrawer quickAttachmentDrawer; private QuickAttachmentDrawer quickAttachmentDrawer;
@ -318,9 +319,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
protected void onDestroy() { protected void onDestroy() {
saveDraft(); saveDraft();
if (recipients != null) recipients.removeListener(this); if (recipients != null) recipients.removeListener(this);
if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver); if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver);
if (groupUpdateReceiver != null) unregisterReceiver(groupUpdateReceiver); if (recipientsStaleReceiver != null) unregisterReceiver(recipientsStaleReceiver);
super.onDestroy(); super.onDestroy();
} }
@ -382,6 +383,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MenuInflater inflater = this.getMenuInflater(); MenuInflater inflater = this.getMenuInflater();
menu.clear(); 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 (isSingleConversation()) {
if (isSecureVoice) inflater.inflate(R.menu.conversation_callable_secure, menu); if (isSecureVoice) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, 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_mute_notifications: handleMuteNotifications(); return true;
case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true; case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true;
case R.id.menu_conversation_settings: handleConversationSettings(); 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; case android.R.id.home: handleReturnToConversationList(); return true;
} }
@ -465,6 +488,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
finish(); 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() { private void handleMuteNotifications() {
MuteDialog.show(this, new MuteDialog.MuteSelectionListener() { MuteDialog.show(this, new MuteDialog.MuteSelectionListener() {
@Override @Override
@ -547,7 +592,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final Context context = getApplicationContext(); final Context context = getApplicationContext();
OutgoingEndSessionMessage endSessionMessage = OutgoingEndSessionMessage endSessionMessage =
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE", -1)); new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE", 0, -1));
new AsyncTask<OutgoingEndSessionMessage, Void, Long>() { new AsyncTask<OutgoingEndSessionMessage, Void, Long>() {
@Override @Override
@ -599,7 +644,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.setType(GroupContext.Type.QUIT) .setType(GroupContext.Type.QUIT)
.build(); .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); MessageSender.send(self, masterSecret, outgoingMessage, threadId, false);
DatabaseFactory.getGroupDatabase(self).remove(groupId, TextSecurePreferences.getLocalNumber(self)); DatabaseFactory.getGroupDatabase(self).remove(groupId, TextSecurePreferences.getLocalNumber(self));
initializeEnabledCheck(); initializeEnabledCheck();
@ -979,7 +1024,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void initializeResources() { private void initializeResources() {
if (recipients != null) recipients.removeListener(this); if (recipients != null) recipients.removeListener(this);
recipients = RecipientFactory.getRecipientsForIds(this, getIntent().getLongArrayExtra(RECIPIENTS_EXTRA), true); recipients = RecipientFactory.getRecipientsForIds(this, getIntent().getLongArrayExtra(RECIPIENTS_EXTRA), true);
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
archived = getIntent().getBooleanExtra(IS_ARCHIVED_EXTRA, false); archived = getIntent().getBooleanExtra(IS_ARCHIVED_EXTRA, false);
@ -1002,6 +1047,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
titleView.setTitle(recipients); titleView.setTitle(recipients);
setBlockedUserState(recipients); setBlockedUserState(recipients);
setActionBarColor(recipients.getColor()); setActionBarColor(recipients.getColor());
invalidateOptionsMenu();
updateRecipientPreferences(); updateRecipientPreferences();
} }
}); });
@ -1016,26 +1062,30 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
}; };
groupUpdateReceiver = new BroadcastReceiver() { recipientsStaleReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
Log.w("ConversationActivity", "Group update received..."); Log.w(TAG, "Group update received...");
if (recipients != null) { if (recipients != null) {
long[] ids = recipients.getIds(); 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 = RecipientFactory.getRecipientsForIds(context, ids, true);
recipients.addListener(ConversationActivity.this); 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, registerReceiver(securityUpdateReceiver,
new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT), new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT),
KeyCachingService.KEY_PERMISSION, null); KeyCachingService.KEY_PERMISSION, null);
registerReceiver(groupUpdateReceiver, registerReceiver(recipientsStaleReceiver, staleFilter);
new IntentFilter(GroupDatabase.DATABASE_UPDATE_ACTION));
} }
//////// Helper Methods //////// Helper Methods
@ -1281,6 +1331,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Recipients recipients = getRecipients(); Recipients recipients = getRecipients();
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms(); boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1); int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
long expiresIn = recipients.getExpireMessages() * 1000;
Log.w(TAG, "isManual Selection: " + sendButton.isManualSelection()); Log.w(TAG, "isManual Selection: " + sendButton.isManualSelection());
Log.w(TAG, "forceSms: " + forceSms); Log.w(TAG, "forceSms: " + forceSms);
@ -1292,9 +1343,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) { if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
handleManualMmsRequired(); handleManualMmsRequired();
} else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) { } else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
sendMediaMessage(forceSms, subscriptionId); sendMediaMessage(forceSms, expiresIn, subscriptionId);
} else { } else {
sendTextMessage(forceSms, subscriptionId); sendTextMessage(forceSms, expiresIn, subscriptionId);
} }
} catch (RecipientFormattingException ex) { } catch (RecipientFormattingException ex) {
Toast.makeText(ConversationActivity.this, 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 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 throws InvalidMessageException
{ {
final SettableFuture<Void> future = new SettableFuture<>(); final SettableFuture<Void> future = new SettableFuture<>();
@ -1324,6 +1375,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
body, body,
System.currentTimeMillis(), System.currentTimeMillis(),
subscriptionId, subscriptionId,
expiresIn,
distributionType); distributionType);
if (isSecureText && !forceSms) { if (isSecureText && !forceSms) {
@ -1349,16 +1401,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return future; 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 throws InvalidMessageException
{ {
final Context context = getApplicationContext(); final Context context = getApplicationContext();
OutgoingTextMessage message; OutgoingTextMessage message;
if (isSecureText && !forceSms) { if (isSecureText && !forceSms) {
message = new OutgoingEncryptedMessage(recipients, getMessage()); message = new OutgoingEncryptedMessage(recipients, getMessage(), expiresIn);
} else { } else {
message = new OutgoingTextMessage(recipients, getMessage(), subscriptionId); message = new OutgoingTextMessage(recipients, getMessage(), expiresIn, subscriptionId);
} }
this.composeText.setText(""); this.composeText.setText("");
@ -1451,11 +1503,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
try { try {
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms(); boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1); 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); AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, ContentType.AUDIO_AAC);
SlideDeck slideDeck = new SlideDeck(); SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide); slideDeck.addSlide(audioSlide);
sendMediaMessage(forceSms, "", slideDeck, subscriptionId).addListener(new AssertedSuccessListener<Void>() { sendMediaMessage(forceSms, "", slideDeck, expiresIn, subscriptionId).addListener(new AssertedSuccessListener<Void>() {
@Override @Override
public void onSuccess(Void nothing) { public void onSuccess(Void nothing) {
new AsyncTask<Void, Void, Void>() { 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.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; 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.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.LRUCache; 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.lang.ref.SoftReference;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -49,9 +50,6 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.VisibleForTesting;
/** /**
* A cursor adapter for a conversation thread. Ultimately * A cursor adapter for a conversation thread. Ultimately
* used by ComposeMessageActivity to display a conversation * used by ComposeMessageActivity to display a conversation
@ -94,8 +92,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
} }
public interface ItemClickListener { public interface ItemClickListener {
void onItemClick(ConversationItem item); void onItemClick(MessageRecord item);
void onItemLongClick(ConversationItem item); void onItemLongClick(MessageRecord item);
} }
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@ -156,21 +154,23 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@Override @Override
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType)); final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
if (viewType == MESSAGE_TYPE_INCOMING || viewType == MESSAGE_TYPE_OUTGOING) { itemView.setOnClickListener(new OnClickListener() {
itemView.setOnClickListener(new OnClickListener() { @Override
@Override public void onClick(View view) {
public void onClick(View view) { if (clickListener != null) {
if (clickListener != null) clickListener.onItemClick((ConversationItem)itemView); clickListener.onItemClick(itemView.getMessageRecord());
} }
}); }
itemView.setOnLongClickListener(new OnLongClickListener() { });
@Override itemView.setOnLongClickListener(new OnLongClickListener() {
public boolean onLongClick(View view) { @Override
if (clickListener != null) clickListener.onItemLongClick((ConversationItem)itemView); public boolean onLongClick(View view) {
return true; if (clickListener != null) {
clickListener.onItemLongClick(itemView.getMessageRecord());
} }
}); return true;
} }
});
return new ViewHolder(itemView); return new ViewHolder(itemView);
} }
@ -195,7 +195,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
MessageRecord messageRecord = getMessageRecord(id, cursor, type); 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; return MESSAGE_TYPE_UPDATE;
} else if (messageRecord.isOutgoing()) { } else if (messageRecord.isOutgoing()) {
return MESSAGE_TYPE_OUTGOING; 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.Fragment;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode; import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; 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.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender; 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;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -165,24 +165,34 @@ public class ConversationFragment extends Fragment
if (this.recipients != null && this.threadId != -1) { if (this.recipients != null && this.threadId != -1) {
list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients)); list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients));
getLoaderManager().restartLoader(0, Bundle.EMPTY, this); getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
list.getItemAnimator().setSupportsChangeAnimations(false);
list.getItemAnimator().setMoveDuration(120); list.getItemAnimator().setMoveDuration(120);
} }
} }
private void setCorrectMenuVisibility(Menu menu) { private void setCorrectMenuVisibility(Menu menu) {
Set<MessageRecord> messageRecords = getListAdapter().getSelectedItems(); Set<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
boolean actionMessage = false;
if (actionMode != null && messageRecords.size() == 0) { if (actionMode != null && messageRecords.size() == 0) {
actionMode.finish(); actionMode.finish();
return; return;
} }
for (MessageRecord messageRecord : messageRecords) {
if (messageRecord.isGroupAction() || messageRecord.isCallLog() ||
messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate())
{
actionMessage = true;
break;
}
}
if (messageRecords.size() > 1) { if (messageRecords.size() > 1) {
menu.findItem(R.id.menu_context_forward).setVisible(false); menu.findItem(R.id.menu_context_forward).setVisible(false);
menu.findItem(R.id.menu_context_details).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_save_attachment).setVisible(false);
menu.findItem(R.id.menu_context_resend).setVisible(false); menu.findItem(R.id.menu_context_resend).setVisible(false);
menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage);
} else { } else {
MessageRecord messageRecord = messageRecords.iterator().next(); MessageRecord messageRecord = messageRecords.iterator().next();
@ -191,9 +201,9 @@ public class ConversationFragment extends Fragment
!messageRecord.isMmsNotification() && !messageRecord.isMmsNotification() &&
((MediaMmsMessageRecord)messageRecord).containsMediaSlide()); ((MediaMmsMessageRecord)messageRecord).containsMediaSlide());
menu.findItem(R.id.menu_context_forward).setVisible(true); menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_details).setVisible(true); menu.findItem(R.id.menu_context_details).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_copy).setVisible(true); menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage);
} }
} }
@ -386,9 +396,8 @@ public class ConversationFragment extends Fragment
private class ConversationFragmentItemClickListener implements ItemClickListener { private class ConversationFragmentItemClickListener implements ItemClickListener {
@Override @Override
public void onItemClick(ConversationItem item) { public void onItemClick(MessageRecord messageRecord) {
if (actionMode != null) { if (actionMode != null) {
MessageRecord messageRecord = item.getMessageRecord();
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged(); list.getAdapter().notifyDataSetChanged();
@ -397,9 +406,9 @@ public class ConversationFragment extends Fragment
} }
@Override @Override
public void onItemLongClick(ConversationItem item) { public void onItemLongClick(MessageRecord messageRecord) {
if (actionMode == null) { if (actionMode == null) {
((ConversationAdapter) list.getAdapter()).toggleSelection(item.getMessageRecord()); ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged(); list.getAdapter().notifyDataSetChanged();
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback); actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);

View File

@ -23,6 +23,7 @@ import android.content.Intent;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog; 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.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView; import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.AlertView; import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.ExpirationTimerView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase; 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.mms.SlideClickListener;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -110,6 +113,7 @@ public class ConversationItem extends LinearLayout
private @NonNull AudioView audioView; private @NonNull AudioView audioView;
private @NonNull Button mmsDownloadButton; private @NonNull Button mmsDownloadButton;
private @NonNull TextView mmsDownloadingLabel; private @NonNull TextView mmsDownloadingLabel;
private @NonNull ExpirationTimerView expirationTimer;
private int defaultBubbleColor; private int defaultBubbleColor;
@ -151,6 +155,7 @@ public class ConversationItem extends LinearLayout
this.bodyBubble = findViewById(R.id.body_bubble); this.bodyBubble = findViewById(R.id.body_bubble);
this.mediaThumbnail = (ThumbnailView) findViewById(R.id.image_view); this.mediaThumbnail = (ThumbnailView) findViewById(R.id.image_view);
this.audioView = (AudioView) findViewById(R.id.audio_view); this.audioView = (AudioView) findViewById(R.id.audio_view);
this.expirationTimer = (ExpirationTimerView) findViewById(R.id.expiration_indicator);
setOnClickListener(new ClickListener(null)); setOnClickListener(new ClickListener(null));
PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
@ -194,6 +199,7 @@ public class ConversationItem extends LinearLayout
setMinimumWidth(); setMinimumWidth();
setMediaAttributes(messageRecord); setMediaAttributes(messageRecord);
setSimInfo(messageRecord); setSimInfo(messageRecord);
setExpiration(messageRecord);
} }
private void initializeAttributes() { private void initializeAttributes() {
@ -211,6 +217,8 @@ public class ConversationItem extends LinearLayout
if (recipient != null) { if (recipient != null) {
recipient.removeListener(this); recipient.removeListener(this);
} }
this.expirationTimer.stopAnimation();
} }
public MessageRecord getMessageRecord() { 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() { private void setFailedStatusIcons() {
alertView.setFailed(); alertView.setFailed();
deliveryStatusIndicator.setNone(); deliveryStatusIndicator.setNone();

View File

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

View File

@ -2,6 +2,9 @@ package org.thoughtcrime.securesms;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
@ -59,6 +62,17 @@ public class ConversationUpdateItem extends LinearLayout
@NonNull Recipients conversationRecipients) @NonNull Recipients conversationRecipients)
{ {
bind(messageRecord, locale); 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) { private void bind(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
@ -68,10 +82,11 @@ public class ConversationUpdateItem extends LinearLayout
this.sender.addListener(this); this.sender.addListener(this);
if (messageRecord.isGroupAction()) setGroupRecord(messageRecord); if (messageRecord.isGroupAction()) setGroupRecord(messageRecord);
else if (messageRecord.isCallLog()) setCallRecord(messageRecord); else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
else if (messageRecord.isJoined()) setJoinedRecord(messageRecord); else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
else throw new AssertionError("Neither group nor log nor joined."); else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
else throw new AssertionError("Neither group nor log nor joined.");
} }
private void setCallRecord(MessageRecord messageRecord) { private void setCallRecord(MessageRecord messageRecord) {
@ -84,8 +99,22 @@ public class ConversationUpdateItem extends LinearLayout
date.setVisibility(View.VISIBLE); 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) { private void setGroupRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_group_grey600_24dp); icon.setImageResource(R.drawable.ic_group_grey600_24dp);
icon.clearColorFilter();
GroupUtil.getDescription(getContext(), messageRecord.getBody().getBody()).addListener(this); GroupUtil.getDescription(getContext(), messageRecord.getBody().getBody()).addListener(this);
body.setText(messageRecord.getDisplayBody()); body.setText(messageRecord.getDisplayBody());
@ -95,6 +124,7 @@ public class ConversationUpdateItem extends LinearLayout
private void setJoinedRecord(MessageRecord messageRecord) { private void setJoinedRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_favorite_grey600_24dp); icon.setImageResource(R.drawable.ic_favorite_grey600_24dp);
icon.clearColorFilter();
body.setText(messageRecord.getDisplayBody()); body.setText(messageRecord.getDisplayBody());
date.setVisibility(View.GONE); 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.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.util.Log; 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.DateUtils;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -79,9 +81,11 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private ConversationItem conversationItem; private ConversationItem conversationItem;
private ViewGroup itemParent; private ViewGroup itemParent;
private View metadataContainer; private View metadataContainer;
private View expiresContainer;
private TextView errorText; private TextView errorText;
private TextView sentDate; private TextView sentDate;
private TextView receivedDate; private TextView receivedDate;
private TextView expiresInText;
private View receivedContainer; private View receivedContainer;
private TextView transport; private TextView transport;
private TextView toFrom; private TextView toFrom;
@ -91,6 +95,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private DynamicTheme dynamicTheme = new DynamicTheme(); private DynamicTheme dynamicTheme = new DynamicTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage(); private DynamicLanguage dynamicLanguage = new DynamicLanguage();
private boolean running;
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
dynamicTheme.onCreate(this); dynamicTheme.onCreate(this);
@ -100,6 +106,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) { public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
setContentView(R.layout.message_details_activity); setContentView(R.layout.message_details_activity);
running = true;
initializeResources(); initializeResources();
initializeActionBar(); initializeActionBar();
@ -122,6 +129,12 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(-1L); MessageNotifier.setVisibleThread(-1L);
} }
@Override
protected void onDestroy() {
super.onDestroy();
running = false;
}
private void initializeActionBar() { private void initializeActionBar() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -165,6 +178,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
receivedDate = (TextView ) header.findViewById(R.id.received_time); receivedDate = (TextView ) header.findViewById(R.id.received_time);
transport = (TextView ) header.findViewById(R.id.transport); transport = (TextView ) header.findViewById(R.id.transport);
toFrom = (TextView ) header.findViewById(R.id.tofrom); 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.setHeaderDividersEnabled(false);
recipientsList.addHeaderView(header, null, 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) { private void updateRecipients(MessageRecord messageRecord, Recipients recipients) {
final int toFromRes; final int toFromRes;
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) { 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) { switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT: case MmsSmsDatabase.SMS_TRANSPORT:
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
@ -257,8 +295,13 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 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));
new MessageRecipientAsyncTask(this, messageRecord).execute();
if (messageRecord == null) {
finish();
} else {
new MessageRecipientAsyncTask(this, messageRecord).execute();
}
} }
@Override @Override
@ -281,7 +324,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private WeakReference<Context> weakContext; private WeakReference<Context> weakContext;
private MessageRecord messageRecord; private MessageRecord messageRecord;
public MessageRecipientAsyncTask(Context context, MessageRecord messageRecord) { public MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
this.weakContext = new WeakReference<>(context); this.weakContext = new WeakReference<>(context);
this.messageRecord = messageRecord; this.messageRecord = messageRecord;
} }
@ -340,6 +383,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
} else { } else {
updateTransport(messageRecord); updateTransport(messageRecord);
updateTime(messageRecord); updateTime(messageRecord);
updateExpirationTime(messageRecord);
errorText.setVisibility(View.GONE); errorText.setVisibility(View.GONE);
metadataContainer.setVisibility(View.VISIBLE); metadataContainer.setVisibility(View.VISIBLE);
} }

View File

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.media.Ringtone; import android.media.Ringtone;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
@ -28,6 +31,7 @@ import org.thoughtcrime.securesms.color.MaterialColors;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState; import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.preferences.AdvancedRingtonePreference; import org.thoughtcrime.securesms.preferences.AdvancedRingtonePreference;
import org.thoughtcrime.securesms.preferences.ColorPreference; import org.thoughtcrime.securesms.preferences.ColorPreference;
@ -53,10 +57,11 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private AvatarImageView avatar; private AvatarImageView avatar;
private Toolbar toolbar; private Toolbar toolbar;
private TextView title; private TextView title;
private TextView blockedIndicator; private TextView blockedIndicator;
private BroadcastReceiver staleReceiver;
@Override @Override
public void onPreCreate() { public void onPreCreate() {
@ -72,6 +77,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
Recipients recipients = RecipientFactory.getRecipientsForIds(this, recipientIds, true); Recipients recipients = RecipientFactory.getRecipientsForIds(this, recipientIds, true);
initializeToolbar(); initializeToolbar();
initializeReceivers();
setHeader(recipients); setHeader(recipients);
recipients.addListener(this); recipients.addListener(this);
@ -87,6 +93,12 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
dynamicLanguage.onResume(this); dynamicLanguage.onResume(this);
} }
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(staleReceiver);
}
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
@ -118,6 +130,23 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
this.blockedIndicator = (TextView) toolbar.findViewById(R.id.blocked_indicator); 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) { private void setHeader(Recipients recipients) {
this.avatar.setAvatar(recipients, true); this.avatar.setAvatar(recipients, true);
this.title.setText(recipients.toShortString()); this.title.setText(recipients.toShortString());
@ -149,18 +178,15 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private final Handler handler = new Handler(); private final Handler handler = new Handler();
private Recipients recipients; private Recipients recipients;
private BroadcastReceiver staleReceiver;
@Override @Override
public void onCreate(Bundle icicle) { public void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
addPreferencesFromResource(R.xml.recipient_preferences); addPreferencesFromResource(R.xml.recipient_preferences);
initializeRecipients();
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(),
getArguments().getLongArray(RECIPIENTS_EXTRA),
true);
this.recipients.addListener(this);
this.findPreference(PREFERENCE_TONE) this.findPreference(PREFERENCE_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener()); .setOnPreferenceChangeListener(new RingtoneChangeListener());
this.findPreference(PREFERENCE_VIBRATE) this.findPreference(PREFERENCE_VIBRATE)
@ -185,6 +211,30 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
this.recipients.removeListener(this); 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) { 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 INTRODUCED_CONVERSATION_LIST_STATUS_VERSION = 25;
private static final int MIGRATED_CONVERSATION_LIST_STATUS_VERSION = 26; private static final int MIGRATED_CONVERSATION_LIST_STATUS_VERSION = 26;
private static final int INTRODUCED_SUBSCRIPTION_ID_VERSION = 27; 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 String DATABASE_NAME = "messages.db";
private static final Object lock = new Object(); 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"); 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.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
} }

View File

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

View File

@ -14,7 +14,9 @@ public interface MmsSmsColumns {
public static final String RECEIPT_COUNT = "delivery_receipt_count"; public static final String RECEIPT_COUNT = "delivery_receipt_count";
public static final String MISMATCHED_IDENTITIES = "mismatched_identities"; public static final String MISMATCHED_IDENTITIES = "mismatched_identities";
public static final String UNIQUE_ROW_ID = "unique_row_id"; public static final String UNIQUE_ROW_ID = "unique_row_id";
public static final String SUBSCRIPTION_ID = "subscription_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 { public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF; protected static final long TOTAL_MASK = 0xFFFFFFFF;
@ -61,8 +63,9 @@ public interface MmsSmsColumns {
protected static final long PUSH_MESSAGE_BIT = 0x200000; protected static final long PUSH_MESSAGE_BIT = 0x200000;
// Group Message Information // Group Message Information
protected static final long GROUP_UPDATE_BIT = 0x10000; protected static final long GROUP_UPDATE_BIT = 0x10000;
protected static final long GROUP_QUIT_BIT = 0x20000; protected static final long GROUP_QUIT_BIT = 0x20000;
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
// Encrypted Storage Information // Encrypted Storage Information
protected static final long ENCRYPTION_MASK = 0xFF000000; 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; 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) { public static boolean isIncomingCall(long type) {
return type == INCOMING_CALL_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.HashSet;
import java.util.Set; import java.util.Set;
import ws.com.google.android.mms.pdu.PduHeaders;
public class MmsSmsDatabase extends Database { public class MmsSmsDatabase extends Database {
private static final String TAG = MmsSmsDatabase.class.getSimpleName(); private static final String TAG = MmsSmsDatabase.class.getSimpleName();
@ -56,7 +54,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
MmsSmsColumns.MISMATCHED_IDENTITIES, MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE, MmsDatabase.NETWORK_FAILURE,
MmsSmsColumns.SUBSCRIPTION_ID, TRANSPORT, MmsSmsColumns.SUBSCRIPTION_ID,
MmsSmsColumns.EXPIRES_IN,
MmsSmsColumns.EXPIRE_STARTED, TRANSPORT,
AttachmentDatabase.ATTACHMENT_ID_ALIAS, AttachmentDatabase.ATTACHMENT_ID_ALIAS,
AttachmentDatabase.UNIQUE_ID, AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID, AttachmentDatabase.MMS_ID,
@ -147,7 +147,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID, MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS, MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES, 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.UNIQUE_ID,
AttachmentDatabase.MMS_ID, AttachmentDatabase.MMS_ID,
AttachmentDatabase.SIZE, AttachmentDatabase.SIZE,
@ -171,7 +172,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID, MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS, MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES, MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED,
MmsDatabase.NETWORK_FAILURE, TRANSPORT, MmsDatabase.NETWORK_FAILURE, TRANSPORT,
AttachmentDatabase.UNIQUE_ID, AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID, AttachmentDatabase.MMS_ID,
@ -209,6 +210,8 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT); mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
mmsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES); mmsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
mmsColumnsPresent.add(MmsSmsColumns.SUBSCRIPTION_ID); mmsColumnsPresent.add(MmsSmsColumns.SUBSCRIPTION_ID);
mmsColumnsPresent.add(MmsSmsColumns.EXPIRES_IN);
mmsColumnsPresent.add(MmsSmsColumns.EXPIRE_STARTED);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE); mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX); mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX);
mmsColumnsPresent.add(MmsDatabase.DATE_SENT); mmsColumnsPresent.add(MmsDatabase.DATE_SENT);
@ -240,6 +243,8 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT); smsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
smsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES); smsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
smsColumnsPresent.add(MmsSmsColumns.SUBSCRIPTION_ID); smsColumnsPresent.add(MmsSmsColumns.SUBSCRIPTION_ID);
smsColumnsPresent.add(MmsSmsColumns.EXPIRES_IN);
smsColumnsPresent.add(MmsSmsColumns.EXPIRE_STARTED);
smsColumnsPresent.add(SmsDatabase.TYPE); smsColumnsPresent.add(SmsDatabase.TYPE);
smsColumnsPresent.add(SmsDatabase.SUBJECT); smsColumnsPresent.add(SmsDatabase.SUBJECT);
smsColumnsPresent.add(SmsDatabase.DATE_SENT); 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 COLOR = "color";
private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder"; private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder";
private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id"; private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id";
private static final String EXPIRE_MESSAGES = "expire_messages";
public enum VibrateState { public enum VibrateState {
DEFAULT(0), ENABLED(1), DISABLED(2); DEFAULT(0), ENABLED(1), DISABLED(2);
@ -62,7 +63,8 @@ public class RecipientPreferenceDatabase extends Database {
MUTE_UNTIL + " INTEGER DEFAULT 0, " + MUTE_UNTIL + " INTEGER DEFAULT 0, " +
COLOR + " TEXT DEFAULT NULL, " + COLOR + " TEXT DEFAULT NULL, " +
SEEN_INVITE_REMINDER + " INTEGER DEFAULT 0, " + 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) { public RecipientPreferenceDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
@ -98,6 +100,7 @@ public class RecipientPreferenceDatabase extends Database {
Uri notificationUri = notification == null ? null : Uri.parse(notification); Uri notificationUri = notification == null ? null : Uri.parse(notification);
boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1; boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1;
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID)); int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES));
MaterialColor color; MaterialColor color;
@ -113,7 +116,7 @@ public class RecipientPreferenceDatabase extends Database {
return Optional.of(new RecipientsPreferences(blocked, muteUntil, return Optional.of(new RecipientsPreferences(blocked, muteUntil,
VibrateState.fromId(vibrateState), VibrateState.fromId(vibrateState),
notificationUri, color, seenInviteReminder, notificationUri, color, seenInviteReminder,
defaultSubscriptionId)); defaultSubscriptionId, expireMessages));
} }
return Optional.absent(); return Optional.absent();
@ -134,7 +137,6 @@ public class RecipientPreferenceDatabase extends Database {
updateOrInsert(recipients, values); updateOrInsert(recipients, values);
} }
public void setBlocked(Recipients recipients, boolean blocked) { public void setBlocked(Recipients recipients, boolean blocked) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(BLOCK, blocked ? 1 : 0); values.put(BLOCK, blocked ? 1 : 0);
@ -166,6 +168,14 @@ public class RecipientPreferenceDatabase extends Database {
updateOrInsert(recipients, values); 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) { private void updateOrInsert(Recipients recipients, ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
@ -193,13 +203,15 @@ public class RecipientPreferenceDatabase extends Database {
private final MaterialColor color; private final MaterialColor color;
private final boolean seenInviteReminder; private final boolean seenInviteReminder;
private final int defaultSubscriptionId; private final int defaultSubscriptionId;
private final int expireMessages;
public RecipientsPreferences(boolean blocked, long muteUntil, public RecipientsPreferences(boolean blocked, long muteUntil,
@NonNull VibrateState vibrateState, @NonNull VibrateState vibrateState,
@Nullable Uri notification, @Nullable Uri notification,
@Nullable MaterialColor color, @Nullable MaterialColor color,
boolean seenInviteReminder, boolean seenInviteReminder,
int defaultSubscriptionId) int defaultSubscriptionId,
int expireMessages)
{ {
this.blocked = blocked; this.blocked = blocked;
this.muteUntil = muteUntil; this.muteUntil = muteUntil;
@ -208,6 +220,7 @@ public class RecipientPreferenceDatabase extends Database {
this.color = color; this.color = color;
this.seenInviteReminder = seenInviteReminder; this.seenInviteReminder = seenInviteReminder;
this.defaultSubscriptionId = defaultSubscriptionId; this.defaultSubscriptionId = defaultSubscriptionId;
this.expireMessages = expireMessages;
} }
public @Nullable MaterialColor getColor() { public @Nullable MaterialColor getColor() {
@ -237,5 +250,9 @@ public class RecipientPreferenceDatabase extends Database {
public Optional<Integer> getDefaultSubscriptionId() { public Optional<Integer> getDefaultSubscriptionId() {
return defaultSubscriptionId != -1 ? Optional.of(defaultSubscriptionId) : Optional.<Integer>absent(); 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, " + DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " + STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0," + SUBJECT + " TEXT, " + BODY + " TEXT, " + 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 = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", "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, DATE_SENT + " AS " + NORMALIZED_DATE_SENT,
PROTOCOL, READ, STATUS, TYPE, PROTOCOL, READ, STATUS, TYPE,
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, RECEIPT_COUNT, 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(); 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); 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) { public void markStatus(long id, int status) {
Log.w("MessageDatabase", "Updating ID: " + id + " to status: " + status); Log.w("MessageDatabase", "Updating ID: " + id + " to status: " + status);
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
@ -402,6 +420,7 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(READ, 0); contentValues.put(READ, 0);
contentValues.put(BODY, record.getBody().getBody()); contentValues.put(BODY, record.getBody().getBody());
contentValues.put(THREAD_ID, record.getThreadId()); contentValues.put(THREAD_ID, record.getThreadId());
contentValues.put(EXPIRES_IN, record.getExpiresIn());
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
long newMessageId = db.insert(TABLE_NAME, null, contentValues); long newMessageId = db.insert(TABLE_NAME, null, contentValues);
@ -505,6 +524,7 @@ public class SmsDatabase extends MessagingDatabase {
values.put(PROTOCOL, message.getProtocol()); values.put(PROTOCOL, message.getProtocol());
values.put(READ, unread ? 0 : 1); values.put(READ, unread ? 0 : 1);
values.put(SUBSCRIPTION_ID, message.getSubscriptionId()); values.put(SUBSCRIPTION_ID, message.getSubscriptionId());
values.put(EXPIRES_IN, message.getExpiresIn());
if (!TextUtils.isEmpty(message.getPseudoSubject())) if (!TextUtils.isEmpty(message.getPseudoSubject()))
values.put(SUBJECT, message.getPseudoSubject()); values.put(SUBJECT, message.getPseudoSubject());
@ -552,6 +572,7 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(READ, 1); contentValues.put(READ, 1);
contentValues.put(TYPE, type); contentValues.put(TYPE, type);
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId()); contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
contentValues.put(EXPIRES_IN, message.getExpiresIn());
try { try {
contentValues.put(RECEIPT_COUNT, earlyReceiptCache.remove(date, canonicalizeNumber(context, address))); 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); 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) { public Cursor getMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""}, 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)); int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.RECEIPT_COUNT));
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.MISMATCHED_IDENTITIES)); String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.MISMATCHED_IDENTITIES));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.SUBSCRIPTION_ID)); 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); List<IdentityKeyMismatch> mismatches = getMismatches(mismatchDocument);
Recipients recipients = getRecipientsFor(address); Recipients recipients = getRecipientsFor(address);
@ -728,7 +757,8 @@ public class SmsDatabase extends MessagingDatabase {
recipients.getPrimaryRecipient(), recipients.getPrimaryRecipient(),
addressDeviceId, addressDeviceId,
dateSent, dateReceived, receiptCount, type, dateSent, dateReceived, receiptCount, type,
threadId, status, mismatches, subscriptionId); threadId, status, mismatches, subscriptionId,
expiresIn, expireStarted);
} }
private Recipients getRecipientsFor(String address) { 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 ARCHIVED = "archived";
public static final String STATUS = "status"; public static final String STATUS = "status";
public static final String RECEIPT_COUNT = "delivery_receipt_count"; 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 + " (" + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + 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, " + TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " + SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " + 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 = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");", "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, 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 contentValues = new ContentValues(7);
contentValues.put(DATE, date - date % 1000); contentValues.put(DATE, date - date % 1000);
@ -143,6 +145,7 @@ public class ThreadDatabase extends Database {
contentValues.put(SNIPPET_TYPE, type); contentValues.put(SNIPPET_TYPE, type);
contentValues.put(STATUS, status); contentValues.put(STATUS, status);
contentValues.put(RECEIPT_COUNT, receiptCount); contentValues.put(RECEIPT_COUNT, receiptCount);
contentValues.put(EXPIRES_IN, expiresIn);
if (unarchive) { if (unarchive) {
contentValues.put(ARCHIVED, 0); contentValues.put(ARCHIVED, 0);
@ -503,7 +506,7 @@ public class ThreadDatabase extends Database {
if (reader != null && (record = reader.getNext()) != null) { if (reader != null && (record = reader.getNext()) != null) {
updateThread(threadId, count, record.getBody().getBody(), getAttachmentUriFor(record), updateThread(threadId, count, record.getBody().getBody(), getAttachmentUriFor(record),
record.getTimestamp(), record.getDeliveryStatus(), record.getReceiptCount(), record.getTimestamp(), record.getDeliveryStatus(), record.getReceiptCount(),
record.getType(), unarchive); record.getType(), unarchive, record.getExpiresIn());
notifyConversationListListeners(); notifyConversationListListeners();
return false; return false;
} else { } else {
@ -572,10 +575,12 @@ public class ThreadDatabase extends Database {
boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0; boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0;
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)); int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS));
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.RECEIPT_COUNT)); int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.RECEIPT_COUNT));
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
Uri snippetUri = getSnippetUri(cursor); Uri snippetUri = getSnippetUri(cursor);
return new ThreadRecord(context, body, snippetUri, recipients, date, count, read == 1, 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) { private DisplayRecord.Body getPlaintextBody(Cursor cursor) {

View File

@ -115,6 +115,10 @@ public abstract class DisplayRecord {
return isGroupUpdate() || isGroupQuit(); return isGroupUpdate() || isGroupQuit();
} }
public boolean isExpirationTimerUpdate() {
return SmsDatabase.Types.isExpirationTimerUpdate(type);
}
public boolean isCallLog() { public boolean isCallLog() {
return SmsDatabase.Types.isCallLog(type); 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.SmsDatabase.Status;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
@ -53,10 +54,12 @@ public class MediaMmsMessageRecord extends MessageRecord {
@NonNull SlideDeck slideDeck, @NonNull SlideDeck slideDeck,
int partCount, long mailbox, int partCount, long mailbox,
List<IdentityKeyMismatch> mismatches, 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, 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.context = context.getApplicationContext();
this.partCount = partCount; this.partCount = partCount;
@ -85,6 +88,17 @@ public class MediaMmsMessageRecord extends MessageRecord {
return false; return false;
} }
@Override
public boolean isMediaPending() {
for (Slide slide : getSlideDeck().getSlides()) {
if (slide.isInProgress() || slide.isPendingDownload()) {
return true;
}
}
return false;
}
@Override @Override
public SpannableString getDisplayBody() { public SpannableString getDisplayBody() {
if (MmsDatabase.Types.isDecryptInProgressType(type)) { 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.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import java.util.List; import java.util.List;
@ -51,6 +52,8 @@ public abstract class MessageRecord extends DisplayRecord {
private final List<IdentityKeyMismatch> mismatches; private final List<IdentityKeyMismatch> mismatches;
private final List<NetworkFailure> networkFailures; private final List<NetworkFailure> networkFailures;
private final int subscriptionId; private final int subscriptionId;
private final long expiresIn;
private final long expireStarted;
MessageRecord(Context context, long id, Body body, Recipients recipients, MessageRecord(Context context, long id, Body body, Recipients recipients,
Recipient individualRecipient, int recipientDeviceId, Recipient individualRecipient, int recipientDeviceId,
@ -58,7 +61,7 @@ public abstract class MessageRecord extends DisplayRecord {
int deliveryStatus, int receiptCount, long type, int deliveryStatus, int receiptCount, long type,
List<IdentityKeyMismatch> mismatches, List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> networkFailures, List<NetworkFailure> networkFailures,
int subscriptionId) int subscriptionId, long expiresIn, long expireStarted)
{ {
super(context, body, recipients, dateSent, dateReceived, threadId, deliveryStatus, receiptCount, super(context, body, recipients, dateSent, dateReceived, threadId, deliveryStatus, receiptCount,
type); type);
@ -68,6 +71,8 @@ public abstract class MessageRecord extends DisplayRecord {
this.mismatches = mismatches; this.mismatches = mismatches;
this.networkFailures = networkFailures; this.networkFailures = networkFailures;
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expireStarted = expireStarted;
} }
public abstract boolean isMms(); 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())); return emphasisAdded(context.getString(R.string.MessageRecord_missed_call_from, getIndividualRecipient().toShortString()));
} else if (isJoined()) { } else if (isJoined()) {
return emphasisAdded(context.getString(R.string.MessageRecord_s_is_on_signal_say_hey, getIndividualRecipient().toShortString())); 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) { } else if (getBody().getBody().length() > MAX_DISPLAY_LENGTH) {
return new SpannableString(getBody().getBody().substring(0, 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); return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
} }
public boolean isMediaPending() {
return false;
}
public Recipient getIndividualRecipient() { public Recipient getIndividualRecipient() {
return individualRecipient; return individualRecipient;
} }
@ -201,4 +214,12 @@ public abstract class MessageRecord extends DisplayRecord {
public int getSubscriptionId() { public int getSubscriptionId() {
return subscriptionId; 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, super(context, id, new Body("", true), recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, Status.STATUS_NONE, receiptCount, mailbox, 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.contentLocation = contentLocation;
this.messageSize = messageSize; this.messageSize = messageSize;
@ -113,6 +114,11 @@ public class NotificationMmsMessageRecord extends MessageRecord {
return true; return true;
} }
@Override
public boolean isMediaPending() {
return true;
}
@Override @Override
public SpannableString getDisplayBody() { public SpannableString getDisplayBody() {
return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_multimedia_message)); return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_multimedia_message));

View File

@ -48,11 +48,12 @@ public class SmsMessageRecord extends MessageRecord {
int receiptCount, int receiptCount,
long type, long threadId, long type, long threadId,
int status, List<IdentityKeyMismatch> mismatches, int status, List<IdentityKeyMismatch> mismatches,
int subscriptionId) int subscriptionId, long expiresIn, long expireStarted)
{ {
super(context, id, body, recipients, individualRecipient, recipientDeviceId, super(context, id, body, recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, status, receiptCount, type, dateSent, dateReceived, threadId, status, receiptCount, type,
mismatches, new LinkedList<NetworkFailure>(), subscriptionId); mismatches, new LinkedList<NetworkFailure>(), subscriptionId,
expiresIn, expireStarted);
} }
public long getType() { 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.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
/** /**
@ -46,11 +47,12 @@ public class ThreadRecord extends DisplayRecord {
private final boolean read; private final boolean read;
private final int distributionType; private final int distributionType;
private final boolean archived; private final boolean archived;
private final long expiresIn;
public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri, public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri,
@NonNull Recipients recipients, long date, long count, boolean read, @NonNull Recipients recipients, long date, long count, boolean read,
long threadId, int receiptCount, int status, long snippetType, 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); super(context, body, recipients, date, date, threadId, status, receiptCount, snippetType);
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
@ -59,6 +61,7 @@ public class ThreadRecord extends DisplayRecord {
this.read = read; this.read = read;
this.distributionType = distributionType; this.distributionType = distributionType;
this.archived = archived; this.archived = archived;
this.expiresIn = expiresIn;
} }
public @Nullable Uri getSnippetUri() { 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)); return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_missed_call));
} else if (SmsDatabase.Types.isJoinedType(type)) { } else if (SmsDatabase.Types.isJoinedType(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal_say_hey, getRecipients().getPrimaryRecipient().toShortString())); 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 { } else {
if (TextUtils.isEmpty(getBody().getBody())) { if (TextUtils.isEmpty(getBody().getBody())) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message))); return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
@ -135,4 +141,8 @@ public class ThreadRecord extends DisplayRecord {
public int getDistributionType() { public int getDistributionType() {
return distributionType; 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); 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); long threadId = MessageSender.send(context, masterSecret, outgoingMessage, -1, false);
return new GroupActionResult(groupRecipient, threadId); return new GroupActionResult(groupRecipient, threadId);

View File

@ -185,7 +185,7 @@ public class GroupMessageProcessor {
if (outgoing) { if (outgoing) {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(group.getGroupId()), false); 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 threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = mmsDatabase.insertMessageOutbox(masterSecret, outgoingMessage, threadId, false); long messageId = mmsDatabase.insertMessageOutbox(masterSecret, outgoingMessage, threadId, false);
@ -195,7 +195,7 @@ public class GroupMessageProcessor {
} else { } else {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = Base64.encodeBytes(storage.toByteArray()); 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); IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, storage, body);
Pair<Long, Long> messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, groupMessage); 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), Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(new MasterSecretUnion(masterSecret),
message, contentLocation, threadId); 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.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor; import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
@ -49,10 +50,11 @@ import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.UntrustedIdentityException; import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.protocol.PreKeySignalMessage; import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SessionStore; import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; 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.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
@ -141,6 +143,7 @@ public class PushDecryptJob extends ContextJob {
if (message.isEndSession()) handleEndSessionMessage(masterSecret, envelope, message, smsMessageId); if (message.isEndSession()) handleEndSessionMessage(masterSecret, envelope, message, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(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 if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message, smsMessageId);
else handleTextMessage(masterSecret, envelope, message, smsMessageId); else handleTextMessage(masterSecret, envelope, message, smsMessageId);
} else if (content.getSyncMessage().isPresent()) { } else if (content.getSyncMessage().isPresent()) {
@ -185,7 +188,7 @@ public class PushDecryptJob extends ContextJob {
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(), IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(), envelope.getSourceDevice(),
message.getTimestamp(), message.getTimestamp(),
"", Optional.<SignalServiceGroup>absent()); "", Optional.<SignalServiceGroup>absent(), 0);
long threadId; 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, private void handleSynchronizeSentMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceEnvelope envelope,
@NonNull SentTranscriptMessage message, @NonNull SentTranscriptMessage message,
@ -228,6 +258,8 @@ public class PushDecryptJob extends ContextJob {
if (message.getMessage().isGroupUpdate()) { if (message.getMessage().isGroupUpdate()) {
threadId = GroupMessageProcessor.process(context, masterSecret, envelope, message.getMessage(), true); 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()) { } else if (message.getMessage().getAttachments().isPresent()) {
threadId = handleSynchronizeSentMediaMessage(masterSecret, message, smsMessageId); threadId = handleSynchronizeSentMediaMessage(masterSecret, message, smsMessageId);
} else { } else {
@ -275,13 +307,19 @@ public class PushDecryptJob extends ContextJob {
{ {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
String localNumber = TextSecurePreferences.getLocalNumber(context); String localNumber = TextSecurePreferences.getLocalNumber(context);
Recipients recipients = getMessageDestination(envelope, message);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, envelope.getSource(), IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, envelope.getSource(),
localNumber, message.getTimestamp(), -1, localNumber, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000, false,
Optional.fromNullable(envelope.getRelay()), Optional.fromNullable(envelope.getRelay()),
message.getBody(), message.getBody(),
message.getGroupInfo(), message.getGroupInfo(),
message.getAttachments()); message.getAttachments());
if (message.getExpiresInSeconds() != recipients.getExpireMessages()) {
handleExpirationUpdate(masterSecret, envelope, message, Optional.<Long>absent());
}
Pair<Long, Long> messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1); Pair<Long, Long> messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageAndThreadId.first); 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); 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, private long handleSynchronizeSentMediaMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SentTranscriptMessage message, @NonNull SentTranscriptMessage message,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
@ -308,10 +380,16 @@ public class PushDecryptJob extends ContextJob {
Recipients recipients = getSyncMessageDestination(message); Recipients recipients = getSyncMessageDestination(message);
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(), OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
PointerAttachment.forPointers(masterSecret, message.getMessage().getAttachments()), 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); mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
if (recipients.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
handleSynchronizeSentExpirationUpdate(masterSecret, message, Optional.<Long>absent());
}
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = database.insertMessageOutbox(masterSecret, mediaMessage, threadId, false); long messageId = database.insertMessageOutbox(masterSecret, mediaMessage, threadId, false);
@ -328,6 +406,15 @@ public class PushDecryptJob extends ContextJob {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); 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; return threadId;
} }
@ -335,9 +422,15 @@ public class PushDecryptJob extends ContextJob {
@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceEnvelope envelope,
@NonNull SignalServiceDataMessage message, @NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
throws MmsException
{ {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : ""; 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; Pair<Long, Long> messageAndThreadId;
@ -347,7 +440,8 @@ public class PushDecryptJob extends ContextJob {
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(), envelope.getSourceDevice(),
message.getTimestamp(), body, message.getTimestamp(), body,
message.getGroupInfo()); message.getGroupInfo(),
message.getExpiresInSeconds() * 1000);
textMessage = new IncomingEncryptedMessage(textMessage, body); textMessage = new IncomingEncryptedMessage(textMessage, body);
messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage); messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
@ -361,11 +455,17 @@ public class PushDecryptJob extends ContextJob {
private long handleSynchronizeSentTextMessage(@NonNull MasterSecretUnion masterSecret, private long handleSynchronizeSentTextMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SentTranscriptMessage message, @NonNull SentTranscriptMessage message,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
throws MmsException
{ {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Recipients recipients = getSyncMessageDestination(message); Recipients recipients = getSyncMessageDestination(message);
String body = message.getMessage().getBody().or(""); 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 threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = database.insertMessageOutbox(masterSecret, threadId, outgoingTextMessage, false, message.getTimestamp()); long messageId = database.insertMessageOutbox(masterSecret, threadId, outgoingTextMessage, false, message.getTimestamp());
@ -378,6 +478,13 @@ public class PushDecryptJob extends ContextJob {
database.deleteMessage(smsMessageId.get()); database.deleteMessage(smsMessageId.get());
} }
if (expiresInMillis > 0) {
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
ApplicationContext.getInstance(context)
.getExpiringMessageManager()
.scheduleDeletion(messageId, false, message.getExpirationStartTimestamp(), expiresInMillis);
}
return threadId; return threadId;
} }
@ -470,7 +577,7 @@ public class PushDecryptJob extends ContextJob {
String encoded = Base64.encodeBytes(envelope.getLegacyMessage()); String encoded = Base64.encodeBytes(envelope.getLegacyMessage());
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(), IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), encoded, envelope.getTimestamp(), encoded,
Optional.<SignalServiceGroup>absent()); Optional.<SignalServiceGroup>absent(), 0);
if (!smsMessageId.isPresent()) { if (!smsMessageId.isPresent()) {
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded); IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
@ -492,7 +599,7 @@ public class PushDecryptJob extends ContextJob {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(), IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), "", envelope.getTimestamp(), "",
Optional.<SignalServiceGroup>absent()); Optional.<SignalServiceGroup>absent(), 0);
textMessage = new IncomingEncryptedMessage(textMessage, ""); textMessage = new IncomingEncryptedMessage(textMessage, "");
return database.insertMessageInbox(textMessage); return database.insertMessageInbox(textMessage);
@ -505,4 +612,12 @@ public class PushDecryptJob extends ContextJob {
return RecipientFactory.getRecipientsFromString(context, message.getDestination().get(), false); 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.content.Context;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; 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.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.JobParameters;
@ -85,6 +87,13 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId); database.markAsSecure(messageId);
database.markAsSent(messageId); database.markAsSent(messageId);
markAttachmentsUploaded(messageId, message.getAttachments()); 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) { } catch (InvalidNumberException | RecipientFormattingException | UndeliverableMessageException e) {
Log.w(TAG, e); Log.w(TAG, e);
database.markAsSentFailed(messageId); database.markAsSentFailed(messageId);
@ -152,7 +161,10 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
messageSender.sendMessage(addresses, groupDataMessage); messageSender.sendMessage(addresses, groupDataMessage);
} else { } else {
SignalServiceGroup group = new SignalServiceGroup(groupId); 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); 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.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
@ -62,8 +63,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
throws RetryLaterException, MmsException, NoSuchMessageException, throws RetryLaterException, MmsException, NoSuchMessageException,
UndeliverableMessageException UndeliverableMessageException
{ {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId);
try { try {
deliver(masterSecret, message); deliver(masterSecret, message);
@ -71,6 +73,12 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId); database.markAsSecure(messageId);
database.markAsSent(messageId); database.markAsSent(messageId);
markAttachmentsUploaded(messageId, message.getAttachments()); markAttachmentsUploaded(messageId, message.getAttachments());
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
}
} catch (InsecureFallbackApprovalException ifae) { } catch (InsecureFallbackApprovalException ifae) {
Log.w(TAG, ifae); Log.w(TAG, ifae);
database.markAsPendingInsecureSmsFallback(messageId); database.markAsPendingInsecureSmsFallback(messageId);
@ -122,6 +130,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.withBody(message.getBody()) .withBody(message.getBody())
.withAttachments(attachmentStreams) .withAttachments(attachmentStreams)
.withTimestamp(message.getSentTimeMillis()) .withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000))
.asExpirationUpdate(message.isExpirationUpdate())
.build(); .build();
messageSender.sendMessage(address, mediaMessage); 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.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
@ -53,8 +54,9 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
@Override @Override
public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException { public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
SmsMessageRecord record = database.getMessage(masterSecret, messageId); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
try { try {
Log.w(TAG, "Sending message: " + messageId); Log.w(TAG, "Sending message: " + messageId);
@ -64,6 +66,11 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId); database.markAsSecure(messageId);
database.markAsSent(messageId); database.markAsSent(messageId);
if (record.getExpiresIn() > 0) {
database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn());
}
} catch (InsecureFallbackApprovalException e) { } catch (InsecureFallbackApprovalException e) {
Log.w(TAG, e); Log.w(TAG, e);
database.markAsPendingInsecureSmsFallback(record.getId()); database.markAsPendingInsecureSmsFallback(record.getId());
@ -108,6 +115,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent()) .withTimestamp(message.getDateSent())
.withBody(message.getBody().getBody()) .withBody(message.getBody().getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.asEndSessionMessage(message.isEndSession()) .asEndSessionMessage(message.isEndSession())
.build(); .build();

View File

@ -20,6 +20,8 @@ public class IncomingMediaMessage {
private final boolean push; private final boolean push;
private final long sentTimeMillis; private final long sentTimeMillis;
private final int subscriptionId; private final int subscriptionId;
private final long expiresIn;
private final boolean expirationUpdate;
private final List<String> to = new LinkedList<>(); private final List<String> to = new LinkedList<>();
private final List<String> cc = new LinkedList<>(); private final List<String> cc = new LinkedList<>();
@ -27,14 +29,17 @@ public class IncomingMediaMessage {
public IncomingMediaMessage(String from, List<String> to, List<String> cc, public IncomingMediaMessage(String from, List<String> to, List<String> cc,
String body, long sentTimeMillis, String body, long sentTimeMillis,
List<Attachment> attachments, int subscriptionId) List<Attachment> attachments, int subscriptionId,
long expiresIn, boolean expirationUpdate)
{ {
this.from = from; this.from = from;
this.sentTimeMillis = sentTimeMillis; this.sentTimeMillis = sentTimeMillis;
this.body = body; this.body = body;
this.groupId = null; this.groupId = null;
this.push = false; this.push = false;
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate;
this.to.addAll(to); this.to.addAll(to);
this.cc.addAll(cc); this.cc.addAll(cc);
@ -46,16 +51,20 @@ public class IncomingMediaMessage {
String to, String to,
long sentTimeMillis, long sentTimeMillis,
int subscriptionId, int subscriptionId,
long expiresIn,
boolean expirationUpdate,
Optional<String> relay, Optional<String> relay,
Optional<String> body, Optional<String> body,
Optional<SignalServiceGroup> group, Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> attachments) Optional<List<SignalServiceAttachment>> attachments)
{ {
this.push = true; this.push = true;
this.from = from; this.from = from;
this.sentTimeMillis = sentTimeMillis; this.sentTimeMillis = sentTimeMillis;
this.body = body.orNull(); this.body = body.orNull();
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate;
if (group.isPresent()) this.groupId = GroupUtil.getEncodedId(group.get().getGroupId()); if (group.isPresent()) this.groupId = GroupUtil.getEncodedId(group.get().getGroupId());
else this.groupId = null; else this.groupId = null;
@ -88,10 +97,18 @@ public class IncomingMediaMessage {
return push; return push;
} }
public boolean isExpirationUpdate() {
return expirationUpdate;
}
public long getSentTimeMillis() { public long getSentTimeMillis() {
return sentTimeMillis; return sentTimeMillis;
} }
public long getExpiresIn() {
return expiresIn;
}
public boolean isGroupMessage() { public boolean isGroupMessage() {
return groupId != null || to.size() > 1 || cc.size() > 0; 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, public OutgoingGroupMediaMessage(@NonNull Recipients recipients,
@NonNull String encodedGroupContext, @NonNull String encodedGroupContext,
@NonNull List<Attachment> avatar, @NonNull List<Attachment> avatar,
long sentTimeMillis) long sentTimeMillis,
long expiresIn)
throws IOException throws IOException
{ {
super(recipients, encodedGroupContext, avatar, sentTimeMillis, super(recipients, encodedGroupContext, avatar, sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION); ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn);
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext)); this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
} }
@ -32,12 +33,13 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
public OutgoingGroupMediaMessage(@NonNull Recipients recipients, public OutgoingGroupMediaMessage(@NonNull Recipients recipients,
@NonNull GroupContext group, @NonNull GroupContext group,
@Nullable final Attachment avatar, @Nullable final Attachment avatar,
long sentTimeMillis) long sentTimeMillis,
long expireIn)
{ {
super(recipients, Base64.encodeBytes(group.toByteArray()), super(recipients, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}}, new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(), System.currentTimeMillis(),
ThreadDatabase.DistributionTypes.CONVERSATION); ThreadDatabase.DistributionTypes.CONVERSATION, expireIn);
this.group = group; this.group = group;
} }

View File

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

View File

@ -14,9 +14,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
public OutgoingSecureMediaMessage(Recipients recipients, String body, public OutgoingSecureMediaMessage(Recipients recipients, String body,
List<Attachment> attachments, List<Attachment> attachments,
long sentTimeMillis, 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) { public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {

View File

@ -72,13 +72,14 @@ public class WearReplyReceiver extends MasterSecretBroadcastReceiver {
long threadId; long threadId;
Optional<RecipientsPreferences> preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(recipientIds); Optional<RecipientsPreferences> preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(recipientIds);
int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1; int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1;
long expiresIn = preferences.isPresent() ? preferences.get().getExpireMessages() * 1000 : 0;
if (recipients.isGroupRecipient()) { 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); threadId = MessageSender.send(context, masterSecret, reply, -1, false);
} else { } 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); threadId = MessageSender.send(context, masterSecret, reply, -1, false);
} }

View File

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

View File

@ -51,11 +51,12 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
private final Set<RecipientsModifiedListener> listeners = Collections.newSetFromMap(new WeakHashMap<RecipientsModifiedListener, Boolean>()); private final Set<RecipientsModifiedListener> listeners = Collections.newSetFromMap(new WeakHashMap<RecipientsModifiedListener, Boolean>());
private final List<Recipient> recipients; private final List<Recipient> recipients;
private Uri ringtone = null; private Uri ringtone = null;
private long mutedUntil = 0; private long mutedUntil = 0;
private boolean blocked = false; private boolean blocked = false;
private VibrateState vibrate = VibrateState.DEFAULT; private VibrateState vibrate = VibrateState.DEFAULT;
private boolean stale = false; private int expireMessages = 0;
private boolean stale = false;
Recipients() { Recipients() {
this(new LinkedList<Recipient>(), null); this(new LinkedList<Recipient>(), null);
@ -65,10 +66,11 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
this.recipients = recipients; this.recipients = recipients;
if (preferences != null) { if (preferences != null) {
ringtone = preferences.getRingtone(); ringtone = preferences.getRingtone();
mutedUntil = preferences.getMuteUntil(); mutedUntil = preferences.getMuteUntil();
vibrate = preferences.getVibrateState(); vibrate = preferences.getVibrateState();
blocked = preferences.isBlocked(); blocked = preferences.isBlocked();
expireMessages = preferences.getExpireMessages();
} }
} }
@ -79,10 +81,11 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
this.recipients = recipients; this.recipients = recipients;
if (stale != null) { if (stale != null) {
ringtone = stale.ringtone; ringtone = stale.ringtone;
mutedUntil = stale.mutedUntil; mutedUntil = stale.mutedUntil;
vibrate = stale.vibrate; vibrate = stale.vibrate;
blocked = stale.blocked; blocked = stale.blocked;
expireMessages = stale.expireMessages;
} }
preferences.addListener(new FutureTaskListener<RecipientsPreferences>() { preferences.addListener(new FutureTaskListener<RecipientsPreferences>() {
@ -93,10 +96,11 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
Set<RecipientsModifiedListener> localListeners; Set<RecipientsModifiedListener> localListeners;
synchronized (Recipients.this) { synchronized (Recipients.this) {
ringtone = result.getRingtone(); ringtone = result.getRingtone();
mutedUntil = result.getMuteUntil(); mutedUntil = result.getMuteUntil();
vibrate = result.getVibrateState(); vibrate = result.getVibrateState();
blocked = result.isBlocked(); blocked = result.isBlocked();
expireMessages = result.getExpireMessages();
localListeners = new HashSet<>(listeners); localListeners = new HashSet<>(listeners);
} }
@ -178,6 +182,18 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
else if (!isEmpty()) recipients.get(0).setColor(color); 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) { public synchronized void addListener(RecipientsModifiedListener listener) {
if (listeners.isEmpty()) { if (listeners.isEmpty()) {
for (Recipient recipient : recipients) { 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); Recipients recipients = RecipientFactory.getRecipientsFromString(this, numbers, false);
Optional<RecipientsPreferences> preferences = DatabaseFactory.getRecipientPreferenceDatabase(this).getRecipientsPreferences(recipients.getIds()); Optional<RecipientsPreferences> preferences = DatabaseFactory.getRecipientPreferenceDatabase(this).getRecipientsPreferences(recipients.getIds());
int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1; int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1;
long expiresIn = preferences.isPresent() ? preferences.get().getExpireMessages() * 1000 : 0;
if (!TextUtils.isEmpty(content)) { if (!TextUtils.isEmpty(content)) {
if (recipients.isSingleRecipient()) { 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 { } else {
MessageSender.send(this, masterSecret, new OutgoingMediaMessage(recipients, new SlideDeck(), content, System.currentTimeMillis(), 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) { } catch (URISyntaxException e) {

View File

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

View File

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

View File

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

View File

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

View File

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