Compare commits

...

150 Commits

Author SHA1 Message Date
Greyson Parrelli
4f4aea22ce Bump version to 5.2.2 2021-01-16 21:27:38 -05:00
Greyson Parrelli
e0ea2bdde4 Updated language translations. 2021-01-16 21:27:14 -05:00
Greyson Parrelli
d40dc1d90b Bump signal-client-java version to 0.1.5 2021-01-16 21:11:42 -05:00
Greyson Parrelli
4571151e3c Revert "Remove reset session button."
This reverts commit f24020e7b7.
2021-01-16 21:11:42 -05:00
Greyson Parrelli
3e43963f67 Put receipts in the recipient's queue. 2021-01-16 21:11:42 -05:00
Greyson Parrelli
fe71d6ac41 Make outage banner color less aggressive. 2021-01-16 21:11:42 -05:00
Greyson Parrelli
0514950333 Feature flag OkHttp automatic network retry. 2021-01-16 21:11:42 -05:00
Greyson Parrelli
a2dc781840 Add an automatic session reset interval. 2021-01-16 21:11:42 -05:00
Greyson Parrelli
2c1c6fab35 Bump version to 5.2.1 2021-01-16 03:41:29 -05:00
Greyson Parrelli
3c2e428c54 Updated language translations. 2021-01-16 03:41:29 -05:00
Greyson Parrelli
8f7fe5c3ee Add jitter to job exponential backoff. 2021-01-16 03:41:29 -05:00
Greyson Parrelli
93e9dd6425 Feature flag the default max backoff interval. 2021-01-16 03:06:54 -05:00
Greyson Parrelli
c95f0fce6e Handle ServerRejectedException.
Handle an exception that indicates we should halt retries.
2021-01-16 02:32:09 -05:00
Greyson Parrelli
a3c7e7e552 Feature flag automatic session reset. 2021-01-16 02:05:43 -05:00
Greyson Parrelli
1e2590af49 Lock the threadId during message send.
Fixes #10659
2021-01-15 12:15:07 -05:00
Greyson Parrelli
562e608e1f Fix issue with previously-enqueued bad encrypted messages. 2021-01-15 11:50:50 -05:00
Greyson Parrelli
417d5a2804 Be extra safe when posting a notification during a migration. 2021-01-15 11:22:15 -05:00
Ewout ter Hoeven
c0c8d2caa7 Update issue template.
Fixes #10626

Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-01-15 11:17:38 -05:00
Greyson Parrelli
727175e4f4 Add 'constraints' and 'key preferences' sections to logs. 2021-01-15 11:17:38 -05:00
Ewout ter Hoeven
577d2b13ca CI: Update to checkout v2, remove install NDK
- Updates to action/checkout v2, which is faster
 - Remove install NDK step, since it's installed by default and speeds up the build
2021-01-14 12:44:08 -04:00
Greyson Parrelli
6ac2f922e2 Fix capitalization in some strings. 2021-01-14 10:47:42 -05:00
Greyson Parrelli
98297e55c1 Don't show menu actions for chat refresh messages. 2021-01-14 10:46:09 -05:00
Alan Evans
aa2094a2cc Fix group recipient showing in verify safety number change "learn more". 2021-01-14 10:19:50 -04:00
Alex Hart
f8c053cc96 Add 'on another device' to participants description 2021-01-14 07:03:19 -04:00
Alex Hart
790f8426ac Fix issue when single user leaves ParticipantCollection. 2021-01-14 06:53:18 -04:00
Greyson Parrelli
ff11609a82 Bump version to 5.2.0 2021-01-13 19:57:58 -05:00
Greyson Parrelli
94346033a8 Updated language translations. 2021-01-13 19:57:35 -05:00
Alan Evans
cb1401f556 Prompt to confirm number before SMS or call. 2021-01-13 19:43:35 -05:00
Alan Evans
ae676d7486 Fast job sorting. 2021-01-13 19:43:35 -05:00
Alan Evans
2d39e43677 Restrict group names to 32 graphemes.
Uses some code from #10132 hence co-author:

Co-authored-by: Fumiaki Yoshimatsu <fumiakiy@gmail.com>
2021-01-13 19:43:35 -05:00
Alex Hart
0ccc7e3c06 Distinguish between primary and secondary devices in participants list. 2021-01-13 19:43:23 -05:00
Alex Hart
2d20ceea01 Show contact profile photo instead of system contact. 2021-01-13 19:43:23 -05:00
Alex Hart
cee2702fdf Add expandable video pip to 1:1 conversations. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
6c94be70dc Update safety number UI. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
f24020e7b7 Remove reset session button. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
728f1707b6 Automatically recover from bad encrypted messages. 2021-01-13 19:43:23 -05:00
Alan Evans
adea15df10 Recover from CDN 416 Range error on attachment download. 2021-01-13 19:43:23 -05:00
Alex Hart
be91f2396c Add toggle to control call bandwidth. 2021-01-13 19:43:23 -05:00
Alex Hart
8724d904b7 Add NotInCallConstraint, restrict auto-download of media and documents when on an active voice or video call. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
ef95479157 Increase versionCode postFixSize from 10 to 100. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
710cd23537 Fix typo in log. 2021-01-13 19:43:23 -05:00
Alex Hart
0af313a81f Add correct margin to in-call menu item. 2021-01-13 19:43:23 -05:00
Alex Hart
71be388989 Order grid by latest speakers and prevent any unnecessary shifts. 2021-01-13 19:43:23 -05:00
Alex Hart
db3098f633 Add immersive mode for calling. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
ac197f42f2 Bump version to 5.1.9 2021-01-13 17:39:05 -05:00
Greyson Parrelli
d82882ba28 Updated language translations. 2021-01-13 17:38:35 -05:00
Greyson Parrelli
957a12875d Fix situations where we might not have detected first-ever-launch. 2021-01-13 17:33:14 -05:00
Greyson Parrelli
796eb5043c Bump version to 5.1.8 2021-01-12 12:47:57 -05:00
Greyson Parrelli
4f8d86828f Updated language translations. 2021-01-12 12:47:57 -05:00
Greyson Parrelli
5370605815 Control CDS refresh interval with a feature flag. 2021-01-12 12:47:57 -05:00
Greyson Parrelli
d5fb71b63f Prevent creating threads for remapped users.
Fixes #10538
2021-01-12 11:41:13 -05:00
Greyson Parrelli
2455c291d8 Bump version to 5.1.7 2021-01-12 02:06:59 -05:00
Greyson Parrelli
80ad28e9cc Updated language translations. 2021-01-12 02:06:00 -05:00
Greyson Parrelli
74552ba545 Fix possible crash with ProcessLifecycleOwner. 2021-01-12 02:06:00 -05:00
Greyson Parrelli
141cab1105 Perfom a migration to notify users of new contacts. 2021-01-11 23:22:01 -05:00
Greyson Parrelli
f012a41345 Fix issue with Signal join notifications. 2021-01-11 23:21:54 -05:00
Alan Evans
1f95df60d4 Fix style of approve new member switch in light bottom sheet. 2021-01-11 19:07:27 -04:00
Alan Evans
560c8c8cac Bump version to 5.1.6 2021-01-11 17:27:32 -04:00
Alan Evans
7cd79f8a94 Updated language translations. 2021-01-11 17:27:32 -04:00
Greyson Parrelli
667304c81e Cause LiveRecipient.refresh() to force a LiveData change. 2021-01-11 17:18:46 -04:00
Greyson Parrelli
2dd95c6ef6 Increase profile timeouts. 2021-01-11 17:18:46 -04:00
Greyson Parrelli
29e66e1d47 Fix the invite share button. 2021-01-11 17:18:46 -04:00
Alan Evans
5eb5af2f87 Bump version to 5.1.5 2021-01-11 14:13:02 -04:00
Alan Evans
e47b62805b Updated language translations. 2021-01-11 14:13:02 -04:00
Alan Evans
57adc73e95 Revert "Fast job sorting."
This reverts commit 373972f5dc.
2021-01-11 13:59:01 -04:00
Greyson Parrelli
8f4d64d37a Update link preview user agent. 2021-01-11 13:46:35 -04:00
Alan Evans
9ce3813044 Add "Enter your phone number" string for translation. 2021-01-11 13:46:35 -04:00
Alan Evans
6436e2836d No cell service hint during registration. 2021-01-11 13:46:35 -04:00
Alan Evans
77c83019d0 Smaller titles on small screen registration. 2021-01-11 13:46:35 -04:00
Greyson Parrelli
e6dfe96569 Add a gradient and background to the onboarding megaphone. 2021-01-11 13:46:35 -04:00
Alan Evans
5d515198e6 Fix initial state for update button. 2021-01-10 11:47:59 -04:00
Greyson Parrelli
1d912c0db2 Fix issue where conversation hero avatars didn't show up. 2021-01-10 10:01:31 -05:00
Greyson Parrelli
bac04dea8d Bump version to 5.1.4 2021-01-09 23:45:05 -05:00
Greyson Parrelli
3b39d13412 Fix possible crash with ProcessLifecycleObserver. 2021-01-09 23:41:31 -05:00
Greyson Parrelli
9838b2cf0a Fix crash in ContactSelectionListFragment. 2021-01-09 23:36:57 -05:00
Greyson Parrelli
0ac56ca571 Fix crash with ExpiringMessageManager. 2021-01-09 23:36:09 -05:00
Greyson Parrelli
12321bc2f0 Bump version to 5.1.3 2021-01-09 23:22:10 -05:00
Greyson Parrelli
3a55dfa32f Updated language translations. 2021-01-09 23:21:50 -05:00
Alan Evans
373972f5dc Fast job sorting. 2021-01-09 23:16:46 -05:00
Alan Evans
60a701f84f Fix missing dialog message on single user add confirm. 2021-01-09 20:12:10 -04:00
Greyson Parrelli
14f7c01fcb Only notify for actual recipient changes. 2021-01-09 18:45:22 -05:00
Greyson Parrelli
caf4f1a7ba Bump version to 5.1.2 2021-01-08 23:08:31 -05:00
Greyson Parrelli
eb55ac9a97 Updated language translations. 2021-01-08 23:07:17 -05:00
Greyson Parrelli
b9d8868aab Added a new onboarding megaphone. 2021-01-08 23:00:41 -05:00
Alex Hart
bec03534ef Animated skip button. 2021-01-08 21:10:40 -04:00
Alan Evans
565eab9dc1 Fix jumping "0 members". 2021-01-08 21:10:40 -04:00
Alan Evans
4d229862b6 Invite Friends bottom sheet. 2021-01-08 21:10:40 -04:00
Greyson Parrelli
3739eb7731 Add extra conditions for the SMS banner. 2021-01-08 21:01:13 -04:00
Alex Hart
ae5f9fb8ac Add empty state for members list in AddGroupDetailsFragment. 2021-01-08 21:01:13 -04:00
Alex Hart
4320a81846 Add invite friends action button and text. 2021-01-08 21:01:13 -04:00
Alan Evans
9fcf40fdc4 Allow empty group creation. 2021-01-08 12:53:23 -04:00
Greyson Parrelli
79d6ac100c Fix issue where megaphone display may be delayed. 2021-01-08 11:31:35 -05:00
Greyson Parrelli
a3e3153ee3 Add the Honor Play to the CameraX blacklist. 2021-01-08 10:13:50 -05:00
Alan Evans
0f525d2b07 Bump version to 5.1.1 2021-01-07 16:08:02 -04:00
Alan Evans
8de3f5045b Updated language translations. 2021-01-07 16:07:21 -04:00
Greyson Parrelli
fba4ae91e3 Fix issue where recipient observing could show stale data. 2021-01-07 16:07:21 -04:00
Alan Evans
dda68d6c95 Revert "Bump libsignal-client to 0.2.0"
This reverts commit e845fba8b3.
2021-01-07 16:07:04 -04:00
Greyson Parrelli
25af25cd19 Fix issue where button to go to archive was missing. 2021-01-07 16:07:04 -04:00
Alan Evans
dfd5b2c225 Ensure consistency and completeness of feature flag remote capable designation.
Make CustomVideoMuxer flag remote capable.
2021-01-07 16:07:04 -04:00
Greyson Parrelli
e850d8e917 Fix badge overlap in archive screen. 2021-01-07 09:54:02 -05:00
Alex Hart
677cf725a1 Fix bad screen lock behaviour. 2021-01-07 10:37:55 -04:00
Alan Evans
e95bb9cb0f Bump version to 5.1.0 2021-01-06 17:05:30 -04:00
Alan Evans
2c223a5826 Updated language translations. 2021-01-06 17:03:38 -04:00
Greyson Parrelli
bbc346bd7a Create a system for scheduling work post-initial-render. 2021-01-06 17:03:38 -04:00
Cody Henthorne
cf32b93269 Better error handling for group calls. 2021-01-06 17:03:38 -04:00
Cody Henthorne
84f1da76ad Fix bug where missing media keys would not always be shown on time. 2021-01-06 17:03:21 -04:00
Jack Lloyd
e845fba8b3 Bump libsignal-client to 0.2.0 2021-01-06 17:03:21 -04:00
Greyson Parrelli
01152ead61 Move the JobDatabase to a separate physical database.
Also removes maxInstancesPerFactory from DB, which was only used during job submission and had no need to be persisted.
2021-01-06 17:03:21 -04:00
Alex Hart
198281aa47 Show 'return to call' if local user is in the call group. 2021-01-06 17:03:21 -04:00
Jim Gustafson
8e8d86606b Update to RingRTC v2.8.9 2021-01-06 17:03:21 -04:00
Alan Evans
b4c2e21415 Custom streaming video muxer. 2021-01-06 17:03:21 -04:00
Alan Evans
6080e1f338 Ensure ProfileKeyCredentials match ProfileKey.
Fixes #10344
2021-01-06 17:03:20 -04:00
Alan Evans
6dd3fdaa55 Remove usages of deprecated Handler constructor. 2021-01-06 17:03:20 -04:00
Alan Evans
64312f9c7f Fix non-rendered previews when differ by trailing slash. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
86542febf9 Move the MegaphoneDatabase to a separate physical database. 2021-01-06 17:03:20 -04:00
Alex Hart
9da49f9f8a Load correct recipient from thread record. 2021-01-06 17:03:20 -04:00
Alex Hart
ce3872ce1a Fix ACTION_OPEN_DOCUMENT_TREE crash when no file picker available.
Fixes #10131
2021-01-06 17:03:20 -04:00
Greyson Parrelli
c466dba8c4 Move the KeyValueDatabase to a separate physical database. 2021-01-06 17:03:20 -04:00
Alex Hart
46d412a6c3 UX update and slight stability fix. 2021-01-06 17:03:20 -04:00
Alex Hart
e2872d9af8 Add emdash instead of 0 if no callers are present and we haven't connected / loaded the group state. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
3474b26f61 Don't include archived threads in recent conversation query. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
740e934e5d Speed up the recipient warm-up phase. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
61c5fc1057 Add shake-to-report for internal users. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
7ef77bf16c Remove unbounded conversation list query. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
aa3eb78956 Clean up and speed up conversation list item view. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
cdd7b2deb9 Improve and streamline Application#onCreate. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
c27300c19d Add a perf buildType for testing performance improvements. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
8927971a19 Replace non-essential conversation list views with stubs. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
1ced115b54 Only force a conversation list re-query for non-cold-starts. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
fcbd594def Add a system to easily trace jobs. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
4b8d02fdba Move Tracer to core-util. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
e10284bd13 Remove Trace annotation. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
4b5f1d64e6 Switch the conversation list to our own paging library. 2021-01-06 17:03:20 -04:00
Alex Hart
b7477d287b Reopen properly when we select launcher icon.
* Reopen properly when we select launcher icon.

* Reduce noise
2021-01-06 17:03:20 -04:00
Greyson Parrelli
6bab6c2454 Increase prekey archive age to 30 days. 2021-01-06 17:03:20 -04:00
Alex Hart
586c45616c Utilize ACTION_GET_CONTENT for one-time-access to backup.
Fixes #10312
2021-01-06 17:03:20 -04:00
Greyson Parrelli
ccd405fdce Don't double-isolate-bidi on phone numbers.
Fixes #10257
2021-01-06 17:03:20 -04:00
henry
dbf78d1b69 Show correct fragment layout preview. 2020-12-18 10:41:14 -04:00
Alex Hart
5f947ea2d6 Remove a few more instances of AsyncTask. 2020-12-18 10:41:14 -04:00
Alex Hart
73afa82147 Remove ViewUtil deprecated methods. 2020-12-18 10:41:14 -04:00
Alex Hart
744b79419b Swap out AsyncTask usage in notification action receivers with bounded threadpool. 2020-12-18 10:41:14 -04:00
Alex Hart
ce20dd97ff Fix bad compose input height. 2020-12-18 10:41:14 -04:00
Greyson Parrelli
3983d5aca4 Log the threadId of a log. 2020-12-18 10:41:14 -04:00
Greyson Parrelli
7b0de2d2a9 Force a feature flag refresh after a version change. 2020-12-18 10:41:14 -04:00
Cody Henthorne
2b65482abd Fix KitKat OOM when rendering rounded material buttons. 2020-12-18 10:41:14 -04:00
Cody Henthorne
fe01e80af5 Fix bug with mute states not dynamically updating in participants list. 2020-12-18 10:41:14 -04:00
Greyson Parrelli
fc43a0d8e9 Put log tag in brackets. 2020-12-18 10:41:14 -04:00
Alex Hart
e709cdc9d5 Remember the last position of emoji and sticker picker as you swap between them. 2020-12-18 10:41:14 -04:00
Jack Lloyd
d2d698f64e Don't rely on the SessionState protobuf.
Instead use the convenient deserialization constructor
2020-12-18 10:41:14 -04:00
Alan Evans
7f1e33be32 Fix not deselecting item that is too large to send. 2020-12-18 10:41:14 -04:00
494 changed files with 20645 additions and 5063 deletions

View File

@@ -1,3 +1,12 @@
---
name: 🛠️ Bug report
about: Let us know that something isn't working as intended
title: ''
labels: ''
assignees: ''
---
<!-- This is a bug report template. By following the instructions below and filling out the sections with your information, you will help the developers get all the necessary data to fix your issue.
You can also preview your report before submitting it. You may remove sections that aren't relevant to your particular case.

20
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
blank_issues_enabled: false
contact_links:
- name: 📃Support Center
url: https://support.signal.org/
about: Find answers to many common questions.
- name: ✨ Feature request
url: https://community.signalusers.org/c/feature-requests/
about: Missing something in Signal? Let us know.
- name: 💬 Community support
url: https://community.signalusers.org/c/support/
about: Feel free to ask anything.
- name: 📖 Developer documentation
url: https://signal.org/docs/
about: Official Signal developer documentation.
- name: 📚 Translation feedback.
url: https://community.signalusers.org/c/translation-feedback/
about: Share feedback on translations.
- name: ❓ Other issue?
url: https://community.signalusers.org/
about: Search on the community forums.

View File

@@ -14,16 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Install NDK
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;21.0.6113669" --sdk_root=${ANDROID_SDK_ROOT}
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1

View File

