diff --git a/build.gradle b/build.gradle
index c251f6107d..5b4ffb1e62 100644
--- a/build.gradle
+++ b/build.gradle
@@ -72,10 +72,14 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
- compile 'org.whispersystems:signal-service-android:2.1.1'
+ compile 'org.whispersystems:signal-service-android:2.2.0'
compile 'com.h6ah4i.android.compat:mulsellistprefcompat:1.0.0'
compile 'com.google.zxing:core:3.2.1'
+ compile ('cn.carbswang.android:NumberPickerView:1.0.9') {
+ exclude group: 'com.android.support', module: 'appcompat-v7'
+ }
+
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1'
testCompile 'org.mockito:mockito-core:1.9.5'
@@ -97,57 +101,58 @@ dependencies {
dependencyVerification {
verify = [
- 'me.leolin:ShortcutBadger:3142d017234bfa0cdd69ccded7cc5ea63f13b97574803c8c616c9bbeaad33ad9',
- 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
- 'com.google.android.gms:play-services-gcm:757ecd2c837ac81c98f4cc7dc783e7454c6d0506f6cc66b10417126b675248c9',
- 'com.google.android.gms:play-services-maps:c58a9d98a98889fb0b27f78100f2d9341ed7722db24ccf832df62b6e8ce1b42e',
- 'com.google.android.gms:play-services-location:8226f778aa86bd15b9143f62425262cc53d64021990f62eb1aaec108d4e25f35',
- 'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa',
- 'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b',
- 'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
- 'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc',
- 'com.github.bumptech.glide:glide:76ef123957b5fbaebb05fcbe6606dd58c3bc3fcdadb257f99811d0ac9ea9b88b',
- 'com.makeramen:roundedimageview:1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1',
- 'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54',
- 'de.greenrobot:eventbus:61d743a748156a372024d083de763b9e91ac2dcb3f6a1cbc74995c7ddab6e968',
- 'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c',
- 'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
- 'com.android.support:appcompat-v7:4b5ccba8c4557ef04f99aa0a80f8aa7d50f05f926a709010a54afd5c878d3618',
- 'com.android.support:recyclerview-v7:b0f530a5b14334d56ce0de85527ffe93ac419bc928e2884287ce1dddfedfb505',
- 'com.android.support:design:58be3ca6a73789615f7ece0937d2f683b98b594bb90aa10565fa760fb10b07ee',
- 'com.android.support:cardview-v7:2c2354761a4e20ba451ae903ab808f15c9acc8343b1e74001869c2d0a672c1fc',
- 'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
- 'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
- 'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
- 'com.android.support:gridlayout-v7:a9b770cffca2c7c5cd83cba4dd12503365de5e8d9c79c479165adf18ab3bc25b',
- 'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
- 'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
- 'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
- 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
- 'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
- 'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
- 'org.whispersystems:signal-service-android:1c89623336505f6511e6f68ea126c85eae7f28f6c72beb6b362e5743bc5e5126',
- 'com.h6ah4i.android.compat:mulsellistprefcompat:47167c5cb796de1a854788e9ff318358e36c8fb88123baaa6e38fb78511dfabe',
- 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
- 'com.google.android.gms:play-services-base:ef36e50fa5c0415ed41f74dd399a889efd2fa327c449036e140c7c3786aa0e1f',
- 'com.android.support:support-annotations:104f353b53d5dd8d64b2f77eece4b37f6b961de9732eb6b706395e91033ec70a',
- 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
- 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
- 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
- 'org.whispersystems:signal-service-java:48db52056aa3510deb8c4ccd2dfb35033ae115bc4176048820c6dff73290ba6e',
- 'org.whispersystems:signal-protocol-android:d83cb3d15b667fc2543fa18ce80791c72c053e8ac54fc2941f0429a5944ca691',
- 'com.google.android.gms:play-services-basement:e1d29b21e02fd2a63e5a31807415cbb17a59568e27e3254181c01ffae10659bf',
- 'com.googlecode.libphonenumber:libphonenumber:9625de9d2270e9a280ff4e6d9ef3106573fb4828773fd32c9b7614f4e17d2811',
- 'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
- 'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af',
- 'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
- 'org.whispersystems:curve25519-android:d6a3ef3a70622af4c728b7fe5f8fdfc9e6cd39b1d39b2c77e7a2add9d876bc23',
- 'org.whispersystems:signal-protocol-java:d518d52eeb3c44210e0b6c687360848a87afbaee0bdf42e2a8dd9974d54fdb3a',
- 'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978',
- 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
- 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
- 'org.whispersystems:curve25519-java:08cc3be52723e0fc4148e5e7002d51d6d7e495b2130022237f2d47b90af6ae0b',
- 'com.android.support:support-v4:c62f0d025dafa86f423f48df9185b0d89496adbc5f6a9be5a7c394d84cf91423',
+ 'me.leolin:ShortcutBadger:3142d017234bfa0cdd69ccded7cc5ea63f13b97574803c8c616c9bbeaad33ad9',
+ 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
+ 'com.google.android.gms:play-services-gcm:757ecd2c837ac81c98f4cc7dc783e7454c6d0506f6cc66b10417126b675248c9',
+ 'com.google.android.gms:play-services-maps:c58a9d98a98889fb0b27f78100f2d9341ed7722db24ccf832df62b6e8ce1b42e',
+ 'com.google.android.gms:play-services-location:8226f778aa86bd15b9143f62425262cc53d64021990f62eb1aaec108d4e25f35',
+ 'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa',
+ 'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b',
+ 'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
+ 'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc',
+ 'com.github.bumptech.glide:glide:76ef123957b5fbaebb05fcbe6606dd58c3bc3fcdadb257f99811d0ac9ea9b88b',
+ 'com.makeramen:roundedimageview:1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1',
+ 'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54',
+ 'de.greenrobot:eventbus:61d743a748156a372024d083de763b9e91ac2dcb3f6a1cbc74995c7ddab6e968',
+ 'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c',
+ 'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
+ 'com.android.support:appcompat-v7:4b5ccba8c4557ef04f99aa0a80f8aa7d50f05f926a709010a54afd5c878d3618',
+ 'com.android.support:recyclerview-v7:b0f530a5b14334d56ce0de85527ffe93ac419bc928e2884287ce1dddfedfb505',
+ 'com.android.support:design:58be3ca6a73789615f7ece0937d2f683b98b594bb90aa10565fa760fb10b07ee',
+ 'com.android.support:cardview-v7:2c2354761a4e20ba451ae903ab808f15c9acc8343b1e74001869c2d0a672c1fc',
+ 'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
+ 'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
+ 'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
+ 'com.android.support:gridlayout-v7:a9b770cffca2c7c5cd83cba4dd12503365de5e8d9c79c479165adf18ab3bc25b',
+ 'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
+ 'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
+ 'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
+ 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
+ 'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
+ 'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
+ 'org.whispersystems:signal-service-android:96a926b0bfd1df8b66be2b574e8b8d6ef1862f715b0f1a5457a2038b28d3ad1b',
+ 'com.h6ah4i.android.compat:mulsellistprefcompat:47167c5cb796de1a854788e9ff318358e36c8fb88123baaa6e38fb78511dfabe',
+ 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
+ 'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729',
+ 'com.google.android.gms:play-services-base:ef36e50fa5c0415ed41f74dd399a889efd2fa327c449036e140c7c3786aa0e1f',
+ 'com.android.support:support-annotations:104f353b53d5dd8d64b2f77eece4b37f6b961de9732eb6b706395e91033ec70a',
+ 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
+ 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
+ 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
+ 'org.whispersystems:signal-protocol-android:d83cb3d15b667fc2543fa18ce80791c72c053e8ac54fc2941f0429a5944ca691',
+ 'org.whispersystems:signal-service-java:7932363fec666fdc0b4b424eeca4bdca235f6bf2f226fb6a6ff742c49fc37087',
+ 'com.google.android.gms:play-services-basement:e1d29b21e02fd2a63e5a31807415cbb17a59568e27e3254181c01ffae10659bf',
+ 'org.whispersystems:curve25519-android:d6a3ef3a70622af4c728b7fe5f8fdfc9e6cd39b1d39b2c77e7a2add9d876bc23',
+ 'org.whispersystems:signal-protocol-java:d518d52eeb3c44210e0b6c687360848a87afbaee0bdf42e2a8dd9974d54fdb3a',
+ 'com.googlecode.libphonenumber:libphonenumber:9625de9d2270e9a280ff4e6d9ef3106573fb4828773fd32c9b7614f4e17d2811',
+ 'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
+ 'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af',
+ 'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
+ 'org.whispersystems:curve25519-java:08cc3be52723e0fc4148e5e7002d51d6d7e495b2130022237f2d47b90af6ae0b',
+ 'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978',
+ 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
+ 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
+ 'com.android.support:support-v4:c62f0d025dafa86f423f48df9185b0d89496adbc5f6a9be5a7c394d84cf91423',
]
}
diff --git a/res/drawable-hdpi/ic_hourglass_empty_white_18dp.png b/res/drawable-hdpi/ic_hourglass_empty_white_18dp.png
new file mode 100644
index 0000000000..785feac7db
Binary files /dev/null and b/res/drawable-hdpi/ic_hourglass_empty_white_18dp.png differ
diff --git a/res/drawable-hdpi/ic_hourglass_full_white_18dp.png b/res/drawable-hdpi/ic_hourglass_full_white_18dp.png
new file mode 100644
index 0000000000..323c14dc33
Binary files /dev/null and b/res/drawable-hdpi/ic_hourglass_full_white_18dp.png differ
diff --git a/res/drawable-hdpi/ic_timer_off_white_24dp.png b/res/drawable-hdpi/ic_timer_off_white_24dp.png
new file mode 100644
index 0000000000..60136f7b51
Binary files /dev/null and b/res/drawable-hdpi/ic_timer_off_white_24dp.png differ
diff --git a/res/drawable-hdpi/ic_timer_white_24dp.png b/res/drawable-hdpi/ic_timer_white_24dp.png
new file mode 100644
index 0000000000..10619fc8c5
Binary files /dev/null and b/res/drawable-hdpi/ic_timer_white_24dp.png differ
diff --git a/res/drawable-mdpi/ic_hourglass_empty_white_18dp.png b/res/drawable-mdpi/ic_hourglass_empty_white_18dp.png
new file mode 100644
index 0000000000..e1db32b3cb
Binary files /dev/null and b/res/drawable-mdpi/ic_hourglass_empty_white_18dp.png differ
diff --git a/res/drawable-mdpi/ic_hourglass_full_white_18dp.png b/res/drawable-mdpi/ic_hourglass_full_white_18dp.png
new file mode 100644
index 0000000000..1b7feed9bb
Binary files /dev/null and b/res/drawable-mdpi/ic_hourglass_full_white_18dp.png differ
diff --git a/res/drawable-mdpi/ic_timer_off_white_24dp.png b/res/drawable-mdpi/ic_timer_off_white_24dp.png
new file mode 100644
index 0000000000..b3a619767c
Binary files /dev/null and b/res/drawable-mdpi/ic_timer_off_white_24dp.png differ
diff --git a/res/drawable-mdpi/ic_timer_white_24dp.png b/res/drawable-mdpi/ic_timer_white_24dp.png
new file mode 100644
index 0000000000..72e1320c46
Binary files /dev/null and b/res/drawable-mdpi/ic_timer_white_24dp.png differ
diff --git a/res/drawable-xhdpi/ic_hourglass_empty_white_18dp.png b/res/drawable-xhdpi/ic_hourglass_empty_white_18dp.png
new file mode 100644
index 0000000000..b24e375a5a
Binary files /dev/null and b/res/drawable-xhdpi/ic_hourglass_empty_white_18dp.png differ
diff --git a/res/drawable-xhdpi/ic_hourglass_full_white_18dp.png b/res/drawable-xhdpi/ic_hourglass_full_white_18dp.png
new file mode 100644
index 0000000000..29ee1d09b3
Binary files /dev/null and b/res/drawable-xhdpi/ic_hourglass_full_white_18dp.png differ
diff --git a/res/drawable-xhdpi/ic_timer_off_white_24dp.png b/res/drawable-xhdpi/ic_timer_off_white_24dp.png
new file mode 100644
index 0000000000..494a59fc8a
Binary files /dev/null and b/res/drawable-xhdpi/ic_timer_off_white_24dp.png differ
diff --git a/res/drawable-xhdpi/ic_timer_white_24dp.png b/res/drawable-xhdpi/ic_timer_white_24dp.png
new file mode 100644
index 0000000000..9d239966f3
Binary files /dev/null and b/res/drawable-xhdpi/ic_timer_white_24dp.png differ
diff --git a/res/drawable-xxhdpi/ic_hourglass_empty_white_18dp.png b/res/drawable-xxhdpi/ic_hourglass_empty_white_18dp.png
new file mode 100644
index 0000000000..c7d64b6352
Binary files /dev/null and b/res/drawable-xxhdpi/ic_hourglass_empty_white_18dp.png differ
diff --git a/res/drawable-xxhdpi/ic_hourglass_full_white_18dp.png b/res/drawable-xxhdpi/ic_hourglass_full_white_18dp.png
new file mode 100644
index 0000000000..4f5a2ecc38
Binary files /dev/null and b/res/drawable-xxhdpi/ic_hourglass_full_white_18dp.png differ
diff --git a/res/drawable-xxhdpi/ic_timer_off_white_24dp.png b/res/drawable-xxhdpi/ic_timer_off_white_24dp.png
new file mode 100644
index 0000000000..52e8dbdb31
Binary files /dev/null and b/res/drawable-xxhdpi/ic_timer_off_white_24dp.png differ
diff --git a/res/drawable-xxhdpi/ic_timer_white_24dp.png b/res/drawable-xxhdpi/ic_timer_white_24dp.png
new file mode 100644
index 0000000000..bb6f9a63b5
Binary files /dev/null and b/res/drawable-xxhdpi/ic_timer_white_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_hourglass_empty_white_18dp.png b/res/drawable-xxxhdpi/ic_hourglass_empty_white_18dp.png
new file mode 100644
index 0000000000..e2e2d74f59
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_hourglass_empty_white_18dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_hourglass_full_white_18dp.png b/res/drawable-xxxhdpi/ic_hourglass_full_white_18dp.png
new file mode 100644
index 0000000000..908b6a098a
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_hourglass_full_white_18dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_timer_off_white_24dp.png b/res/drawable-xxxhdpi/ic_timer_off_white_24dp.png
new file mode 100644
index 0000000000..0b38943842
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_timer_off_white_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_timer_white_24dp.png b/res/drawable-xxxhdpi/ic_timer_white_24dp.png
new file mode 100644
index 0000000000..b8914c4a21
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_timer_white_24dp.png differ
diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml
index ace096b692..4a15fa8de1 100644
--- a/res/layout/conversation_item_received.xml
+++ b/res/layout/conversation_item_received.xml
@@ -46,7 +46,8 @@
android:layout_toRightOf="@id/contact_photo"
android:layout_marginRight="35dp"
android:background="@drawable/received_bubble"
- android:orientation="vertical">
+ android:orientation="vertical"
+ tools:backgroundTint="@color/blue_900">
+ android:tintMode="multiply"
+ tools:visibility="visible"/>
+
+
+ tools:text="Now"
+ tools:visibility="visible"/>
+
+
+ android:contentDescription="@string/conversation_item__secure_message_description"
+ tools:visibility="visible"/>
diff --git a/res/layout/conversation_item_update.xml b/res/layout/conversation_item_update.xml
index 29557e17a1..f22e7248cf 100644
--- a/res/layout/conversation_item_update.xml
+++ b/res/layout/conversation_item_update.xml
@@ -5,6 +5,8 @@
android:id="@+id/conversation_update_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:focusable="true"
+ android:background="@drawable/conversation_item_background"
android:orientation="horizontal"
android:gravity="center"
android:padding="20dp">
diff --git a/res/layout/delivery_status_view.xml b/res/layout/delivery_status_view.xml
index 8fe3b40cd8..a28be96731 100644
--- a/res/layout/delivery_status_view.xml
+++ b/res/layout/delivery_status_view.xml
@@ -1,10 +1,12 @@
-
+
+ android:layout_height="wrap_content"
+ tools:visibility="gone"/>
+ android:contentDescription="@string/conversation_item_sent__delivered_description"
+ tools:visibility="visible"/>
\ No newline at end of file
diff --git a/res/layout/expiration_dialog.xml b/res/layout/expiration_dialog.xml
new file mode 100644
index 0000000000..6401a95645
--- /dev/null
+++ b/res/layout/expiration_dialog.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/expiration_timer_menu.xml b/res/layout/expiration_timer_menu.xml
new file mode 100644
index 0000000000..a4f9bd3fb0
--- /dev/null
+++ b/res/layout/expiration_timer_menu.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/message_details_header.xml b/res/layout/message_details_header.xml
index 39e7d55ed2..fd8aec7081 100644
--- a/res/layout/message_details_header.xml
+++ b/res/layout/message_details_header.xml
@@ -75,6 +75,26 @@
+
+
+
+
+
+
+
+
diff --git a/res/menu/conversation_callable_insecure.xml b/res/menu/conversation_callable_insecure.xml
index 3ebc79704f..04be0b922a 100644
--- a/res/menu/conversation_callable_insecure.xml
+++ b/res/menu/conversation_callable_insecure.xml
@@ -4,6 +4,6 @@
+ app:showAsAction="always" />
diff --git a/res/menu/conversation_callable_secure.xml b/res/menu/conversation_callable_secure.xml
index 01658d6e7f..6cf92f0624 100644
--- a/res/menu/conversation_callable_secure.xml
+++ b/res/menu/conversation_callable_secure.xml
@@ -5,6 +5,6 @@
+ app:showAsAction="always" />
\ No newline at end of file
diff --git a/res/menu/conversation_expiring_off.xml b/res/menu/conversation_expiring_off.xml
new file mode 100644
index 0000000000..155f45d3dd
--- /dev/null
+++ b/res/menu/conversation_expiring_off.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/res/menu/conversation_expiring_on.xml b/res/menu/conversation_expiring_on.xml
new file mode 100644
index 0000000000..5dbf88b001
--- /dev/null
+++ b/res/menu/conversation_expiring_on.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index 0b00997220..e7279e9aab 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -238,4 +238,19 @@
- @null
+
+ - 0
+ - 5
+ - 10
+ - 30
+ - 60
+ - 300
+ - 1800
+ - 3600
+ - 21600
+ - 43200
+ - 86400
+ - 604800
+
+
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 8fcaeb14e5..43c6510696 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -166,4 +166,12 @@
+
+
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 772d6aebbd..e31215873a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -379,6 +379,8 @@
Called %s
Missed call from %s
%s is on Signal, say hey!
+ You
+ %1$s set disappearing message time to %2$s
@@ -407,6 +409,11 @@
Link a Signal device?
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.
+
+ Disappearing messages
+ Your messages will not expire.
+ Messages sent and received in this conversation will disappear %s after they have been seen.
+
Enter passphrase
Signal icon
@@ -537,6 +544,7 @@
Missed call
Media message
%s is on Signal, say hey!
+ Disappearing message time set to %s
You do not have an identity key.
@@ -711,10 +719,48 @@
No devices linked...
Link new device
-
continue
+
+
+ Off
+
+
+ - 1 second
+ - %d seconds
+
+
+ %ds
+
+
+ - 1 minute
+ - %d minutes
+
+
+ %dm
+
+
+ - 1 hour
+ - %d hours
+
+
+ %dh
+
+
+ - 1 day
+ - %d days
+
+
+ %dd
+
+
+ - 1 week
+ - %d weeks
+
+
+ %dw
+
Could not read the log on your device. You can still use ADB to get a debug log instead.
Thanks for your help!
@@ -1078,6 +1124,12 @@
Save attachment
+
+ Disappearing messages
+
+
+ Messages expiring
+
Invite
diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java
index 25ea0de618..7d9c329a68 100644
--- a/src/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/src/org/thoughtcrime/securesms/ApplicationContext.java
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.jobs.persistence.EncryptingJobSerializer;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirementProvider;
import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirementProvider;
import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirementProvider;
+import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.dependencies.DependencyInjector;
@@ -52,8 +53,9 @@ import dagger.ObjectGraph;
*/
public class ApplicationContext extends Application implements DependencyInjector {
- private JobManager jobManager;
- private ObjectGraph objectGraph;
+ private ExpiringMessageManager expiringMessageManager;
+ private JobManager jobManager;
+ private ObjectGraph objectGraph;
private MediaNetworkRequirementProvider mediaNetworkRequirementProvider = new MediaNetworkRequirementProvider();
@@ -69,6 +71,7 @@ public class ApplicationContext extends Application implements DependencyInjecto
initializeLogging();
initializeDependencyInjection();
initializeJobManager();
+ initializeExpiringMessageManager();
initializeGcmCheck();
initializeSignedPreKeyCheck();
}
@@ -84,9 +87,12 @@ public class ApplicationContext extends Application implements DependencyInjecto
return jobManager;
}
+ public ExpiringMessageManager getExpiringMessageManager() {
+ return expiringMessageManager;
+ }
+
private void initializeDeveloperBuild() {
if (BuildConfig.DEV_BUILD) {
-// LeakCanary.install(this);
StrictMode.setThreadPolicy(new ThreadPolicy.Builder().detectAll()
.penaltyLog()
.build());
@@ -139,4 +145,8 @@ public class ApplicationContext extends Application implements DependencyInjecto
}
}
+ private void initializeExpiringMessageManager() {
+ this.expiringMessageManager = new ExpiringMessageManager(this);
+ }
+
}
diff --git a/src/org/thoughtcrime/securesms/BindableConversationItem.java b/src/org/thoughtcrime/securesms/BindableConversationItem.java
index c6fd33cfd9..2be2e70716 100644
--- a/src/org/thoughtcrime/securesms/BindableConversationItem.java
+++ b/src/org/thoughtcrime/securesms/BindableConversationItem.java
@@ -15,4 +15,6 @@ public interface BindableConversationItem extends Unbindable {
@NonNull Locale locale,
@NonNull Set batchSelected,
@NonNull Recipients recipients);
+
+ MessageRecord getMessageRecord();
}
diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java
index 3c510ba44b..d44e7a35cc 100644
--- a/src/org/thoughtcrime/securesms/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationActivity.java
@@ -36,6 +36,7 @@ import android.os.Vibrator;
import android.provider.Browser;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
+import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.WindowCompat;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
@@ -91,7 +92,6 @@ import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
import org.thoughtcrime.securesms.database.GroupDatabase;
-import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences;
@@ -103,6 +103,7 @@ import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.LocationSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints;
+import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
@@ -127,6 +128,7 @@ import org.thoughtcrime.securesms.util.DirectoryHelper.UserCapabilities;
import org.thoughtcrime.securesms.util.DirectoryHelper.UserCapabilities.Capability;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -137,7 +139,6 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
-import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
@@ -200,7 +201,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private AttachmentManager attachmentManager;
private AudioRecorder audioRecorder;
private BroadcastReceiver securityUpdateReceiver;
- private BroadcastReceiver groupUpdateReceiver;
+ private BroadcastReceiver recipientsStaleReceiver;
private EmojiDrawer emojiDrawer;
protected HidingLinearLayout quickAttachmentToggle;
private QuickAttachmentDrawer quickAttachmentDrawer;
@@ -318,9 +319,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected void onDestroy() {
saveDraft();
- if (recipients != null) recipients.removeListener(this);
- if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver);
- if (groupUpdateReceiver != null) unregisterReceiver(groupUpdateReceiver);
+ if (recipients != null) recipients.removeListener(this);
+ if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver);
+ if (recipientsStaleReceiver != null) unregisterReceiver(recipientsStaleReceiver);
super.onDestroy();
}
@@ -382,6 +383,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MenuInflater inflater = this.getMenuInflater();
menu.clear();
+ if (isSecureText) {
+ if (recipients.getExpireMessages() > 0) {
+ inflater.inflate(R.menu.conversation_expiring_on, menu);
+
+ final MenuItem item = menu.findItem(R.id.menu_expiring_messages);
+ final View actionView = MenuItemCompat.getActionView(item);
+ final TextView badgeView = (TextView)actionView.findViewById(R.id.expiration_badge);
+
+ badgeView.setText(ExpirationUtil.getExpirationAbbreviatedDisplayValue(this, recipients.getExpireMessages()));
+ actionView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onOptionsItemSelected(item);
+ }
+ });
+ } else {
+ inflater.inflate(R.menu.conversation_expiring_off, menu);
+ }
+ }
+
if (isSingleConversation()) {
if (isSecureVoice) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
@@ -438,6 +459,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_mute_notifications: handleMuteNotifications(); return true;
case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true;
case R.id.menu_conversation_settings: handleConversationSettings(); return true;
+ case R.id.menu_expiring_messages_off:
+ case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true;
case android.R.id.home: handleReturnToConversationList(); return true;
}
@@ -465,6 +488,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
finish();
}
+ private void handleSelectMessageExpiration() {
+ ExpirationDialog.show(this, recipients.getExpireMessages(), new ExpirationDialog.OnClickListener() {
+ @Override
+ public void onClick(final int expirationTime) {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this)
+ .setExpireMessages(recipients, expirationTime);
+ recipients.setExpireMessages(expirationTime);
+
+ OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipients(), System.currentTimeMillis(), expirationTime * 1000);
+ MessageSender.send(ConversationActivity.this, masterSecret, outgoingMessage, threadId, false);
+
+ invalidateOptionsMenu();
+ return null;
+ }
+ }.execute();
+ }
+ });
+ }
+
private void handleMuteNotifications() {
MuteDialog.show(this, new MuteDialog.MuteSelectionListener() {
@Override
@@ -547,7 +592,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final Context context = getApplicationContext();
OutgoingEndSessionMessage endSessionMessage =
- new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE", -1));
+ new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE", 0, -1));
new AsyncTask() {
@Override
@@ -599,7 +644,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.setType(GroupContext.Type.QUIT)
.build();
- OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipients(), context, null, System.currentTimeMillis());
+ OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipients(), context, null, System.currentTimeMillis(), 0);
MessageSender.send(self, masterSecret, outgoingMessage, threadId, false);
DatabaseFactory.getGroupDatabase(self).remove(groupId, TextSecurePreferences.getLocalNumber(self));
initializeEnabledCheck();
@@ -979,7 +1024,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void initializeResources() {
if (recipients != null) recipients.removeListener(this);
-
+
recipients = RecipientFactory.getRecipientsForIds(this, getIntent().getLongArrayExtra(RECIPIENTS_EXTRA), true);
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
archived = getIntent().getBooleanExtra(IS_ARCHIVED_EXTRA, false);
@@ -1002,6 +1047,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
titleView.setTitle(recipients);
setBlockedUserState(recipients);
setActionBarColor(recipients.getColor());
+ invalidateOptionsMenu();
updateRecipientPreferences();
}
});
@@ -1016,26 +1062,30 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
};
- groupUpdateReceiver = new BroadcastReceiver() {
+ recipientsStaleReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- Log.w("ConversationActivity", "Group update received...");
+ Log.w(TAG, "Group update received...");
if (recipients != null) {
long[] ids = recipients.getIds();
- Log.w("ConversationActivity", "Looking up new recipients...");
+ Log.w(TAG, "Looking up new recipients...");
recipients = RecipientFactory.getRecipientsForIds(context, ids, true);
recipients.addListener(ConversationActivity.this);
- titleView.setTitle(recipients);
+ onModified(recipients);
+ fragment.reloadList();
}
}
};
+ IntentFilter staleFilter = new IntentFilter();
+ staleFilter.addAction(GroupDatabase.DATABASE_UPDATE_ACTION);
+ staleFilter.addAction(RecipientFactory.RECIPIENT_CLEAR_ACTION);
+
registerReceiver(securityUpdateReceiver,
new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT),
KeyCachingService.KEY_PERMISSION, null);
- registerReceiver(groupUpdateReceiver,
- new IntentFilter(GroupDatabase.DATABASE_UPDATE_ACTION));
+ registerReceiver(recipientsStaleReceiver, staleFilter);
}
//////// Helper Methods
@@ -1281,6 +1331,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Recipients recipients = getRecipients();
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
+ long expiresIn = recipients.getExpireMessages() * 1000;
Log.w(TAG, "isManual Selection: " + sendButton.isManualSelection());
Log.w(TAG, "forceSms: " + forceSms);
@@ -1292,9 +1343,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
handleManualMmsRequired();
} else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
- sendMediaMessage(forceSms, subscriptionId);
+ sendMediaMessage(forceSms, expiresIn, subscriptionId);
} else {
- sendTextMessage(forceSms, subscriptionId);
+ sendTextMessage(forceSms, expiresIn, subscriptionId);
}
} catch (RecipientFormattingException ex) {
Toast.makeText(ConversationActivity.this,
@@ -1308,13 +1359,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
- private void sendMediaMessage(final boolean forceSms, final int subscriptionId)
+ private void sendMediaMessage(final boolean forceSms, final long expiresIn, final int subscriptionId)
throws InvalidMessageException
{
- sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), subscriptionId);
+ sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), expiresIn, subscriptionId);
}
- private ListenableFuture sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, final int subscriptionId)
+ private ListenableFuture sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, final long expiresIn, final int subscriptionId)
throws InvalidMessageException
{
final SettableFuture future = new SettableFuture<>();
@@ -1324,6 +1375,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
body,
System.currentTimeMillis(),
subscriptionId,
+ expiresIn,
distributionType);
if (isSecureText && !forceSms) {
@@ -1349,16 +1401,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return future;
}
- private void sendTextMessage(final boolean forceSms, final int subscriptionId)
+ private void sendTextMessage(final boolean forceSms, final long expiresIn, final int subscriptionId)
throws InvalidMessageException
{
final Context context = getApplicationContext();
OutgoingTextMessage message;
if (isSecureText && !forceSms) {
- message = new OutgoingEncryptedMessage(recipients, getMessage());
+ message = new OutgoingEncryptedMessage(recipients, getMessage(), expiresIn);
} else {
- message = new OutgoingTextMessage(recipients, getMessage(), subscriptionId);
+ message = new OutgoingTextMessage(recipients, getMessage(), expiresIn, subscriptionId);
}
this.composeText.setText("");
@@ -1451,11 +1503,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
try {
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
+ long expiresIn = recipients.getExpireMessages() * 1000;
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, ContentType.AUDIO_AAC);
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
- sendMediaMessage(forceSms, "", slideDeck, subscriptionId).addListener(new AssertedSuccessListener() {
+ sendMediaMessage(forceSms, "", slideDeck, expiresIn, subscriptionId).addListener(new AssertedSuccessListener() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask() {
diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java
index 3befd8afc2..8ae6b194fd 100644
--- a/src/org/thoughtcrime/securesms/ConversationAdapter.java
+++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java
@@ -22,7 +22,6 @@ import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -39,6 +38,8 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.LRUCache;
+import org.thoughtcrime.securesms.util.ViewUtil;
+import org.thoughtcrime.securesms.util.VisibleForTesting;
import java.lang.ref.SoftReference;
import java.security.MessageDigest;
@@ -49,9 +50,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import org.thoughtcrime.securesms.util.ViewUtil;
-import org.thoughtcrime.securesms.util.VisibleForTesting;
-
/**
* A cursor adapter for a conversation thread. Ultimately
* used by ComposeMessageActivity to display a conversation
@@ -94,8 +92,8 @@ public class ConversationAdapter
}
public interface ItemClickListener {
- void onItemClick(ConversationItem item);
- void onItemLongClick(ConversationItem item);
+ void onItemClick(MessageRecord item);
+ void onItemLongClick(MessageRecord item);
}
@SuppressWarnings("ConstantConditions")
@@ -156,21 +154,23 @@ public class ConversationAdapter
@Override
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
- if (viewType == MESSAGE_TYPE_INCOMING || viewType == MESSAGE_TYPE_OUTGOING) {
- itemView.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- if (clickListener != null) clickListener.onItemClick((ConversationItem)itemView);
+ itemView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (clickListener != null) {
+ clickListener.onItemClick(itemView.getMessageRecord());
}
- });
- itemView.setOnLongClickListener(new OnLongClickListener() {
- @Override
- public boolean onLongClick(View view) {
- if (clickListener != null) clickListener.onItemLongClick((ConversationItem)itemView);
- return true;
+ }
+ });
+ itemView.setOnLongClickListener(new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ if (clickListener != null) {
+ clickListener.onItemLongClick(itemView.getMessageRecord());
}
- });
- }
+ return true;
+ }
+ });
return new ViewHolder(itemView);
}
@@ -195,7 +195,7 @@ public class ConversationAdapter
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
- if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined()) {
+ if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate()) {
return MESSAGE_TYPE_UPDATE;
} else if (messageRecord.isOutgoing()) {
return MESSAGE_TYPE_OUTGOING;
diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java
index 7811cfac23..4a9af02b5a 100644
--- a/src/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationFragment.java
@@ -27,8 +27,8 @@ import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
-import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -57,10 +57,10 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
-import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.ViewUtil;
+import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import java.util.Collections;
import java.util.Comparator;
@@ -165,24 +165,34 @@ public class ConversationFragment extends Fragment
if (this.recipients != null && this.threadId != -1) {
list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients));
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
- list.getItemAnimator().setSupportsChangeAnimations(false);
list.getItemAnimator().setMoveDuration(120);
}
}
private void setCorrectMenuVisibility(Menu menu) {
Set messageRecords = getListAdapter().getSelectedItems();
+ boolean actionMessage = false;
if (actionMode != null && messageRecords.size() == 0) {
actionMode.finish();
return;
}
+ for (MessageRecord messageRecord : messageRecords) {
+ if (messageRecord.isGroupAction() || messageRecord.isCallLog() ||
+ messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate())
+ {
+ actionMessage = true;
+ break;
+ }
+ }
+
if (messageRecords.size() > 1) {
menu.findItem(R.id.menu_context_forward).setVisible(false);
menu.findItem(R.id.menu_context_details).setVisible(false);
menu.findItem(R.id.menu_context_save_attachment).setVisible(false);
menu.findItem(R.id.menu_context_resend).setVisible(false);
+ menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage);
} else {
MessageRecord messageRecord = messageRecords.iterator().next();
@@ -191,9 +201,9 @@ public class ConversationFragment extends Fragment
!messageRecord.isMmsNotification() &&
((MediaMmsMessageRecord)messageRecord).containsMediaSlide());
- menu.findItem(R.id.menu_context_forward).setVisible(true);
- menu.findItem(R.id.menu_context_details).setVisible(true);
- menu.findItem(R.id.menu_context_copy).setVisible(true);
+ menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage);
+ menu.findItem(R.id.menu_context_details).setVisible(!actionMessage);
+ menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage);
}
}
@@ -386,9 +396,8 @@ public class ConversationFragment extends Fragment
private class ConversationFragmentItemClickListener implements ItemClickListener {
@Override
- public void onItemClick(ConversationItem item) {
+ public void onItemClick(MessageRecord messageRecord) {
if (actionMode != null) {
- MessageRecord messageRecord = item.getMessageRecord();
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged();
@@ -397,9 +406,9 @@ public class ConversationFragment extends Fragment
}
@Override
- public void onItemLongClick(ConversationItem item) {
+ public void onItemLongClick(MessageRecord messageRecord) {
if (actionMode == null) {
- ((ConversationAdapter) list.getAdapter()).toggleSelection(item.getMessageRecord());
+ ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged();
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java
index 01e1ca07a2..72b6bf1a3f 100644
--- a/src/org/thoughtcrime/securesms/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationItem.java
@@ -23,6 +23,7 @@ import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
+import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
@@ -42,6 +43,7 @@ import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.AlertView;
+import org.thoughtcrime.securesms.components.ExpirationTimerView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@@ -61,6 +63,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -110,6 +113,7 @@ public class ConversationItem extends LinearLayout
private @NonNull AudioView audioView;
private @NonNull Button mmsDownloadButton;
private @NonNull TextView mmsDownloadingLabel;
+ private @NonNull ExpirationTimerView expirationTimer;
private int defaultBubbleColor;
@@ -151,6 +155,7 @@ public class ConversationItem extends LinearLayout
this.bodyBubble = findViewById(R.id.body_bubble);
this.mediaThumbnail = (ThumbnailView) findViewById(R.id.image_view);
this.audioView = (AudioView) findViewById(R.id.audio_view);
+ this.expirationTimer = (ExpirationTimerView) findViewById(R.id.expiration_indicator);
setOnClickListener(new ClickListener(null));
PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
@@ -194,6 +199,7 @@ public class ConversationItem extends LinearLayout
setMinimumWidth();
setMediaAttributes(messageRecord);
setSimInfo(messageRecord);
+ setExpiration(messageRecord);
}
private void initializeAttributes() {
@@ -211,6 +217,8 @@ public class ConversationItem extends LinearLayout
if (recipient != null) {
recipient.removeListener(this);
}
+
+ this.expirationTimer.stopAnimation();
}
public MessageRecord getMessageRecord() {
@@ -353,6 +361,36 @@ public class ConversationItem extends LinearLayout
}
}
+ private void setExpiration(final MessageRecord messageRecord) {
+ if (messageRecord.getExpiresIn() > 0) {
+ this.expirationTimer.setVisibility(View.VISIBLE);
+ this.expirationTimer.setPercentage(0);
+
+ if (messageRecord.getExpireStarted() > 0) {
+ this.expirationTimer.setExpirationTime(messageRecord.getExpireStarted(),
+ messageRecord.getExpiresIn());
+ this.expirationTimer.startAnimation();
+ } else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
+ long id = messageRecord.getId();
+ boolean mms = messageRecord.isMms();
+
+ if (mms) DatabaseFactory.getMmsDatabase(context).markExpireStarted(id);
+ else DatabaseFactory.getSmsDatabase(context).markExpireStarted(id);
+
+ expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
+ return null;
+ }
+ }.execute();
+ }
+ } else {
+ this.expirationTimer.setVisibility(View.GONE);
+ }
+ }
+
private void setFailedStatusIcons() {
alertView.setFailed();
deliveryStatusIndicator.setNone();
diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java
index cff90af531..0fc53dcedc 100644
--- a/src/org/thoughtcrime/securesms/ConversationListActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java
@@ -221,7 +221,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
public void onChange(boolean selfChange) {
super.onChange(selfChange);
Log.w(TAG, "Detected android contact data changed, refreshing cache");
- RecipientFactory.clearCache();
+ RecipientFactory.clearCache(ConversationListActivity.this);
ConversationListActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
diff --git a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
index de9e51ebf4..1521ce0002 100644
--- a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
@@ -2,6 +2,9 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
@@ -59,6 +62,17 @@ public class ConversationUpdateItem extends LinearLayout
@NonNull Recipients conversationRecipients)
{
bind(messageRecord, locale);
+
+ if (batchSelected.contains(messageRecord)) {
+ setSelected(true);
+ } else {
+ setSelected(false);
+ }
+ }
+
+ @Override
+ public MessageRecord getMessageRecord() {
+ return messageRecord;
}
private void bind(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
@@ -68,10 +82,11 @@ public class ConversationUpdateItem extends LinearLayout
this.sender.addListener(this);
- if (messageRecord.isGroupAction()) setGroupRecord(messageRecord);
- else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
- else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
- else throw new AssertionError("Neither group nor log nor joined.");
+ if (messageRecord.isGroupAction()) setGroupRecord(messageRecord);
+ else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
+ else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
+ else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
+ else throw new AssertionError("Neither group nor log nor joined.");
}
private void setCallRecord(MessageRecord messageRecord) {
@@ -84,8 +99,22 @@ public class ConversationUpdateItem extends LinearLayout
date.setVisibility(View.VISIBLE);
}
+ private void setTimerRecord(final MessageRecord messageRecord) {
+ if (messageRecord.getExpiresIn() > 0) {
+ icon.setImageResource(R.drawable.ic_timer_white_24dp);
+ icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
+ } else {
+ icon.setImageResource(R.drawable.ic_timer_off_white_24dp);
+ icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
+ }
+
+ body.setText(messageRecord.getDisplayBody());
+ date.setVisibility(View.GONE);
+ }
+
private void setGroupRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_group_grey600_24dp);
+ icon.clearColorFilter();
GroupUtil.getDescription(getContext(), messageRecord.getBody().getBody()).addListener(this);
body.setText(messageRecord.getDisplayBody());
@@ -95,6 +124,7 @@ public class ConversationUpdateItem extends LinearLayout
private void setJoinedRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_favorite_grey600_24dp);
+ icon.clearColorFilter();
body.setText(messageRecord.getDisplayBody());
date.setVisibility(View.GONE);
}
diff --git a/src/org/thoughtcrime/securesms/ExpirationDialog.java b/src/org/thoughtcrime/securesms/ExpirationDialog.java
new file mode 100644
index 0000000000..7a8a74df47
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/ExpirationDialog.java
@@ -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[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);
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/MessageDetailsActivity.java b/src/org/thoughtcrime/securesms/MessageDetailsActivity.java
index d5cd6492aa..8e96c42463 100644
--- a/src/org/thoughtcrime/securesms/MessageDetailsActivity.java
+++ b/src/org/thoughtcrime/securesms/MessageDetailsActivity.java
@@ -23,6 +23,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.Log;
@@ -49,6 +50,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -79,9 +81,11 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private ConversationItem conversationItem;
private ViewGroup itemParent;
private View metadataContainer;
+ private View expiresContainer;
private TextView errorText;
private TextView sentDate;
private TextView receivedDate;
+ private TextView expiresInText;
private View receivedContainer;
private TextView transport;
private TextView toFrom;
@@ -91,6 +95,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private DynamicTheme dynamicTheme = new DynamicTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
+ private boolean running;
+
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
@@ -100,6 +106,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
@Override
public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
setContentView(R.layout.message_details_activity);
+ running = true;
initializeResources();
initializeActionBar();
@@ -122,6 +129,12 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(-1L);
}
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ running = false;
+ }
+
private void initializeActionBar() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -165,6 +178,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
receivedDate = (TextView ) header.findViewById(R.id.received_time);
transport = (TextView ) header.findViewById(R.id.transport);
toFrom = (TextView ) header.findViewById(R.id.tofrom);
+ expiresContainer = header.findViewById(R.id.expires_container);
+ expiresInText = (TextView) header.findViewById(R.id.expires_in);
recipientsList.setHeaderDividersEnabled(false);
recipientsList.addHeaderView(header, null, false);
}
@@ -204,6 +219,29 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}
}
+ private void updateExpirationTime(final MessageRecord messageRecord) {
+ if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
+ expiresContainer.setVisibility(View.GONE);
+ return;
+ }
+
+ expiresContainer.setVisibility(View.VISIBLE);
+ expiresInText.post(new Runnable() {
+ @Override
+ public void run() {
+ long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
+ long remaining = messageRecord.getExpiresIn() - elapsed;
+
+ String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1));
+ expiresInText.setText(duration);
+
+ if (running) {
+ expiresInText.postDelayed(this, 500);
+ }
+ }
+ });
+ }
+
private void updateRecipients(MessageRecord messageRecord, Recipients recipients) {
final int toFromRes;
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
@@ -233,7 +271,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}
}
- private MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
+ private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT:
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
@@ -257,8 +295,13 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
@Override
public void onLoadFinished(Loader loader, Cursor cursor) {
- final MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
- new MessageRecipientAsyncTask(this, messageRecord).execute();
+ MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
+
+ if (messageRecord == null) {
+ finish();
+ } else {
+ new MessageRecipientAsyncTask(this, messageRecord).execute();
+ }
}
@Override
@@ -281,7 +324,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private WeakReference weakContext;
private MessageRecord messageRecord;
- public MessageRecipientAsyncTask(Context context, MessageRecord messageRecord) {
+ public MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
this.weakContext = new WeakReference<>(context);
this.messageRecord = messageRecord;
}
@@ -340,6 +383,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
} else {
updateTransport(messageRecord);
updateTime(messageRecord);
+ updateExpirationTime(messageRecord);
errorText.setVisibility(View.GONE);
metadataContainer.setVisibility(View.VISIBLE);
}
diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
index f8bf4b923f..068e569f55 100644
--- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
+++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
@@ -1,7 +1,10 @@
package org.thoughtcrime.securesms;
+import android.content.BroadcastReceiver;
+import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.IntentFilter;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
@@ -28,6 +31,7 @@ import org.thoughtcrime.securesms.color.MaterialColors;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.preferences.AdvancedRingtonePreference;
import org.thoughtcrime.securesms.preferences.ColorPreference;
@@ -53,10 +57,11 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
- private AvatarImageView avatar;
- private Toolbar toolbar;
- private TextView title;
- private TextView blockedIndicator;
+ private AvatarImageView avatar;
+ private Toolbar toolbar;
+ private TextView title;
+ private TextView blockedIndicator;
+ private BroadcastReceiver staleReceiver;
@Override
public void onPreCreate() {
@@ -72,6 +77,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
Recipients recipients = RecipientFactory.getRecipientsForIds(this, recipientIds, true);
initializeToolbar();
+ initializeReceivers();
setHeader(recipients);
recipients.addListener(this);
@@ -87,6 +93,12 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
dynamicLanguage.onResume(this);
}
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(staleReceiver);
+ }
+
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@@ -118,6 +130,23 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
this.blockedIndicator = (TextView) toolbar.findViewById(R.id.blocked_indicator);
}
+ private void initializeReceivers() {
+ this.staleReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Recipients recipients = RecipientFactory.getRecipientsForIds(context, getIntent().getLongArrayExtra(RECIPIENTS_EXTRA), true);
+ recipients.addListener(RecipientPreferenceActivity.this);
+ onModified(recipients);
+ }
+ };
+
+ IntentFilter staleFilter = new IntentFilter();
+ staleFilter.addAction(GroupDatabase.DATABASE_UPDATE_ACTION);
+ staleFilter.addAction(RecipientFactory.RECIPIENT_CLEAR_ACTION);
+
+ registerReceiver(staleReceiver, staleFilter);
+ }
+
private void setHeader(Recipients recipients) {
this.avatar.setAvatar(recipients, true);
this.title.setText(recipients.toShortString());
@@ -149,18 +178,15 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private final Handler handler = new Handler();
private Recipients recipients;
+ private BroadcastReceiver staleReceiver;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.recipient_preferences);
+ initializeRecipients();
- this.recipients = RecipientFactory.getRecipientsForIds(getActivity(),
- getArguments().getLongArray(RECIPIENTS_EXTRA),
- true);
-
- this.recipients.addListener(this);
this.findPreference(PREFERENCE_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener());
this.findPreference(PREFERENCE_VIBRATE)
@@ -185,6 +211,30 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
public void onDestroy() {
super.onDestroy();
this.recipients.removeListener(this);
+ getActivity().unregisterReceiver(staleReceiver);
+ }
+
+ private void initializeRecipients() {
+ this.recipients = RecipientFactory.getRecipientsForIds(getActivity(),
+ getArguments().getLongArray(RECIPIENTS_EXTRA),
+ true);
+
+ this.recipients.addListener(this);
+
+ this.staleReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ recipients.removeListener(RecipientPreferenceFragment.this);
+ recipients = RecipientFactory.getRecipientsForIds(getActivity(), getArguments().getLongArray(RECIPIENTS_EXTRA), true);
+ onModified(recipients);
+ }
+ };
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(GroupDatabase.DATABASE_UPDATE_ACTION);
+ intentFilter.addAction(RecipientFactory.RECIPIENT_CLEAR_ACTION);
+
+ getActivity().registerReceiver(staleReceiver, intentFilter);
}
private void setSummaries(Recipients recipients) {
diff --git a/src/org/thoughtcrime/securesms/components/ExpirationTimerView.java b/src/org/thoughtcrime/securesms/components/ExpirationTimerView.java
new file mode 100644
index 0000000000..5e9deac6b0
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/ExpirationTimerView.java
@@ -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;
+ }
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/components/HourglassView.java b/src/org/thoughtcrime/securesms/components/HourglassView.java
new file mode 100644
index 0000000000..7ca4d950e9
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/HourglassView.java
@@ -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();
+ }
+
+
+
+}
diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
index c541564d91..42c9a7f26d 100644
--- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
+++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
@@ -72,7 +72,8 @@ public class DatabaseFactory {
private static final int INTRODUCED_CONVERSATION_LIST_STATUS_VERSION = 25;
private static final int MIGRATED_CONVERSATION_LIST_STATUS_VERSION = 26;
private static final int INTRODUCED_SUBSCRIPTION_ID_VERSION = 27;
- private static final int DATABASE_VERSION = 27;
+ private static final int INTRODUCED_EXPIRE_MESSAGES_VERSION = 28;
+ private static final int DATABASE_VERSION = 28;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@@ -820,6 +821,15 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE mms ADD COLUMN subscription_id INTEGER DEFAULT -1");
}
+ if (oldVersion < INTRODUCED_EXPIRE_MESSAGES_VERSION) {
+ db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN expire_messages INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE sms ADD COLUMN expires_in INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE mms ADD COLUMN expires_in INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE sms ADD COLUMN expire_started INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE mms ADD COLUMN expire_started INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE thread ADD COLUMN expires_in INTEGER DEFAULT 0");
+ }
+
db.setTransactionSuccessful();
db.endTransaction();
}
diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java
index cb687b3bd6..af5506c7de 100644
--- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java
@@ -11,11 +11,9 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.util.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
-import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
@@ -147,7 +145,7 @@ public class GroupDatabase extends Database {
GROUP_ID + " = ?",
new String[] {GroupUtil.getEncodedId(groupId)});
- RecipientFactory.clearCache();
+ RecipientFactory.clearCache(context);
notifyDatabaseListeners();
}
@@ -157,7 +155,7 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
new String[] {GroupUtil.getEncodedId(groupId)});
- RecipientFactory.clearCache();
+ RecipientFactory.clearCache(context);
notifyDatabaseListeners();
}
@@ -172,7 +170,7 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
new String[] {GroupUtil.getEncodedId(groupId)});
- RecipientFactory.clearCache();
+ RecipientFactory.clearCache(context);
notifyDatabaseListeners();
}
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index 8f0aa7e2ea..23b7fc63a9 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
+import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
@@ -110,7 +111,8 @@ public class MmsDatabase extends MessagingDatabase {
"ct_cls" + " INTEGER, " + "resp_txt" + " TEXT, " + "d_tm" + " INTEGER, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " +
NETWORK_FAILURE + " TEXT DEFAULT NULL," + "d_rpt" + " INTEGER, " +
- SUBSCRIPTION_ID + " INTEGER DEFAULT -1);";
+ SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
+ EXPIRE_STARTED + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@@ -130,6 +132,7 @@ public class MmsDatabase extends MessagingDatabase {
MESSAGE_SIZE, STATUS, TRANSACTION_ID,
BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
+ EXPIRES_IN, EXPIRE_STARTED,
AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS,
AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID,
@@ -340,6 +343,11 @@ public class MmsDatabase extends MessagingDatabase {
return cursor;
}
+ public Reader getExpireStartedMessages(@Nullable MasterSecret masterSecret) {
+ String where = EXPIRE_STARTED + " > 0";
+ return readerFor(masterSecret, rawQuery(where, null));
+ }
+
public Reader getDecryptInProgressMessages(MasterSecret masterSecret) {
String where = MESSAGE_BOX + " & " + (Types.ENCRYPTION_ASYMMETRIC_BIT) + " != 0";
return readerFor(masterSecret, rawQuery(where, null));
@@ -432,6 +440,21 @@ public class MmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
}
+ public void markExpireStarted(long messageId) {
+ markExpireStarted(messageId, System.currentTimeMillis());
+ }
+
+ public void markExpireStarted(long messageId, long startedTimestamp) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(EXPIRE_STARTED, startedTimestamp);
+
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
+
+ long threadId = getThreadIdForMessage(messageId);
+ notifyConversationListeners(threadId);
+ }
+
public List setMessagesRead(long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String where = THREAD_ID + " = ? AND " + READ + " = 0";
@@ -579,6 +602,7 @@ public class MmsDatabase extends MessagingDatabase {
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID));
+ long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
List attachments = new LinkedList(attachmentDatabase.getAttachmentsForMessage(messageId));
MmsAddresses addresses = addr.getAddressesForId(messageId);
List destinations = new LinkedList<>();
@@ -591,10 +615,12 @@ public class MmsDatabase extends MessagingDatabase {
Recipients recipients = RecipientFactory.getRecipientsFromStrings(context, destinations, false);
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
- return new OutgoingGroupMediaMessage(recipients, body, attachments, timestamp);
+ return new OutgoingGroupMediaMessage(recipients, body, attachments, timestamp, 0);
+ } else if (Types.isExpirationTimerUpdate(outboxType)) {
+ return new OutgoingExpirationUpdateMessage(recipients, timestamp, expiresIn);
}
- OutgoingMediaMessage message = new OutgoingMediaMessage(recipients, body, attachments, timestamp, subscriptionId,
+ OutgoingMediaMessage message = new OutgoingMediaMessage(recipients, body, attachments, timestamp, subscriptionId, expiresIn,
!addresses.getBcc().isEmpty() ? ThreadDatabase.DistributionTypes.BROADCAST :
ThreadDatabase.DistributionTypes.DEFAULT);
if (Types.isSecureType(outboxType)) {
@@ -623,6 +649,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(THREAD_ID, getThreadIdForMessage(messageId));
contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
+ contentValues.put(EXPIRES_IN, request.getExpiresIn());
List attachments = new LinkedList<>();
@@ -678,6 +705,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(DATE_RECEIVED, generatePduCompatTimestamp());
contentValues.put(PART_COUNT, retrieved.getAttachments().size());
contentValues.put(SUBSCRIPTION_ID, retrieved.getSubscriptionId());
+ contentValues.put(EXPIRES_IN, retrieved.getExpiresIn());
contentValues.put(READ, 0);
if (!contentValues.containsKey(DATE_SENT)) {
@@ -688,8 +716,11 @@ public class MmsDatabase extends MessagingDatabase {
retrieved.getBody(), retrieved.getAttachments(),
contentValues);
- DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
- DatabaseFactory.getThreadDatabase(context).update(threadId, true);
+ if (!Types.isExpirationTimerUpdate(mailbox)) {
+ DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
+ DatabaseFactory.getThreadDatabase(context).update(threadId, true);
+ }
+
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
@@ -713,6 +744,10 @@ public class MmsDatabase extends MessagingDatabase {
type |= Types.PUSH_MESSAGE_BIT;
}
+ if (retrieved.isExpirationUpdate()) {
+ type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
+ }
+
return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId, type);
}
@@ -733,6 +768,10 @@ public class MmsDatabase extends MessagingDatabase {
type |= Types.PUSH_MESSAGE_BIT;
}
+ if (retrieved.isExpirationUpdate()) {
+ type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
+ }
+
return insertMessageInbox(masterSecret, retrieved, "", threadId, type);
}
@@ -805,6 +844,10 @@ public class MmsDatabase extends MessagingDatabase {
else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT;
}
+ if (message.isExpirationUpdate()) {
+ type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
+ }
+
List recipientNumbers = message.getRecipients().toNumberStringList(true);
MmsAddresses addresses;
@@ -826,6 +869,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
+ contentValues.put(EXPIRES_IN, message.getExpiresIn());
if (message.getRecipients().isSingleRecipient()) {
try {
@@ -1118,6 +1162,8 @@ public class MmsDatabase extends MessagingDatabase {
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.SUBSCRIPTION_ID));
+ long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRES_IN));
+ long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED));
Recipients recipients = getRecipientsFor(address);
List mismatches = getMismatchedIdentities(mismatchDocument);
@@ -1127,7 +1173,7 @@ public class MmsDatabase extends MessagingDatabase {
return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
addressDeviceId, dateSent, dateReceived, receiptCount,
threadId, body, slideDeck, partCount, box, mismatches,
- networkFailures, subscriptionId);
+ networkFailures, subscriptionId, expiresIn, expireStarted);
}
private Recipients getRecipientsFor(String address) {
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
index 0044948a93..b024a3aebb 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
@@ -14,7 +14,9 @@ public interface MmsSmsColumns {
public static final String RECEIPT_COUNT = "delivery_receipt_count";
public static final String MISMATCHED_IDENTITIES = "mismatched_identities";
public static final String UNIQUE_ROW_ID = "unique_row_id";
- public static final String SUBSCRIPTION_ID = "subscription_id";
+ public static final String SUBSCRIPTION_ID = "subscription_id";
+ public static final String EXPIRES_IN = "expires_in";
+ public static final String EXPIRE_STARTED = "expire_started";
public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF;
@@ -61,8 +63,9 @@ public interface MmsSmsColumns {
protected static final long PUSH_MESSAGE_BIT = 0x200000;
// Group Message Information
- protected static final long GROUP_UPDATE_BIT = 0x10000;
- protected static final long GROUP_QUIT_BIT = 0x20000;
+ protected static final long GROUP_UPDATE_BIT = 0x10000;
+ protected static final long GROUP_QUIT_BIT = 0x20000;
+ protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
// Encrypted Storage Information
protected static final long ENCRYPTION_MASK = 0xFF000000;
@@ -166,6 +169,10 @@ public interface MmsSmsColumns {
return type == INCOMING_CALL_TYPE || type == OUTGOING_CALL_TYPE || type == MISSED_CALL_TYPE;
}
+ public static boolean isExpirationTimerUpdate(long type) {
+ return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0;
+ }
+
public static boolean isIncomingCall(long type) {
return type == INCOMING_CALL_TYPE;
}
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index dfccd983ef..3ad42dec9f 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -33,8 +33,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.HashSet;
import java.util.Set;
-import ws.com.google.android.mms.pdu.PduHeaders;
-
public class MmsSmsDatabase extends Database {
private static final String TAG = MmsSmsDatabase.class.getSimpleName();
@@ -56,7 +54,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE,
- MmsSmsColumns.SUBSCRIPTION_ID, TRANSPORT,
+ MmsSmsColumns.SUBSCRIPTION_ID,
+ MmsSmsColumns.EXPIRES_IN,
+ MmsSmsColumns.EXPIRE_STARTED, TRANSPORT,
AttachmentDatabase.ATTACHMENT_ID_ALIAS,
AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID,
@@ -147,7 +147,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
- MmsSmsColumns.SUBSCRIPTION_ID, MmsDatabase.NETWORK_FAILURE, TRANSPORT,
+ MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED,
+ MmsDatabase.NETWORK_FAILURE, TRANSPORT,
AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID,
AttachmentDatabase.SIZE,
@@ -171,7 +172,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
- MmsSmsColumns.SUBSCRIPTION_ID,
+ MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED,
MmsDatabase.NETWORK_FAILURE, TRANSPORT,
AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID,
@@ -209,6 +210,8 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
mmsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
mmsColumnsPresent.add(MmsSmsColumns.SUBSCRIPTION_ID);
+ mmsColumnsPresent.add(MmsSmsColumns.EXPIRES_IN);
+ mmsColumnsPresent.add(MmsSmsColumns.EXPIRE_STARTED);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX);
mmsColumnsPresent.add(MmsDatabase.DATE_SENT);
@@ -240,6 +243,8 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
smsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
smsColumnsPresent.add(MmsSmsColumns.SUBSCRIPTION_ID);
+ smsColumnsPresent.add(MmsSmsColumns.EXPIRES_IN);
+ smsColumnsPresent.add(MmsSmsColumns.EXPIRE_STARTED);
smsColumnsPresent.add(SmsDatabase.TYPE);
smsColumnsPresent.add(SmsDatabase.SUBJECT);
smsColumnsPresent.add(SmsDatabase.DATE_SENT);
diff --git a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java
index 1b626486f2..8578bd1d42 100644
--- a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java
@@ -33,6 +33,7 @@ public class RecipientPreferenceDatabase extends Database {
private static final String COLOR = "color";
private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder";
private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id";
+ private static final String EXPIRE_MESSAGES = "expire_messages";
public enum VibrateState {
DEFAULT(0), ENABLED(1), DISABLED(2);
@@ -62,7 +63,8 @@ public class RecipientPreferenceDatabase extends Database {
MUTE_UNTIL + " INTEGER DEFAULT 0, " +
COLOR + " TEXT DEFAULT NULL, " +
SEEN_INVITE_REMINDER + " INTEGER DEFAULT 0, " +
- DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1);";
+ DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
+ EXPIRE_MESSAGES + " INTEGER DEFAULT 0);";
public RecipientPreferenceDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
@@ -98,6 +100,7 @@ public class RecipientPreferenceDatabase extends Database {
Uri notificationUri = notification == null ? null : Uri.parse(notification);
boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1;
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
+ int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES));
MaterialColor color;
@@ -113,7 +116,7 @@ public class RecipientPreferenceDatabase extends Database {
return Optional.of(new RecipientsPreferences(blocked, muteUntil,
VibrateState.fromId(vibrateState),
notificationUri, color, seenInviteReminder,
- defaultSubscriptionId));
+ defaultSubscriptionId, expireMessages));
}
return Optional.absent();
@@ -134,7 +137,6 @@ public class RecipientPreferenceDatabase extends Database {
updateOrInsert(recipients, values);
}
-
public void setBlocked(Recipients recipients, boolean blocked) {
ContentValues values = new ContentValues();
values.put(BLOCK, blocked ? 1 : 0);
@@ -166,6 +168,14 @@ public class RecipientPreferenceDatabase extends Database {
updateOrInsert(recipients, values);
}
+ public void setExpireMessages(Recipients recipients, int expiration) {
+ recipients.setExpireMessages(expiration);
+
+ ContentValues values = new ContentValues(1);
+ values.put(EXPIRE_MESSAGES, expiration);
+ updateOrInsert(recipients, values);
+ }
+
private void updateOrInsert(Recipients recipients, ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
@@ -193,13 +203,15 @@ public class RecipientPreferenceDatabase extends Database {
private final MaterialColor color;
private final boolean seenInviteReminder;
private final int defaultSubscriptionId;
+ private final int expireMessages;
public RecipientsPreferences(boolean blocked, long muteUntil,
@NonNull VibrateState vibrateState,
@Nullable Uri notification,
@Nullable MaterialColor color,
boolean seenInviteReminder,
- int defaultSubscriptionId)
+ int defaultSubscriptionId,
+ int expireMessages)
{
this.blocked = blocked;
this.muteUntil = muteUntil;
@@ -208,6 +220,7 @@ public class RecipientPreferenceDatabase extends Database {
this.color = color;
this.seenInviteReminder = seenInviteReminder;
this.defaultSubscriptionId = defaultSubscriptionId;
+ this.expireMessages = expireMessages;
}
public @Nullable MaterialColor getColor() {
@@ -237,5 +250,9 @@ public class RecipientPreferenceDatabase extends Database {
public Optional getDefaultSubscriptionId() {
return defaultSubscriptionId != -1 ? Optional.of(defaultSubscriptionId) : Optional.absent();
}
+
+ public int getExpireMessages() {
+ return expireMessages;
+ }
}
}
diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
index 16038b7174..0b5b3d2c15 100644
--- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -77,7 +77,8 @@ public class SmsDatabase extends MessagingDatabase {
DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0," + SUBJECT + " TEXT, " + BODY + " TEXT, " +
- MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1);";
+ MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
+ EXPIRES_IN + " INTEGER DEFAULT 0, " + EXPIRE_STARTED + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@@ -94,7 +95,7 @@ public class SmsDatabase extends MessagingDatabase {
DATE_SENT + " AS " + NORMALIZED_DATE_SENT,
PROTOCOL, READ, STATUS, TYPE,
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, RECEIPT_COUNT,
- MISMATCHED_IDENTITIES, SUBSCRIPTION_ID
+ MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED
};
private static final EarlyReceiptCache earlyReceiptCache = new EarlyReceiptCache();
@@ -235,6 +236,23 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE);
}
+ public void markExpireStarted(long id) {
+ markExpireStarted(id, System.currentTimeMillis());
+ }
+
+ public void markExpireStarted(long id, long startedAtTimestamp) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(EXPIRE_STARTED, startedAtTimestamp);
+
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(id)});
+
+ long threadId = getThreadIdForMessage(id);
+
+ DatabaseFactory.getThreadDatabase(context).update(threadId, false);
+ notifyConversationListeners(threadId);
+ }
+
public void markStatus(long id, int status) {
Log.w("MessageDatabase", "Updating ID: " + id + " to status: " + status);
ContentValues contentValues = new ContentValues();
@@ -402,6 +420,7 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(READ, 0);
contentValues.put(BODY, record.getBody().getBody());
contentValues.put(THREAD_ID, record.getThreadId());
+ contentValues.put(EXPIRES_IN, record.getExpiresIn());
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long newMessageId = db.insert(TABLE_NAME, null, contentValues);
@@ -505,6 +524,7 @@ public class SmsDatabase extends MessagingDatabase {
values.put(PROTOCOL, message.getProtocol());
values.put(READ, unread ? 0 : 1);
values.put(SUBSCRIPTION_ID, message.getSubscriptionId());
+ values.put(EXPIRES_IN, message.getExpiresIn());
if (!TextUtils.isEmpty(message.getPseudoSubject()))
values.put(SUBJECT, message.getPseudoSubject());
@@ -552,6 +572,7 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(READ, 1);
contentValues.put(TYPE, type);
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
+ contentValues.put(EXPIRES_IN, message.getExpiresIn());
try {
contentValues.put(RECEIPT_COUNT, earlyReceiptCache.remove(date, canonicalizeNumber(context, address)));
@@ -594,6 +615,12 @@ public class SmsDatabase extends MessagingDatabase {
return db.query(TABLE_NAME, MESSAGE_PROJECTION, selection, args, null, null, null);
}
+ public Cursor getExpirationStartedMessages() {
+ String where = EXPIRE_STARTED + " > 0";
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ return db.query(TABLE_NAME, MESSAGE_PROJECTION, where, null, null, null, null);
+ }
+
public Cursor getMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""},
@@ -719,6 +746,8 @@ public class SmsDatabase extends MessagingDatabase {
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.RECEIPT_COUNT));
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.MISMATCHED_IDENTITIES));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.SUBSCRIPTION_ID));
+ long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRES_IN));
+ long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED));
List mismatches = getMismatches(mismatchDocument);
Recipients recipients = getRecipientsFor(address);
@@ -728,7 +757,8 @@ public class SmsDatabase extends MessagingDatabase {
recipients.getPrimaryRecipient(),
addressDeviceId,
dateSent, dateReceived, receiptCount, type,
- threadId, status, mismatches, subscriptionId);
+ threadId, status, mismatches, subscriptionId,
+ expiresIn, expireStarted);
}
private Recipients getRecipientsFor(String address) {
diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
index 0eb544d45c..69cbb9621d 100644
--- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -67,6 +67,7 @@ public class ThreadDatabase extends Database {
public static final String ARCHIVED = "archived";
public static final String STATUS = "status";
public static final String RECEIPT_COUNT = "delivery_receipt_count";
+ private static final String EXPIRES_IN = "expires_in";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " +
@@ -75,7 +76,7 @@ public class ThreadDatabase extends Database {
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
- RECEIPT_COUNT + " INTEGER DEFAULT 0);";
+ RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");",
@@ -133,7 +134,8 @@ public class ThreadDatabase extends Database {
}
private void updateThread(long threadId, long count, String body, @Nullable Uri attachment,
- long date, int status, int receiptCount, long type, boolean unarchive)
+ long date, int status, int receiptCount, long type, boolean unarchive,
+ long expiresIn)
{
ContentValues contentValues = new ContentValues(7);
contentValues.put(DATE, date - date % 1000);
@@ -143,6 +145,7 @@ public class ThreadDatabase extends Database {
contentValues.put(SNIPPET_TYPE, type);
contentValues.put(STATUS, status);
contentValues.put(RECEIPT_COUNT, receiptCount);
+ contentValues.put(EXPIRES_IN, expiresIn);
if (unarchive) {
contentValues.put(ARCHIVED, 0);
@@ -503,7 +506,7 @@ public class ThreadDatabase extends Database {
if (reader != null && (record = reader.getNext()) != null) {
updateThread(threadId, count, record.getBody().getBody(), getAttachmentUriFor(record),
record.getTimestamp(), record.getDeliveryStatus(), record.getReceiptCount(),
- record.getType(), unarchive);
+ record.getType(), unarchive, record.getExpiresIn());
notifyConversationListListeners();
return false;
} else {
@@ -572,10 +575,12 @@ public class ThreadDatabase extends Database {
boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0;
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS));
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.RECEIPT_COUNT));
+ long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
Uri snippetUri = getSnippetUri(cursor);
return new ThreadRecord(context, body, snippetUri, recipients, date, count, read == 1,
- threadId, receiptCount, status, type, distributionType, archived);
+ threadId, receiptCount, status, type, distributionType, archived,
+ expiresIn);
}
private DisplayRecord.Body getPlaintextBody(Cursor cursor) {
diff --git a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
index f3a2c6d64f..df6f6f80c5 100644
--- a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
@@ -115,6 +115,10 @@ public abstract class DisplayRecord {
return isGroupUpdate() || isGroupQuit();
}
+ public boolean isExpirationTimerUpdate() {
+ return SmsDatabase.Types.isExpirationTimerUpdate(type);
+ }
+
public boolean isCallLog() {
return SmsDatabase.Types.isCallLog(type);
}
diff --git a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java
index 7615e7a021..8ef55e2322 100644
--- a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
+import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -53,10 +54,12 @@ public class MediaMmsMessageRecord extends MessageRecord {
@NonNull SlideDeck slideDeck,
int partCount, long mailbox,
List mismatches,
- List failures, int subscriptionId)
+ List failures, int subscriptionId,
+ long expiresIn, long expireStarted)
{
super(context, id, body, recipients, individualRecipient, recipientDeviceId, dateSent,
- dateReceived, threadId, Status.STATUS_NONE, receiptCount, mailbox, mismatches, failures, subscriptionId);
+ dateReceived, threadId, Status.STATUS_NONE, receiptCount, mailbox, mismatches, failures,
+ subscriptionId, expiresIn, expireStarted);
this.context = context.getApplicationContext();
this.partCount = partCount;
@@ -85,6 +88,17 @@ public class MediaMmsMessageRecord extends MessageRecord {
return false;
}
+ @Override
+ public boolean isMediaPending() {
+ for (Slide slide : getSlideDeck().getSlides()) {
+ if (slide.isInProgress() || slide.isPendingDownload()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
@Override
public SpannableString getDisplayBody() {
if (MmsDatabase.Types.isDecryptInProgressType(type)) {
diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
index 608e14304e..563165d8d8 100644
--- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import java.util.List;
@@ -51,6 +52,8 @@ public abstract class MessageRecord extends DisplayRecord {
private final List mismatches;
private final List networkFailures;
private final int subscriptionId;
+ private final long expiresIn;
+ private final long expireStarted;
MessageRecord(Context context, long id, Body body, Recipients recipients,
Recipient individualRecipient, int recipientDeviceId,
@@ -58,7 +61,7 @@ public abstract class MessageRecord extends DisplayRecord {
int deliveryStatus, int receiptCount, long type,
List mismatches,
List networkFailures,
- int subscriptionId)
+ int subscriptionId, long expiresIn, long expireStarted)
{
super(context, body, recipients, dateSent, dateReceived, threadId, deliveryStatus, receiptCount,
type);
@@ -68,6 +71,8 @@ public abstract class MessageRecord extends DisplayRecord {
this.mismatches = mismatches;
this.networkFailures = networkFailures;
this.subscriptionId = subscriptionId;
+ this.expiresIn = expiresIn;
+ this.expireStarted = expireStarted;
}
public abstract boolean isMms();
@@ -103,6 +108,10 @@ public abstract class MessageRecord extends DisplayRecord {
return emphasisAdded(context.getString(R.string.MessageRecord_missed_call_from, getIndividualRecipient().toShortString()));
} else if (isJoined()) {
return emphasisAdded(context.getString(R.string.MessageRecord_s_is_on_signal_say_hey, getIndividualRecipient().toShortString()));
+ } else if (isExpirationTimerUpdate()) {
+ String sender = isOutgoing() ? context.getString(R.string.MessageRecord_you) : getIndividualRecipient().toShortString();
+ String time = ExpirationUtil.getExpirationDisplayValue(context, (int)(getExpiresIn() / 1000));
+ return emphasisAdded(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, sender, time));
} else if (getBody().getBody().length() > MAX_DISPLAY_LENGTH) {
return new SpannableString(getBody().getBody().substring(0, MAX_DISPLAY_LENGTH));
}
@@ -155,6 +164,10 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
}
+ public boolean isMediaPending() {
+ return false;
+ }
+
public Recipient getIndividualRecipient() {
return individualRecipient;
}
@@ -201,4 +214,12 @@ public abstract class MessageRecord extends DisplayRecord {
public int getSubscriptionId() {
return subscriptionId;
}
+
+ public long getExpiresIn() {
+ return expiresIn;
+ }
+
+ public long getExpireStarted() {
+ return expireStarted;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java
index 8da31f5308..c78413517e 100644
--- a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java
@@ -54,7 +54,8 @@ public class NotificationMmsMessageRecord extends MessageRecord {
{
super(context, id, new Body("", true), recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, Status.STATUS_NONE, receiptCount, mailbox,
- new LinkedList(), new LinkedList(), subscriptionId);
+ new LinkedList(), new LinkedList(), subscriptionId,
+ 0, 0);
this.contentLocation = contentLocation;
this.messageSize = messageSize;
@@ -113,6 +114,11 @@ public class NotificationMmsMessageRecord extends MessageRecord {
return true;
}
+ @Override
+ public boolean isMediaPending() {
+ return true;
+ }
+
@Override
public SpannableString getDisplayBody() {
return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_multimedia_message));
diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
index 6000aea9db..88eaa25be4 100644
--- a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
@@ -48,11 +48,12 @@ public class SmsMessageRecord extends MessageRecord {
int receiptCount,
long type, long threadId,
int status, List mismatches,
- int subscriptionId)
+ int subscriptionId, long expiresIn, long expireStarted)
{
super(context, id, body, recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, status, receiptCount, type,
- mismatches, new LinkedList(), subscriptionId);
+ mismatches, new LinkedList(), subscriptionId,
+ expiresIn, expireStarted);
}
public long getType() {
diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
index f6a8af9045..0a950ac6e4 100644
--- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
/**
@@ -46,11 +47,12 @@ public class ThreadRecord extends DisplayRecord {
private final boolean read;
private final int distributionType;
private final boolean archived;
+ private final long expiresIn;
public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri,
@NonNull Recipients recipients, long date, long count, boolean read,
long threadId, int receiptCount, int status, long snippetType,
- int distributionType, boolean archived)
+ int distributionType, boolean archived, long expiresIn)
{
super(context, body, recipients, date, date, threadId, status, receiptCount, snippetType);
this.context = context.getApplicationContext();
@@ -59,6 +61,7 @@ public class ThreadRecord extends DisplayRecord {
this.read = read;
this.distributionType = distributionType;
this.archived = archived;
+ this.expiresIn = expiresIn;
}
public @Nullable Uri getSnippetUri() {
@@ -96,6 +99,9 @@ public class ThreadRecord extends DisplayRecord {
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_missed_call));
} else if (SmsDatabase.Types.isJoinedType(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal_say_hey, getRecipients().getPrimaryRecipient().toShortString()));
+ } else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
+ String time = ExpirationUtil.getExpirationDisplayValue(context, (int)(getExpiresIn() / 1000));
+ return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
} else {
if (TextUtils.isEmpty(getBody().getBody())) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
@@ -135,4 +141,8 @@ public class ThreadRecord extends DisplayRecord {
public int getDistributionType() {
return distributionType;
}
+
+ public long getExpiresIn() {
+ return expiresIn;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java
index 0b61f86e78..9e0842bb8f 100644
--- a/src/org/thoughtcrime/securesms/groups/GroupManager.java
+++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java
@@ -107,7 +107,7 @@ public class GroupManager {
avatarAttachment = new UriAttachment(avatarUri, ContentType.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length);
}
- OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis());
+ OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0);
long threadId = MessageSender.send(context, masterSecret, outgoingMessage, -1, false);
return new GroupActionResult(groupRecipient, threadId);
diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
index 1b9b17e306..3a740115c3 100644
--- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
+++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
@@ -185,7 +185,7 @@ public class GroupMessageProcessor {
if (outgoing) {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(group.getGroupId()), false);
- OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipients, storage, null, envelope.getTimestamp());
+ OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipients, storage, null, envelope.getTimestamp(), 0);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = mmsDatabase.insertMessageOutbox(masterSecret, outgoingMessage, threadId, false);
@@ -195,7 +195,7 @@ public class GroupMessageProcessor {
} else {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = Base64.encodeBytes(storage.toByteArray());
- IncomingTextMessage incoming = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), body, Optional.of(group));
+ IncomingTextMessage incoming = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), body, Optional.of(group), 0);
IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, storage, body);
Pair messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, groupMessage);
diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
index 9a40bc701e..2d21b01fc4 100644
--- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
@@ -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 messageAndThreadId = database.insertMessageInbox(new MasterSecretUnion(masterSecret),
message, contentLocation, threadId);
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index c9bdfa469b..b8d99772ee 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
+import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
@@ -49,10 +50,11 @@ import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
-import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SessionStore;
+import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
+import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
@@ -141,6 +143,7 @@ public class PushDecryptJob extends ContextJob {
if (message.isEndSession()) handleEndSessionMessage(masterSecret, envelope, message, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(masterSecret, envelope, message, smsMessageId);
+ else if (message.isExpirationUpdate()) handleExpirationUpdate(masterSecret, envelope, message, smsMessageId);
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message, smsMessageId);
else handleTextMessage(masterSecret, envelope, message, smsMessageId);
} else if (content.getSyncMessage().isPresent()) {
@@ -185,7 +188,7 @@ public class PushDecryptJob extends ContextJob {
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(),
- "", Optional.absent());
+ "", Optional.absent(), 0);
long threadId;
@@ -218,6 +221,33 @@ public class PushDecryptJob extends ContextJob {
}
}
+ private void handleExpirationUpdate(@NonNull MasterSecretUnion masterSecret,
+ @NonNull SignalServiceEnvelope envelope,
+ @NonNull SignalServiceDataMessage message,
+ @NonNull Optional 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.absent(), message.getGroupInfo(),
+ Optional.>absent());
+
+
+
+ database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
+
+ DatabaseFactory.getRecipientPreferenceDatabase(context).setExpireMessages(recipients, message.getExpiresInSeconds());
+
+ if (smsMessageId.isPresent()) {
+ DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
+ }
+ }
+
private void handleSynchronizeSentMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope,
@NonNull SentTranscriptMessage message,
@@ -228,6 +258,8 @@ public class PushDecryptJob extends ContextJob {
if (message.getMessage().isGroupUpdate()) {
threadId = GroupMessageProcessor.process(context, masterSecret, envelope, message.getMessage(), true);
+ } else if (message.getMessage().isExpirationUpdate()) {
+ threadId = handleSynchronizeSentExpirationUpdate(masterSecret, message, smsMessageId);
} else if (message.getMessage().getAttachments().isPresent()) {
threadId = handleSynchronizeSentMediaMessage(masterSecret, message, smsMessageId);
} else {
@@ -275,13 +307,19 @@ public class PushDecryptJob extends ContextJob {
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
String localNumber = TextSecurePreferences.getLocalNumber(context);
+ Recipients recipients = getMessageDestination(envelope, message);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, envelope.getSource(),
localNumber, message.getTimestamp(), -1,
+ message.getExpiresInSeconds() * 1000, false,
Optional.fromNullable(envelope.getRelay()),
message.getBody(),
message.getGroupInfo(),
message.getAttachments());
+ if (message.getExpiresInSeconds() != recipients.getExpireMessages()) {
+ handleExpirationUpdate(masterSecret, envelope, message, Optional.absent());
+ }
+
Pair messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
List attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageAndThreadId.first);
@@ -299,6 +337,40 @@ public class PushDecryptJob extends ContextJob {
MessageNotifier.updateNotification(context, masterSecret.getMasterSecret().orNull(), messageAndThreadId.second);
}
+ private long handleSynchronizeSentExpirationUpdate(@NonNull MasterSecretUnion masterSecret,
+ @NonNull SentTranscriptMessage message,
+ @NonNull Optional smsMessageId)
+ throws MmsException
+ {
+ MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
+ Recipients recipients = getSyncMessageDestination(message);
+
+ OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipients,
+ message.getTimestamp(),
+ message.getMessage().getExpiresInSeconds() * 1000);
+
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
+ long messageId = database.insertMessageOutbox(masterSecret, expirationUpdateMessage, threadId, false);
+
+ database.markAsSent(messageId);
+ database.markAsPush(messageId);
+
+ DatabaseFactory.getRecipientPreferenceDatabase(context).setExpireMessages(recipients, message.getMessage().getExpiresInSeconds());
+
+ database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
+ ApplicationContext.getInstance(context)
+ .getExpiringMessageManager()
+ .scheduleDeletion(messageId, true,
+ message.getExpirationStartTimestamp(),
+ message.getMessage().getExpiresInSeconds());
+
+ if (smsMessageId.isPresent()) {
+ DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
+ }
+
+ return threadId;
+ }
+
private long handleSynchronizeSentMediaMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SentTranscriptMessage message,
@NonNull Optional smsMessageId)
@@ -308,10 +380,16 @@ public class PushDecryptJob extends ContextJob {
Recipients recipients = getSyncMessageDestination(message);
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
PointerAttachment.forPointers(masterSecret, message.getMessage().getAttachments()),
- message.getTimestamp(), -1, ThreadDatabase.DistributionTypes.DEFAULT);
+ message.getTimestamp(), -1,
+ message.getMessage().getExpiresInSeconds() * 1000,
+ ThreadDatabase.DistributionTypes.DEFAULT);
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
+ if (recipients.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
+ handleSynchronizeSentExpirationUpdate(masterSecret, message, Optional.absent());
+ }
+
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = database.insertMessageOutbox(masterSecret, mediaMessage, threadId, false);
@@ -328,6 +406,15 @@ public class PushDecryptJob extends ContextJob {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
+ if (message.getMessage().getExpiresInSeconds() > 0) {
+ database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
+ ApplicationContext.getInstance(context)
+ .getExpiringMessageManager()
+ .scheduleDeletion(messageId, true,
+ message.getExpirationStartTimestamp(),
+ message.getMessage().getExpiresInSeconds());
+ }
+
return threadId;
}
@@ -335,9 +422,15 @@ public class PushDecryptJob extends ContextJob {
@NonNull SignalServiceEnvelope envelope,
@NonNull SignalServiceDataMessage message,
@NonNull Optional smsMessageId)
+ throws MmsException
{
- EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
- String body = message.getBody().isPresent() ? message.getBody().get() : "";
+ EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
+ String body = message.getBody().isPresent() ? message.getBody().get() : "";
+ Recipients recipients = getMessageDestination(envelope, message);
+
+ if (message.getExpiresInSeconds() != recipients.getExpireMessages()) {
+ handleExpirationUpdate(masterSecret, envelope, message, Optional.absent());
+ }
Pair messageAndThreadId;
@@ -347,7 +440,8 @@ public class PushDecryptJob extends ContextJob {
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(), body,
- message.getGroupInfo());
+ message.getGroupInfo(),
+ message.getExpiresInSeconds() * 1000);
textMessage = new IncomingEncryptedMessage(textMessage, body);
messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
@@ -361,11 +455,17 @@ public class PushDecryptJob extends ContextJob {
private long handleSynchronizeSentTextMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SentTranscriptMessage message,
@NonNull Optional smsMessageId)
+ throws MmsException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Recipients recipients = getSyncMessageDestination(message);
String body = message.getMessage().getBody().or("");
- OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipients, body, -1);
+ long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000;
+ OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipients, body, expiresInMillis, -1);
+
+ if (recipients.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
+ handleSynchronizeSentExpirationUpdate(masterSecret, message, Optional.absent());
+ }
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long messageId = database.insertMessageOutbox(masterSecret, threadId, outgoingTextMessage, false, message.getTimestamp());
@@ -378,6 +478,13 @@ public class PushDecryptJob extends ContextJob {
database.deleteMessage(smsMessageId.get());
}
+ if (expiresInMillis > 0) {
+ database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
+ ApplicationContext.getInstance(context)
+ .getExpiringMessageManager()
+ .scheduleDeletion(messageId, false, message.getExpirationStartTimestamp(), expiresInMillis);
+ }
+
return threadId;
}
@@ -470,7 +577,7 @@ public class PushDecryptJob extends ContextJob {
String encoded = Base64.encodeBytes(envelope.getLegacyMessage());
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), encoded,
- Optional.absent());
+ Optional.absent(), 0);
if (!smsMessageId.isPresent()) {
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
@@ -492,7 +599,7 @@ public class PushDecryptJob extends ContextJob {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), "",
- Optional.absent());
+ Optional.absent(), 0);
textMessage = new IncomingEncryptedMessage(textMessage, "");
return database.insertMessageInbox(textMessage);
@@ -505,4 +612,12 @@ public class PushDecryptJob extends ContextJob {
return RecipientFactory.getRecipientsFromString(context, message.getDestination().get(), false);
}
}
+
+ private Recipients getMessageDestination(SignalServiceEnvelope envelope, SignalServiceDataMessage message) {
+ if (message.getGroupInfo().isPresent()) {
+ return RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId()), false);
+ } else {
+ return RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
index 40f6a654c1..e5e74ad7a5 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
+import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
@@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.jobqueue.JobParameters;
@@ -85,6 +87,13 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId);
database.markAsSent(messageId);
markAttachmentsUploaded(messageId, message.getAttachments());
+
+ if (message.getExpiresIn() > 0) {
+ database.markExpireStarted(messageId);
+ ApplicationContext.getInstance(context)
+ .getExpiringMessageManager()
+ .scheduleDeletion(messageId, true, message.getExpiresIn());
+ }
} catch (InvalidNumberException | RecipientFormattingException | UndeliverableMessageException e) {
Log.w(TAG, e);
database.markAsSentFailed(messageId);
@@ -152,7 +161,10 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
messageSender.sendMessage(addresses, groupDataMessage);
} else {
SignalServiceGroup group = new SignalServiceGroup(groupId);
- SignalServiceDataMessage groupMessage = new SignalServiceDataMessage(message.getSentTimeMillis(), group, attachments, message.getBody());
+ SignalServiceDataMessage groupMessage = new SignalServiceDataMessage(message.getSentTimeMillis(), group,
+ attachments, message.getBody(), false,
+ (int)(message.getExpiresIn() / 1000),
+ message.isExpirationUpdate());
messageSender.sendMessage(addresses, groupMessage);
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
index 416113f802..af5d74758a 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
@@ -62,8 +63,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
throws RetryLaterException, MmsException, NoSuchMessageException,
UndeliverableMessageException
{
- MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
- OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId);
+ ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
+ MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
+ OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId);
try {
deliver(masterSecret, message);
@@ -71,6 +73,12 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId);
database.markAsSent(messageId);
markAttachmentsUploaded(messageId, message.getAttachments());
+
+ if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
+ database.markExpireStarted(messageId);
+ expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
+ }
+
} catch (InsecureFallbackApprovalException ifae) {
Log.w(TAG, ifae);
database.markAsPendingInsecureSmsFallback(messageId);
@@ -122,6 +130,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.withBody(message.getBody())
.withAttachments(attachmentStreams)
.withTimestamp(message.getSentTimeMillis())
+ .withExpiration((int)(message.getExpiresIn() / 1000))
+ .asExpirationUpdate(message.isExpirationUpdate())
.build();
messageSender.sendMessage(address, mediaMessage);
diff --git a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
index f7db1af6bf..45d9145c53 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
@@ -53,8 +54,9 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
@Override
public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException {
- EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
- SmsMessageRecord record = database.getMessage(masterSecret, messageId);
+ ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
+ EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
+ SmsMessageRecord record = database.getMessage(masterSecret, messageId);
try {
Log.w(TAG, "Sending message: " + messageId);
@@ -64,6 +66,11 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
database.markAsSecure(messageId);
database.markAsSent(messageId);
+ if (record.getExpiresIn() > 0) {
+ database.markExpireStarted(messageId);
+ expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn());
+ }
+
} catch (InsecureFallbackApprovalException e) {
Log.w(TAG, e);
database.markAsPendingInsecureSmsFallback(record.getId());
@@ -108,6 +115,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent())
.withBody(message.getBody().getBody())
+ .withExpiration((int)(message.getExpiresIn() / 1000))
.asEndSessionMessage(message.isEndSession())
.build();
diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
index c4777c9fb6..ee464b518f 100644
--- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
@@ -20,6 +20,8 @@ public class IncomingMediaMessage {
private final boolean push;
private final long sentTimeMillis;
private final int subscriptionId;
+ private final long expiresIn;
+ private final boolean expirationUpdate;
private final List to = new LinkedList<>();
private final List cc = new LinkedList<>();
@@ -27,14 +29,17 @@ public class IncomingMediaMessage {
public IncomingMediaMessage(String from, List to, List cc,
String body, long sentTimeMillis,
- List attachments, int subscriptionId)
+ List attachments, int subscriptionId,
+ long expiresIn, boolean expirationUpdate)
{
- this.from = from;
- this.sentTimeMillis = sentTimeMillis;
- this.body = body;
- this.groupId = null;
- this.push = false;
- this.subscriptionId = subscriptionId;
+ this.from = from;
+ this.sentTimeMillis = sentTimeMillis;
+ this.body = body;
+ this.groupId = null;
+ this.push = false;
+ this.subscriptionId = subscriptionId;
+ this.expiresIn = expiresIn;
+ this.expirationUpdate = expirationUpdate;
this.to.addAll(to);
this.cc.addAll(cc);
@@ -46,16 +51,20 @@ public class IncomingMediaMessage {
String to,
long sentTimeMillis,
int subscriptionId,
+ long expiresIn,
+ boolean expirationUpdate,
Optional relay,
Optional body,
Optional group,
Optional> attachments)
{
- this.push = true;
- this.from = from;
- this.sentTimeMillis = sentTimeMillis;
- this.body = body.orNull();
- this.subscriptionId = subscriptionId;
+ this.push = true;
+ this.from = from;
+ this.sentTimeMillis = sentTimeMillis;
+ this.body = body.orNull();
+ this.subscriptionId = subscriptionId;
+ this.expiresIn = expiresIn;
+ this.expirationUpdate = expirationUpdate;
if (group.isPresent()) this.groupId = GroupUtil.getEncodedId(group.get().getGroupId());
else this.groupId = null;
@@ -88,10 +97,18 @@ public class IncomingMediaMessage {
return push;
}
+ public boolean isExpirationUpdate() {
+ return expirationUpdate;
+ }
+
public long getSentTimeMillis() {
return sentTimeMillis;
}
+ public long getExpiresIn() {
+ return expiresIn;
+ }
+
public boolean isGroupMessage() {
return groupId != null || to.size() > 1 || cc.size() > 0;
}
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java
new file mode 100644
index 0000000000..1dbe336393
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java
@@ -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(), sentTimeMillis,
+ ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn);
+ }
+
+ @Override
+ public boolean isExpirationUpdate() {
+ return true;
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
index cd226389e0..de49a0e066 100644
--- a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
@@ -20,11 +20,12 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
public OutgoingGroupMediaMessage(@NonNull Recipients recipients,
@NonNull String encodedGroupContext,
@NonNull List avatar,
- long sentTimeMillis)
+ long sentTimeMillis,
+ long expiresIn)
throws IOException
{
super(recipients, encodedGroupContext, avatar, sentTimeMillis,
- ThreadDatabase.DistributionTypes.CONVERSATION);
+ ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn);
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
}
@@ -32,12 +33,13 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
public OutgoingGroupMediaMessage(@NonNull Recipients recipients,
@NonNull GroupContext group,
@Nullable final Attachment avatar,
- long sentTimeMillis)
+ long sentTimeMillis,
+ long expireIn)
{
super(recipients, Base64.encodeBytes(group.toByteArray()),
new LinkedList() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(),
- ThreadDatabase.DistributionTypes.CONVERSATION);
+ ThreadDatabase.DistributionTypes.CONVERSATION, expireIn);
this.group = group;
}
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
index 93f5c23a47..8376e0204a 100644
--- a/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
@@ -15,10 +15,11 @@ public class OutgoingMediaMessage {
private final long sentTimeMillis;
private final int distributionType;
private final int subscriptionId;
+ private final long expiresIn;
public OutgoingMediaMessage(Recipients recipients, String message,
List attachments, long sentTimeMillis,
- int subscriptionId,
+ int subscriptionId, long expiresIn,
int distributionType)
{
this.recipients = recipients;
@@ -27,15 +28,16 @@ public class OutgoingMediaMessage {
this.distributionType = distributionType;
this.attachments = attachments;
this.subscriptionId = subscriptionId;
+ this.expiresIn = expiresIn;
}
- public OutgoingMediaMessage(Recipients recipients, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, int distributionType)
+ public OutgoingMediaMessage(Recipients recipients, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, long expiresIn, int distributionType)
{
this(recipients,
buildMessage(slideDeck, message),
slideDeck.asAttachments(),
sentTimeMillis, subscriptionId,
- distributionType);
+ expiresIn, distributionType);
}
public OutgoingMediaMessage(OutgoingMediaMessage that) {
@@ -45,6 +47,7 @@ public class OutgoingMediaMessage {
this.attachments = that.attachments;
this.sentTimeMillis = that.sentTimeMillis;
this.subscriptionId = that.subscriptionId;
+ this.expiresIn = that.expiresIn;
}
public Recipients getRecipients() {
@@ -71,6 +74,10 @@ public class OutgoingMediaMessage {
return false;
}
+ public boolean isExpirationUpdate() {
+ return false;
+ }
+
public long getSentTimeMillis() {
return sentTimeMillis;
}
@@ -79,6 +86,10 @@ public class OutgoingMediaMessage {
return subscriptionId;
}
+ public long getExpiresIn() {
+ return expiresIn;
+ }
+
private static String buildMessage(SlideDeck slideDeck, String message) {
if (!TextUtils.isEmpty(message) && !TextUtils.isEmpty(slideDeck.getBody())) {
return slideDeck.getBody() + "\n\n" + message;
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java
index ae20107388..e6567c6197 100644
--- a/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java
@@ -14,9 +14,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
public OutgoingSecureMediaMessage(Recipients recipients, String body,
List attachments,
long sentTimeMillis,
- int distributionType)
+ int distributionType,
+ long expiresIn)
{
- super(recipients, body, attachments, sentTimeMillis, -1, distributionType);
+ super(recipients, body, attachments, sentTimeMillis, -1, expiresIn, distributionType);
}
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
diff --git a/src/org/thoughtcrime/securesms/notifications/WearReplyReceiver.java b/src/org/thoughtcrime/securesms/notifications/WearReplyReceiver.java
index f846a2e0fd..d28cfb5221 100644
--- a/src/org/thoughtcrime/securesms/notifications/WearReplyReceiver.java
+++ b/src/org/thoughtcrime/securesms/notifications/WearReplyReceiver.java
@@ -72,13 +72,14 @@ public class WearReplyReceiver extends MasterSecretBroadcastReceiver {
long threadId;
Optional 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()) {
- OutgoingMediaMessage reply = new OutgoingMediaMessage(recipients, responseText.toString(), new LinkedList(), System.currentTimeMillis(), subscriptionId, 0);
+ OutgoingMediaMessage reply = new OutgoingMediaMessage(recipients, responseText.toString(), new LinkedList(), System.currentTimeMillis(), subscriptionId, expiresIn, 0);
threadId = MessageSender.send(context, masterSecret, reply, -1, false);
} else {
- OutgoingTextMessage reply = new OutgoingTextMessage(recipients, responseText.toString(), subscriptionId);
+ OutgoingTextMessage reply = new OutgoingTextMessage(recipients, responseText.toString(), expiresIn, subscriptionId);
threadId = MessageSender.send(context, masterSecret, reply, -1, false);
}
diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java b/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java
index 632ba15018..cfe9667a49 100644
--- a/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java
+++ b/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java
@@ -17,6 +17,7 @@
package org.thoughtcrime.securesms.recipients;
import android.content.Context;
+import android.content.Intent;
import android.support.annotation.NonNull;
import android.text.TextUtils;
@@ -31,6 +32,8 @@ import java.util.StringTokenizer;
public class RecipientFactory {
+ public static final String RECIPIENT_CLEAR_ACTION = "org.thoughtcrime.securesms.database.RecipientFactory.CLEAR";
+
private static final RecipientProvider provider = new RecipientProvider();
public static Recipients getRecipientsForIds(Context context, String recipientIds, boolean asynchronous) {
@@ -133,8 +136,9 @@ public class RecipientFactory {
return value;
}
- public static void clearCache() {
+ public static void clearCache(Context context) {
provider.clearCache();
+ context.sendBroadcast(new Intent(RECIPIENT_CLEAR_ACTION));
}
}
diff --git a/src/org/thoughtcrime/securesms/recipients/Recipients.java b/src/org/thoughtcrime/securesms/recipients/Recipients.java
index 0f1e51b39b..fb708de838 100644
--- a/src/org/thoughtcrime/securesms/recipients/Recipients.java
+++ b/src/org/thoughtcrime/securesms/recipients/Recipients.java
@@ -51,11 +51,12 @@ public class Recipients implements Iterable, RecipientModifiedListene
private final Set listeners = Collections.newSetFromMap(new WeakHashMap());
private final List recipients;
- private Uri ringtone = null;
- private long mutedUntil = 0;
- private boolean blocked = false;
- private VibrateState vibrate = VibrateState.DEFAULT;
- private boolean stale = false;
+ private Uri ringtone = null;
+ private long mutedUntil = 0;
+ private boolean blocked = false;
+ private VibrateState vibrate = VibrateState.DEFAULT;
+ private int expireMessages = 0;
+ private boolean stale = false;
Recipients() {
this(new LinkedList(), null);
@@ -65,10 +66,11 @@ public class Recipients implements Iterable, RecipientModifiedListene
this.recipients = recipients;
if (preferences != null) {
- ringtone = preferences.getRingtone();
- mutedUntil = preferences.getMuteUntil();
- vibrate = preferences.getVibrateState();
- blocked = preferences.isBlocked();
+ ringtone = preferences.getRingtone();
+ mutedUntil = preferences.getMuteUntil();
+ vibrate = preferences.getVibrateState();
+ blocked = preferences.isBlocked();
+ expireMessages = preferences.getExpireMessages();
}
}
@@ -79,10 +81,11 @@ public class Recipients implements Iterable, RecipientModifiedListene
this.recipients = recipients;
if (stale != null) {
- ringtone = stale.ringtone;
- mutedUntil = stale.mutedUntil;
- vibrate = stale.vibrate;
- blocked = stale.blocked;
+ ringtone = stale.ringtone;
+ mutedUntil = stale.mutedUntil;
+ vibrate = stale.vibrate;
+ blocked = stale.blocked;
+ expireMessages = stale.expireMessages;
}
preferences.addListener(new FutureTaskListener() {
@@ -93,10 +96,11 @@ public class Recipients implements Iterable, RecipientModifiedListene
Set localListeners;
synchronized (Recipients.this) {
- ringtone = result.getRingtone();
- mutedUntil = result.getMuteUntil();
- vibrate = result.getVibrateState();
- blocked = result.isBlocked();
+ ringtone = result.getRingtone();
+ mutedUntil = result.getMuteUntil();
+ vibrate = result.getVibrateState();
+ blocked = result.isBlocked();
+ expireMessages = result.getExpireMessages();
localListeners = new HashSet<>(listeners);
}
@@ -178,6 +182,18 @@ public class Recipients implements Iterable, RecipientModifiedListene
else if (!isEmpty()) recipients.get(0).setColor(color);
}
+ public synchronized int getExpireMessages() {
+ return expireMessages;
+ }
+
+ public void setExpireMessages(int expireMessages) {
+ synchronized (this) {
+ this.expireMessages = expireMessages;
+ }
+
+ notifyListeners();
+ }
+
public synchronized void addListener(RecipientsModifiedListener listener) {
if (listeners.isEmpty()) {
for (Recipient recipient : recipients) {
diff --git a/src/org/thoughtcrime/securesms/service/ExpirationListener.java b/src/org/thoughtcrime/securesms/service/ExpirationListener.java
new file mode 100644
index 0000000000..97513bcde8
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/service/ExpirationListener.java
@@ -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);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/src/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
new file mode 100644
index 0000000000..455817ad07
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
@@ -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 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 {
+ @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;
+ }
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/service/QuickResponseService.java b/src/org/thoughtcrime/securesms/service/QuickResponseService.java
index 0452998086..1a0bfbe9c2 100644
--- a/src/org/thoughtcrime/securesms/service/QuickResponseService.java
+++ b/src/org/thoughtcrime/securesms/service/QuickResponseService.java
@@ -56,13 +56,14 @@ public class QuickResponseService extends MasterSecretIntentService {
Recipients recipients = RecipientFactory.getRecipientsFromString(this, numbers, false);
Optional preferences = DatabaseFactory.getRecipientPreferenceDatabase(this).getRecipientsPreferences(recipients.getIds());
int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1;
+ long expiresIn = preferences.isPresent() ? preferences.get().getExpireMessages() * 1000 : 0;
if (!TextUtils.isEmpty(content)) {
if (recipients.isSingleRecipient()) {
- MessageSender.send(this, masterSecret, new OutgoingTextMessage(recipients, content, subscriptionId), -1, false);
+ MessageSender.send(this, masterSecret, new OutgoingTextMessage(recipients, content, expiresIn, subscriptionId), -1, false);
} else {
MessageSender.send(this, masterSecret, new OutgoingMediaMessage(recipients, new SlideDeck(), content, System.currentTimeMillis(),
- subscriptionId, ThreadDatabase.DistributionTypes.DEFAULT), -1, false);
+ subscriptionId, expiresIn, ThreadDatabase.DistributionTypes.DEFAULT), -1, false);
}
}
} catch (URISyntaxException e) {
diff --git a/src/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java
index 5dedce9bd6..56fc399078 100644
--- a/src/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java
@@ -6,7 +6,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
public class IncomingJoinedMessage extends IncomingTextMessage {
public IncomingJoinedMessage(String sender) {
- super(sender, 1, System.currentTimeMillis(), null, Optional.absent());
+ super(sender, 1, System.currentTimeMillis(), null, Optional.absent(), 0);
}
@Override
diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
index 75efaf6460..86f5799b53 100644
--- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
@@ -36,6 +36,7 @@ public class IncomingTextMessage implements Parcelable {
private final String groupId;
private final boolean push;
private final int subscriptionId;
+ private final long expiresInMillis;
public IncomingTextMessage(SmsMessage message, int subscriptionId) {
this.message = message.getDisplayMessageBody();
@@ -47,12 +48,14 @@ public class IncomingTextMessage implements Parcelable {
this.pseudoSubject = message.getPseudoSubject();
this.sentTimestampMillis = message.getTimestampMillis();
this.subscriptionId = subscriptionId;
+ this.expiresInMillis = 0;
this.groupId = null;
this.push = false;
}
public IncomingTextMessage(String sender, int senderDeviceId, long sentTimestampMillis,
- String encodedBody, Optional group)
+ String encodedBody, Optional group,
+ long expiresInMillis)
{
this.message = encodedBody;
this.sender = sender;
@@ -64,6 +67,7 @@ public class IncomingTextMessage implements Parcelable {
this.sentTimestampMillis = sentTimestampMillis;
this.push = true;
this.subscriptionId = -1;
+ this.expiresInMillis = expiresInMillis;
if (group.isPresent()) {
this.groupId = GroupUtil.getEncodedId(group.get().getGroupId());
@@ -84,6 +88,7 @@ public class IncomingTextMessage implements Parcelable {
this.groupId = in.readString();
this.push = (in.readInt() == 1);
this.subscriptionId = in.readInt();
+ this.expiresInMillis = in.readLong();
}
public IncomingTextMessage(IncomingTextMessage base, String newBody) {
@@ -98,6 +103,7 @@ public class IncomingTextMessage implements Parcelable {
this.groupId = base.getGroupId();
this.push = base.isPush();
this.subscriptionId = base.getSubscriptionId();
+ this.expiresInMillis = base.getExpiresIn();
}
public IncomingTextMessage(List fragments) {
@@ -118,6 +124,7 @@ public class IncomingTextMessage implements Parcelable {
this.groupId = fragments.get(0).getGroupId();
this.push = fragments.get(0).isPush();
this.subscriptionId = fragments.get(0).getSubscriptionId();
+ this.expiresInMillis = fragments.get(0).getExpiresIn();
}
protected IncomingTextMessage(String sender, String groupId)
@@ -133,12 +140,17 @@ public class IncomingTextMessage implements Parcelable {
this.groupId = groupId;
this.push = true;
this.subscriptionId = -1;
+ this.expiresInMillis = 0;
}
public int getSubscriptionId() {
return subscriptionId;
}
+ public long getExpiresIn() {
+ return expiresInMillis;
+ }
+
public long getSentTimestampMillis() {
return sentTimestampMillis;
}
diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java
index 77d80b0b16..2660a8a7e0 100644
--- a/src/org/thoughtcrime/securesms/sms/MessageSender.java
+++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java
@@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -77,7 +78,7 @@ public class MessageSender {
long messageId = database.insertMessageOutbox(new MasterSecretUnion(masterSecret), allocatedThreadId,
message, forceSms, System.currentTimeMillis());
- sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
+ sendTextMessage(context, recipients, forceSms, keyExchange, messageId, message.getExpiresIn());
return allocatedThreadId;
}
@@ -103,7 +104,7 @@ public class MessageSender {
Recipients recipients = message.getRecipients();
long messageId = database.insertMessageOutbox(new MasterSecretUnion(masterSecret), message, allocatedThreadId, forceSms);
- sendMediaMessage(context, masterSecret, recipients, forceSms, messageId);
+ sendMediaMessage(context, masterSecret, recipients, forceSms, messageId, message.getExpiresIn());
return allocatedThreadId;
} catch (MmsException e) {
@@ -124,13 +125,14 @@ public class MessageSender {
long messageId = messageRecord.getId();
boolean forceSms = messageRecord.isForcedSms();
boolean keyExchange = messageRecord.isKeyExchange();
+ long expiresIn = messageRecord.getExpiresIn();
if (messageRecord.isMms()) {
Recipients recipients = DatabaseFactory.getMmsAddressDatabase(context).getRecipientsForId(messageId);
- sendMediaMessage(context, masterSecret, recipients, forceSms, messageId);
+ sendMediaMessage(context, masterSecret, recipients, forceSms, messageId, expiresIn);
} else {
Recipients recipients = messageRecord.getRecipients();
- sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
+ sendTextMessage(context, recipients, forceSms, keyExchange, messageId, expiresIn);
}
} catch (MmsException e) {
Log.w(TAG, e);
@@ -138,11 +140,12 @@ public class MessageSender {
}
private static void sendMediaMessage(Context context, MasterSecret masterSecret,
- Recipients recipients, boolean forceSms, long messageId)
+ Recipients recipients, boolean forceSms,
+ long messageId, long expiresIn)
throws MmsException
{
if (!forceSms && isSelfSend(context, recipients)) {
- sendMediaSelf(context, masterSecret, messageId);
+ sendMediaSelf(context, masterSecret, messageId, expiresIn);
} else if (isGroupPushSend(recipients)) {
sendGroupPush(context, recipients, messageId, -1);
} else if (!forceSms && isPushMediaSend(context, recipients)) {
@@ -153,10 +156,11 @@ public class MessageSender {
}
private static void sendTextMessage(Context context, Recipients recipients,
- boolean forceSms, boolean keyExchange, long messageId)
+ boolean forceSms, boolean keyExchange,
+ long messageId, long expiresIn)
{
if (!forceSms && isSelfSend(context, recipients)) {
- sendTextSelf(context, messageId);
+ sendTextSelf(context, messageId, expiresIn);
} else if (!forceSms && isPushTextSend(context, recipients, keyExchange)) {
sendTextPush(context, recipients, messageId);
} else {
@@ -164,7 +168,7 @@ public class MessageSender {
}
}
- private static void sendTextSelf(Context context, long messageId) {
+ private static void sendTextSelf(Context context, long messageId, long expiresIn) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
database.markAsSent(messageId);
@@ -172,17 +176,32 @@ public class MessageSender {
Pair messageAndThreadId = database.copyMessageInbox(messageId);
database.markAsPush(messageAndThreadId.first);
+
+ if (expiresIn > 0) {
+ ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
+
+ database.markExpireStarted(messageId);
+ expiringMessageManager.scheduleDeletion(messageId, false, expiresIn);
+ }
}
- private static void sendMediaSelf(Context context, MasterSecret masterSecret, long messageId)
+ private static void sendMediaSelf(Context context, MasterSecret masterSecret,
+ long messageId, long expiresIn)
throws MmsException
{
- MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
+ ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
+ MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
+
database.markAsSent(messageId);
database.markAsPush(messageId);
long newMessageId = database.copyMessageInbox(masterSecret, messageId);
database.markAsPush(newMessageId);
+
+ if (expiresIn > 0) {
+ database.markExpireStarted(messageId);
+ expiringMessageManager.scheduleDeletion(messageId, true, expiresIn);
+ }
}
private static void sendTextPush(Context context, Recipients recipients, long messageId) {
diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingEncryptedMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingEncryptedMessage.java
index dfc0c66002..042facf703 100644
--- a/src/org/thoughtcrime/securesms/sms/OutgoingEncryptedMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/OutgoingEncryptedMessage.java
@@ -1,12 +1,11 @@
package org.thoughtcrime.securesms.sms;
-import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class OutgoingEncryptedMessage extends OutgoingTextMessage {
- public OutgoingEncryptedMessage(Recipients recipients, String body) {
- super(recipients, body, -1);
+ public OutgoingEncryptedMessage(Recipients recipients, String body, long expiresIn) {
+ super(recipients, body, expiresIn, -1);
}
private OutgoingEncryptedMessage(OutgoingEncryptedMessage base, String body) {
diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
index cb24306e4e..2a90dd0f42 100644
--- a/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
@@ -8,19 +8,30 @@ public class OutgoingTextMessage {
private final Recipients recipients;
private final String message;
private final int subscriptionId;
+ private final long expiresIn;
public OutgoingTextMessage(Recipients recipients, String message, int subscriptionId) {
+ this(recipients, message, 0, subscriptionId);
+ }
+
+ public OutgoingTextMessage(Recipients recipients, String message, long expiresIn, int subscriptionId) {
this.recipients = recipients;
this.message = message;
+ this.expiresIn = expiresIn;
this.subscriptionId = subscriptionId;
}
protected OutgoingTextMessage(OutgoingTextMessage base, String body) {
this.recipients = base.getRecipients();
this.subscriptionId = base.getSubscriptionId();
+ this.expiresIn = base.getExpiresIn();
this.message = body;
}
+ public long getExpiresIn() {
+ return expiresIn;
+ }
+
public int getSubscriptionId() {
return subscriptionId;
}
@@ -51,13 +62,13 @@ public class OutgoingTextMessage {
public static OutgoingTextMessage from(SmsMessageRecord record) {
if (record.isSecure()) {
- return new OutgoingEncryptedMessage(record.getRecipients(), record.getBody().getBody());
+ return new OutgoingEncryptedMessage(record.getRecipients(), record.getBody().getBody(), record.getExpiresIn());
} else if (record.isKeyExchange()) {
return new OutgoingKeyExchangeMessage(record.getRecipients(), record.getBody().getBody());
} else if (record.isEndSession()) {
- return new OutgoingEndSessionMessage(new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody(), -1));
+ return new OutgoingEndSessionMessage(new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody(), 0, -1));
} else {
- return new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody(), record.getSubscriptionId());
+ return new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody(), record.getExpiresIn(), record.getSubscriptionId());
}
}
diff --git a/src/org/thoughtcrime/securesms/util/ExpirationUtil.java b/src/org/thoughtcrime/securesms/util/ExpirationUtil.java
new file mode 100644
index 0000000000..f5a83a7448
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/ExpirationUtil.java
@@ -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);
+ }
+ }
+
+
+}