From d7e4928f22e0a0e123eadd7442c32296b6eb6fad Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 15 Aug 2016 20:23:56 -0700 Subject: [PATCH] Support for disappearing messages // FREEBIE --- build.gradle | 109 +++++++------ .../ic_hourglass_empty_white_18dp.png | Bin 0 -> 298 bytes .../ic_hourglass_full_white_18dp.png | Bin 0 -> 230 bytes res/drawable-hdpi/ic_timer_off_white_24dp.png | Bin 0 -> 555 bytes res/drawable-hdpi/ic_timer_white_24dp.png | Bin 0 -> 499 bytes .../ic_hourglass_empty_white_18dp.png | Bin 0 -> 224 bytes .../ic_hourglass_full_white_18dp.png | Bin 0 -> 195 bytes res/drawable-mdpi/ic_timer_off_white_24dp.png | Bin 0 -> 361 bytes res/drawable-mdpi/ic_timer_white_24dp.png | Bin 0 -> 329 bytes .../ic_hourglass_empty_white_18dp.png | Bin 0 -> 273 bytes .../ic_hourglass_full_white_18dp.png | Bin 0 -> 197 bytes .../ic_timer_off_white_24dp.png | Bin 0 -> 641 bytes res/drawable-xhdpi/ic_timer_white_24dp.png | Bin 0 -> 628 bytes .../ic_hourglass_empty_white_18dp.png | Bin 0 -> 509 bytes .../ic_hourglass_full_white_18dp.png | Bin 0 -> 349 bytes .../ic_timer_off_white_24dp.png | Bin 0 -> 986 bytes res/drawable-xxhdpi/ic_timer_white_24dp.png | Bin 0 -> 901 bytes .../ic_hourglass_empty_white_18dp.png | Bin 0 -> 417 bytes .../ic_hourglass_full_white_18dp.png | Bin 0 -> 279 bytes .../ic_timer_off_white_24dp.png | Bin 0 -> 1322 bytes res/drawable-xxxhdpi/ic_timer_white_24dp.png | Bin 0 -> 1241 bytes res/layout/conversation_item_received.xml | 26 ++- res/layout/conversation_item_sent.xml | 24 ++- res/layout/conversation_item_update.xml | 2 + res/layout/delivery_status_view.xml | 9 +- res/layout/expiration_dialog.xml | 31 ++++ res/layout/expiration_timer_menu.xml | 26 +++ res/layout/message_details_header.xml | 20 +++ res/menu/conversation_callable_insecure.xml | 2 +- res/menu/conversation_callable_secure.xml | 2 +- res/menu/conversation_expiring_off.xml | 9 ++ res/menu/conversation_expiring_on.xml | 10 ++ res/values/arrays.xml | 15 ++ res/values/attrs.xml | 8 + res/values/strings.xml | 54 ++++++- .../securesms/ApplicationContext.java | 16 +- .../securesms/BindableConversationItem.java | 2 + .../securesms/ConversationActivity.java | 101 +++++++++--- .../securesms/ConversationAdapter.java | 40 ++--- .../securesms/ConversationFragment.java | 29 ++-- .../securesms/ConversationItem.java | 38 +++++ .../securesms/ConversationListActivity.java | 2 +- .../securesms/ConversationUpdateItem.java | 38 ++++- .../securesms/ExpirationDialog.java | 94 +++++++++++ .../securesms/MessageDetailsActivity.java | 52 +++++- .../RecipientPreferenceActivity.java | 68 ++++++-- .../components/ExpirationTimerView.java | 88 +++++++++++ .../securesms/components/HourglassView.java | 85 ++++++++++ .../securesms/database/DatabaseFactory.java | 12 +- .../securesms/database/GroupDatabase.java | 8 +- .../securesms/database/MmsDatabase.java | 58 ++++++- .../securesms/database/MmsSmsColumns.java | 13 +- .../securesms/database/MmsSmsDatabase.java | 15 +- .../database/RecipientPreferenceDatabase.java | 25 ++- .../securesms/database/SmsDatabase.java | 36 ++++- .../securesms/database/ThreadDatabase.java | 13 +- .../database/model/DisplayRecord.java | 4 + .../database/model/MediaMmsMessageRecord.java | 18 ++- .../database/model/MessageRecord.java | 23 ++- .../model/NotificationMmsMessageRecord.java | 8 +- .../database/model/SmsMessageRecord.java | 5 +- .../database/model/ThreadRecord.java | 12 +- .../securesms/groups/GroupManager.java | 2 +- .../groups/GroupMessageProcessor.java | 4 +- .../securesms/jobs/MmsDownloadJob.java | 2 +- .../securesms/jobs/PushDecryptJob.java | 133 ++++++++++++++-- .../securesms/jobs/PushGroupSendJob.java | 14 +- .../securesms/jobs/PushMediaSendJob.java | 14 +- .../securesms/jobs/PushTextSendJob.java | 12 +- .../securesms/mms/IncomingMediaMessage.java | 41 +++-- .../mms/OutgoingExpirationUpdateMessage.java | 21 +++ .../mms/OutgoingGroupMediaMessage.java | 10 +- .../securesms/mms/OutgoingMediaMessage.java | 17 +- .../mms/OutgoingSecureMediaMessage.java | 5 +- .../notifications/WearReplyReceiver.java | 7 +- .../recipients/RecipientFactory.java | 6 +- .../securesms/recipients/Recipients.java | 50 ++++-- .../securesms/service/ExpirationListener.java | 26 +++ .../service/ExpiringMessageManager.java | 148 ++++++++++++++++++ .../service/QuickResponseService.java | 5 +- .../securesms/sms/IncomingJoinedMessage.java | 2 +- .../securesms/sms/IncomingTextMessage.java | 14 +- .../securesms/sms/MessageSender.java | 41 +++-- .../sms/OutgoingEncryptedMessage.java | 5 +- .../securesms/sms/OutgoingTextMessage.java | 17 +- .../securesms/util/ExpirationUtil.java | 50 ++++++ 86 files changed, 1635 insertions(+), 261 deletions(-) create mode 100644 res/drawable-hdpi/ic_hourglass_empty_white_18dp.png create mode 100644 res/drawable-hdpi/ic_hourglass_full_white_18dp.png create mode 100644 res/drawable-hdpi/ic_timer_off_white_24dp.png create mode 100644 res/drawable-hdpi/ic_timer_white_24dp.png create mode 100644 res/drawable-mdpi/ic_hourglass_empty_white_18dp.png create mode 100644 res/drawable-mdpi/ic_hourglass_full_white_18dp.png create mode 100644 res/drawable-mdpi/ic_timer_off_white_24dp.png create mode 100644 res/drawable-mdpi/ic_timer_white_24dp.png create mode 100644 res/drawable-xhdpi/ic_hourglass_empty_white_18dp.png create mode 100644 res/drawable-xhdpi/ic_hourglass_full_white_18dp.png create mode 100644 res/drawable-xhdpi/ic_timer_off_white_24dp.png create mode 100644 res/drawable-xhdpi/ic_timer_white_24dp.png create mode 100644 res/drawable-xxhdpi/ic_hourglass_empty_white_18dp.png create mode 100644 res/drawable-xxhdpi/ic_hourglass_full_white_18dp.png create mode 100644 res/drawable-xxhdpi/ic_timer_off_white_24dp.png create mode 100644 res/drawable-xxhdpi/ic_timer_white_24dp.png create mode 100644 res/drawable-xxxhdpi/ic_hourglass_empty_white_18dp.png create mode 100644 res/drawable-xxxhdpi/ic_hourglass_full_white_18dp.png create mode 100644 res/drawable-xxxhdpi/ic_timer_off_white_24dp.png create mode 100644 res/drawable-xxxhdpi/ic_timer_white_24dp.png create mode 100644 res/layout/expiration_dialog.xml create mode 100644 res/layout/expiration_timer_menu.xml create mode 100644 res/menu/conversation_expiring_off.xml create mode 100644 res/menu/conversation_expiring_on.xml create mode 100644 src/org/thoughtcrime/securesms/ExpirationDialog.java create mode 100644 src/org/thoughtcrime/securesms/components/ExpirationTimerView.java create mode 100644 src/org/thoughtcrime/securesms/components/HourglassView.java create mode 100644 src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java create mode 100644 src/org/thoughtcrime/securesms/service/ExpirationListener.java create mode 100644 src/org/thoughtcrime/securesms/service/ExpiringMessageManager.java create mode 100644 src/org/thoughtcrime/securesms/util/ExpirationUtil.java 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 0000000000000000000000000000000000000000..785feac7dbf66975892a41bc21651d6f47e8a075 GIT binary patch literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^+(0bI!2%=~zL*yPq?nSt-CY>|xA&jf59DzcctjQh zRSAPIBg3pY5H=O_6Hoo42-gu0^>o(?e=tW43W58dciP@sZikP!}t4d3b=Tt zFI<|+rOlt9__ByKe`4?x3(L-JaSBQuIvp(<`bS?)I<#lyGsDl1&raF%>%-gi^Y(#ip@F2Cs<~t&-Xo+GRODZ z%|xA&jf59DzcctjQh zRSAPIBg3pY5H=O_6Hn5L8V$9=Q5yBxu=U`h{Wa63ATI=2?8$nxBrw{dWcoV zVQGlQk={9n-o`WsWT`Ax{C7<|-fZ$r-mI6AJ11XoHR8IL()L5?@X?OGUXAN()HfG7 zY+W{cP6#toNS5W3MUz&y&T=+i{$uyI&3Dz@Tz_TF4U(K~oc>!@UPf9;`SA1mkNG~% Wt(^TX_Fx3i2@IaDelF{r5}E)!)KRej literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..60136f7b519e1eb16f6c14ccbcd92e60e00be069 GIT binary patch literal 555 zcmV+`0@VG9P)~p$VNJYb{S9AuSXb5xe_(VdCh8Ef3bHFoPVp~8zkX`@ zi=nk1YECS7xMa=q27%PzODiHlun3!4~6o$55U7R8Xx-vS+{^ zA5pe50aZ|M7pQ`gY1i%7s9LQxy4*0Ma;9vKpzqY+BC5@DAyI>?<^hv7W!<&<1eIQ= zY4FsNEGW1{L=~e)v0WLQ&4QZD@`Gy0gn}dBi&g1RKNvBzh}xx?odRltO%I27zO z3##sF&LErI^AmiqqgIO?Fa@Z1s9TE$jau}0_E)T!2%>DXT~c7DW)WEcNd2L?fqx=19_YU9+AaB z6~Z9Q$S`Y;1W=H@#M9T6{Q-xNxRuh+0RQ zAe+aVgG?%$H75UA9We1ltEMO2M%pF7;x z)ZFfIy7$f{qi1_+e6t+IGv{sn;=}#2n6b$7exzj2hXm8mYd;09t@^jV?lr5ysuH8e R0zk_dJYD@<);T3K0RZaoPQw5I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1b7feed9bb90121bf81287f55b1760a5e67e28b0 GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)T!2%>DXT~c7DW)WEcNd2L?fqx=19_YU9+AaB z6~Z9Q$S`Y;1W=H@#M9T6{Q-xNxV7?hA3qPEP>83CV+hCf(7ubj4GJR6{p#Es8w;{5MdOFb_ynB2^G;F##mlQurdP1m1oS-4=4YaoL|Y*75i`&zG+ mrBC{*9-b4mBp(mnkVBd+EmEXR2?XD0mxZDNj zeC01WR_?tUK5JZ(K)8~~MQ=P%+fdncU=H}?d;&Qs+A;#yMGgi&(jiDMBfKlYKE63i zWRgJGw!mHvPH^AVM0QL>>JY~{n4uOKNx%@(9IUV*GLo{1UpZK!5^pc~fK+X)+}K%{gtFU?G=5rY9N9}ByH|kzTJlyppf&pgfp@f8a}@|$00000NkvXX Hu0mjf8B~)a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..72e1320c468757b4836322414c31b987c3b1fb54 GIT binary patch literal 329 zcmV-P0k-~$P)?rzuNW|X&KPxAvD%Lgg>RIe0ilWGxjm7C@cjn{9t_OYV>GXXQ^wgB) z(6YQjGze6ndHL1^=Ca&gzDtce;$&p$%z+ni)sD-;ooX&6& z0$r3dfo*g`V1|4qP{1q%UcLpUA<)5A=Kd>q45dq)W&(9ILg^TN#f9lBhB%m)Z_)f% zTtd%jp`G?A8KY@=&b@_^%|%b|8Drqgwt|9+x*<>7E8JHBW2^aVQg%)|bIEHAPUph&buQ@=VW&ig>mqTsY zgqW8*Ct5ItXEXH$^ky9j543rnAaA~RFCoU}qAdx-DuX-lLY z&c5b%&fJ87w=nn~1M{=KtaJf^=DAk0n5Uh~+I#8htDK!i(UvRMo(!A2Cd`$QTU|U~ z&rfXq9r4fh9R9g;W^WI_eZEyP_v*j(U*%lc4jlcf*|@;NLc+Y|^Y%(6MZx*^5A0p1 Q4smdKI;Vst0NCnZ!~g&Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..29ee1d09b3a08d96e3e5440789dc34fb60b26961 GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp@KrF_=0wgznzPBDoF(rAsyD<>7E8JGoMUMyS;6bkcnaSYKoe|DlF*8v3qmg|3xUvm5Z zpa1QXs@I;19v(%WucufS@We1!%3P9LWx1@=zopr0EvA=yZ`_I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..494a59fc8a7e5ac16f66a682648f6fab1811d3be GIT binary patch literal 641 zcmV-{0)G98P)S=>pa>ASWn%>m5&dP{Xisb5`vS#P5132l|N*`iL!$B%gj;0RaB;vdDm(C~H;+ z0BunP)F;N=0r076z>8!=azqUE5erp7%LNIM0bxuB@T+Dyum}L)L*Y6_0))pEfY-`_ z9{^i62M~*j1PD8TBROCiP?!`MkpMG#fXN)V1^7mYoDc&$0KAa{w*ha9G9m%i0DC!b z4tPQgAU+bM%>#bUfh)iRqCQ~)@J9|5fQG0~7y)#1;4WZT)F-s6flI)+s81LNT;{+} zfH_f0Y?glH(;V0XtO`;4E#3v}=D-SIU(_dj2UyO5DL~J3ZQ?Ni59dHQ25e_Q^6L&? z01kv4&;}G{YZ6O(;FWTqsRQU3t55hvqZ|m&1GrbBgME(}l*Mk%obpoJc zwmwk>goj)JDD0RK1A5e#R1qN~egr_zfi?3cjA&h79S`-OiU?VwK2vua36S=zj;fto z@JUXD@|s?EB-^1^HH65C@K2hu>?9gYO;M!|3KwTTajWr{}EgiG20|5T^x{wc4odRfjR*>BZ zEdb03F~HXVzEc%)TC)paH3oP^dc1**Pe%Ax2U3C;@_>GAKvPwS0)m0Co)s5iR>3z--J<|0`0W7V-A!@xBmZfXAHxXqm{FeH*}$ zN*oXj`VBzGhVhJxxhVnA^rR30q{J}*68n}-tE;G}o3Ww+0Del3&j->$eJM7ds}$Q0 zUeuD8bF+#sAWGhL;PxaM-q3d!E7YhDee1-9D}VXJu`TZ#e%Lba2k;M1c7|f0@VMIm O0000JGE zg^jTxUgBwNX^zHo$1^whji3DBFaM7oQ-0&Ncdin5ZbxL!Dlx}?`6_>y5tcwQ;Cq>I z%Nup!X$6<$0Yda3HO`xY&GfIfLtli;BbYVL(;RWRPqUzjZdo+X>c;bnVYUa7;X6mCSCQIgD;$x@Po=qJu zMJs$%>Mv$9YNGKS4VbXUPLiQIUujCXZ4Cej*Wx;Y#ZUj!M;Vi+f|u2M_Oa- zuU1D|Yr=tb*1xj5NMLg%W85V4S#Y+ZKQI0P(V@(kRI#2)00000NkvXXu0mjfy`$VT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4f5a2ecc3886c9aed6b5732ea5094ee2bbb0bad3 GIT binary patch literal 349 zcmeAS@N?(olHy`uVBq!ia0vp^qCl+3!2%@beAQS6q?nSt-CY>|xA&jf59DzcctjR6 zFmMZlFeAgPITAoY_7YEDSM~=SLM-yUON91w0EOOpx;TbdoW6R+Fo?-fp!H$>E)Fr~ z6-gT>T=*o>QlQZ9U_svUe+;~Sl01s+B1T$vvR|6H0^C2JVcxT<*upmBD#Lw|AVG## zFQ1D4SaWZ?cg2FItUE3|$mrX#{=0m^(tn(59$MRKs)>DFaoafJu<%=4P)~h_^Y!ai+M!H5R$d6vz0O?L0`q3$zO$gH)i4!~8*7 zvm9h25%RySq|F9WP8w+yJE&sq3~#a$L09Ap&al=7+6It9D)2fN3GHWIB)ZP|9DoXI zEueNTq>}cLw^8mzC~}m4JI&qMAmRR0QX|9U?J}=&1VBmLl?GDMw3$@nD|WM)c2-lM zffh=X8DNM7vO7xA0uo9~NPCwhO|XR~XG1}&m<9vM3KG^ck5;82478IQwJ#?~*g>+M5KzodXeXITC5;(DG2f$A zi0q&zNw$&BRMPEALQ>a7vd8S8Ptm?Z5Hm?KHi0&11+Cyuv{w)t$pq>_n<27+9!6W> z4#Y|_fYwk$d%ygK;vke5j)AWZnQBQ zXbSB<#F^BAR<(hypshxnNv&v?Y@lms1;m+DL|e3h{zhv+oJnyw=y$Xh#F^BLHfIB! zM=K%Dq;+WLZJ=SaG7?1Ep9w05f9*d+8$g^%?}q;{ZbutJoJpf-Z`nW_&=zQNCAD%D zZG#OIF^jepaU}KTdHfI#qMbn8ND(K|K0>UZ$4R!yjnqxDN9>@8b7+&qPNYLhzlwCe(AL^P>~C0f6?hoI?HWgAmJsFRcLV~J%xmVB1TCzPNN%XAm7nKJC~M_ zoUT&-vySIT1{Jr@k{y)sZhD@6VtDYwu* zPNUgL29OZ3m*uK4!XCQmpp_yq&6Ftf4kKJe3nU4)J;NMc?e_o)?TnDOV@z8~Ldag= zS3>)Rmx(A@NkZ6)yupuHJ4^5N13V*12w%jb9AL(N#AzYr2PtbkeSE+-oMV<6i_B4F zj05x~4}nuir)UZV`AMLK@Bjb+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bb6f9a63b5b54a9c6070d229f99df1fa66be1537 GIT binary patch literal 901 zcmV;01A6?4P)H}E!95p8+ywss z9jI^1Ok`BqvZ^m5bY^o?VQccx*4iXczJj2?lX$7;o_ksX=JR_DfiLal=HByD{}UtF zP%fizo8b={1&xCI&nRfvf;3YUxk8l(wCSyobS2@qi?AyyA%`vXgVkfIy5!Z{$59q@f@Q*eDLGMtF4J$5LWG;%7)7=i^o8U^4Vt0fJzhjboE#1M zy@hfMnqULdEmA0lIlvO8bzX1_stx2kYw>KPLT8+BG2L(pI*aKZDU@@N;s6ay(@}#0 zenV`^wc{A1ILsE}A<1Y#3z!P1*g=Z-FwI2?(lilu_IU{!<9kE{Z69Z3^+Ae4c(P6*WpcGxi5xbxwqJr`oq^Keac0pGV z7rh6S5M{fdD&my)pgiImyPyY%N$){f#6!EFHX`jkD1+Fr3+f?~-h%?f_OJzYhwWds zI&2qp(=O<1#A%f0$s;OuK_4M5c@HWf7VU!0BW`*Rsv!#QKd!zeO%IW?3(~X@vtEPd z5l_%A4`_G12Hi!JT_0gjc*}Vm(^1!l{AI?xc? zqJwxuGTy6ngLJH%3@ezgFi6n6Z7ul^H{jf9H||CLg%>)Avp;j z+aS${gy~YHM4l`e0s=B*$y1_6k1+51Tu+^0ojBHb!}q>yf-0^m9NL`)-rElAsmDz3 z=0-7Ko}aLFpLa;^#ftM4KISPqc|w_^=)GM6ALd;asnDcFmoBR`sjx_a9C}#S-ww(m^PYf%%PR}U0 za7nc-J--pHX3x(xY*-$`acoEg{ah38p}b{tbJJHfM#a>n;|?KdNCM0|XQcOwS?k)`f+xyS#2l6-zJR*x3 z7`TN&n2}-D90{Nxdx@v7EBgZuVFp&ojRA4uK%o_$E{-7{$KPJH<~w8{;CfN~j*6e_ zxA{uiC#I_fP1v-_yYYxQ8@~w0$;(mh+vcUe;YnFHcS?Yhy;JW^lS76HE9KNV{pEeF z{-nAe(=fUtBNS+~U)A{O;Uhr>6`pEc%RL18k|*fwII#P`ywb^QLK=TeblE3((xWc! z?<(bY-&geNkl9)v8^`gdNN3V^tOXj>K$v04?r)7kq=v*;C9EWbfB_6|qP%HALa}K;aHSE7AiiMY zR(F6FD*>Z_6QabI1hf!BShke52x*;8DKqnR?K6G3=ec)Iy12h*eSSF~GSi$h_abqk zNt`&iMxq%Znu!51AO^&M7%+t=2Kb*C@CL_Bg$Wk1ofDknDt+WAQXt1|{-l#*yv|cp z`T#ToaNMaX!!9n6W3v21Cp(!_PQWez2SODaxrkee{Kz_z<90T(!wT395STE70}Noz zZCa@vbNfTUZVO-!KtMv07VZ%?12n7e#bSm4fVbQMCm|r=Ic^YDuJSCU+XvX=3aAK7 zSjq^|rN|MI2+uIcf3r6LKoLF*NO+MPQRa76F-U2e-2g6x7bqp%=O^B0Een}TH5DY7 zNt#ABbC^ryan0W}xB={h9AB}V6f(iovVv3Gk7oM{pa`Ej5qf!>SxCT{MH>U*x32+K zLKCf2BaTx;E7`Ey*8m&AKJz#eZu=TgO8BJA1YsK^jFshx9pIPHKsTlNgQOkcn~>o; zr8!~+=y@It1h-54Y)T158lwOtxd8@(8$ig=gH9LI!vk6X3 z*4hF3s(y=VDl7yCK#Du6`v)r^jZO=K7zq-1_M<KRqdJ8XdS zs^&`sF%!gL^cl6&0!XMYA-sgx2~L2O=wI`uy8{-X@47XJo!|thqo7)!bO&rx75_(& z5Cn0W-&E%ocfbi%@g8C&7^Yn{e&`PPPF1WyJQB93zO(LtE>*D*@km&r+OD_*`c%bS z#3Nz8>gsU^3*j*yM02UnpwotUARIy;Bj!Z)!`1*bc0#~EjJ4<=w!TNKfDP!hB4s8VLg!^W zpo)IMN! zol!jGyNOOW87~CmLq0uCl;%3K%T8Eof6S`q&kYm}y&2LA*bKv=*a zr5Rzne}WAlEF;fY`I@wUf(0OKqR3du(niV|P|Ld%!xIFT*)a|Q?y#Twz&($0P#@W> z1i@`Kk!QS&a)y=EP4e%)n$wJ;V1nV?$lc>_g627nz1p6e}$6dmvkDW|Mm}Vmg;iisv=*OCF z_L4#n#7YptPZb-uh+7JrXBCyI$v_aInl#OvW0=V@NC(?^T(6&y@PQFvm`a}JHI8$Z zEA)^hPo6BdxJ(B}+06fVNUxcckY|k@;D0osV1Hw4Di8{+L!t-xA{2N9i5}pK@Hy+_ g|IIqt#DEy^FUPI2koksLrT_o{07*qoM6N<$f|g)GwEzGB literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b8914c4a2106b7aa50121c6df58ad134568a2244 GIT binary patch literal 1241 zcmV;~1Sb25P)l4N5{jIAh*NsJNktu<}E zJQVt%e?V&*Vk(H$hhl?O+iF>2DSarpnVTlN*=)MAU!Tv+Bw=RGoNiz~=Xv4x&1Ejh zIU*1UsDV>L%@QbDK|l}?1Ox#=K>n}%4+sK+fCw+s##dbBE>om2Fi11SpA2)JH`z@n zUjw%BK0mU|dQx1agDv?KP)su;IFsXN-XMaoU)1xFo`6k!${cP@(M>tRvi6b$zNWz6 zfFgQWz^fTf60(jDz-7Yz1svc3swCKJ9Up*iDexnpl#8g9<1B>;_K-yO4Ns^J*umfU z;SU;E#Ogz}0k4o`Em>}GnnvoVCQ6tvQL3qyjD4D4L;@K{jAfV1)4 z2kan;Rhl@$<9dr_?7W6h7f{M5Ru9>UP_Swl!|Hb;>H;oem7oftXvG-C>N8aV2W-cG zdYw->dx_tSH6cfxihx2MVD^wIgwg;(jB(8FQs6zH8?!V!5y}GuwJc-yj@N)F|6vwK z_!1y!!)%%|ZvmfRb_wBIfM5`_PA>t)%pzN4i?0DOQphF=yANo@>=eS+06`CC$J__} zg6toPeGe#Q7TGno0b4QaM))2e=tY*}Id=i?AUwsvUYa?KUl9nS^@+E$cEhn zgh?Yiq%GhGvLpr0171S*h%#*f6&T1~a30W#>;^(xfZ!&wxbuK7k)75W(2wk#^MGMw zjamcZ$cCH;{DrJeYd}4+JI({9kX369s6jUFJRprMsx_bz*^=`B16f#WKm?hw(F3v@ z{YNP2JYaI8{}SidW%Rc98(9-V<7z=R=se&HWM^~+sCblUMK+=};3l$W?~l`2%C(LE z6b7%s;HdS5M#U@TWo-dhkbQ@66Ywsw44br!Z+)qM+g(5mvmPx0eX9Pf zBDjuhh7#Y$FJlhbPYAaG$1&^jJ)jq}*WCvcF^w!mm9OL1u!L-qLiYiJk1-qYHDJi; z_k8_Z%Db%l@wYO8S%Q%F0KtB& zM%k=%{Aaj_S&qG`{?UEGHX)|)tA7ux^9WS|B_yyKqgKiIyO_Z0HihZ}1lw7_YMC|# zSDp-3^K4VSZ}cEp>|CMR!)>v20d?9I|k zrGp3NKIX7z(Cp{l=|-}wW`zNcP_fRP{V0PxVl^u?_`Ui2APcM|%dhkkr=A)ri4dWZ z8tQ4GpWiT8%RKvZ>|Wna0zceho38Em#auwG9Oo$1dwyU)_faLm9`ql8AVMeecs0Wb zLXYnRM+JRMo{X@jblXe=@3vgC-|Nv*0abE?L3>W$FM@YNDH4c z#2v;-k>wFd#<;~5&eB9JPX-6!f&g_Z2nYg#fFR(1fgwO7*(gng00000NkvXXu0mjf DeI+{> literal 0 HcmV?d00001 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); + } + } + + +}