@@ -61,10 +61,10 @@ protobuf {
}
}
def canonicalVersionCode = 759
def canonicalVersionName = "5.0.8"
def canonicalVersionCode = 771
def canonicalVersionName = "5.2.2"
def postFixSize = 10
def postFixSize = 100
def abiPostFix = ['universal' : 0,
'armeabi-v7a' : 1,
'arm64-v8a' : 2,
@@ -128,7 +128,6 @@ android {
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "int", "TRACE_EVENT_MAX", "3500"
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
@@ -206,6 +205,12 @@ android {
minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles
}
perf {
initWith debug
isDefault false
debuggable false
matchingFallbacks = ['debug']
}
}
productFlavors {
@@ -229,7 +234,6 @@ android {
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "int", "TRACE_EVENT_MAX", "30_000"
}
prod {
@@ -311,8 +315,6 @@ dependencies {
implementation "androidx.camera:camera-view:1.0.0-alpha18"
implementation "androidx.concurrent:concurrent-futures:1.0.0"
implementation "androidx.autofill:autofill:1.0.0"
implementation "androidx.paging:paging-common:2.1.2"
implementation "androidx.paging:paging-runtime:2.1.2"
implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1'
@@ -335,12 +337,14 @@ dependencies {
implementation project(':libsignal-service')
implementation project(':paging')
implementation project(':core-util')
implementation project(':video')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation 'org.whispersystems:signal-client-android:0.1.5'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.8.7'
implementation 'org.signal:ringrtc-android:2.8.9'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
@@ -24,6 +25,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
@@ -42,8 +44,16 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
try {
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
databaseHelperField.setAccessible(true);
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())));
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
return Arrays.asList(new Descriptor(mainOpenHelper),
new Descriptor(keyValueOpenHelper),
new Descriptor(megaphoneOpenHelper),
new Descriptor(jobManagerOpenHelper));
} catch (Exception e) {
Log.i(TAG, "Unable to use reflection to access raw database.", e);
}
@@ -235,9 +245,9 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
}
static class Descriptor implements DatabaseDescriptor {
private final SQLCipherOpenHelper sqlCipherOpenHelper;
private final SignalDatabase sqlCipherOpenHelper;
Descriptor(@NonNull SQLCipherOpenHelper sqlCipherOpenHelper) {
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) {
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
}
@@ -247,11 +257,11 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
}
public @NonNull SQLiteDatabase getReadable() {
return sqlCipherOpenHelper.getReadableDatabase().getSqlCipherDatabase();
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
public @NonNull SQLiteDatabase getWritable() {
return sqlCipherOpenHelper.getWritableDatabase().getSqlCipherDatabase();
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
}
}

View File

@@ -225,6 +225,17 @@
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher" />
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher" />
</activity-alias>
<activity android:name=".deeplinks.DeepLinkEntryActivity"
android:noHistory="true"
android:theme="@style/Signal.Transparent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -241,13 +252,7 @@
<data android:scheme="https"
android:host="signal.group"/>
</intent-filter>
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher" />
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher" />
</activity-alias>
</activity>
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
@@ -504,7 +509,6 @@
<activity android:name=".MainActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".pin.PinRestoreActivity"

View File

@@ -50,6 +50,28 @@ public final class AppInitialization {
public static void onPostBackupRestore(@NonNull Context context) {
Log.i(TAG, "onPostBackupRestore()");
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
SignalStore.onboarding().clearAll();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
/**
* Temporary migration method that does the safest bits of {@link #onFirstEverAppLaunch(Context)}
*/
public static void onRepairFirstEverAppLaunch(@NonNull Context context) {
Log.w(TAG, "onRepairFirstEverAppLaunch()");
InsightsOptOut.userRequestedOptOut(context);
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));

View File

@@ -16,13 +16,13 @@
*/
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
import android.hardware.SensorManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
@@ -33,10 +33,12 @@ import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.Conscrypt;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.core.util.ShakeDetector;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.logging.PersistentLogger;
import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -44,6 +46,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
@@ -68,14 +71,15 @@ import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.shakereport.ShakeToReport;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.tracing.Tracer;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
@@ -94,7 +98,6 @@ import java.util.concurrent.TimeUnit;
*
* @author Moxie Marlinspike
*/
@Trace
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
private static final String TAG = ApplicationContext.class.getSimpleName();
@@ -112,62 +115,80 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
@Override
public void onCreate() {
Tracer.getInstance().start("Application#onCreate()");
AppStartup.getInstance().onApplicationCreate();
long startTime = System.currentTimeMillis();
if (FeatureFlags.internalUser()) {
Tracer.getInstance().setMaxBufferSize(35_000);
}
super.onCreate();
initializeSecurityProvider();
initializeLogging();
Log.i(TAG, "onCreate()");
initializeCrashHandling();
initializeAppDependencies();
initializeFirstEverAppLaunch();
initializeApplicationMigrations();
initializeMessageRetrieval();
initializeExpiringMessageManager();
initializeRevealableMessageManager();
initializeGcmCheck();
initializeSignedPreKeyCheck();
initializePeriodicTasks();
initializeCircumvention();
initializeRingRtc();
initializePendingMessages();
initializeBlobProvider();
initializeCleanup();
initializeGlideCodecs();
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("logging", () -> {
initializeLogging();
Log.i(TAG, "onCreate()");
})
.addBlocking("crash-handling", this::initializeCrashHandling)
.addBlocking("eat-db", () -> DatabaseFactory.getInstance(this))
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
.addBlocking("app-migrations", this::initializeApplicationMigrations)
.addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("mark-registration", () -> RegistrationUtil.maybeMarkRegistrationComplete(this))
.addBlocking("lifecycle-observer", () -> ProcessLifecycleOwner.get().getLifecycle().addObserver(this))
.addBlocking("message-retriever", this::initializeMessageRetrieval)
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
.addBlocking("vector-compat", () -> {
if (Build.VERSION.SDK_INT < 21) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
})
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializeGcmCheck)
.addNonBlocking(this::initializeSignedPreKeyCheck)
.addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention)
.addNonBlocking(this::initializePendingMessages)
.addNonBlocking(this::initializeCleanup)
.addNonBlocking(this::initializeGlideCodecs)
.addNonBlocking(FeatureFlags::init)
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(this::initializeBlobProvider)
.addPostRender(() -> NotificationChannels.create(this))
.execute();
FeatureFlags.init();
NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync();
RegistrationUtil.maybeMarkRegistrationComplete(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
if (Build.VERSION.SDK_INT < 21) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
ApplicationDependencies.getJobManager().beginJobLoop();
DynamicTheme.setDefaultDayNightMode(this);
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
Tracer.getInstance().end("Application#onCreate()");
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
long startTime = System.currentTimeMillis();
isAppVisible = true;
Log.i(TAG, "App is now visible.");
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
checkBuildExpiration();
SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getShakeToReport().enable();
checkBuildExpiration();
});
Log.d(TAG, "onStart() took " + (System.currentTimeMillis() - startTime) + " ms");
}
@Override
@@ -177,9 +198,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
KeyCachingService.onAppBackgrounded(this);
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
ApplicationDependencies.getFrameRateTracker().end();
ApplicationDependencies.getShakeToReport().disable();
}
public ExpiringMessageManager getExpiringMessageManager() {
if (expiringMessageManager == null) {
initializeExpiringMessageManager();
}
return expiringMessageManager;
}
@@ -252,13 +277,16 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
private void initializeFirstEverAppLaunch() {
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
if (!SQLCipherOpenHelper.databaseFileExists(this)) {
if (!SQLCipherOpenHelper.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
Log.i(TAG, "First ever app launch!");
AppInitialization.onFirstEverAppLaunch(this);
}
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
TextSecurePreferences.setFirstInstallVersion(this, BuildConfig.CANONICAL_VERSION_CODE);
} else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 90) {
Log.i(TAG, "Detected a new install that doesn't have passphrases disabled -- assuming bad initialization.");
AppInitialization.onRepairFirstEverAppLaunch(this);
}
}
@@ -333,23 +361,15 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
}
}
@SuppressLint("StaticFieldLeak")
@WorkerThread
private void initializeCircumvention() {
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
try {
ProviderInstaller.installIfNeeded(ApplicationContext.this);
} catch (Throwable t) {
Log.w(TAG, t);
}
}
return null;
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
try {
ProviderInstaller.installIfNeeded(ApplicationContext.this);
} catch (Throwable t) {
Log.w(TAG, t);
}
};
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private void executePendingContactSync() {
@@ -370,17 +390,15 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
}
}
@WorkerThread
private void initializeBlobProvider() {
SignalExecutors.BOUNDED.execute(() -> {
BlobProvider.getInstance().onSessionStart(this);
});
BlobProvider.getInstance().onSessionStart(this);
}
@WorkerThread
private void initializeCleanup() {
SignalExecutors.BOUNDED.execute(() -> {
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
});
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
}
private void initializeGlideCodecs() {

View File

@@ -39,9 +39,9 @@ import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.DataAndStoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
@@ -65,6 +65,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
implements SharedPreferences.OnSharedPreferenceChangeListener
{
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment";
@SuppressWarnings("unused")
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
@@ -104,6 +105,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
initFragment(android.R.id.content, new BackupsPreferenceFragment());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_HELP_FRAGMENT, false)) {
initFragment(android.R.id.content, new HelpFragment());
} else if (icicle == null) {
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
} else {
@@ -309,7 +312,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
fragment = new ChatsPreferenceFragment();
break;
case PREFERENCE_CATEGORY_STORAGE:
fragment = new StoragePreferenceFragment();
fragment = new DataAndStoragePreferenceFragment();
break;
case PREFERENCE_CATEGORY_DEVICES:
Intent intent = new Intent(getActivity(), DeviceActivity.class);

View File

@@ -15,6 +15,8 @@ import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.ConfigurationUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
@@ -31,8 +33,10 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
AppStartup.getInstance().onCriticalRenderEventStart();
logEvent("onCreate()");
super.onCreate(savedInstanceState);
AppStartup.getInstance().onCriticalRenderEventEnd();
}
@Override
@@ -44,6 +48,7 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onStart() {
logEvent("onStart()");
ApplicationDependencies.getShakeToReport().registerActivity(this);
super.onStart();
}

View File

@@ -61,7 +61,10 @@ public interface BindableConversationItem extends Unbindable {
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
void onDecryptionFailedLearnMoreClicked();
void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient);
void onJoinGroupCallClicked();
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View File

@@ -448,8 +448,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
swipeRefresh.setVisibility(View.VISIBLE);
reset();
} else {
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
initializeNoContactsPermission();
Context context = getContext();
if (context != null) {
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
initializeNoContactsPermission();
}
}
}
}.execute();

View File

@@ -9,6 +9,7 @@ import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Parcelable;
import android.view.View;
@@ -150,7 +151,7 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
} else {
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
startActivity(MainActivity.clearTop(this));
}
}
@@ -158,6 +159,11 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
}
private class ImportStateHandler extends Handler {
public ImportStateHandler() {
super(Looper.getMainLooper());
}
@Override
public void handleMessage(Message message) {
switch (message.what) {

View File

@@ -32,9 +32,9 @@ public class DeviceAddFragment extends LoggingFragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
this.overlay = ViewUtil.findById(this.container, R.id.overlay);
this.scannerView = ViewUtil.findById(this.container, R.id.scanner);
this.devicesImage = ViewUtil.findById(this.container, R.id.devices);
this.overlay = this.container.findViewById(R.id.overlay);
this.scannerView = this.container.findViewById(R.id.scanner);
this.devicesImage = this.container.findViewById(R.id.devices);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
this.overlay.setOrientation(LinearLayout.HORIZONTAL);

View File

@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicelist.Device;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
@@ -68,7 +67,7 @@ public class DeviceListFragment extends ListFragment
this.empty = view.findViewById(R.id.empty);
this.progressContainer = view.findViewById(R.id.progress_container);
this.addDeviceButton = ViewUtil.findById(view, R.id.add_device);
this.addDeviceButton = view.findViewById(R.id.add_device);
this.addDeviceButton.setOnClickListener(this);
return view;

View File

@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
@@ -93,26 +94,33 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
slideInAnimation = loadAnimation(R.anim.slide_from_bottom);
slideOutAnimation = loadAnimation(R.anim.slide_to_bottom);
View shareButton = ViewUtil.findById(this, R.id.share_button);
View smsButton = ViewUtil.findById(this, R.id.sms_button);
Button smsCancelButton = ViewUtil.findById(this, R.id.cancel_sms_button);
ContactFilterToolbar contactFilter = ViewUtil.findById(this, R.id.contact_filter);
View shareButton = findViewById(R.id.share_button);
Button smsButton = findViewById(R.id.sms_button);
Button smsCancelButton = findViewById(R.id.cancel_sms_button);
ContactFilterToolbar contactFilter = findViewById(R.id.contact_filter);
inviteText = ViewUtil.findById(this, R.id.invite_text);
smsSendFrame = ViewUtil.findById(this, R.id.sms_send_frame);
smsSendButton = ViewUtil.findById(this, R.id.send_sms_button);
inviteText = findViewById(R.id.invite_text);
smsSendFrame = findViewById(R.id.sms_send_frame);
smsSendButton = findViewById(R.id.send_sms_button);
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
contactsFragment.setOnContactSelectedListener(this);
shareButton.setOnClickListener(new ShareClickListener());
smsButton.setOnClickListener(new SmsClickListener());
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
contactFilter.setNavigationIcon(R.drawable.ic_search_conversation_24);
if (Util.isDefaultSmsProvider(this)) {
shareButton.setOnClickListener(new ShareClickListener());
smsButton.setOnClickListener(new SmsClickListener());
} else {
shareButton.setVisibility(View.GONE);
smsButton.setOnClickListener(new ShareClickListener());
smsButton.setText(R.string.InviteActivity_share);
}
}
private Animation loadAnimation(@AnimRes int animResId) {

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -8,13 +9,12 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@Trace
public class MainActivity extends PassphraseRequiredActivity {
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
@@ -22,8 +22,19 @@ public class MainActivity extends PassphraseRequiredActivity {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final MainNavigator navigator = new MainNavigator(this);
public static @NonNull Intent clearTop(@NonNull Context context) {
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
AppStartup.getInstance().onCriticalRenderEventStart();
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.main_activity);
@@ -34,6 +45,13 @@ public class MainActivity extends PassphraseRequiredActivity {
CachedInflater.from(this).clear();
}
@Override
public Intent getIntent() {
return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);

View File

@@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
@@ -25,7 +26,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.tracing.Tracer;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@@ -51,6 +52,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
@Override
protected final void onCreate(Bundle savedInstanceState) {
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
AppStartup.getInstance().onCriticalRenderEventStart();
this.networkAccess = new SignalServiceNetworkAccess(this);
onPreCreate();
@@ -63,6 +65,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
initializeClearKeyReceiver();
onCreate(savedInstanceState, true);
}
AppStartup.getInstance().onCriticalRenderEventEnd();
Tracer.getInstance().end(Log.tag(getClass()) + "#onCreate()");
}
@@ -221,15 +225,17 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private Intent getConversationListIntent() {
// TODO [greyson] Navigation
return new Intent(this, MainActivity.class);
return MainActivity.clearTop(this);
}
private void initializeClearKeyReceiver() {
this.clearKeyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive() for clear key event");
onMasterSecretCleared();
Log.i(TAG, "onReceive() for clear key event. PasswordDisabled: " + TextSecurePreferences.isPasswordDisabled(context) + ", ScreenLock: " + TextSecurePreferences.isScreenLockEnabled(context));
if (TextSecurePreferences.isScreenLockEnabled(context) || !TextSecurePreferences.isPasswordDisabled(context)) {
onMasterSecretCleared();
}
}
};

View File

@@ -35,7 +35,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
if (rawId == null) {
Toast.makeText(this, R.string.ShortcutLauncherActivity_invalid_shortcut, Toast.LENGTH_SHORT).show();
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
startActivity(MainActivity.clearTop(this));
finish();
return;
}
@@ -43,7 +43,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
Recipient recipient = Recipient.live(RecipientId.from(rawId)).get();
// TODO [greyson] Navigation
TaskStackBuilder backStack = TaskStackBuilder.create(this)
.addNextIntent(new Intent(this, MainActivity.class));
.addNextIntent(MainActivity.clearTop(this));
CommunicationActions.startConversation(this, recipient, null, backStack);
finish();

View File

@@ -11,8 +11,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
public class TransportOptionsAdapter extends BaseAdapter {
@@ -55,9 +53,9 @@ public class TransportOptionsAdapter extends BaseAdapter {
}
TransportOption transport = (TransportOption) getItem(position);
ImageView imageView = ViewUtil.findById(convertView, R.id.icon);
TextView textView = ViewUtil.findById(convertView, R.id.text);
TextView subtextView = ViewUtil.findById(convertView, R.id.subtext);
ImageView imageView = convertView.findViewById(R.id.icon);
TextView textView = convertView.findViewById(R.id.text);
TextView subtextView = convertView.findViewById(R.id.subtext);
imageView.getBackground().setColorFilter(transport.getBackgroundColor(), Mode.MULTIPLY);
imageView.setImageResource(transport.getDrawable());

View File

@@ -258,24 +258,24 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
this.numbersContainer = ViewUtil.findById(container, R.id.number_table);
this.qrCode = ViewUtil.findById(container, R.id.qr_code);
this.verified = ViewUtil.findById(container, R.id.verified_switch);
this.qrVerified = ViewUtil.findById(container, R.id.qr_verified);
this.description = ViewUtil.findById(container, R.id.description);
this.tapLabel = ViewUtil.findById(container, R.id.tap_label);
this.codes[0] = ViewUtil.findById(container, R.id.code_first);
this.codes[1] = ViewUtil.findById(container, R.id.code_second);
this.codes[2] = ViewUtil.findById(container, R.id.code_third);
this.codes[3] = ViewUtil.findById(container, R.id.code_fourth);
this.codes[4] = ViewUtil.findById(container, R.id.code_fifth);
this.codes[5] = ViewUtil.findById(container, R.id.code_sixth);
this.codes[6] = ViewUtil.findById(container, R.id.code_seventh);
this.codes[7] = ViewUtil.findById(container, R.id.code_eighth);
this.codes[8] = ViewUtil.findById(container, R.id.code_ninth);
this.codes[9] = ViewUtil.findById(container, R.id.code_tenth);
this.codes[10] = ViewUtil.findById(container, R.id.code_eleventh);
this.codes[11] = ViewUtil.findById(container, R.id.code_twelth);
this.numbersContainer = container.findViewById(R.id.number_table);
this.qrCode = container.findViewById(R.id.qr_code);
this.verified = container.findViewById(R.id.verified_switch);
this.qrVerified = container.findViewById(R.id.qr_verified);
this.description = container.findViewById(R.id.description);
this.tapLabel = container.findViewById(R.id.tap_label);
this.codes[0] = container.findViewById(R.id.code_first);
this.codes[1] = container.findViewById(R.id.code_second);
this.codes[2] = container.findViewById(R.id.code_third);
this.codes[3] = container.findViewById(R.id.code_fourth);
this.codes[4] = container.findViewById(R.id.code_fifth);
this.codes[5] = container.findViewById(R.id.code_sixth);
this.codes[6] = container.findViewById(R.id.code_seventh);
this.codes[7] = container.findViewById(R.id.code_eighth);
this.codes[8] = container.findViewById(R.id.code_ninth);
this.codes[9] = container.findViewById(R.id.code_tenth);
this.codes[10] = container.findViewById(R.id.code_eleventh);
this.codes[11] = container.findViewById(R.id.code_twelth);
this.qrCode.setOnClickListener(clickListener);
this.registerForContextMenu(numbersContainer);
@@ -664,7 +664,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
this.cameraView = ViewUtil.findById(container, R.id.scanner);
this.cameraView = container.findViewById(R.id.scanner);
return container;
}

View File

@@ -27,7 +27,9 @@ import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Rational;
import android.view.View;
import android.view.Window;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import androidx.annotation.NonNull;
@@ -35,6 +37,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.transition.Transition;
import androidx.transition.TransitionListenerAdapter;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -58,8 +62,10 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
@@ -80,6 +86,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
@@ -100,8 +107,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
//noinspection ConstantConditions
getSupportActionBar().hide();
fullscreenHelper = new FullscreenHelper(this);
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
@@ -141,9 +148,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
EventBus.getDefault().unregister(this);
}
if (!viewModel.isCallingStarted()) {
if (!viewModel.isCallStarting()) {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
finish();
}
}
@@ -156,9 +163,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
EventBus.getDefault().unregister(this);
if (!viewModel.isCallingStarted()) {
if (!viewModel.isCallStarting()) {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
@@ -471,7 +478,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void handleServerFailure() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish();
}
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
@@ -529,7 +535,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.putExtra(WebRtcCallService.EXTRA_RECIPIENT_IDS, RecipientId.toSerializedList(changedRecipients));
startService(intent);
} else {
startCall(state.getLocalParticipant().isVideoEnabled());
viewModel.startCall(state.getLocalParticipant().isVideoEnabled());
}
}
@@ -540,7 +546,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public void onCanceled() {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getGroupCallState().isNotIdle()) {
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
@@ -637,6 +643,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
}
@Override
public void showSystemUI() {
fullscreenHelper.showSystemUI();
}
@Override
public void hideSystemUI() {
fullscreenHelper.hideSystemUI();
}
@Override
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
switch (audioOutput) {
@@ -702,5 +718,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
viewModel.setIsViewingFocusedParticipant(page);
}
@Override
public void onLocalPictureInPictureClicked() {
viewModel.onLocalPictureInPictureClicked();
}
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.backup;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -126,7 +127,12 @@ public class BackupDialog {
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
fragment.startActivityForResult(intent, requestCode);
try {
fragment.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException e) {
Toast.makeText(fragment.requireContext(), R.string.BackupDialog_no_file_picker_available, Toast.LENGTH_LONG)
.show();
}
dialog.dismiss();
}))

View File

@@ -74,11 +74,7 @@ public class FullBackupExporter extends FullBackupBase {
OneTimePreKeyDatabase.TABLE_NAME,
SessionDatabase.TABLE_NAME,
SearchDatabase.SMS_FTS_TABLE_NAME,
SearchDatabase.MMS_FTS_TABLE_NAME,
JobDatabase.JOBS_TABLE_NAME,
JobDatabase.CONSTRAINTS_TABLE_NAME,
JobDatabase.DEPENDENCIES_TABLE_NAME,
KeyValueDatabase.TABLE_NAME
SearchDatabase.MMS_FTS_TABLE_NAME
);
public static void export(@NonNull Context context,

View File

@@ -81,8 +81,8 @@ public class ComposeText extends EmojiEditText {
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!TextUtils.isEmpty(hint)) {
if (!TextUtils.isEmpty(subHint)) {
@@ -92,6 +92,7 @@ public class ComposeText extends EmojiEditText {
} else {
setHint(ellipsizeToWidth(hint));
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

View File

@@ -6,7 +6,6 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
@@ -20,6 +19,7 @@ import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -185,20 +185,16 @@ public class ConversationItemFooter extends LinearLayout {
ApplicationContext.getInstance(getContext()).getExpiringMessageManager().checkSchedule();
}
} else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(getContext()).getExpiringMessageManager();
long id = messageRecord.getId();
boolean mms = messageRecord.isMms();
SignalExecutors.BOUNDED.execute(() -> {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(getContext()).getExpiringMessageManager();
long id = messageRecord.getId();
boolean mms = messageRecord.isMms();
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
});
}
} else {
this.timerView.setVisibility(View.GONE);

View File

@@ -8,6 +8,7 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -91,4 +92,18 @@ public class LabeledEditText extends FrameLayout implements View.OnFocusChangeLi
super.setEnabled(enabled);
input.setEnabled(enabled);
}
public void focusAndMoveCursorToEndAndOpenKeyboard() {
input.requestFocus();
int numberLength = getText().length();
input.setSelection(numberLength, numberLength);
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
if (!imm.isAcceptingText()) {
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
}
}
}

View File

@@ -23,7 +23,6 @@ import androidx.core.view.ViewCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ViewUtil;
public final class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
@@ -55,7 +54,7 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
floatingRecordButton = new FloatingRecordButton(getContext(), findViewById(R.id.quick_audio_fab));
lockDropTarget = new LockDropTarget (getContext(), findViewById(R.id.lock_drop_target));
View recordButton = ViewUtil.findById(this, R.id.quick_audio_toggle);
View recordButton = findViewById(R.id.quick_audio_toggle);
recordButton.setOnTouchListener(this);
}

View File

@@ -32,7 +32,6 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.ViewUtil;
public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks<Cursor> {
@@ -52,7 +51,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
inflate(context, R.layout.recent_photo_view, this);
this.recyclerView = ViewUtil.findById(this, R.id.photo_list);
this.recyclerView = findViewById(R.id.photo_list);
this.recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
this.recyclerView.setItemAnimator(new DefaultItemAnimator());
}
@@ -158,7 +157,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
RecentPhotoViewHolder(View itemView) {
super(itemView);
this.imageView = ViewUtil.findById(itemView, R.id.thumbnail);
this.imageView = itemView.findViewById(R.id.thumbnail);
}
}
}

View File

@@ -36,7 +36,6 @@ import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
public final class RecyclerViewFastScroller extends LinearLayout {
private static final int BUBBLE_ANIMATION_DURATION = 100;
@@ -75,8 +74,8 @@ public final class RecyclerViewFastScroller extends LinearLayout {
setClipChildren(false);
setScrollContainer(true);
inflate(context, R.layout.recycler_view_fast_scroller, this);
bubble = ViewUtil.findById(this, R.id.fastscroller_bubble);
handle = ViewUtil.findById(this, R.id.fastscroller_handle);
bubble = findViewById(R.id.fastscroller_bubble);
handle = findViewById(R.id.fastscroller_handle);
}
@Override

View File

@@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
public class ThreadPhotoRailView extends FrameLayout {
@@ -41,7 +40,7 @@ public class ThreadPhotoRailView extends FrameLayout {
inflate(context, R.layout.recipient_preference_photo_rail, this);
this.recyclerView = ViewUtil.findById(this, R.id.photo_list);
this.recyclerView = findViewById(R.id.photo_list);
this.recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
this.recyclerView.setItemAnimator(new DefaultItemAnimator());
this.recyclerView.setNestedScrollingEnabled(false);
@@ -112,7 +111,7 @@ public class ThreadPhotoRailView extends FrameLayout {
ThreadPhotoViewHolder(View itemView) {
super(itemView);
this.imageView = ViewUtil.findById(itemView, R.id.thumbnail);
this.imageView = itemView.findViewById(R.id.thumbnail);
}
}
}

View File

@@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Collections;
import java.util.HashMap;
@@ -61,16 +60,16 @@ public final class TransferControlView extends FrameLayout {
inflate(context, R.layout.transfer_controls_view, this);
setLongClickable(false);
ViewUtil.setBackground(this, ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
setBackground(ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
setVisibility(GONE);
setLayoutTransition(new LayoutTransition());
this.networkProgress = new HashMap<>();
this.compresssionProgress = new HashMap<>();
this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text);
this.progressWheel = findViewById(R.id.progress_wheel);
this.downloadDetails = findViewById(R.id.download_details);
this.downloadDetailsText = findViewById(R.id.download_details_text);
}
@Override

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
@@ -21,11 +22,9 @@ public class TypingStatusSender {
private static final long REFRESH_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private static final long PAUSE_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(3);
private final Context context;
private final Map<Long, TimerPair> selfTypingTimers;
public TypingStatusSender(@NonNull Context context) {
this.context = context;
public TypingStatusSender() {
this.selfTypingTimers = new HashMap<>();
}

View File

@@ -3,14 +3,11 @@ package org.thoughtcrime.securesms.components;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.Target;
@@ -29,6 +26,8 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.IOException;
import java.io.InputStream;
@@ -83,32 +82,27 @@ public class ZoomingImageView extends FrameLayout {
Log.i(TAG, "Max texture size: " + maxTextureSize);
new AsyncTask<Void, Void, Pair<Integer, Integer>>() {
@Override
protected @Nullable Pair<Integer, Integer> doInBackground(Void... params) {
if (MediaUtil.isGif(contentType)) return null;
SimpleTask.run(ViewUtil.getActivityLifecycle(this), () -> {
if (MediaUtil.isGif(contentType)) return null;
try {
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
return BitmapUtil.getDimensions(inputStream);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
return null;
}
try {
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
return BitmapUtil.getDimensions(inputStream);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
return null;
}
}, dimensions -> {
Log.i(TAG, "Dimensions: " + (dimensions == null ? "(null)" : dimensions.first + ", " + dimensions.second));
protected void onPostExecute(@Nullable Pair<Integer, Integer> dimensions) {
Log.i(TAG, "Dimensions: " + (dimensions == null ? "(null)" : dimensions.first + ", " + dimensions.second));
if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) {
Log.i(TAG, "Loading in standard image view...");
setImageViewUri(glideRequests, uri);
} else {
Log.i(TAG, "Loading in subsampling image view...");
setSubsamplingImageViewUri(uri);
}
if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) {
Log.i(TAG, "Loading in standard image view...");
setImageViewUri(glideRequests, uri);
} else {
Log.i(TAG, "Loading in subsampling image view...");
setSubsamplingImageViewUri(uri);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
}
private void setImageViewUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {

View File

@@ -39,6 +39,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
private final EmojiEventListener emojiEventListener;
private Controller controller;
private int currentPosition;
public EmojiKeyboardProvider(@NonNull Context context, @Nullable EmojiEventListener emojiEventListener) {
this.context = context;
@@ -66,11 +67,18 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
models.add(recentModel);
models.addAll(EmojiPages.DISPLAY_PAGES);
currentPosition = recentModel.getEmoji().size() > 0 ? 0 : 1;
}
@Override
public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) {
presenter.present(this, emojiPagerAdapter, this, this, null, null, recentModel.getEmoji().size() > 0 ? 0 : 1);
presenter.present(this, emojiPagerAdapter, this, this, null, null, currentPosition);
}
@Override
public void setCurrentPosition(int currentPosition) {
this.currentPosition = currentPosition;
}
@Override

View File

@@ -212,6 +212,7 @@ public class MediaKeyboard extends FrameLayout implements InputView,
public void onPageSelected(int i) {
categoryTabAdapter.setActivePosition(i);
categoryTabs.smoothScrollToPosition(i);
providers[providerIndex].setCurrentPosition(i);
}
@Override

View File

@@ -14,6 +14,7 @@ public interface MediaKeyboardProvider {
/** @return True if the click was handled with provider-specific logic, otherwise false */
void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider);
void setController(@Nullable Controller controller);
void setCurrentPosition(int currentPosition);
interface BackspaceObserver {
void onBackspaceClicked();

View File

@@ -5,7 +5,6 @@ import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
@@ -14,6 +13,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.IOException;
import java.io.InputStream;
@@ -56,16 +56,11 @@ public class EmojiPageBitmap {
return null;
};
task = new ListenableFutureTask<>(callable);
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
task.run();
return null;
}
@Override protected void onPostExecute(Void aVoid) {
task = null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
SimpleTask.run(() -> {
task.run();
return null;
},
unused -> task = null);
}
return task;
}

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.identity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
@@ -12,6 +11,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.util.List;
@@ -42,23 +42,15 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
public void onClick(DialogInterface dialog, int which) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
synchronized (SESSION_LOCK) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
}
SimpleTask.run(() -> {
synchronized (SESSION_LOCK) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
resendListener.onResendMessage();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return null;
}, unused -> resendListener.onResendMessage());
}
public interface ResendListener {

View File

@@ -17,7 +17,6 @@ import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
@@ -53,9 +52,9 @@ public class UnverifiedBannerView extends LinearLayout {
private void initialize() {
LayoutInflater.from(getContext()).inflate(R.layout.unverified_banner_view, this, true);
this.container = ViewUtil.findById(this, R.id.container);
this.text = ViewUtil.findById(this, R.id.unverified_text);
this.closeButton = ViewUtil.findById(this, R.id.cancel);
this.container = findViewById(R.id.container);
this.text = findViewById(R.id.unverified_text);
this.closeButton = findViewById(R.id.cancel);
}
public void display(@NonNull final String text,

View File

@@ -18,7 +18,6 @@ import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.MarkerOptions;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
@@ -47,9 +46,9 @@ public class SignalMapView extends LinearLayout {
setOrientation(LinearLayout.VERTICAL);
LayoutInflater.from(context).inflate(R.layout.signal_map_view, this, true);
this.mapView = ViewUtil.findById(this, R.id.map_view);
this.imageView = ViewUtil.findById(this, R.id.image_view);
this.textView = ViewUtil.findById(this, R.id.address_view);
this.mapView = findViewById(R.id.map_view);
this.imageView = findViewById(R.id.image_view);
this.textView = findViewById(R.id.address_view);
}
public ListenableFuture<Bitmap> display(final SignalPlace place) {

View File

@@ -1,46 +0,0 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import android.view.View;
import android.view.View.OnClickListener;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.SmsUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
public class DefaultSmsReminder extends Reminder {
public DefaultSmsReminder(@NonNull Fragment fragment, short requestCode) {
super(fragment.getString(R.string.reminder_header_sms_default_title),
fragment.getString(R.string.reminder_header_sms_default_text));
final OnClickListener okListener = new OnClickListener() {
@Override
public void onClick(View v) {
TextSecurePreferences.setPromptedDefaultSmsProvider(fragment.requireContext(), true);
fragment.startActivityForResult(SmsUtil.getSmsRoleIntent(fragment.requireContext()), requestCode);
}
};
final OnClickListener dismissListener = new OnClickListener() {
@Override
public void onClick(View v) {
TextSecurePreferences.setPromptedDefaultSmsProvider(fragment.requireContext(), true);
}
};
setOkListener(okListener);
setDismissListener(dismissListener);
}
public static boolean isEligible(Context context) {
final boolean isDefault = Util.isDefaultSmsProvider(context);
if (isDefault) {
TextSecurePreferences.setPromptedDefaultSmsProvider(context, false);
}
return !isDefault && !TextSecurePreferences.hasPromptedDefaultSmsProvider(context);
}
}

View File

@@ -14,6 +14,7 @@ import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
@@ -78,6 +79,7 @@ public final class ReminderView extends FrameLayout {
}
text.setText(reminder.getText());
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_button_primary_text));
switch (reminder.getImportance()) {
case NORMAL:
@@ -85,6 +87,7 @@ public final class ReminderView extends FrameLayout {
break;
case ERROR:
container.setBackgroundResource(R.drawable.reminder_background_error);
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
break;
case TERMINAL:
container.setBackgroundResource(R.drawable.reminder_background_terminal);

View File

@@ -1,51 +0,0 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.view.View;
import android.view.View.OnClickListener;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.InviteActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class ShareReminder extends Reminder {
public ShareReminder(final @NonNull Context context) {
super(context.getString(R.string.reminder_header_share_title),
context.getString(R.string.reminder_header_share_text));
setDismissListener(new OnClickListener() {
@Override public void onClick(View v) {
TextSecurePreferences.setPromptedShare(context, true);
}
});
setOkListener(new OnClickListener() {
@Override public void onClick(View v) {
TextSecurePreferences.setPromptedShare(context, true);
context.startActivity(new Intent(context, InviteActivity.class));
}
});
}
public static boolean isEligible(final @NonNull Context context) {
if (!TextSecurePreferences.isPushRegistered(context) ||
TextSecurePreferences.hasPromptedShare(context))
{
return false;
}
Cursor cursor = null;
try {
cursor = DatabaseFactory.getThreadDatabase(context).getConversationList();
return cursor.getCount() >= 1;
} finally {
if (cursor != null) cursor.close();
}
}
}

View File

@@ -22,7 +22,7 @@ public class SystemSmsImportReminder extends Reminder {
context.startService(intent);
// TODO [greyson] Navigation
Intent nextIntent = new Intent(context, MainActivity.class);
Intent nextIntent = MainActivity.clearTop(context);
Intent activityIntent = new Intent(context, DatabaseMigrationActivity.class);
activityIntent.putExtra("next_intent", nextIntent);
context.startActivity(activityIntent);

View File

@@ -5,6 +5,7 @@ import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
@@ -208,17 +209,20 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
private static class ProgressEventHandler extends Handler {
private final MediaControllerCompat mediaController;
private final MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState;
private final MediaControllerCompat mediaController;
private final MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState;
private ProgressEventHandler(@NonNull MediaControllerCompat mediaController,
@NonNull MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState) {
@NonNull MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState)
{
super(Looper.getMainLooper());
this.mediaController = mediaController;
this.voiceNotePlaybackState = voiceNotePlaybackState;
}
@Override
public void handleMessage(Message msg) {
public void handleMessage(@NonNull Message msg) {
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
if (isPlayerActive(mediaController.getPlaybackState()) &&
mediaMetadataCompat != null &&

View File

@@ -64,7 +64,6 @@ final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.V
static class ViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener {
private final TextView textView;
private final RadioButton radioButton;
private final Consumer<Integer> onPressed;
@@ -72,16 +71,14 @@ final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.V
public ViewHolder(@NonNull View itemView, @NonNull Consumer<Integer> onPressed) {
super(itemView);
this.textView = itemView.findViewById(R.id.text);
this.radioButton = itemView.findViewById(R.id.radio);
this.onPressed = onPressed;
}
@CallSuper
void bind(@NonNull WebRtcAudioOutput audioOutput, @Nullable WebRtcAudioOutput selected) {
textView.setText(audioOutput.getLabelRes());
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
radioButton.setText(audioOutput.getLabelRes());
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
radioButton.setOnCheckedChangeListener(null);
radioButton.setChecked(audioOutput == selected);
radioButton.setOnCheckedChangeListener(this);

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
@@ -21,19 +22,19 @@ import java.util.Set;
*/
public final class CallParticipantListUpdate {
private final Set<Holder> added;
private final Set<Holder> removed;
private final Set<Wrapper> added;
private final Set<Wrapper> removed;
CallParticipantListUpdate(@NonNull Set<Holder> added, @NonNull Set<Holder> removed) {
CallParticipantListUpdate(@NonNull Set<Wrapper> added, @NonNull Set<Wrapper> removed) {
this.added = added;
this.removed = removed;
}
public @NonNull Set<Holder> getAdded() {
public @NonNull Set<Wrapper> getAdded() {
return added;
}
public @NonNull Set<Holder> getRemoved() {
public @NonNull Set<Wrapper> getRemoved() {
return removed;
}
@@ -68,66 +69,47 @@ public final class CallParticipantListUpdate {
public static @NonNull CallParticipantListUpdate computeDeltaUpdate(@NonNull List<CallParticipant> oldList,
@NonNull List<CallParticipant> newList)
{
Set<CallParticipantId> primaries = getPrimaries(oldList, newList);
Set<CallParticipantListUpdate.Holder> oldParticipants = Stream.of(oldList)
Set<CallParticipantListUpdate.Wrapper> oldParticipants = Stream.of(oldList)
.filter(p -> p.getCallParticipantId().getDemuxId() != CallParticipantId.DEFAULT_ID)
.map(p -> createHolder(p, primaries.contains(p.getCallParticipantId())))
.map(CallParticipantListUpdate::createWrapper)
.collect(Collectors.toSet());
Set<CallParticipantListUpdate.Holder> newParticipants = Stream.of(newList)
Set<CallParticipantListUpdate.Wrapper> newParticipants = Stream.of(newList)
.filter(p -> p.getCallParticipantId().getDemuxId() != CallParticipantId.DEFAULT_ID)
.map(p -> createHolder(p, primaries.contains(p.getCallParticipantId())))
.map(CallParticipantListUpdate::createWrapper)
.collect(Collectors.toSet());
Set<CallParticipantListUpdate.Holder> added = SetUtil.difference(newParticipants, oldParticipants);
Set<CallParticipantListUpdate.Holder> removed = SetUtil.difference(oldParticipants, newParticipants);
Set<CallParticipantListUpdate.Wrapper> added = SetUtil.difference(newParticipants, oldParticipants);
Set<CallParticipantListUpdate.Wrapper> removed = SetUtil.difference(oldParticipants, newParticipants);
return new CallParticipantListUpdate(added, removed);
}
static Holder createHolder(@NonNull CallParticipant callParticipant, boolean isPrimary) {
return new Holder(callParticipant.getCallParticipantId(), callParticipant.getRecipient(), isPrimary);
@VisibleForTesting
static Wrapper createWrapper(@NonNull CallParticipant callParticipant) {
return new Wrapper(callParticipant);
}
private static @NonNull Set<CallParticipantId> getPrimaries(@NonNull List<CallParticipant> oldList, @NonNull List<CallParticipant> newList) {
return Stream.concat(Stream.of(oldList), Stream.of(newList))
.map(CallParticipant::getCallParticipantId)
.distinctBy(CallParticipantId::getRecipientId)
.collect(Collectors.toSet());
}
static final class Wrapper {
private final CallParticipant callParticipant;
static final class Holder {
private final CallParticipantId callParticipantId;
private final Recipient recipient;
private final boolean isPrimary;
private Holder(@NonNull CallParticipantId callParticipantId, @NonNull Recipient recipient, boolean isPrimary) {
this.callParticipantId = callParticipantId;
this.recipient = recipient;
this.isPrimary = isPrimary;
private Wrapper(@NonNull CallParticipant callParticipant) {
this.callParticipant = callParticipant;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
/**
* Denotes whether this was the first detected instance of this recipient when generating an update. See
* {@link CallParticipantListUpdate#computeDeltaUpdate(List, List)}
*/
public boolean isPrimary() {
return isPrimary;
public @NonNull CallParticipant getCallParticipant() {
return callParticipant;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Holder holder = (Holder) o;
return callParticipantId.equals(holder.callParticipantId);
Wrapper wrapper = (Wrapper) o;
return callParticipant.getCallParticipantId().equals(wrapper.callParticipant.getCallParticipantId());
}
@Override
public int hashCode() {
return Objects.hash(callParticipantId);
return Objects.hash(callParticipant.getCallParticipantId());
}
}
}

View File

@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon;
@@ -49,6 +50,7 @@ public class CallParticipantView extends ConstraintLayout {
private RecipientId recipientId;
private boolean infoMode;
private Runnable missingMediaKeysUpdater;
private AppCompatImageView backgroundAvatar;
private AvatarImageView avatar;
@@ -77,6 +79,7 @@ public class CallParticipantView extends ConstraintLayout {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
backgroundAvatar = findViewById(R.id.call_participant_background_avatar);
avatar = findViewById(R.id.call_participant_item_avatar);
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
@@ -102,7 +105,7 @@ public class CallParticipantView extends ConstraintLayout {
void setCallParticipant(@NonNull CallParticipant participant) {
boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId());
recipientId = participant.getRecipient().getId();
infoMode = participant.getRecipient().isBlocked() || (!participant.isMediaKeysReceived() && (System.currentTimeMillis() - participant.getAddedToCallTime()) > DELAY_SHOWING_MISSING_MEDIA_KEYS);
infoMode = participant.getRecipient().isBlocked() || isMissingMediaKeys(participant);
if (infoMode) {
renderer.setVisibility(View.GONE);
@@ -149,6 +152,28 @@ public class CallParticipantView extends ConstraintLayout {
}
}
private boolean isMissingMediaKeys(@NonNull CallParticipant participant) {
if (missingMediaKeysUpdater != null) {
Util.cancelRunnableOnMain(missingMediaKeysUpdater);
missingMediaKeysUpdater = null;
}
if (!participant.isMediaKeysReceived()) {
long time = System.currentTimeMillis() - participant.getAddedToCallTime();
if (time > DELAY_SHOWING_MISSING_MEDIA_KEYS) {
return true;
} else {
missingMediaKeysUpdater = () -> {
if (recipientId.equals(participant.getRecipient().getId())) {
setCallParticipant(participant);
}
};
Util.runOnMainDelayed(missingMediaKeysUpdater, DELAY_SHOWING_MISSING_MEDIA_KEYS - time);
}
}
return false;
}
void setRenderInPip(boolean shouldRenderInPip) {
if (infoMode) {
infoMessage.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);

View File

@@ -31,8 +31,8 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
private final AvatarImageView avatarImageView;
private final TextView descriptionTextView;
private final Set<CallParticipantListUpdate.Holder> pendingAdditions = new HashSet<>();
private final Set<CallParticipantListUpdate.Holder> pendingRemovals = new HashSet<>();
private final Set<CallParticipantListUpdate.Wrapper> pendingAdditions = new HashSet<>();
private final Set<CallParticipantListUpdate.Wrapper> pendingRemovals = new HashSet<>();
private boolean isEnabled = true;
@@ -112,18 +112,18 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
avatarImageView.setVisibility(recipient == null ? View.GONE : View.VISIBLE);
}
private void setDescription(@NonNull Set<CallParticipantListUpdate.Holder> holders, boolean isAdded) {
if (holders.isEmpty()) {
private void setDescription(@NonNull Set<CallParticipantListUpdate.Wrapper> wrappers, boolean isAdded) {
if (wrappers.isEmpty()) {
descriptionTextView.setText("");
} else {
setDescriptionForRecipients(holders, isAdded);
setDescriptionForRecipients(wrappers, isAdded);
}
}
private void setDescriptionForRecipients(@NonNull Set<CallParticipantListUpdate.Holder> recipients, boolean isAdded) {
Iterator<CallParticipantListUpdate.Holder> iterator = recipients.iterator();
Context context = getContentView().getContext();
String description;
private void setDescriptionForRecipients(@NonNull Set<CallParticipantListUpdate.Wrapper> recipients, boolean isAdded) {
Iterator<CallParticipantListUpdate.Wrapper> iterator = recipients.iterator();
Context context = getContentView().getContext();
String description;
switch (recipients.size()) {
case 0:
@@ -144,22 +144,14 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
descriptionTextView.setText(description);
}
private @NonNull Recipient getNextRecipient(@NonNull Iterator<CallParticipantListUpdate.Holder> holderIterator) {
return holderIterator.next().getRecipient();
private @NonNull Recipient getNextRecipient(@NonNull Iterator<CallParticipantListUpdate.Wrapper> wrapperIterator) {
return wrapperIterator.next().getCallParticipant().getRecipient();
}
private @NonNull String getNextDisplayName(@NonNull Iterator<CallParticipantListUpdate.Holder> holderIterator) {
CallParticipantListUpdate.Holder holder = holderIterator.next();
Recipient recipient = holder.getRecipient();
private @NonNull String getNextDisplayName(@NonNull Iterator<CallParticipantListUpdate.Wrapper> wrapperIterator) {
CallParticipantListUpdate.Wrapper wrapper = wrapperIterator.next();
if (recipient.isSelf()) {
return getContentView().getContext().getString(R.string.CallParticipantsListUpdatePopupWindow__you_on_another_device);
} else if (holder.isPrimary()) {
return recipient.getDisplayName(getContentView().getContext());
} else {
return getContentView().getContext().getString(R.string.CallParticipantsListUpdatePopupWindow__s_on_another_device,
recipient.getDisplayName(getContentView().getContext()));
}
return wrapper.getCallParticipant().getRecipientDisplayName(getContentView().getContext());
}
private static @StringRes int getOneMemberDescriptionResourceId(boolean isAdded) {

View File

@@ -6,12 +6,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.ComparatorCompat;
import com.annimon.stream.OptionalLong;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection;
import java.util.ArrayList;
import java.util.Collections;
@@ -28,36 +30,36 @@ public final class CallParticipantsState {
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
WebRtcViewModel.GroupCallState.IDLE,
Collections.emptyList(),
new ParticipantCollection(SMALL_GROUP_MAX),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
WebRtcLocalRenderState.GONE,
false,
false,
false,
0);
OptionalLong.empty());
private final WebRtcViewModel.State callState;
private final WebRtcViewModel.GroupCallState groupCallState;
private final List<CallParticipant> remoteParticipants;
private final ParticipantCollection remoteParticipants;
private final CallParticipant localParticipant;
private final CallParticipant focusedParticipant;
private final WebRtcLocalRenderState localRenderState;
private final boolean isInPipMode;
private final boolean showVideoForOutgoing;
private final boolean isViewingFocusedParticipant;
private final long remoteDevicesCount;
private final OptionalLong remoteDevicesCount;
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
@NonNull WebRtcViewModel.GroupCallState groupCallState,
@NonNull List<CallParticipant> remoteParticipants,
@NonNull ParticipantCollection remoteParticipants,
@NonNull CallParticipant localParticipant,
@Nullable CallParticipant focusedParticipant,
@NonNull WebRtcLocalRenderState localRenderState,
boolean isInPipMode,
boolean showVideoForOutgoing,
boolean isViewingFocusedParticipant,
long remoteDevicesCount)
OptionalLong remoteDevicesCount)
{
this.callState = callState;
this.groupCallState = groupCallState;
@@ -80,11 +82,7 @@ public final class CallParticipantsState {
}
public @NonNull List<CallParticipant> getGridParticipants() {
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
} else {
return getAllRemoteParticipants();
}
return remoteParticipants.getGridParticipants();
}
public @NonNull List<CallParticipant> getListParticipants() {
@@ -93,14 +91,11 @@ public final class CallParticipantsState {
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
listParticipants.addAll(getAllRemoteParticipants());
listParticipants.remove(focusedParticipant);
} else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size()));
} else {
return Collections.emptyList();
listParticipants.addAll(remoteParticipants.getListParticipants());
}
listParticipants.add(CallParticipant.EMPTY);
Collections.reverse(listParticipants);
return listParticipants;
@@ -112,26 +107,26 @@ public final class CallParticipantsState {
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
case 1:
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getRecipient().getShortDisplayName(context));
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getShortRecipientDisplayName(context));
} else {
return remoteParticipants.get(0).getRecipient().getDisplayName(context);
return remoteParticipants.get(0).getRecipientDisplayName(context);
}
case 2:
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
remoteParticipants.get(1).getRecipient().getShortDisplayName(context));
remoteParticipants.get(0).getShortRecipientDisplayName(context),
remoteParticipants.get(1).getShortRecipientDisplayName(context));
default:
int others = remoteParticipants.size() - 2;
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
others,
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
remoteParticipants.get(1).getRecipient().getShortDisplayName(context),
remoteParticipants.get(0).getShortRecipientDisplayName(context),
remoteParticipants.get(1).getShortRecipientDisplayName(context),
others);
}
}
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
return remoteParticipants;
return remoteParticipants.getAllParticipants();
}
public @NonNull CallParticipant getLocalParticipant() {
@@ -158,10 +153,17 @@ public final class CallParticipantsState {
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
}
public long getRemoteDevicesCount() {
public @NonNull OptionalLong getRemoteDevicesCount() {
return remoteDevicesCount;
}
public @NonNull OptionalLong getParticipantCount() {
boolean includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
return remoteDevicesCount.map(l -> l + (includeSelf ? 1L : 0L))
.or(() -> includeSelf ? OptionalLong.of(1L) : OptionalLong.empty());
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
@NonNull WebRtcViewModel webRtcViewModel,
boolean enableVideo)
@@ -179,7 +181,8 @@ public final class CallParticipantsState {
webRtcViewModel.getGroupState().isNotIdle(),
webRtcViewModel.getState(),
webRtcViewModel.getRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
oldState.isViewingFocusedParticipant,
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(webRtcViewModel.getRemoteParticipants());
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
@@ -188,7 +191,7 @@ public final class CallParticipantsState {
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),
webRtcViewModel.getRemoteParticipants(),
oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()),
webRtcViewModel.getLocalParticipant(),
focused,
localRenderState,
@@ -205,7 +208,8 @@ public final class CallParticipantsState {
oldState.getGroupCallState().isNotIdle(),
oldState.callState,
oldState.getAllRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
oldState.isViewingFocusedParticipant,
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
@@ -221,6 +225,28 @@ public final class CallParticipantsState {
oldState.remoteDevicesCount);
}
public static @NonNull CallParticipantsState setExpanded(@NonNull CallParticipantsState oldState, boolean expanded) {
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
oldState.getGroupCallState().isNotIdle(),
oldState.callState,
oldState.getAllRemoteParticipants().size(),
oldState.isViewingFocusedParticipant,
expanded);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
oldState.remoteParticipants,
oldState.localParticipant,
oldState.focusedParticipant,
localRenderState,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
oldState.isViewingFocusedParticipant,
oldState.remoteDevicesCount);
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
@@ -230,7 +256,8 @@ public final class CallParticipantsState {
oldState.getGroupCallState().isNotIdle(),
oldState.callState,
oldState.getAllRemoteParticipants().size(),
selectedPage == SelectedPage.FOCUSED);
selectedPage == SelectedPage.FOCUSED,
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
@@ -250,12 +277,15 @@ public final class CallParticipantsState {
boolean isNonIdleGroupCall,
@NonNull WebRtcViewModel.State callState,
int numberOfRemoteParticipants,
boolean isViewingFocusedParticipant)
boolean isViewingFocusedParticipant,
boolean isExpanded)
{
boolean displayLocal = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled());
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
if (displayLocal || showVideoForOutgoing) {
if (isExpanded && (localParticipant.isVideoEnabled() || isNonIdleGroupCall)) {
return WebRtcLocalRenderState.EXPANDED;
} else if (displayLocal || showVideoForOutgoing) {
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;

View File

@@ -0,0 +1,197 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
/**
* Helps manage the expansion and shrinking of the in-app pip.
*/
@MainThread
final class PictureInPictureExpansionHelper {
private State state = State.IS_SHRUNKEN;
public boolean isExpandedOrExpanding() {
return state == State.IS_EXPANDED || state == State.IS_EXPANDING;
}
public boolean isShrunkenOrShrinking() {
return state == State.IS_SHRUNKEN || state == State.IS_SHRINKING;
}
public void expand(@NonNull View toExpand, @NonNull Callback callback) {
if (isExpandedOrExpanding()) {
return;
}
performExpandAnimation(toExpand, new Callback() {
@Override
public void onAnimationWillStart() {
state = State.IS_EXPANDING;
callback.onAnimationWillStart();
}
@Override
public void onPictureInPictureExpanded() {
callback.onPictureInPictureExpanded();
}
@Override
public void onPictureInPictureNotVisible() {
callback.onPictureInPictureNotVisible();
}
@Override
public void onAnimationHasFinished() {
state = State.IS_EXPANDED;
callback.onAnimationHasFinished();
}
});
}
public void shrink(@NonNull View toExpand, @NonNull Callback callback) {
if (isShrunkenOrShrinking()) {
return;
}
performShrinkAnimation(toExpand, new Callback() {
@Override
public void onAnimationWillStart() {
state = State.IS_SHRINKING;
callback.onAnimationWillStart();
}
@Override
public void onPictureInPictureExpanded() {
callback.onPictureInPictureExpanded();
}
@Override
public void onPictureInPictureNotVisible() {
callback.onPictureInPictureNotVisible();
}
@Override
public void onAnimationHasFinished() {
state = State.IS_SHRUNKEN;
callback.onAnimationHasFinished();
}
});
}
private void performExpandAnimation(@NonNull View target, @NonNull Callback callback) {
ViewGroup parent = (ViewGroup) target.getParent();
float x = target.getX();
float y = target.getY();
float scaleX = parent.getMeasuredWidth() / (float) target.getMeasuredWidth();
float scaleY = parent.getMeasuredHeight() / (float) target.getMeasuredHeight();
float scale = Math.max(scaleX, scaleY);
callback.onAnimationWillStart();
target.animate()
.setDuration(200)
.x((parent.getMeasuredWidth() - target.getMeasuredWidth()) / 2f)
.y((parent.getMeasuredHeight() - target.getMeasuredHeight()) / 2f)
.scaleX(scale)
.scaleY(scale)
.withEndAction(() -> {
callback.onPictureInPictureExpanded();
target.animate()
.setDuration(100)
.alpha(0f)
.withEndAction(() -> {
callback.onPictureInPictureNotVisible();
target.setX(x);
target.setY(y);
target.setScaleX(0f);
target.setScaleY(0f);
target.setAlpha(1f);
target.animate()
.setDuration(200)
.scaleX(1f)
.scaleY(1f)
.withEndAction(callback::onAnimationHasFinished);
});
});
}
private void performShrinkAnimation(@NonNull View target, @NonNull Callback callback) {
ViewGroup parent = (ViewGroup) target.getParent();
float x = target.getX();
float y = target.getY();
float scaleX = parent.getMeasuredWidth() / (float) target.getMeasuredWidth();
float scaleY = parent.getMeasuredHeight() / (float) target.getMeasuredHeight();
float scale = Math.max(scaleX, scaleY);
callback.onAnimationWillStart();
target.animate()
.setDuration(200)
.scaleX(0f)
.scaleY(0f)
.withEndAction(() -> {
target.setX((parent.getMeasuredWidth() - target.getMeasuredWidth()) / 2f);
target.setY((parent.getMeasuredHeight() - target.getMeasuredHeight()) / 2f);
target.setAlpha(0f);
target.setScaleX(scale);
target.setScaleY(scale);
callback.onPictureInPictureNotVisible();
target.animate()
.setDuration(100)
.alpha(1f)
.withEndAction(() -> {
callback.onPictureInPictureExpanded();
target.animate()
.scaleX(1f)
.scaleY(1f)
.x(x)
.y(y)
.withEndAction(callback::onAnimationHasFinished);
});
});
}
enum State {
IS_EXPANDING,
IS_EXPANDED,
IS_SHRINKING,
IS_SHRUNKEN
}
public interface Callback {
/**
* Called when an animation (shrink or expand) will begin. This happens before any animation
* is executed.
*/
void onAnimationWillStart();
/**
* Called when the PiP is covering the whole screen. This is when any staging / teardown of the
* large local renderer should occur.
*/
void onPictureInPictureExpanded();
/**
* Called when the PiP is not visible on the screen anymore. This is when any staging / teardown
* of the pip should occur.
*/
void onPictureInPictureNotVisible();
/**
* Called when the animation is complete. Useful for e.g. adjusting the pip's final location to
* make sure it is respecting the screen space available.
*/
void onAnimationHasFinished();
}
}

View File

@@ -222,8 +222,9 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
@Override
public boolean onSingleTapUp(MotionEvent e) {
child.performClick();
isDragging = false;
child.performClick();
return true;
}

View File

@@ -1,13 +1,24 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -95,11 +106,14 @@ public class WebRtcCallView extends FrameLayout {
private TextView participantCount;
private Stub<FrameLayout> groupCallSpeakerHint;
private Stub<View> groupCallFullStub;
private View errorButton;
private int pagerBottomMarginDp;
private boolean controlsVisible = true;
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
private PictureInPictureExpansionHelper pictureInPictureExpansionHelper;
private final Set<View> incomingCallViews = new HashSet<>();
private final Set<View> topViews = new HashSet<>();
@@ -150,6 +164,7 @@ public class WebRtcCallView extends FrameLayout {
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
toolbar = findViewById(R.id.call_screen_toolbar);
startCall = findViewById(R.id.call_screen_start_call_start_call);
errorButton = findViewById(R.id.call_screen_error_cancel);
groupCallSpeakerHint = new Stub<>(findViewById(R.id.call_screen_group_call_speaker_hint));
groupCallFullStub = new Stub<>(findViewById(R.id.group_call_call_full_view));
@@ -157,7 +172,6 @@ public class WebRtcCallView extends FrameLayout {
View decline = findViewById(R.id.call_screen_decline_call);
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
@@ -209,7 +223,14 @@ public class WebRtcCallView extends FrameLayout {
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
pictureInPictureExpansionHelper = new PictureInPictureExpansionHelper();
smallLocalRenderFrame.setOnClickListener(v -> {
if (controlsListener != null) {
controlsListener.onLocalPictureInPictureClicked();
}
});
startCall.setOnClickListener(v -> {
if (controlsListener != null) {
@@ -224,8 +245,11 @@ public class WebRtcCallView extends FrameLayout {
largeLocalRenderNoVideoAvatar.setAlpha(0.6f);
largeLocalRenderNoVideoAvatar.setColorFilter(new ColorMatrixColorFilter(greyScaleMatrix));
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
statusBarGuideline.setGuidelineBegin(statusBarHeight);
errorButton.setOnClickListener(v -> {
if (controlsListener != null) {
controlsListener.onCancelStartCall();
}
});
}
@Override
@@ -237,6 +261,26 @@ public class WebRtcCallView extends FrameLayout {
}
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
Guideline navigationBarGuideline = findViewById(R.id.call_screen_navigation_bar_guideline);
statusBarGuideline.setGuidelineBegin(insets.top);
navigationBarGuideline.setGuidelineEnd(insets.bottom);
return true;
}
@Override
public void onWindowSystemUiVisibilityChanged(int visible) {
if ((visible & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
} else {
pictureInPictureGestureHelper.clearVerticalBoundaries();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
@@ -262,21 +306,21 @@ public class WebRtcCallView extends FrameLayout {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
}
if ((state.getGroupCallState().isNotIdle() && state.getRemoteDevicesCount() > 0) || state.getGroupCallState().isConnected()) {
if ((state.getGroupCallState().isNotIdle() && state.getRemoteDevicesCount().orElse(0) > 0) || state.getGroupCallState().isConnected()) {
recipientName.setText(state.getRemoteParticipantsDescription(getContext()));
} else if (state.getGroupCallState().isNotIdle()) {
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, Recipient.resolved(recipientId).getDisplayName(getContext())));
}
if (state.getGroupCallState().isNotIdle() && participantCount != null) {
boolean includeSelf = state.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
participantCount.setText(String.valueOf(state.getRemoteDevicesCount() + (includeSelf ? 1 : 0)));
participantCount.setText(state.getParticipantCount()
.mapToObj(String::valueOf).orElse("\u2014"));
participantCount.setEnabled(state.getParticipantCount().isPresent());
}
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant(), state.getFocusedParticipant());
if (state.isLargeVideoGroup() && !state.isInPipMode()) {
layoutParticipantsForLargeCount();
@@ -285,7 +329,7 @@ public class WebRtcCallView extends FrameLayout {
}
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
@@ -296,9 +340,18 @@ public class WebRtcCallView extends FrameLayout {
largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
}
smallLocalRender.setCallParticipant(localCallParticipant);
smallLocalRender.setRenderInPip(true);
videoToggle.setChecked(localCallParticipant.isVideoEnabled(), false);
smallLocalRender.setRenderInPip(true);
if (state == WebRtcLocalRenderState.EXPANDED) {
expandPip(localCallParticipant, focusedParticipant);
return;
} else if (state == WebRtcLocalRenderState.SMALL_RECTANGLE && pictureInPictureExpansionHelper.isExpandedOrExpanding()) {
shrinkPip(localCallParticipant);
return;
} else {
smallLocalRender.setCallParticipant(localCallParticipant);
}
switch (state) {
case GONE:
@@ -425,6 +478,11 @@ public class WebRtcCallView extends FrameLayout {
startCall.setEnabled(webRtcControls.isStartCallEnabled());
}
if (webRtcControls.displayErrorControls()) {
visibleViewSet.add(footerGradient);
visibleViewSet.add(errorButton);
}
if (webRtcControls.displayGroupCallFull()) {
groupCallFullStub.get().setVisibility(View.VISIBLE);
((TextView) groupCallFullStub.get().findViewById(R.id.group_call_call_full_message)).setText(webRtcControls.getGroupCallFullMessage(getContext()));
@@ -501,6 +559,10 @@ public class WebRtcCallView extends FrameLayout {
}
} else {
cancelFadeOut();
if (controlsListener != null) {
controlsListener.showSystemUI();
}
}
controls = webRtcControls;
@@ -525,6 +587,54 @@ public class WebRtcCallView extends FrameLayout {
}
}
private void expandPip(@NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
pictureInPictureExpansionHelper.expand(smallLocalRenderFrame, new PictureInPictureExpansionHelper.Callback() {
@Override
public void onAnimationWillStart() {
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
}
@Override
public void onPictureInPictureExpanded() {
largeLocalRenderFrame.setVisibility(View.VISIBLE);
}
@Override
public void onPictureInPictureNotVisible() {
smallLocalRender.setCallParticipant(focusedParticipant);
}
@Override
public void onAnimationHasFinished() {
pictureInPictureGestureHelper.adjustPip();
}
});
}
private void shrinkPip(@NonNull CallParticipant localCallParticipant) {
pictureInPictureExpansionHelper.shrink(smallLocalRenderFrame, new PictureInPictureExpansionHelper.Callback() {
@Override
public void onAnimationWillStart() {
}
@Override
public void onPictureInPictureExpanded() {
largeLocalRenderFrame.setVisibility(View.GONE);
largeLocalRender.attachBroadcastVideoSink(null);
}
@Override
public void onPictureInPictureNotVisible() {
smallLocalRender.setCallParticipant(localCallParticipant);
}
@Override
public void onAnimationHasFinished() {
pictureInPictureGestureHelper.adjustPip();
}
});
}
private void animatePipToLargeRectangle() {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
animation.setDuration(PIP_RESIZE_DURATION);
@@ -567,12 +677,10 @@ public class WebRtcCallView extends FrameLayout {
private void fadeOutControls() {
fadeControls(ConstraintSet.GONE);
controlsListener.onControlsFadeOut();
pictureInPictureGestureHelper.clearVerticalBoundaries();
}
private void fadeInControls() {
fadeControls(ConstraintSet.VISIBLE);
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
scheduleFadeOut();
}
@@ -616,6 +724,15 @@ public class WebRtcCallView extends FrameLayout {
.setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.endTransitions(parent);
if (controlsListener != null) {
if (controlsVisible) {
controlsListener.showSystemUI();
} else {
controlsListener.hideSystemUI();
}
}
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@@ -700,6 +817,8 @@ public class WebRtcCallView extends FrameLayout {
void onStartCall(boolean isVideoCall);
void onCancelStartCall();
void onControlsFadeOut();
void showSystemUI();
void hideSystemUI();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
void onVideoChanged(boolean isVideoEnabled);
void onMicChanged(boolean isMicEnabled);
@@ -710,5 +829,6 @@ public class WebRtcCallView extends FrameLayout {
void onAcceptCallPressed();
void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
void onLocalPictureInPictureClicked();
}
}

View File

@@ -56,8 +56,8 @@ public class WebRtcCallViewModel extends ViewModel {
private boolean answerWithVideoAvailable = false;
private Runnable elapsedTimeRunnable = this::handleTick;
private boolean canEnterPipMode = false;
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
private boolean callingStarted = false;
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
private boolean callStarting = false;
private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication());
@@ -113,8 +113,8 @@ public class WebRtcCallViewModel extends ViewModel {
return answerWithVideoAvailable;
}
public boolean isCallingStarted() {
return callingStarted;
public boolean isCallStarting() {
return callStarting;
}
@MainThread
@@ -135,13 +135,26 @@ public class WebRtcCallViewModel extends ViewModel {
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page));
}
public void onLocalPictureInPictureClicked() {
CallParticipantsState state = participantsState.getValue();
if (state.getGroupCallState() != WebRtcViewModel.GroupCallState.IDLE) {
return;
}
participantsState.setValue(CallParticipantsState.setExpanded(participantsState.getValue(),
state.getLocalRenderState() != WebRtcLocalRenderState.EXPANDED));
}
public void onDismissedVideoTooltip() {
canDisplayTooltipIfNeeded = false;
}
@MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
canEnterPipMode = webRtcViewModel.getState() != WebRtcViewModel.State.CALL_PRE_JOIN;
canEnterPipMode = !webRtcViewModel.getState().isPreJoinOrNetworkUnavailable();
if (callStarting && webRtcViewModel.getState().isPassedPreJoin()) {
callStarting = false;
}
CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
@@ -170,7 +183,7 @@ public class WebRtcCallViewModel extends ViewModel {
webRtcViewModel.isBluetoothAvailable(),
Util.hasItems(webRtcViewModel.getRemoteParticipants()),
repository.getAudioOutput(),
webRtcViewModel.getRemoteDevicesCount(),
webRtcViewModel.getRemoteDevicesCount().orElse(0),
webRtcViewModel.getParticipantLimit());
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
@@ -232,6 +245,9 @@ public class WebRtcCallViewModel extends ViewModel {
case CALL_DISCONNECTED:
callState = WebRtcControls.CallState.ENDING;
break;
case NETWORK_FAILURE:
callState = WebRtcControls.CallState.ERROR;
break;
default:
callState = WebRtcControls.CallState.ONGOING;
}
@@ -274,9 +290,9 @@ public class WebRtcCallViewModel extends ViewModel {
}
private boolean shouldShowSpeakerHint(@NonNull CallParticipantsState state) {
return !state.isInPipMode() &&
state.getRemoteDevicesCount() > 1 &&
state.getGroupCallState().isConnected() &&
return !state.isInPipMode() &&
state.getRemoteDevicesCount().orElse(0) > 1 &&
state.getGroupCallState().isConnected() &&
!SignalStore.tooltips().hasSeenGroupCallSpeakerView();
}
@@ -309,7 +325,7 @@ public class WebRtcCallViewModel extends ViewModel {
}
public void startCall(boolean isVideoCall) {
callingStarted = true;
callStarting = true;
Recipient recipient = getRecipient().get();
if (recipient.isGroup()) {
repository.getIdentityRecords(recipient, identityRecords -> {

View File

@@ -51,6 +51,10 @@ public final class WebRtcControls {
this.participantLimit = participantLimit;
}
boolean displayErrorControls() {
return isError();
}
boolean displayStartCallControls() {
return isPreJoin();
}
@@ -145,6 +149,10 @@ public final class WebRtcControls {
return audioOutput;
}
private boolean isError() {
return callState == CallState.ERROR;
}
private boolean isPreJoin() {
return callState == CallState.PRE_JOIN;
}
@@ -167,6 +175,7 @@ public final class WebRtcControls {
public enum CallState {
NONE,
ERROR,
PRE_JOIN,
INCOMING,
OUTGOING,

View File

@@ -5,5 +5,6 @@ public enum WebRtcLocalRenderState {
SMALL_RECTANGLE,
SMALLER_RECTANGLE,
LARGE,
LARGE_NO_VIDEO
LARGE_NO_VIDEO,
EXPANDED
}

View File

@@ -25,8 +25,7 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
@Override
public @NonNull String getName(@NonNull Context context) {
return callParticipant.getRecipient().isSelf() ? context.getString(R.string.GroupMembersDialog_you)
: super.getName(context);
return callParticipant.getRecipientDisplayName(context);
}
public int getVideoMutedVisibility() {
@@ -36,4 +35,16 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
public int getAudioMutedVisibility() {
return callParticipant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE;
}
@Override
public boolean areItemsTheSame(@NonNull CallParticipantViewState newItem) {
return callParticipant.getCallParticipantId().equals(newItem.callParticipant.getCallParticipantId());
}
@Override
public boolean areContentsTheSame(@NonNull CallParticipantViewState newItem) {
return super.areContentsTheSame(newItem) &&
callParticipant.isVideoEnabled() == newItem.callParticipant.isVideoEnabled() &&
callParticipant.isMicrophoneEnabled() == newItem.callParticipant.isMicrophoneEnabled();
}
}

View File

@@ -15,6 +15,7 @@ import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.OptionalLong;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
@@ -88,19 +89,21 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment {
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
List<MappingModel<?>> items = new ArrayList<>();
boolean includeSelf = callParticipantsState.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
boolean includeSelf = callParticipantsState.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
OptionalLong headerCount = callParticipantsState.getParticipantCount();
items.add(new CallParticipantsListHeader((int) callParticipantsState.getRemoteDevicesCount() + (includeSelf ? 1 : 0)));
headerCount.executeIfPresent(count -> {
items.add(new CallParticipantsListHeader((int) count));
if (includeSelf) {
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
}
if (includeSelf) {
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
}
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
items.add(new CallParticipantViewState(callParticipant));
}
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
items.add(new CallParticipantViewState(callParticipant));
}
});
adapter.submitList(items);
}
}

View File

@@ -43,7 +43,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
@@ -73,7 +72,6 @@ import java.util.concurrent.TimeoutException;
/**
* Manages all the stuff around determining if a user is registered or not.
*/
@Trace
public class DirectoryHelper {
private static final String TAG = Log.tag(DirectoryHelper.class);
@@ -254,6 +252,8 @@ public class DirectoryHelper {
stopwatch.split("handle-unlisted");
Set<RecipientId> preExistingRegisteredUsers = new HashSet<>(recipientDatabase.getRegistered());
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
stopwatch.split("update-registered");
@@ -267,14 +267,13 @@ public class DirectoryHelper {
}
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
Set<RecipientId> systemContacts = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyRegisteredSystemContacts = new HashSet<>(activeIds);
newlyActiveIds.removeAll(existingSignalIds);
newlyActiveIds.retainAll(existingSystemIds);
newlyRegisteredSystemContacts.removeAll(preExistingRegisteredUsers);
newlyRegisteredSystemContacts.retainAll(systemContacts);
notifyNewUsers(context, newlyActiveIds);
notifyNewUsers(context, newlyRegisteredSystemContacts);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
}
@@ -403,8 +402,11 @@ public class DirectoryHelper {
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isSelf()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) &&
!recipient.isSelf() &&
recipient.hasAUserSetDisplayName(context))
{
IncomingJoinedMessage message = new IncomingJoinedMessage(recipient.getId());
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
if (insertResult.isPresent()) {

View File

@@ -243,7 +243,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.BitmapUtil;
@@ -302,7 +301,6 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
* @author Moxie Marlinspike
*
*/
@Trace
@SuppressLint("StaticFieldLeak")
public class ConversationActivity extends PassphraseRequiredActivity
implements ConversationFragment.ConversationFragmentListener,
@@ -409,7 +407,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (ConversationIntents.isInvalid(getIntent())) {
Log.w(TAG, "[onCreate] Missing recipientId!");
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
startActivity(MainActivity.clearTop(this));
finish();
return;
}
@@ -488,7 +486,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (ConversationIntents.isInvalid(intent)) {
Log.w(TAG, "[onNewIntent] Missing recipientId!");
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
startActivity(MainActivity.clearTop(this));
finish();
return;
}
@@ -538,6 +536,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
.startChain(new RequestGroupV2InfoJob(groupId))
.then(new GroupV2UpdateSelfProfileKeyJob(groupId))
.enqueue();
if (viewModel.getArgs().isFirstTimeInSelfCreatedGroup()) {
groupViewModel.inviteFriendsOneTimeIfJustSelfInGroup(getSupportFragmentManager(), groupId);
}
}
if (groupCallViewModel != null) {
@@ -1087,6 +1089,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
final long thread = this.threadId;
ExpirationDialog.show(this, recipient.get().getExpireMessages(),
expirationTime ->
SimpleTask.run(
@@ -1102,7 +1106,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
} else {
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient.getId(), expirationTime);
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000L);
MessageSender.send(ConversationActivity.this, outgoingMessage, threadId, false, null);
MessageSender.send(ConversationActivity.this, outgoingMessage, thread, false, null);
}
return GroupChangeResult.SUCCESS;
},
@@ -1882,31 +1886,31 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void initializeViews() {
titleView = findViewById(R.id.conversation_title_view);
buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
sendButton = ViewUtil.findById(this, R.id.send_button);
attachButton = ViewUtil.findById(this, R.id.attach_button);
composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
charactersLeft = ViewUtil.findById(this, R.id.space_left);
buttonToggle = findViewById(R.id.button_toggle);
sendButton = findViewById(R.id.send_button);
attachButton = findViewById(R.id.attach_button);
composeText = findViewById(R.id.embedded_text_editor);
charactersLeft = findViewById(R.id.space_left);
emojiDrawerStub = ViewUtil.findStubById(this, R.id.emoji_drawer_stub);
attachmentKeyboardStub = ViewUtil.findStubById(this, R.id.attachment_keyboard_stub);
unblockButton = ViewUtil.findById(this, R.id.unblock_button);
makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button);
registerButton = ViewUtil.findById(this, R.id.register_button);
container = ViewUtil.findById(this, R.id.layout_container);
unblockButton = findViewById(R.id.unblock_button);
makeDefaultSmsButton = findViewById(R.id.make_default_sms_button);
registerButton = findViewById(R.id.register_button);
container = findViewById(R.id.layout_container);
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
reviewBanner = ViewUtil.findStubById(this, R.id.review_banner_stub);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
panelParent = ViewUtil.findById(this, R.id.conversation_activity_panel_parent);
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
messageRequestBottomView = ViewUtil.findById(this, R.id.conversation_activity_message_request_bottom_bar);
reactionOverlay = ViewUtil.findById(this, R.id.conversation_reaction_scrubber);
quickAttachmentToggle = findViewById(R.id.quick_attachment_toggle);
inlineAttachmentToggle = findViewById(R.id.inline_attachment_container);
inputPanel = findViewById(R.id.bottom_panel);
panelParent = findViewById(R.id.conversation_activity_panel_parent);
searchNav = findViewById(R.id.conversation_search_nav);
messageRequestBottomView = findViewById(R.id.conversation_activity_message_request_bottom_bar);
reactionOverlay = findViewById(R.id.conversation_reaction_scrubber);
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
ImageButton quickCameraToggle = findViewById(R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = findViewById(R.id.inline_attachment_button);
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
@@ -1982,7 +1986,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isInBubble()) {
supportActionBar.setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_notification));
toolbar.setNavigationOnClickListener(unused -> startActivity(new Intent(Intent.ACTION_MAIN).setClass(this, MainActivity.class)));
toolbar.setNavigationOnClickListener(unused -> startActivity(MainActivity.clearTop(this)));
}
}
@@ -2675,13 +2679,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private void sendMediaMessage(@NonNull MediaSendActivityResult result) {
long thread = this.threadId;
long expiresIn = recipient.get().getExpireMessages() * 1000L;
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
List<Mention> mentions = new ArrayList<>(result.getMentions());
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList(), mentions);
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
inputPanel.clearQuote();
attachmentManager.clear(glideRequests, false);
@@ -2690,7 +2695,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
long id = fragment.stageOutgoingMessage(message);
SimpleTask.run(() -> {
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), threadId, () -> fragment.releaseOutgoingMessage(id));
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), thread, () -> fragment.releaseOutgoingMessage(id));
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
@@ -2735,6 +2740,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
return new SettableFuture<>(null);
}
final long thread = this.threadId;
if (isSecureText && !forceSms) {
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(this, body, sendButton.getSelectedTransport().calculateCharacters(body).maxPrimaryMessageSize);
body = splitMessage.getBody();
@@ -2753,7 +2760,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isSecureText && !forceSms) {
outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessageCandidate);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
} else {
outgoingMessage = outgoingMessageCandidate;
}
@@ -2772,7 +2779,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
final long id = fragment.stageOutgoingMessage(outgoingMessage);
SimpleTask.run(() -> {
return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
return MessageSender.send(context, outgoingMessage, thread, forceSms, () -> fragment.releaseOutgoingMessage(id));
}, result -> {
sendComplete(result);
future.set(null);
@@ -2792,6 +2799,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
final long thread = this.threadId;
final Context context = getApplicationContext();
final String messageBody = getMessage();
@@ -2799,7 +2807,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isSecureText && !forceSms) {
message = new OutgoingEncryptedMessage(recipient.get(), messageBody, expiresIn);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
} else {
message = new OutgoingTextMessage(recipient.get(), messageBody, expiresIn, subscriptionId);
}
@@ -2815,7 +2823,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
new AsyncTask<OutgoingTextMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingTextMessage... messages) {
return MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
return MessageSender.send(context, messages[0], thread, forceSms, () -> fragment.releaseOutgoingMessage(id));
}
@Override

View File

@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.ArrayList;
@@ -27,7 +26,6 @@ import java.util.Map;
/**
* Core data source for loading an individual conversation.
*/
@Trace
class ConversationDataSource implements PagedDataSource<ConversationMessage> {
private static final String TAG = Log.tag(ConversationDataSource.class);

View File

@@ -66,9 +66,11 @@ import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationScrollToView;
import org.thoughtcrime.securesms.components.ConversationTypingView;
@@ -93,6 +95,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
@@ -121,7 +124,6 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.HtmlUtil;
@@ -147,9 +149,9 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
@Trace
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends LoggingFragment {
private static final String TAG = ConversationFragment.class.getSimpleName();
@@ -1416,10 +1418,63 @@ public class ConversationFragment extends LoggingFragment {
GroupsV1MigrationInfoBottomSheetDialogFragment.show(requireFragmentManager(), membershipChange);
}
@Override
public void onDecryptionFailedLearnMoreClicked() {
new AlertDialog.Builder(requireContext())
.setView(R.layout.decryption_failed_dialog)
.setPositiveButton(android.R.string.ok, (d, w) -> {
d.dismiss();
})
.setNeutralButton(R.string.ConversationFragment_contact_us, (d, w) -> {
Intent intent = new Intent(requireContext(), ApplicationPreferencesActivity.class);
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_HELP_FRAGMENT, true);
startActivity(intent);
d.dismiss();
})
.show();
}
@Override
public void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient) {
if (recipient.isGroup()) {
throw new AssertionError("Must be individual");
}
AlertDialog dialog = new AlertDialog.Builder(requireContext())
.setView(R.layout.safety_number_changed_learn_more_dialog)
.setPositiveButton(R.string.ConversationFragment_verify, (d, w) -> {
SimpleTask.run(getLifecycle(), () -> {
return DatabaseFactory.getIdentityDatabase(requireContext()).getIdentity(recipient.getId());
}, identityRecord -> {
if (identityRecord.isPresent()) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord.get()));
}});
d.dismiss();
})
.setNegativeButton(R.string.ConversationFragment_not_now, (d, w) -> {
d.dismiss();
})
.create();
dialog.setOnShowListener(d -> {
TextView title = Objects.requireNonNull(dialog.findViewById(R.id.safety_number_learn_more_title));
TextView body = Objects.requireNonNull(dialog.findViewById(R.id.safety_number_learn_more_body));
title.setText(getString(R.string.ConversationFragment_your_safety_number_with_s_changed, recipient.getDisplayName(requireContext())));
body.setText(getString(R.string.ConversationFragment_your_safety_number_with_s_changed_likey_because_they_reinstalled_signal, recipient.getDisplayName(requireContext())));
});
dialog.show();
}
@Override
public void onJoinGroupCallClicked() {
CommunicationActions.startVideoCall(requireActivity(), recipient.get());
}
@Override
public void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId) {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(requireActivity().getSupportFragmentManager(), groupId);
}
}
@Override

View File

@@ -6,6 +6,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
@@ -25,12 +26,14 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException;
@@ -50,6 +53,8 @@ final class ConversationGroupViewModel extends ViewModel {
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
private final LiveData<Boolean> gv1MigrationReminder;
private boolean firstTimeInviteFriendsTriggered;
private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>();
@@ -225,6 +230,28 @@ final class ConversationGroupViewModel extends ViewModel {
});
}
void inviteFriendsOneTimeIfJustSelfInGroup(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
if (firstTimeInviteFriendsTriggered) {
return;
}
firstTimeInviteFriendsTriggered = true;
SimpleTask.run(() -> DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication())
.requireGroup(groupId)
.getMembers().equals(Collections.singletonList(Recipient.self().getId())),
justSelf -> {
if (justSelf) {
inviteFriends(supportFragmentManager, groupId);
}
}
);
}
void inviteFriends(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(supportFragmentManager, groupId);
}
static final class ReviewState {
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);

View File

@@ -18,15 +18,16 @@ import java.util.Objects;
public class ConversationIntents {
private static final String BUBBLE_AUTHORITY = "bubble";
private static final String EXTRA_RECIPIENT = "recipient_id";
private static final String EXTRA_THREAD_ID = "thread_id";
private static final String EXTRA_TEXT = "draft_text";
private static final String EXTRA_MEDIA = "media_list";
private static final String EXTRA_STICKER = "sticker_extra";
private static final String EXTRA_BORDERLESS = "borderless_extra";
private static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
private static final String EXTRA_STARTING_POSITION = "starting_position";
private static final String BUBBLE_AUTHORITY = "bubble";
private static final String EXTRA_RECIPIENT = "recipient_id";
private static final String EXTRA_THREAD_ID = "thread_id";
private static final String EXTRA_TEXT = "draft_text";
private static final String EXTRA_MEDIA = "media_list";
private static final String EXTRA_STICKER = "sticker_extra";
private static final String EXTRA_BORDERLESS = "borderless_extra";
private static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
private static final String EXTRA_STARTING_POSITION = "starting_position";
private static final String EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP = "first_time_in_group";
private ConversationIntents() {
}
@@ -63,7 +64,8 @@ public class ConversationIntents {
private final StickerLocator stickerLocator;
private final boolean isBorderless;
private final int distributionType;
private final int startingPosition;
private final int startingPosition;
private final boolean firstTimeInSelfCreatedGroup;
static Args from(@NonNull Intent intent) {
if (isBubbleIntent(intent)) {
@@ -74,7 +76,8 @@ public class ConversationIntents {
null,
false,
ThreadDatabase.DistributionTypes.DEFAULT,
-1);
-1,
false);
}
return new Args(RecipientId.from(Objects.requireNonNull(intent.getStringExtra(EXTRA_RECIPIENT))),
@@ -84,7 +87,8 @@ public class ConversationIntents {
intent.getParcelableExtra(EXTRA_STICKER),
intent.getBooleanExtra(EXTRA_BORDERLESS, false),
intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, ThreadDatabase.DistributionTypes.DEFAULT),
intent.getIntExtra(EXTRA_STARTING_POSITION, -1));
intent.getIntExtra(EXTRA_STARTING_POSITION, -1),
intent.getBooleanExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, false));
}
private Args(@NonNull RecipientId recipientId,
@@ -94,16 +98,18 @@ public class ConversationIntents {
@Nullable StickerLocator stickerLocator,
boolean isBorderless,
int distributionType,
int startingPosition)
int startingPosition,
boolean firstTimeInSelfCreatedGroup)
{
this.recipientId = recipientId;
this.threadId = threadId;
this.draftText = draftText;
this.media = media;
this.stickerLocator = stickerLocator;
this.isBorderless = isBorderless;
this.distributionType = distributionType;
this.startingPosition = startingPosition;
this.recipientId = recipientId;
this.threadId = threadId;
this.draftText = draftText;
this.media = media;
this.stickerLocator = stickerLocator;
this.isBorderless = isBorderless;
this.distributionType = distributionType;
this.startingPosition = startingPosition;
this.firstTimeInSelfCreatedGroup = firstTimeInSelfCreatedGroup;
}
public @NonNull RecipientId getRecipientId() {
@@ -137,6 +143,10 @@ public class ConversationIntents {
public boolean isBorderless() {
return isBorderless;
}
public boolean isFirstTimeInSelfCreatedGroup() {
return firstTimeInSelfCreatedGroup;
}
}
public final static class Builder {
@@ -153,6 +163,7 @@ public class ConversationIntents {
private int startingPosition = -1;
private Uri dataUri;
private String dataType;
private boolean firstTimeInSelfCreatedGroup;
private Builder(@NonNull Context context,
@NonNull RecipientId recipientId,
@@ -212,6 +223,11 @@ public class ConversationIntents {
return this;
}
public Builder firstTimeInSelfCreatedGroup() {
this.firstTimeInSelfCreatedGroup = true;
return this;
}
public @NonNull Intent build() {
if (stickerLocator != null && media != null) {
throw new IllegalStateException("Cannot have both sticker and media array");
@@ -235,6 +251,7 @@ public class ConversationIntents {
intent.putExtra(EXTRA_DISTRIBUTION_TYPE, distributionType);
intent.putExtra(EXTRA_STARTING_POSITION, startingPosition);
intent.putExtra(EXTRA_BORDERLESS, isBorderless);
intent.putExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, firstTimeInSelfCreatedGroup);
if (draftText != null) {
intent.putExtra(EXTRA_TEXT, draftText);

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -35,7 +36,7 @@ class ConversationStickerViewModel extends ViewModel {
this.stickers = new MutableLiveData<>();
this.stickersAvailable = new MutableLiveData<>();
this.availabilityThrottler = new Throttler(500);
this.packObserver = new ContentObserver(new Handler()) {
this.packObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));

View File

@@ -206,16 +206,36 @@ public final class ConversationUpdateItem extends LinearLayout
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationMembershipChanges());
}
});
} else if (conversationMessage.getMessageRecord().isFailedDecryptionType() &&
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isFailedDecryptionType()))
{
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onDecryptionFailedLearnMoreClicked();
}
});
} else if (conversationMessage.getMessageRecord().isIdentityUpdate()) {
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onSafetyNumberLearnMoreClicked(conversationMessage.getMessageRecord().getIndividualRecipient());
}
});
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<UUID> uuids = updateDescription.getMentioned();
int text = 0;
if (Util.hasItems(uuids)) {
if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
if (uuids.contains(TextSecurePreferences.getLocalUuid(getContext()))) {
text = R.string.ConversationUpdateItem_return_to_call;
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
text = R.string.ConversationUpdateItem_call_is_full;
} else {
text = uuids.contains(TextSecurePreferences.getLocalUuid(getContext())) ? R.string.ConversationUpdateItem_return_to_call : R.string.ConversationUpdateItem_join_call;
text = R.string.ConversationUpdateItem_join_call;
}
}
@@ -231,6 +251,14 @@ public final class ConversationUpdateItem extends LinearLayout
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
}
} else if (conversationMessage.getMessageRecord().isSelfCreatedGroup()) {
actionButton.setText(R.string.ConversationUpdateItem_invite_friends);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onInviteFriendsToGroupClicked(conversationRecipient.requireGroupId().requireV2());
}
});
} else {
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);

View File

@@ -131,7 +131,8 @@ final class MenuState {
messageRecord.isIdentityUpdate() ||
messageRecord.isIdentityVerified() ||
messageRecord.isIdentityDefault() ||
messageRecord.isProfileChange();
messageRecord.isProfileChange() ||
messageRecord.isFailedDecryptionType();
}
private final static class Builder {

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -32,7 +33,7 @@ public class MentionsPickerFragment extends LoggingFragment {
private BottomSheetBehavior<View> behavior;
private MentionsPickerViewModel viewModel;
private final Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
private final Handler handler = new Handler();
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

View File

@@ -7,10 +7,13 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
@@ -28,7 +31,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerView.ViewHolder> {
class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.ViewHolder> {
private static final int TYPE_THREAD = 1;
private static final int TYPE_ACTION = 2;
@@ -46,6 +49,8 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
private boolean batchMode = false;
private final Set<Long> typingSet = new HashSet<>();
private PagingController pagingController;
protected ConversationListAdapter(@NonNull GlideRequests glideRequests,
@NonNull OnConversationClickListener onConversationClickListener)
{
@@ -156,6 +161,19 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
}
}
@Override
protected Conversation getItem(int position) {
if (pagingController != null) {
pagingController.onDataNeededAroundIndex(position);
}
return super.getItem(position);
}
public void setPagingController(@Nullable PagingController pagingController) {
this.pagingController = pagingController;
}
void setTypingThreads(@NonNull Set<Long> typingThreadSet) {
this.typingSet.clear();
this.typingSet.addAll(typingThreadSet);

View File

@@ -37,19 +37,19 @@ import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.thoughtcrime.securesms.util.views.Stub;
import java.util.Set;
@Trace
public class ConversationListArchiveFragment extends ConversationListFragment implements ActionMode.Callback
{
private RecyclerView list;
private View emptyState;
private Stub<View> emptyState;
private PulsingFloatingActionButton fab;
private PulsingFloatingActionButton cameraFab;
private Stub<Toolbar> toolbar;
public static ConversationListArchiveFragment newInstance() {
return new ConversationListArchiveFragment();
@@ -63,26 +63,30 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
toolbar = new Stub<>(view.findViewById(R.id.toolbar_basic));
super.onViewCreated(view, savedInstanceState);
list = view.findViewById(R.id.list);
fab = view.findViewById(R.id.fab);
cameraFab = view.findViewById(R.id.camera_fab);
emptyState = view.findViewById(R.id.empty_state);
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Toolbar toolbar = view.findViewById(R.id.toolbar_basic);
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
toolbar.setTitle(R.string.AndroidManifest_archived_conversations);
toolbar.get().setNavigationOnClickListener(v -> requireActivity().onBackPressed());
toolbar.get().setTitle(R.string.AndroidManifest_archived_conversations);
fab.hide();
cameraFab.hide();
}
@Override
protected void onPostSubmitList() {
protected void onPostSubmitList(int conversationCount) {
list.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
if (emptyState.resolved()) {
emptyState.get().setVisibility(View.GONE);
}
}
@Override
@@ -91,8 +95,8 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
}
@Override
protected int getToolbarRes() {
return R.id.toolbar_basic;
protected @NonNull Toolbar getToolbar(@NonNull View rootView) {
return toolbar.get();
}
@Override
@@ -144,6 +148,11 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
}
@Override
void updateEmptyState(boolean isConversationEmpty) {
// Do nothing
}
}

View File

@@ -7,110 +7,70 @@ import android.database.MergeCursor;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedDataSource;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
@Trace
abstract class ConversationListDataSource extends PositionalDataSource<Conversation> {
public static final Executor EXECUTOR = SignalExecutors.newFixedLifoThreadExecutor("signal-conversation-list", 1, 1);
abstract class ConversationListDataSource implements PagedDataSource<Conversation> {
private static final String TAG = Log.tag(ConversationListDataSource.class);
protected final ThreadDatabase threadDatabase;
protected ConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
protected ConversationListDataSource(@NonNull Context context) {
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
DatabaseObserver.Observer observer = new DatabaseObserver.Observer() {
@Override
public void onChanged() {
invalidate();
ApplicationDependencies.getDatabaseObserver().unregisterObserver(this);
}
};
invalidator.observe(() -> {
invalidate();
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
});
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
}
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
else return new ArchivedConversationListDataSource(context, invalidator);
public static ConversationListDataSource create(@NonNull Context context, boolean isArchived) {
if (!isArchived) return new UnarchivedConversationListDataSource(context);
else return new ArchivedConversationListDataSource(context);
}
@Override
public final void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Conversation> callback) {
long start = System.currentTimeMillis();
public int size() {
long startTime = System.currentTimeMillis();
int count = getTotalCount();
List<Conversation> conversations = new ArrayList<>(params.requestedLoadSize);
int totalCount = getTotalCount();
int effectiveCount = params.requestedStartPosition;
Log.d(TAG, "[size(), " + getClass().getSimpleName() + "] " + (System.currentTimeMillis() - startTime) + " ms");
return count;
}
@Override
public @NonNull List<Conversation> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName());
List<Conversation> conversations = new ArrayList<>(length);
List<Recipient> recipients = new LinkedList<>();
try (ConversationReader reader = new ConversationReader(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
try (ConversationReader reader = new ConversationReader(getCursor(start, length))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
conversations.add(new Conversation(record));
recipients.add(record.getRecipient());
effectiveCount++;
}
}
ApplicationDependencies.getRecipientCache().addToCache(recipients);
if (!isInvalid()) {
SizeFixResult<Conversation> result = SizeFixResult.ensureMultipleOfPageSize(conversations, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", actualSize: " + result.getItems().size() + ", totalCount: " + result.getTotal() + ", class: " + getClass().getSimpleName());
} else {
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", totalCount: " + totalCount + ", class: " + getClass().getSimpleName() + " -- invalidated");
}
}
@Override
public final void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Conversation> callback) {
long start = System.currentTimeMillis();
List<Conversation> conversations = new ArrayList<>(params.loadSize);
List<Recipient> recipients = new LinkedList<>();
try (ConversationReader reader = new ConversationReader(getCursor(params.startPosition, params.loadSize))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && !isInvalid()) {
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
conversations.add(new Conversation(record));
recipients.add(record.getRecipient());
}
}
stopwatch.split("cursor");
ApplicationDependencies.getRecipientCache().addToCache(recipients);
callback.onResult(conversations);
stopwatch.split("cache-recipients");
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | start: " + params.startPosition + ", size: " + params.loadSize + ", class: " + getClass().getSimpleName() + (isInvalid() ? " -- invalidated" : ""));
stopwatch.stop(TAG);
return conversations;
}
protected abstract int getTotalCount();
@@ -118,8 +78,8 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
private static class ArchivedConversationListDataSource extends ConversationListDataSource {
ArchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
super(context, invalidator);
ArchivedConversationListDataSource(@NonNull Context context) {
super(context);
}
@Override
@@ -141,8 +101,8 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
private int archivedCount;
private int unpinnedCount;
UnarchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
super(context, invalidator);
UnarchivedConversationListDataSource(@NonNull Context context) {
super(context);
}
@Override
@@ -152,14 +112,27 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
pinnedCount = threadDatabase.getPinnedConversationListCount();
archivedCount = threadDatabase.getArchivedConversationListCount();
unpinnedCount = unarchivedCount - pinnedCount;
totalCount = unarchivedCount + (archivedCount != 0 ? 1 : 0) + (pinnedCount != 0 ? (unpinnedCount != 0 ? 2 : 1) : 0);
totalCount = unarchivedCount;
if (archivedCount != 0) {
totalCount++;
}
if (pinnedCount != 0) {
if (unpinnedCount != 0) {
totalCount += 2;
} else {
totalCount += 1;
}
}
return totalCount;
}
@Override
protected Cursor getCursor(long offset, long limit) {
List<Cursor> cursors = new ArrayList<>(5);
List<Cursor> cursors = new ArrayList<>(5);
long originalLimit = limit;
if (offset == 0 && hasPinnedHeader()) {
MatrixCursor pinnedHeaderCursor = new MatrixCursor(ConversationReader.HEADER_COLUMN);
@@ -183,7 +156,7 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
Cursor unpinnedCursor = threadDatabase.getUnarchivedConversationList(false, unpinnedOffset, limit);
cursors.add(unpinnedCursor);
if (offset + limit >= totalCount && hasArchivedFooter()) {
if (offset + originalLimit >= totalCount && hasArchivedFooter()) {
MatrixCursor archivedFooterCursor = new MatrixCursor(ConversationReader.ARCHIVED_COLUMNS);
archivedFooterCursor.addRow(ConversationReader.createArchivedFooterRow(archivedCount));
cursors.add(archivedFooterCursor);
@@ -212,22 +185,4 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
return archivedCount != 0;
}
}
static class Factory extends DataSource.Factory<Integer, Conversation> {
private final Context context;
private final Invalidator invalidator;
private final boolean isArchived;
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
this.context = context;
this.invalidator = invalidator;
this.isArchived = isArchived;
}
@Override
public @NonNull DataSource<Integer, Conversation> create() {
return ConversationListDataSource.create(context, invalidator, isArchived);
}
}
}

View File

@@ -80,7 +80,6 @@ import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder;
@@ -88,8 +87,6 @@ import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder;
import org.thoughtcrime.securesms.components.reminder.Reminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
@@ -117,18 +114,19 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collections;
@@ -142,7 +140,6 @@ import java.util.Set;
import static android.app.Activity.RESULT_OK;
@Trace
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
ConversationListAdapter.OnConversationClickListener,
ConversationListSearchAdapter.EventListener,
@@ -156,21 +153,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private static final int MAXIMUM_PINNED_CONVERSATIONS = 4;
private static final int[] EMPTY_IMAGES = new int[] { R.drawable.empty_inbox_1,
R.drawable.empty_inbox_2,
R.drawable.empty_inbox_3,
R.drawable.empty_inbox_4,
R.drawable.empty_inbox_5 };
private ActionMode actionMode;
private RecyclerView list;
private ReminderView reminderView;
private View emptyState;
private ImageView emptyImage;
private Stub<ReminderView> reminderView;
private Stub<ViewGroup> emptyState;
private TextView searchEmptyState;
private PulsingFloatingActionButton fab;
private PulsingFloatingActionButton cameraFab;
private SearchToolbar searchToolbar;
private Stub<SearchToolbar> searchToolbar;
private ImageView searchAction;
private View toolbarShadow;
private ConversationListViewModel viewModel;
@@ -178,11 +168,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private ConversationListAdapter defaultAdapter;
private ConversationListSearchAdapter searchAdapter;
private StickyHeaderDecoration searchAdapterDecoration;
private ViewGroup megaphoneContainer;
private Stub<ViewGroup> megaphoneContainer;
private SnapToTopDataObserver snapToTopDataObserver;
private Drawable archiveDrawable;
private LifecycleObserver visibilityLifecycleObserver;
private Stopwatch startupStopwatch;
public static ConversationListFragment newInstance() {
return new ConversationListFragment();
}
@@ -191,6 +183,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setHasOptionsMenu(true);
startupStopwatch = new Stopwatch("startup");
}
@Override
@@ -200,28 +193,24 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
reminderView = view.findViewById(R.id.reminder);
list = view.findViewById(R.id.list);
fab = view.findViewById(R.id.fab);
cameraFab = view.findViewById(R.id.camera_fab);
emptyState = view.findViewById(R.id.empty_state);
emptyImage = view.findViewById(R.id.empty);
searchEmptyState = view.findViewById(R.id.search_no_results);
searchToolbar = view.findViewById(R.id.search_toolbar);
searchAction = view.findViewById(R.id.search_action);
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
megaphoneContainer = view.findViewById(R.id.megaphone_container);
reminderView = new Stub<>(view.findViewById(R.id.reminder));
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar));
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
Toolbar toolbar = view.findViewById(getToolbarRes());
Toolbar toolbar = getToolbar(view);
toolbar.setVisibility(View.VISIBLE);
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
fab.show();
cameraFab.show();
reminderView.setOnDismissListener(this::updateReminders);
reminderView.setOnActionClickListener(this::onReminderAction);
list.setLayoutManager(new LinearLayoutManager(requireActivity()));
list.setItemAnimator(new DeleteItemAnimator());
list.addOnScrollListener(new ScrollListener());
@@ -242,8 +231,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
.execute();
});
initializeListAdapters();
initializeViewModel();
initializeListAdapters();
initializeTypingObserver();
initializeSearchListener();
@@ -265,7 +254,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), Recipient::self, this::initializeProfileIcon);
if (!searchToolbar.isVisible() && list.getAdapter() != defaultAdapter) {
if ((!searchToolbar.resolved() || !searchToolbar.get().isVisible()) && list.getAdapter() != defaultAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
@@ -331,10 +320,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
private boolean closeSearchIfOpen() {
if (searchToolbar.isVisible() || activeAdapter == searchAdapter) {
if ((searchToolbar.resolved() && searchToolbar.get().isVisible()) || activeAdapter == searchAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
searchToolbar.collapse();
searchToolbar.get().collapse();
return true;
}
@@ -431,6 +420,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
dialogFragment.show(getChildFragmentManager(), "megaphone_dialog");
}
private void initializeReminderView() {
reminderView.get().setOnDismissListener(this::updateReminders);
reminderView.get().setOnActionClickListener(this::onReminderAction);
}
private void onReminderAction(@IdRes int reminderActionId) {
if (reminderActionId == R.id.reminder_action_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
@@ -451,36 +445,36 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private void initializeSearchListener() {
searchAction.setOnClickListener(v -> {
searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2.0f),
searchAction.getY() + (searchAction.getHeight() / 2.0f));
});
searchToolbar.get().display(searchAction.getX() + (searchAction.getWidth() / 2.0f),
searchAction.getY() + (searchAction.getHeight() / 2.0f));
searchToolbar.setListener(new SearchToolbar.SearchListener() {
@Override
public void onSearchTextChange(String text) {
String trimmed = text.trim();
searchToolbar.get().setListener(new SearchToolbar.SearchListener() {
@Override
public void onSearchTextChange(String text) {
String trimmed = text.trim();
viewModel.updateQuery(trimmed);
viewModel.updateQuery(trimmed);
if (trimmed.length() > 0) {
if (activeAdapter != searchAdapter) {
setAdapter(searchAdapter);
list.removeItemDecoration(searchAdapterDecoration);
list.addItemDecoration(searchAdapterDecoration);
}
} else {
if (activeAdapter != defaultAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
if (trimmed.length() > 0) {
if (activeAdapter != searchAdapter) {
setAdapter(searchAdapter);
list.removeItemDecoration(searchAdapterDecoration);
list.addItemDecoration(searchAdapterDecoration);
}
} else {
if (activeAdapter != defaultAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
}
}
}
@Override
public void onSearchClosed() {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
@Override
public void onSearchClosed() {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
});
});
}
@@ -490,6 +484,19 @@ public class ConversationListFragment extends MainFragment implements ActionMode
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
setAdapter(defaultAdapter);
defaultAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
startupStopwatch.split("data-set");
defaultAdapter.unregisterAdapterDataObserver(this);
list.post(() -> {
AppStartup.getInstance().onCriticalRenderEventEnd();
startupStopwatch.split("first-render");
startupStopwatch.stop(TAG);
});
}
});
}
@SuppressWarnings("rawtypes")
@@ -502,6 +509,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return;
}
if (adapter instanceof ConversationListAdapter) {
((ConversationListAdapter) adapter).setPagingController(viewModel.getPagingController());
}
list.setAdapter(adapter);
if (adapter == defaultAdapter) {
@@ -555,20 +566,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private void onMegaphoneChanged(@Nullable Megaphone megaphone) {
if (megaphone == null) {
megaphoneContainer.setVisibility(View.GONE);
megaphoneContainer.removeAllViews();
if (megaphoneContainer.resolved()) {
megaphoneContainer.get().setVisibility(View.GONE);
megaphoneContainer.get().removeAllViews();
}
return;
}
View view = MegaphoneViewBuilder.build(requireContext(), megaphone, this);
megaphoneContainer.removeAllViews();
megaphoneContainer.get().removeAllViews();
if (view != null) {
megaphoneContainer.addView(view);
megaphoneContainer.setVisibility(View.VISIBLE);
megaphoneContainer.get().addView(view);
megaphoneContainer.get().setVisibility(View.VISIBLE);
} else {
megaphoneContainer.setVisibility(View.GONE);
megaphoneContainer.get().setVisibility(View.GONE);
if (megaphone.getOnVisibleListener() != null) {
megaphone.getOnVisibleListener().onEvent(megaphone, this);
@@ -591,14 +604,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return Optional.of(new ServiceOutageReminder(context));
} else if (OutdatedBuildReminder.isEligible()) {
return Optional.of(new OutdatedBuildReminder(context));
} else if (DefaultSmsReminder.isEligible(context)) {
return Optional.of(new DefaultSmsReminder(this, SMS_ROLE_REQUEST_CODE));
} else if (Util.isDefaultSmsProvider(context) && SystemSmsImportReminder.isEligible(context)) {
return Optional.of((new SystemSmsImportReminder(context)));
} else if (PushRegistrationReminder.isEligible(context)) {
return Optional.of((new PushRegistrationReminder(context)));
} else if (ShareReminder.isEligible(context)) {
return Optional.of(new ShareReminder(context));
} else if (DozeReminder.isEligible(context)) {
return Optional.of(new DozeReminder(context));
} else {
@@ -606,9 +613,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
}, reminder -> {
if (reminder.isPresent() && getActivity() != null && !isRemoving()) {
reminderView.showReminder(reminder.get());
} else if (!reminder.isPresent()) {
reminderView.hide();
if (!reminderView.resolved()) {
initializeReminderView();
}
reminderView.get().showReminder(reminder.get());
} else if (reminderView.resolved() && !reminder.isPresent()) {
reminderView.get().hide();
}
});
}
@@ -820,33 +830,37 @@ public class ConversationListFragment extends MainFragment implements ActionMode
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
}
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
if (conversationList.getConversations().isDetached()) {
return;
}
defaultAdapter.submitList(conversationList.getConversations());
onPostSubmitList();
private void onSubmitList(@NonNull List<Conversation> conversationList) {
defaultAdapter.submitList(conversationList);
onPostSubmitList(conversationList.size());
}
private void updateEmptyState(boolean isConversationEmpty) {
void updateEmptyState(boolean isConversationEmpty) {
if (isConversationEmpty) {
Log.i(TAG, "Received an empty data set.");
list.setVisibility(View.INVISIBLE);
emptyState.setVisibility(View.VISIBLE);
emptyImage.setImageResource(EMPTY_IMAGES[(int) (Math.random() * EMPTY_IMAGES.length)]);
emptyState.get().setVisibility(View.VISIBLE);
fab.startPulse(3 * 1000);
cameraFab.startPulse(3 * 1000);
SignalStore.onboarding().setShowNewGroup(true);
SignalStore.onboarding().setShowInviteFriends(true);
} else {
list.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
fab.stopPulse();
cameraFab.stopPulse();
if (emptyState.resolved()) {
emptyState.get().setVisibility(View.GONE);
}
}
}
protected void onPostSubmitList() {
protected void onPostSubmitList(int conversationCount) {
if (conversationCount >= 6 && (SignalStore.onboarding().shouldShowInviteFriends() || SignalStore.onboarding().shouldShowNewGroup())) {
SignalStore.onboarding().clearAll();
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.ONBOARDING);
}
}
@Override
@@ -976,8 +990,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
}
protected @IdRes int getToolbarRes() {
return R.id.toolbar;
protected Toolbar getToolbar(@NonNull View rootView) {
return rootView.findViewById(R.id.toolbar);
}
protected @PluralsRes int getArchivedSnackbarTitleRes() {

View File

@@ -20,18 +20,18 @@ import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Typeface;
import android.graphics.drawable.RippleDrawable;
import android.os.Build.VERSION;
import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
@@ -64,7 +64,6 @@ import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Collections;
@@ -73,7 +72,7 @@ import java.util.Set;
import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync;
public final class ConversationListItem extends RelativeLayout
public final class ConversationListItem extends ConstraintLayout
implements RecipientForeverObserver,
BindableConversationListItem,
Unbindable,
@@ -90,7 +89,6 @@ public final class ConversationListItem extends RelativeLayout
private LiveRecipient recipient;
private long threadId;
private GlideRequests glideRequests;
private View subjectContainer;
private TextView subjectView;
private TypingIndicatorView typingView;
private FromTextView fromView;
@@ -122,21 +120,17 @@ public final class ConversationListItem extends RelativeLayout
@Override
protected void onFinishInflate() {
super.onFinishInflate();
this.subjectContainer = findViewById(R.id.subject_container);
this.subjectView = findViewById(R.id.subject);
this.typingView = findViewById(R.id.typing_indicator);
this.fromView = findViewById(R.id.from);
this.dateView = findViewById(R.id.date);
this.deliveryStatusIndicator = findViewById(R.id.delivery_status);
this.alertView = findViewById(R.id.indicators_parent);
this.contactPhotoImage = findViewById(R.id.contact_photo_image);
this.thumbnailView = findViewById(R.id.thumbnail);
this.archivedView = findViewById(R.id.archived);
this.unreadIndicator = findViewById(R.id.unread_indicator);
this.subjectView = findViewById(R.id.conversation_list_item_summary);
this.typingView = findViewById(R.id.conversation_list_item_typing_indicator);
this.fromView = findViewById(R.id.conversation_list_item_name);
this.dateView = findViewById(R.id.conversation_list_item_date);
this.deliveryStatusIndicator = findViewById(R.id.conversation_list_item_status);
this.alertView = findViewById(R.id.conversation_list_item_alert);
this.contactPhotoImage = findViewById(R.id.conversation_list_item_avatar);
this.thumbnailView = findViewById(R.id.conversation_list_item_thumbnail);
this.archivedView = findViewById(R.id.conversation_list_item_archived);
this.unreadIndicator = findViewById(R.id.conversation_list_item_unread_indicator);
thumbnailView.setClickable(false);
ViewUtil.setTextViewGravityStart(this.fromView, getContext());
ViewUtil.setTextViewGravityStart(this.subjectView, getContext());
}
@Override
@@ -158,19 +152,17 @@ public final class ConversationListItem extends RelativeLayout
boolean batchMode,
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
observeRecipient(thread.getRecipient().live());
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = selectedThreads;
this.recipient = thread.getRecipient().live();
this.threadId = thread.getThreadId();
this.glideRequests = glideRequests;
this.unreadCount = thread.getUnreadCount();
this.lastSeen = thread.getLastSeen();
this.thread = thread;
this.recipient.observeForever(this);
if (highlightSubstring != null) {
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
@@ -215,15 +207,13 @@ public final class ConversationListItem extends RelativeLayout
@NonNull Locale locale,
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
observeRecipient(contact.live());
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = Collections.emptySet();
this.recipient = contact.live();
this.glideRequests = glideRequests;
this.recipient.observeForever(this);
fromView.setText(contact);
fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), new SpannableString(fromView.getText()), highlightSubstring));
@@ -245,16 +235,13 @@ public final class ConversationListItem extends RelativeLayout
@NonNull Locale locale,
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
observeRecipient(messageResult.conversationRecipient.live());
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = Collections.emptySet();
this.recipient = messageResult.conversationRecipient.live();
this.glideRequests = glideRequests;
this.recipient.observeForever(this);
fromView.setText(recipient.get(), true);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), messageResult.bodySnippet, highlightSubstring));
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.receivedTimestampMs));
@@ -272,9 +259,7 @@ public final class ConversationListItem extends RelativeLayout
@Override
public void unbind() {
if (this.recipient != null) {
this.recipient.removeForeverObserver(this);
this.recipient = null;
observeRecipient(null);
setBatchMode(false);
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
}
@@ -323,6 +308,18 @@ public final class ConversationListItem extends RelativeLayout
return lastSeen;
}
private void observeRecipient(@Nullable LiveRecipient newRecipient) {
if (this.recipient != null) {
this.recipient.removeForeverObserver(this);
}
this.recipient = newRecipient;
if (this.recipient != null) {
this.recipient.observeForever(this);
}
}
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) {
if (this.displayBody != null) {
this.displayBody.removeObserver(this);
@@ -349,19 +346,8 @@ public final class ConversationListItem extends RelativeLayout
if (thread.getSnippetUri() != null) {
this.thumbnailView.setVisibility(View.VISIBLE);
this.thumbnailView.setImageResource(glideRequests, thread.getSnippetUri());
LayoutParams subjectParams = (RelativeLayout.LayoutParams)this.subjectContainer .getLayoutParams();
subjectParams.addRule(RelativeLayout.LEFT_OF, R.id.thumbnail);
subjectParams.addRule(RelativeLayout.START_OF, R.id.thumbnail);
this.subjectContainer.setLayoutParams(subjectParams);
this.post(new ThumbnailPositioner(thumbnailView, archivedView, deliveryStatusIndicator, dateView));
} else {
this.thumbnailView.setVisibility(View.GONE);
LayoutParams subjectParams = (RelativeLayout.LayoutParams)this.subjectContainer.getLayoutParams();
subjectParams.addRule(RelativeLayout.LEFT_OF, R.id.status);
subjectParams.addRule(RelativeLayout.START_OF, R.id.status);
this.subjectContainer.setLayoutParams(subjectParams);
}
}
@@ -403,7 +389,7 @@ public final class ConversationListItem extends RelativeLayout
}
private void setRippleColor(Recipient recipient) {
if (VERSION.SDK_INT >= 21) {
if (Build.VERSION.SDK_INT >= 21) {
((RippleDrawable)(getBackground()).mutate())
.setColor(ColorStateList.valueOf(recipient.getColor().toConversationColor(getContext())));
}
@@ -421,6 +407,11 @@ public final class ConversationListItem extends RelativeLayout
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
if (this.recipient == null || !this.recipient.getId().equals(recipient.getId())) {
Log.w(TAG, "Bad change! Local recipient doesn't match. Ignoring. Local: " + (this.recipient == null ? "null" : this.recipient.getId()) + ", Changed: " + recipient.getId());
return;
}
fromView.setText(recipient, unreadCount == 0);
contactPhotoImage.setAvatar(glideRequests, recipient, !batchMode);
setRippleColor(recipient);
@@ -443,7 +434,8 @@ public final class ConversationListItem extends RelativeLayout
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message), defaultTint);
} else if (SmsDatabase.Types.isFailedDecryptType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_bad_encrypted_message), defaultTint);
UpdateDescription description = UpdateDescription.staticDescription(context.getString(R.string.ThreadRecord_chat_session_refreshed), R.drawable.ic_refresh_16);
return emphasisAdded(context, description, defaultTint);
} else if (SmsDatabase.Types.isNoRemoteSessionType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session), defaultTint);
} else if (SmsDatabase.Types.isEndSessionType(thread.getType())) {
@@ -549,37 +541,4 @@ public final class ConversationListItem extends RelativeLayout
updateTypingIndicator(typingThreads);
}
}
private static class ThumbnailPositioner implements Runnable {
private final View thumbnailView;
private final View archivedView;
private final View deliveryStatusView;
private final View dateView;
ThumbnailPositioner(View thumbnailView, View archivedView, View deliveryStatusView, View dateView) {
this.thumbnailView = thumbnailView;
this.archivedView = archivedView;
this.deliveryStatusView = deliveryStatusView;
this.dateView = dateView;
}
@Override
public void run() {
LayoutParams thumbnailParams = (RelativeLayout.LayoutParams)thumbnailView.getLayoutParams();
if (archivedView.getVisibility() == View.VISIBLE &&
(archivedView.getWidth() + deliveryStatusView.getWidth()) > dateView.getWidth())
{
thumbnailParams.addRule(RelativeLayout.LEFT_OF, R.id.status);
thumbnailParams.addRule(RelativeLayout.START_OF, R.id.status);
} else {
thumbnailParams.addRule(RelativeLayout.LEFT_OF, R.id.date);
thumbnailParams.addRule(RelativeLayout.START_OF, R.id.date);
}
thumbnailView.setLayoutParams(thumbnailParams);
}
}
}

View File

@@ -4,6 +4,7 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -13,12 +14,11 @@ import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Locale;
import java.util.Set;
public class ConversationListItemAction extends LinearLayout implements BindableConversationListItem {
public class ConversationListItemAction extends FrameLayout implements BindableConversationListItem {
private TextView description;
@@ -38,7 +38,7 @@ public class ConversationListItemAction extends LinearLayout implements Bindable
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.description = ViewUtil.findById(this, R.id.description);
this.description = findViewById(R.id.description);
}
@Override

View File

@@ -6,15 +6,13 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -26,24 +24,29 @@ import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import java.util.Objects;
import java.util.List;
class ConversationListViewModel extends ViewModel {
private static final String TAG = Log.tag(ConversationListViewModel.class);
private final MutableLiveData<Megaphone> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final LiveData<ConversationList> conversationList;
private final SearchRepository searchRepository;
private final MegaphoneRepository megaphoneRepository;
private final Debouncer debouncer;
private final DatabaseObserver.Observer observer;
private final Invalidator invalidator;
private static boolean coldStart = true;
private final MutableLiveData<Megaphone> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final PagedData<Conversation> pagedData;
private final LiveData<Boolean> hasNoConversations;
private final SearchRepository searchRepository;
private final MegaphoneRepository megaphoneRepository;
private final Debouncer debouncer;
private final DatabaseObserver.Observer observer;
private final Invalidator invalidator;
private String lastQuery;
private int pinnedCount;
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
this.megaphone = new MutableLiveData<>();
@@ -52,49 +55,33 @@ class ConversationListViewModel extends ViewModel {
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
this.debouncer = new Debouncer(300);
this.invalidator = new Invalidator();
this.pagedData = PagedData.create(ConversationListDataSource.create(application, isArchived),
new PagingConfig.Builder()
.setPageSize(15)
.setBufferPages(2)
.build());
this.observer = () -> {
if (!TextUtils.isEmpty(getLastQuery())) {
searchRepository.query(getLastQuery(), searchResult::postValue);
}
pagedData.getController().onDataInvalidated();
};
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, isArchived);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(15)
.setInitialLoadSizeHint(30)
.setEnablePlaceholders(true)
.build();
this.hasNoConversations = LiveDataUtil.mapAsync(pagedData.getData(), conversations -> {
pinnedCount = DatabaseFactory.getThreadDatabase(application).getPinnedConversationListCount();
LiveData<PagedList<Conversation>> conversationList = new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationListDataSource.EXECUTOR)
.setInitialLoadKey(0)
.build();
if (conversations.size() > 0) {
return false;
} else {
return DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount() == 0;
}
});
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
this.conversationList = Transformations.switchMap(conversationList, conversation -> {
if (conversation.getDataSource().isInvalid()) {
Log.w(TAG, "Received an invalid conversation list. Ignoring.");
return new MutableLiveData<>();
}
MutableLiveData<ConversationList> updated = new MutableLiveData<>();
if (isArchived) {
updated.postValue(new ConversationList(conversation, 0, 0));
} else {
SignalExecutors.BOUNDED.execute(() -> {
int archiveCount = DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount();
int pinnedCount = DatabaseFactory.getThreadDatabase(application).getPinnedConversationListCount();
updated.postValue(new ConversationList(conversation, archiveCount, pinnedCount));
});
}
return updated;
});
}
public LiveData<Boolean> hasNoConversations() {
return Transformations.map(getConversationList(), ConversationList::isEmpty);
return hasNoConversations;
}
@NonNull LiveData<SearchResult> getSearchResult() {
@@ -105,17 +92,26 @@ class ConversationListViewModel extends ViewModel {
return megaphone;
}
@NonNull LiveData<ConversationList> getConversationList() {
return conversationList;
@NonNull LiveData<List<Conversation>> getConversationList() {
return pagedData.getData();
}
@NonNull PagingController getPagingController() {
return pagedData.getController();
}
public int getPinnedCount() {
return Objects.requireNonNull(getConversationList().getValue()).pinnedCount;
return pinnedCount;
}
void onVisible() {
megaphoneRepository.getNextMegaphone(megaphone::postValue);
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
if (!coldStart) {
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
}
coldStart = false;
}
void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
@@ -168,32 +164,4 @@ class ConversationListViewModel extends ViewModel {
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository(), isArchived));
}
}
final static class ConversationList {
private final PagedList<Conversation> conversations;
private final int archivedCount;
private final int pinnedCount;
ConversationList(PagedList<Conversation> conversations, int archivedCount, int pinnedCount) {
this.conversations = conversations;
this.archivedCount = archivedCount;
this.pinnedCount = pinnedCount;
}
PagedList<Conversation> getConversations() {
return conversations;
}
int getArchivedCount() {
return archivedCount;
}
public int getPinnedCount() {
return pinnedCount;
}
boolean isEmpty() {
return conversations.isEmpty() && archivedCount == 0;
}
}
}

View File

@@ -11,18 +11,30 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.IOException;
import java.security.SecureRandom;
public class DatabaseSecretProvider {
/**
* It can be rather expensive to read from the keystore, so this class caches the key in memory
* after it is created.
*/
public final class DatabaseSecretProvider {
@SuppressWarnings("unused")
private static final String TAG = DatabaseSecretProvider.class.getSimpleName();
private static volatile DatabaseSecret instance;
private final Context context;
public static DatabaseSecret getOrCreateDatabaseSecret(@NonNull Context context) {
if (instance == null) {
synchronized (DatabaseSecretProvider.class) {
if (instance == null) {
instance = getOrCreate(context);
}
}
}
public DatabaseSecretProvider(@NonNull Context context) {
this.context = context.getApplicationContext();
return instance;
}
public DatabaseSecret getOrCreateDatabaseSecret() {
private DatabaseSecretProvider() {
}
private static @NonNull DatabaseSecret getOrCreate(@NonNull Context context) {
String unencryptedSecret = TextSecurePreferences.getDatabaseUnencryptedSecret(context);
String encryptedSecret = TextSecurePreferences.getDatabaseEncryptedSecret(context);
@@ -31,12 +43,12 @@ public class DatabaseSecretProvider {
else return createAndStoreDatabaseSecret(context);
}
private DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret)
private static @NonNull DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret)
{
try {
DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT < 23) {
return databaseSecret;
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
@@ -51,8 +63,8 @@ public class DatabaseSecretProvider {
}
}
private DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
private static @NonNull DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
if (Build.VERSION.SDK_INT < 23) {
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
@@ -60,14 +72,14 @@ public class DatabaseSecretProvider {
}
}
private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
private static @NonNull DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32];
random.nextBytes(secret);
DatabaseSecret databaseSecret = new DatabaseSecret(secret);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= 23) {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
} else {

View File

@@ -82,10 +82,6 @@ public final class ProfileKeyUtil {
return Optional.of(profileKeyOrThrow(profileKey));
}
public static @NonNull Optional<ProfileKeyCredential> profileKeyCredentialOptional(@Nullable byte[] profileKey) {
return Optional.fromNullable(profileKeyCredentialOrNull(profileKey));
}
public static @NonNull ProfileKey createNew() {
try {
return new ProfileKey(Util.getSecretBytes(32));

View File

@@ -29,4 +29,7 @@ public class SessionUtil {
new TextSecureSessionStore(context).archiveAllSessions();
}
public static void archiveSession(Context context, RecipientId recipientId, int deviceId) {
new TextSecureSessionStore(context).archiveSession(recipientId, deviceId);
}
}

View File

@@ -103,6 +103,16 @@ public class TextSecureSessionStore implements SessionStore {
}
}
public void archiveSession(@NonNull RecipientId recipientId, int deviceId) {
synchronized (FILE_LOCK) {
SessionRecord session = DatabaseFactory.getSessionDatabase(context).load(recipientId, deviceId);
if (session != null) {
session.archiveCurrentState();
DatabaseFactory.getSessionDatabase(context).store(recipientId, deviceId, session);
}
}
}
public void archiveSiblingSessions(@NonNull SignalProtocolAddress address) {
synchronized (FILE_LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {

View File

@@ -53,7 +53,6 @@ import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FileUtils;
@@ -82,7 +81,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
@Trace
public class AttachmentDatabase extends Database {
private static final String TAG = AttachmentDatabase.class.getSimpleName();
@@ -751,7 +749,7 @@ public class AttachmentDatabase extends Database {
}
/**
* @param onlyModifyThisAttachment If false and more than one attachment shares this file, they will all up updated.
* @param onlyModifyThisAttachment If false and more than one attachment shares this file, they will all be updated.
* If true, then guarantees not to affect other attachments.
*/
public void updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment,
@@ -1032,7 +1030,7 @@ public class AttachmentDatabase extends Database {
}
}
private File newFile() throws IOException {
public File newFile() throws IOException {
File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
return File.createTempFile("part", ".mms", partsDirectory);
}

View File

@@ -32,13 +32,14 @@ import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class DatabaseFactory {
private static final Object lock = new Object();
private static DatabaseFactory instance;
private static volatile DatabaseFactory instance;
private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms;
@@ -58,21 +59,20 @@ public class DatabaseFactory {
private final SignedPreKeyDatabase signedPreKeyDatabase;
private final SessionDatabase sessionDatabase;
private final SearchDatabase searchDatabase;
private final JobDatabase jobDatabase;
private final StickerDatabase stickerDatabase;
private final StorageKeyDatabase storageKeyDatabase;
private final KeyValueDatabase keyValueDatabase;
private final MegaphoneDatabase megaphoneDatabase;
private final RemappedRecordsDatabase remappedRecordsDatabase;
private final MentionDatabase mentionDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
if (instance == null)
instance = new DatabaseFactory(context.getApplicationContext());
return instance;
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new DatabaseFactory(context.getApplicationContext());
}
}
}
return instance;
}
public static MmsSmsDatabase getMmsSmsDatabase(Context context) {
@@ -143,10 +143,6 @@ public class DatabaseFactory {
return getInstance(context).searchDatabase;
}
public static JobDatabase getJobDatabase(Context context) {
return getInstance(context).jobDatabase;
}
public static StickerDatabase getStickerDatabase(Context context) {
return getInstance(context).stickerDatabase;
}
@@ -155,14 +151,6 @@ public class DatabaseFactory {
return getInstance(context).storageKeyDatabase;
}
public static KeyValueDatabase getKeyValueDatabase(Context context) {
return getInstance(context).keyValueDatabase;
}
public static MegaphoneDatabase getMegaphoneDatabase(Context context) {
return getInstance(context).megaphoneDatabase;
}
static RemappedRecordsDatabase getRemappedRecordsDatabase(Context context) {
return getInstance(context).remappedRecordsDatabase;
}
@@ -180,6 +168,11 @@ public class DatabaseFactory {
getInstance(context).databaseHelper.onUpgrade(database, database.getVersion(), -1);
getInstance(context).databaseHelper.markCurrent(database);
getInstance(context).mms.trimEntriesForExpiredMessages();
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS key_value");
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS megaphone");
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS job_spec");
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS constraint_spec");
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS dependency_spec");
instance.databaseHelper.close();
instance = null;
@@ -193,8 +186,8 @@ public class DatabaseFactory {
private DatabaseFactory(@NonNull Context context) {
SQLiteDatabase.loadLibs(context);
DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
DatabaseSecret databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context);
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
this.sms = new SmsDatabase(context, databaseHelper);
@@ -214,11 +207,8 @@ public class DatabaseFactory {
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.jobDatabase = new JobDatabase(context, databaseHelper);
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper);
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
}
@@ -250,4 +240,12 @@ public class DatabaseFactory {
public void triggerDatabaseAccess() {
databaseHelper.getWritableDatabase();
}
public SQLiteDatabase getRawDatabase() {
return databaseHelper.getWritableDatabase().getSqlCipherDatabase();
}
public boolean hasTable(String table) {
return SqlUtil.tableExists(databaseHelper.getReadableDatabase().getSqlCipherDatabase(), table);
}
}

View File

@@ -10,13 +10,11 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@Trace
public class DraftDatabase extends Database {
static final String TABLE_NAME = "drafts";

View File

@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
@@ -53,7 +52,6 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
@Trace
public final class GroupDatabase extends Database {
private static final String TAG = Log.tag(GroupDatabase.class);

View File

@@ -9,14 +9,12 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.whispersystems.libsignal.util.Pair;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@Trace
public class GroupReceiptDatabase extends Database {
public static final String TABLE_NAME = "group_receipts";

View File

@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.whispersystems.libsignal.IdentityKey;
@@ -39,7 +38,6 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
@Trace
public class IdentityDatabase extends Database {
@SuppressWarnings("unused")

View File

@@ -1,36 +1,37 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import net.sqlcipher.database.SQLiteOpenHelper;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import java.util.LinkedList;
import java.util.List;
@Trace
public class JobDatabase extends Database {
public class JobDatabase extends SQLiteOpenHelper implements SignalDatabase {
public static String JOBS_TABLE_NAME = "job_spec";
public static String CONSTRAINTS_TABLE_NAME = "constraint_spec";
public static String DEPENDENCIES_TABLE_NAME = "dependency_spec";
private static final String TAG = Log.tag(JobDatabase.class);
public static final String[] CREATE_TABLE = new String[] { Jobs.CREATE_TABLE,
Constraints.CREATE_TABLE,
Dependencies.CREATE_TABLE };
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "signal-jobmanager.db";
private static final class Jobs {
private static final String TABLE_NAME = JOBS_TABLE_NAME;
private static final String TABLE_NAME = "job_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String FACTORY_KEY = "factory_key";
@@ -40,7 +41,6 @@ public class JobDatabase extends Database {
private static final String RUN_ATTEMPT = "run_attempt";
private static final String MAX_ATTEMPTS = "max_attempts";
private static final String MAX_BACKOFF = "max_backoff";
private static final String MAX_INSTANCES = "max_instances";
private static final String LIFESPAN = "lifespan";
private static final String SERIALIZED_DATA = "serialized_data";
private static final String SERIALIZED_INPUT_DATA = "serialized_input_data";
@@ -55,7 +55,6 @@ public class JobDatabase extends Database {
RUN_ATTEMPT + " INTEGER, " +
MAX_ATTEMPTS + " INTEGER, " +
MAX_BACKOFF + " INTEGER, " +
MAX_INSTANCES + " INTEGER, " +
LIFESPAN + " INTEGER, " +
SERIALIZED_DATA + " TEXT, " +
SERIALIZED_INPUT_DATA + " TEXT DEFAULT NULL, " +
@@ -63,7 +62,7 @@ public class JobDatabase extends Database {
}
private static final class Constraints {
private static final String TABLE_NAME = CONSTRAINTS_TABLE_NAME;
private static final String TABLE_NAME = "constraint_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String FACTORY_KEY = "factory_key";
@@ -75,7 +74,7 @@ public class JobDatabase extends Database {
}
private static final class Dependencies {
private static final String TABLE_NAME = DEPENDENCIES_TABLE_NAME;
private static final String TABLE_NAME = "dependency_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id";
@@ -87,8 +86,65 @@ public class JobDatabase extends Database {
}
public JobDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
private static volatile JobDatabase instance;
private final Application application;
private final DatabaseSecret databaseSecret;
public static @NonNull JobDatabase getInstance(@NonNull Application context) {
if (instance == null) {
synchronized (JobDatabase.class) {
if (instance == null) {
instance = new JobDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context));
}
}
}
return instance;
}
public JobDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
super(application, DATABASE_NAME, null, DATABASE_VERSION, new SqlCipherDatabaseHook());
this.application = application;
this.databaseSecret = databaseSecret;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "onCreate()");
db.execSQL(Jobs.CREATE_TABLE);
db.execSQL(Constraints.CREATE_TABLE);
db.execSQL(Dependencies.CREATE_TABLE);
if (DatabaseFactory.getInstance(application).hasTable("job_spec")) {
Log.i(TAG, "Found old job_spec table. Migrating data.");
migrateJobSpecsFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
}
if (DatabaseFactory.getInstance(application).hasTable("constraint_spec")) {
Log.i(TAG, "Found old constraint_spec table. Migrating data.");
migrateConstraintSpecsFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
}
if (DatabaseFactory.getInstance(application).hasTable("dependency_spec")) {
Log.i(TAG, "Found old dependency_spec table. Migrating data.");
migrateDependencySpecsFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "onUpgrade(" + oldVersion + ", " + newVersion + ")");
}
@Override
public void onOpen(SQLiteDatabase db) {
Log.i(TAG, "onOpen()");
dropTableIfPresent("job_spec");
dropTableIfPresent("constraint_spec");
dropTableIfPresent("dependency_spec");
}
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
@@ -96,7 +152,7 @@ public class JobDatabase extends Database {
return;
}
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
@@ -116,7 +172,7 @@ public class JobDatabase extends Database {
public synchronized @NonNull List<JobSpec> getAllJobSpecs() {
List<JobSpec> jobs = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) {
try (Cursor cursor = getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) {
while (cursor != null && cursor.moveToNext()) {
jobs.add(jobSpecFromCursor(cursor));
}
@@ -132,7 +188,7 @@ public class JobDatabase extends Database {
String query = Jobs.JOB_SPEC_ID + " = ?";
String[] args = new String[]{ id };
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
}
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime, @NonNull String serializedData) {
@@ -145,14 +201,14 @@ public class JobDatabase extends Database {
String query = Jobs.JOB_SPEC_ID + " = ?";
String[] args = new String[]{ id };
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
}
public synchronized void updateAllJobsToBePending() {
ContentValues contentValues = new ContentValues();
contentValues.put(Jobs.IS_RUNNING, 0);
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null);
getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null);
}
public synchronized void updateJobs(@NonNull List<JobSpec> jobs) {
@@ -160,7 +216,7 @@ public class JobDatabase extends Database {
return;
}
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
@@ -177,7 +233,6 @@ public class JobDatabase extends Database {
values.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
values.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
values.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
values.put(Jobs.MAX_INSTANCES, job.getMaxInstancesForFactory());
values.put(Jobs.LIFESPAN, job.getLifespan());
values.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
values.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
@@ -196,7 +251,7 @@ public class JobDatabase extends Database {
}
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
@@ -219,7 +274,7 @@ public class JobDatabase extends Database {
public synchronized @NonNull List<ConstraintSpec> getAllConstraintSpecs() {
List<ConstraintSpec> constraints = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) {
try (Cursor cursor = getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
constraints.add(constraintSpecFromCursor(cursor));
}
@@ -231,7 +286,7 @@ public class JobDatabase extends Database {
public synchronized @NonNull List<DependencySpec> getAllDependencySpecs() {
List<DependencySpec> dependencies = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) {
try (Cursor cursor = getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
dependencies.add(dependencySpecFromCursor(cursor));
}
@@ -254,7 +309,6 @@ public class JobDatabase extends Database {
contentValues.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
contentValues.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
contentValues.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
contentValues.put(Jobs.MAX_INSTANCES, job.getMaxInstancesForFactory());
contentValues.put(Jobs.LIFESPAN, job.getLifespan());
contentValues.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
contentValues.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
@@ -295,7 +349,6 @@ public class JobDatabase extends Database {
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_ATTEMPTS)),
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.MAX_BACKOFF)),
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.LIFESPAN)),
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)),
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_INPUT_DATA)),
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1,
@@ -313,4 +366,73 @@ public class JobDatabase extends Database {
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)),
false);
}
private @NonNull SQLiteDatabase getReadableDatabase() {
return getWritableDatabase(databaseSecret.asString());
}
private @NonNull SQLiteDatabase getWritableDatabase() {
return getWritableDatabase(databaseSecret.asString());
}
@Override
public @NonNull SQLiteDatabase getSqlCipherDatabase() {
return getWritableDatabase();
}
private void dropTableIfPresent(@NonNull String table) {
if (DatabaseFactory.getInstance(application).hasTable(table)) {
Log.i(TAG, "Dropping original " + table + " table from the main database.");
DatabaseFactory.getInstance(application).getRawDatabase().rawExecSQL("DROP TABLE " + table);
}
}
private static void migrateJobSpecsFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM job_spec", null)) {
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(Jobs.JOB_SPEC_ID, CursorUtil.requireString(cursor, "job_spec_id"));
values.put(Jobs.FACTORY_KEY, CursorUtil.requireString(cursor, "factory_key"));
values.put(Jobs.QUEUE_KEY, CursorUtil.requireString(cursor, "queue_key"));
values.put(Jobs.CREATE_TIME, CursorUtil.requireLong(cursor, "create_time"));
values.put(Jobs.NEXT_RUN_ATTEMPT_TIME, CursorUtil.requireLong(cursor, "next_run_attempt_time"));
values.put(Jobs.RUN_ATTEMPT, CursorUtil.requireInt(cursor, "run_attempt"));
values.put(Jobs.MAX_ATTEMPTS, CursorUtil.requireInt(cursor, "max_attempts"));
values.put(Jobs.MAX_BACKOFF, CursorUtil.requireLong(cursor, "max_backoff"));
values.put(Jobs.LIFESPAN, CursorUtil.requireLong(cursor, "lifespan"));
values.put(Jobs.SERIALIZED_DATA, CursorUtil.requireString(cursor, "serialized_data"));
values.put(Jobs.SERIALIZED_INPUT_DATA, CursorUtil.requireString(cursor, "serialized_input_data"));
values.put(Jobs.IS_RUNNING, CursorUtil.requireInt(cursor, "is_running"));
newDb.insert(Jobs.TABLE_NAME, null, values);
}
}
}
private static void migrateConstraintSpecsFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM constraint_spec", null)) {
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(Constraints.JOB_SPEC_ID, CursorUtil.requireString(cursor, "job_spec_id"));
values.put(Constraints.FACTORY_KEY, CursorUtil.requireString(cursor, "factory_key"));
newDb.insert(Constraints.TABLE_NAME, null, values);
}
}
}
private static void migrateDependencySpecsFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM dependency_spec", null)) {
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(Dependencies.JOB_SPEC_ID, CursorUtil.requireString(cursor, "job_spec_id"));
values.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, CursorUtil.requireString(cursor, "depends_on_job_spec_id"));
newDb.insert(Dependencies.TABLE_NAME, null, values);
}
}
}
}

View File

@@ -1,44 +1,106 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import java.util.Collection;
import java.util.Map;
@Trace
public class KeyValueDatabase extends Database {
/**
* Persists data for the {@link org.thoughtcrime.securesms.keyvalue.KeyValueStore}.
*
* This is it's own separate physical database, so it cannot do joins or queries with any other
* tables.
*/
public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase {
public static final String TABLE_NAME = "key_value";
private static final String TAG = Log.tag(KeyValueDatabase.class);
private static final String ID = "_id";
private static final String KEY = "key";
private static final String VALUE = "value";
private static final String TYPE = "type";
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "signal-key-value.db";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
KEY + " TEXT UNIQUE, " +
VALUE + " TEXT, " +
TYPE + " INTEGER)";
private static final String TABLE_NAME = "key_value";
private static final String ID = "_id";
private static final String KEY = "key";
private static final String VALUE = "value";
private static final String TYPE = "type";
KeyValueDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
KEY + " TEXT UNIQUE, " +
VALUE + " TEXT, " +
TYPE + " INTEGER)";
private static volatile KeyValueDatabase instance;
private final Application application;
private final DatabaseSecret databaseSecret;
public static @NonNull KeyValueDatabase getInstance(@NonNull Application context) {
if (instance == null) {
synchronized (KeyValueDatabase.class) {
if (instance == null) {
instance = new KeyValueDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context));
}
}
}
return instance;
}
public KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
super(application, DATABASE_NAME, null, DATABASE_VERSION, new SqlCipherDatabaseHook());
this.application = application;
this.databaseSecret = databaseSecret;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "onCreate()");
db.execSQL(CREATE_TABLE);
if (DatabaseFactory.getInstance(application).hasTable("key_value")) {
Log.i(TAG, "Found old key_value table. Migrating data.");
migrateDataFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "onUpgrade(" + oldVersion + ", " + newVersion + ")");
}
@Override
public void onOpen(SQLiteDatabase db) {
Log.i(TAG, "onOpen()");
if (DatabaseFactory.getInstance(application).hasTable("key_value")) {
Log.i(TAG, "Dropping original key_value table from the main database.");
DatabaseFactory.getInstance(application).getRawDatabase().rawExecSQL("DROP TABLE key_value");
}
}
public @NonNull KeyValueDataSet getDataSet() {
KeyValueDataSet dataSet = new KeyValueDataSet();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)){
try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)){
while (cursor != null && cursor.moveToNext()) {
Type type = Type.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)));
String key = cursor.getString(cursor.getColumnIndexOrThrow(KEY));
Type type = Type.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)));
String key = cursor.getString(cursor.getColumnIndexOrThrow(KEY));
switch (type) {
case BLOB:
@@ -67,7 +129,7 @@ public class KeyValueDatabase extends Database {
}
public void writeDataSet(@NonNull KeyValueDataSet dataSet, @NonNull Collection<String> removes) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
try {
@@ -115,6 +177,53 @@ public class KeyValueDatabase extends Database {
}
}
private @NonNull SQLiteDatabase getReadableDatabase() {
return getReadableDatabase(databaseSecret.asString());
}
private @NonNull SQLiteDatabase getWritableDatabase() {
return getWritableDatabase(databaseSecret.asString());
}
@Override
public @NonNull SQLiteDatabase getSqlCipherDatabase() {
return getWritableDatabase();
}
private static void migrateDataFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM key_value", null)) {
while (cursor.moveToNext()) {
int type = CursorUtil.requireInt(cursor, "type");
ContentValues values = new ContentValues();
values.put(KEY, CursorUtil.requireString(cursor, "key"));
values.put(TYPE, type);
switch (type) {
case 0:
values.put(VALUE, CursorUtil.requireBlob(cursor, "value"));
break;
case 1:
values.put(VALUE, CursorUtil.requireBoolean(cursor, "value"));
break;
case 2:
values.put(VALUE, CursorUtil.requireFloat(cursor, "value"));
break;
case 3:
values.put(VALUE, CursorUtil.requireInt(cursor, "value"));
break;
case 4:
values.put(VALUE, CursorUtil.requireLong(cursor, "value"));
break;
case 5:
values.put(VALUE, CursorUtil.requireString(cursor, "value"));
break;
}
newDb.insert(TABLE_NAME, null, values);
}
}
}
private enum Type {
BLOB(0), BOOLEAN(1), FLOAT(2), INTEGER(3), LONG(4), STRING(5);

View File

@@ -10,12 +10,10 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.util.List;
@Trace
public class MediaDatabase extends Database {
public static final int ALL_THREADS = -1;

View File

@@ -1,16 +1,20 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import java.util.ArrayList;
import java.util.Collection;
@@ -21,13 +25,14 @@ import java.util.Set;
/**
* IMPORTANT: Writes should only be made through {@link org.thoughtcrime.securesms.megaphone.MegaphoneRepository}.
*/
@Trace
public class MegaphoneDatabase extends Database {
public class MegaphoneDatabase extends SQLiteOpenHelper implements SignalDatabase {
private static final String TAG = Log.tag(MegaphoneDatabase.class);
private static final String TABLE_NAME = "megaphone";
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "signal-megaphone.db";
private static final String TABLE_NAME = "megaphone";
private static final String ID = "_id";
private static final String EVENT = "event";
private static final String SEEN_COUNT = "seen_count";
@@ -35,19 +40,65 @@ public class MegaphoneDatabase extends Database {
private static final String FIRST_VISIBLE = "first_visible";
private static final String FINISHED = "finished";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
EVENT + " TEXT UNIQUE, " +
SEEN_COUNT + " INTEGER, " +
LAST_SEEN + " INTEGER, " +
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
EVENT + " TEXT UNIQUE, " +
SEEN_COUNT + " INTEGER, " +
LAST_SEEN + " INTEGER, " +
FIRST_VISIBLE + " INTEGER, " +
FINISHED + " INTEGER)";
FINISHED + " INTEGER)";
MegaphoneDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
private static volatile MegaphoneDatabase instance;
private final Application application;
private final DatabaseSecret databaseSecret;
public static @NonNull MegaphoneDatabase getInstance(@NonNull Application context) {
if (instance == null) {
synchronized (MegaphoneDatabase.class) {
if (instance == null) {
instance = new MegaphoneDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context));
}
}
}
return instance;
}
public MegaphoneDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
super(application, DATABASE_NAME, null, DATABASE_VERSION, new SqlCipherDatabaseHook());
this.application = application;
this.databaseSecret = databaseSecret;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "onCreate()");
db.execSQL(CREATE_TABLE);
if (DatabaseFactory.getInstance(application).hasTable("megaphone")) {
Log.i(TAG, "Found old megaphone table. Migrating data.");
migrateDataFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "onUpgrade(" + oldVersion + ", " + newVersion + ")");
}
@Override
public void onOpen(SQLiteDatabase db) {
Log.i(TAG, "onOpen()");
if (DatabaseFactory.getInstance(application).hasTable("megaphone")) {
Log.i(TAG, "Dropping original megaphone table from the main database.");
DatabaseFactory.getInstance(application).getRawDatabase().rawExecSQL("DROP TABLE megaphone");
}
}
public void insert(@NonNull Collection<Event> events) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
try {
@@ -65,14 +116,14 @@ public class MegaphoneDatabase extends Database {
}
public @NonNull List<MegaphoneRecord> getAllAndDeleteMissing() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
List<MegaphoneRecord> records = new ArrayList<>();
db.beginTransaction();
try {
Set<String> missingKeys = new HashSet<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)) {
try (Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
String event = cursor.getString(cursor.getColumnIndexOrThrow(EVENT));
int seenCount = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_COUNT));
@@ -93,7 +144,7 @@ public class MegaphoneDatabase extends Database {
String query = EVENT + " = ?";
String[] args = new String[]{missing};
databaseHelper.getWritableDatabase().delete(TABLE_NAME, query, args);
db.delete(TABLE_NAME, query, args);
}
db.setTransactionSuccessful();
@@ -111,7 +162,7 @@ public class MegaphoneDatabase extends Database {
ContentValues values = new ContentValues();
values.put(FIRST_VISIBLE, time);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
getWritableDatabase().update(TABLE_NAME, values, query, args);
}
public void markSeen(@NonNull Event event, int seenCount, long lastSeen) {
@@ -122,7 +173,7 @@ public class MegaphoneDatabase extends Database {
values.put(SEEN_COUNT, seenCount);
values.put(LAST_SEEN, lastSeen);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
getWritableDatabase().update(TABLE_NAME, values, query, args);
}
public void markFinished(@NonNull Event event) {
@@ -132,13 +183,38 @@ public class MegaphoneDatabase extends Database {
ContentValues values = new ContentValues();
values.put(FINISHED, 1);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
getWritableDatabase().update(TABLE_NAME, values, query, args);
}
public void delete(@NonNull Event event) {
String query = EVENT + " = ?";
String[] args = new String[]{event.getKey()};
databaseHelper.getWritableDatabase().delete(TABLE_NAME, query, args);
getWritableDatabase().delete(TABLE_NAME, query, args);
}
private @NonNull SQLiteDatabase getWritableDatabase() {
return getWritableDatabase(databaseSecret.asString());
}
@Override
public @NonNull SQLiteDatabase getSqlCipherDatabase() {
return getWritableDatabase();
}
private static void migrateDataFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM megaphone", null)) {
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(EVENT, CursorUtil.requireString(cursor, "event"));
values.put(SEEN_COUNT, CursorUtil.requireInt(cursor, "seen_count"));
values.put(LAST_SEEN, CursorUtil.requireLong(cursor, "last_seen"));
values.put(FIRST_VISIBLE, CursorUtil.requireLong(cursor, "first_visible"));
values.put(FINISHED, CursorUtil.requireInt(cursor, "finished"));
newDb.insert(TABLE_NAME, null, values);
}
}
}
}

View File

@@ -13,7 +13,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
@@ -23,7 +22,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@Trace
public class MentionDatabase extends Database {
static final String TABLE_NAME = "mention";

View File

@@ -149,6 +149,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract Optional<InsertResult> insertMessageInbox(IncomingMediaMessage retrieved, String contentLocation, long threadId) throws MmsException;
public abstract Pair<Long, Long> insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId);
public abstract Optional<InsertResult> insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) throws MmsException;
public abstract @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp);
public abstract long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener);
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;

View File

@@ -72,7 +72,6 @@ import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo;
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.SqlUtil;
@@ -96,7 +95,6 @@ import java.util.UUID;
import static org.thoughtcrime.securesms.contactshare.Contact.Avatar;
@Trace
public class MmsDatabase extends MessageDatabase {
private static final String TAG = MmsDatabase.class.getSimpleName();
@@ -1385,6 +1383,11 @@ public class MmsDatabase extends MessageDatabase {
return new Pair<>(messageId, threadId);
}
@Override
public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
throw new UnsupportedOperationException();
}
@Override
public void markIncomingNotificationReceived(long threadId) {
notifyConversationListeners(threadId);

View File

@@ -32,7 +32,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.whispersystems.libsignal.util.Pair;
@@ -44,7 +43,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@Trace
public class MmsSmsDatabase extends Database {
@SuppressWarnings("unused")

View File

@@ -10,7 +10,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
@@ -19,7 +18,6 @@ import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
@Trace
public class PushDatabase extends Database {
private static final String TAG = PushDatabase.class.getSimpleName();

View File

@@ -10,11 +10,15 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import net.sqlcipher.SQLException;
import net.sqlcipher.database.SQLiteConstraintException;
import org.signal.core.util.logging.Log;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
@@ -25,6 +29,8 @@ import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
@@ -39,7 +45,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
import org.thoughtcrime.securesms.storage.StorageSyncModels;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Bitmask;
import org.thoughtcrime.securesms.util.CursorUtil;
@@ -79,7 +84,6 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Trace
public class RecipientDatabase extends Database {
private static final String TAG = RecipientDatabase.class.getSimpleName();
@@ -126,6 +130,7 @@ public class RecipientDatabase extends Database {
private static final String MENTION_SETTING = "mention_setting";
private static final String STORAGE_PROTO = "storage_proto";
private static final String LAST_GV1_MIGRATE_REMINDER = "last_gv1_migrate_reminder";
private static final String LAST_SESSION_RESET = "last_session_reset";
public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
private static final String SORT_NAME = "sort_name";
@@ -344,7 +349,8 @@ public class RecipientDatabase extends Database {
MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ", " +
STORAGE_PROTO + " TEXT DEFAULT NULL, " +
CAPABILITIES + " INTEGER DEFAULT 0, " +
LAST_GV1_MIGRATE_REMINDER + " INTEGER DEFAULT 0);";
LAST_GV1_MIGRATE_REMINDER + " INTEGER DEFAULT 0, " +
LAST_SESSION_RESET + " BLOB DEFAULT NULL);";
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
" FROM " + TABLE_NAME +
@@ -1219,7 +1225,11 @@ public class RecipientDatabase extends Database {
}
static @NonNull RecipientSettings getRecipientSettings(@NonNull Context context, @NonNull Cursor cursor) {
long id = CursorUtil.requireLong(cursor, ID);
return getRecipientSettings(context, cursor, ID);
}
static @NonNull RecipientSettings getRecipientSettings(@NonNull Context context, @NonNull Cursor cursor, @NonNull String idColumnName) {
long id = CursorUtil.requireLong(cursor, idColumnName);
UUID uuid = UuidUtil.parseOrNull(CursorUtil.requireString(cursor, UUID));
String username = CursorUtil.requireString(cursor, USERNAME);
String e164 = CursorUtil.requireString(cursor, PHONE);
@@ -1255,9 +1265,9 @@ public class RecipientDatabase extends Database {
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
MaterialColor color;
byte[] profileKey = null;
byte[] profileKeyCredential = null;
MaterialColor color;
byte[] profileKey = null;
ProfileKeyCredential profileKeyCredential = null;
try {
color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor);
@@ -1276,10 +1286,17 @@ public class RecipientDatabase extends Database {
if (profileKeyCredentialString != null) {
try {
profileKeyCredential = Base64.decode(profileKeyCredentialString);
} catch (IOException e) {
Log.w(TAG, e);
profileKeyCredential = null;
byte[] columnDataBytes = Base64.decode(profileKeyCredentialString);
ProfileKeyCredentialColumnData columnData = ProfileKeyCredentialColumnData.parseFrom(columnDataBytes);
if (Arrays.equals(columnData.getProfileKey().toByteArray(), profileKey)) {
profileKeyCredential = new ProfileKeyCredential(columnData.getProfileKeyCredential().toByteArray());
} else {
Log.i(TAG, "Out of date profile key credential data ignored on read");
}
} catch (InvalidInputException | IOException e) {
Log.w(TAG, "Profile key credential column data could not be read", e);
}
}
}
@@ -1499,6 +1516,30 @@ public class RecipientDatabase extends Database {
return 0;
}
public void setLastSessionResetTime(@NonNull RecipientId id, DeviceLastResetTime lastResetTime) {
ContentValues values = new ContentValues(1);
values.put(LAST_SESSION_RESET, lastResetTime.toByteArray());
update(id, values);
}
public @NonNull DeviceLastResetTime getLastSessionResetTimes(@NonNull RecipientId id) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
try (Cursor cursor = db.query(TABLE_NAME, new String[] {LAST_SESSION_RESET}, ID_WHERE, SqlUtil.buildArgs(id), null, null, null)) {
if (cursor.moveToFirst()) {
try {
return DeviceLastResetTime.parseFrom(CursorUtil.requireBlob(cursor, LAST_SESSION_RESET));
} catch (InvalidProtocolBufferException | SQLException e) {
Log.w(TAG, e);
return DeviceLastResetTime.newBuilder().build();
}
}
}
return DeviceLastResetTime.newBuilder().build();
}
public void setCapabilities(@NonNull RecipientId id, @NonNull SignalServiceProfile.Capabilities capabilities) {
long value = 0;
@@ -1579,23 +1620,30 @@ public class RecipientDatabase extends Database {
/**
* Updates the profile key credential as long as the profile key matches.
*/
public void setProfileKeyCredential(@NonNull RecipientId id,
@NonNull ProfileKey profileKey,
@NonNull ProfileKeyCredential profileKeyCredential)
public boolean setProfileKeyCredential(@NonNull RecipientId id,
@NonNull ProfileKey profileKey,
@NonNull ProfileKeyCredential profileKeyCredential)
{
String selection = ID + " = ? AND " + PROFILE_KEY + " = ?";
String[] args = new String[]{id.serialize(), Base64.encodeBytes(profileKey.serialize())};
ContentValues values = new ContentValues(1);
values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(profileKeyCredential.serialize()));
ProfileKeyCredentialColumnData columnData = ProfileKeyCredentialColumnData.newBuilder()
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.setProfileKeyCredential(ByteString.copyFrom(profileKeyCredential.serialize()))
.build();
values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(columnData.toByteArray()));
SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
if (update(updateQuery, values)) {
// TODO [greyson] If we sync this in future, mark dirty
//markDirty(id, DirtyState.UPDATE);
boolean updated = update(updateQuery, values);
if (updated) {
Recipient.live(id).refresh();
}
return updated;
}
private void clearProfileKeyCredential(@NonNull RecipientId id) {
@@ -2594,7 +2642,7 @@ public class RecipientDatabase extends Database {
private static void updateProfileValuesForMerge(@NonNull ContentValues values, @NonNull RecipientSettings settings) {
values.put(PROFILE_KEY, settings.getProfileKey() != null ? Base64.encodeBytes(settings.getProfileKey()) : null);
values.put(PROFILE_KEY_CREDENTIAL, settings.getProfileKeyCredential() != null ? Base64.encodeBytes(settings.getProfileKeyCredential()) : null);
values.putNull(PROFILE_KEY_CREDENTIAL);
values.put(SIGNAL_PROFILE_AVATAR, settings.getProfileAvatar());
values.put(PROFILE_GIVEN_NAME, settings.getProfileName().getGivenName());
values.put(PROFILE_FAMILY_NAME, settings.getProfileName().getFamilyName());
@@ -2716,7 +2764,7 @@ public class RecipientDatabase extends Database {
private final int expireMessages;
private final RegisteredState registered;
private final byte[] profileKey;
private final byte[] profileKeyCredential;
private final ProfileKeyCredential profileKeyCredential;
private final String systemDisplayName;
private final String systemContactPhoto;
private final String systemPhoneLabel;
@@ -2755,7 +2803,7 @@ public class RecipientDatabase extends Database {
int expireMessages,
@NonNull RegisteredState registered,
@Nullable byte[] profileKey,
@Nullable byte[] profileKeyCredential,
@Nullable ProfileKeyCredential profileKeyCredential,
@Nullable String systemDisplayName,
@Nullable String systemContactPhoto,
@Nullable String systemPhoneLabel,
@@ -2890,7 +2938,7 @@ public class RecipientDatabase extends Database {
return profileKey;
}
public @Nullable byte[] getProfileKeyCredential() {
public @Nullable ProfileKeyCredential getProfileKeyCredential() {
return profileKeyCredential;
}

View File

@@ -9,7 +9,6 @@ import net.sqlcipher.Cursor;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import java.util.HashMap;
@@ -20,7 +19,6 @@ import java.util.Map;
/**
* The backing datastore for {@link RemappedRecords}. See that class for more details.
*/
@Trace
public class RemappedRecordsDatabase extends Database {
public static final String[] CREATE_TABLE = { Recipients.CREATE_TABLE,

View File

@@ -3,17 +3,14 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import androidx.annotation.Nullable;
import com.google.android.gms.vision.Tracker;
import net.sqlcipher.Cursor;
import net.sqlcipher.SQLException;
import net.sqlcipher.database.SQLiteQueryStats;
import net.sqlcipher.database.SQLiteStatement;
import net.sqlcipher.database.SQLiteTransactionListener;
import org.thoughtcrime.securesms.tracing.Tracer;
import org.signal.core.util.tracing.Tracer;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import java.util.HashMap;
import java.util.Locale;

View File

@@ -10,12 +10,10 @@ import com.annimon.stream.Stream;
import net.sqlcipher.Cursor;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.tracing.Trace;
/**
* Contains all databases necessary for full-text search (FTS).
*/
@Trace
public class SearchDatabase extends Database {
public static final String SMS_FTS_TABLE_NAME = "sms_fts";

View File

@@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -22,7 +21,6 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
@Trace
public class SessionDatabase extends Database {
private static final String TAG = SessionDatabase.class.getSimpleName();

Some files were not shown because too many files have changed in this diff Show More