Compare commits

...

463 Commits

Author SHA1 Message Date
Andrew bdb6e7d12b
Merge dev into master for 1.17.5 2023-12-11 17:22:31 +10:30
Andrew 377460a60f
build: update version code 2023-12-11 16:57:13 +10:30
andrew a57b7ef121 build: update version code 2023-12-11 15:22:13 +10:30
Andrew b96a5c561e
Merge pull request #1371 from 0x330a/remove-call-server
Remove unsupported call servers
2023-12-11 15:16:22 +10:30
0x330a 9a83daa53f fix: remove the two unsupported servers 2023-12-08 11:44:28 +11:00
wafflesvsfrankie 7fe40ea9f1
Content descriptions (#1358)
* Adding in Content Description for automated testing including: Community input (URL) box, changes `Configuration message` to `Control message` and also `Message Body` to `Message body`Created checkout page along with checkoutItems and checkout cart functionality

* Forgot the button at the end
2023-11-20 15:32:57 +11:00
0x330a 9d02eb33c7 build: update version code 2023-10-13 16:48:47 +11:00
0x330a b6bb586509 feat: add foreground types to key caching service and call service 2023-10-13 14:32:04 +11:00
0x330a 82cbf830ae
fix: need quotes on job type key 2023-10-11 17:52:04 +11:00
0x330a c1102a2a50
build: update build number 2023-10-11 17:37:55 +11:00
0x330a 862a47e7e3
feat: add drop attachment download migration in case there are a lot of pending failed attachment downloads 2023-10-11 17:37:39 +11:00
0x330a 6f22eb659b
fix: use the context compat register receiver to fix Android 14 crash (#1338) 2023-10-11 17:12:28 +11:00
0x330a cb1b5b0f78
build: update version name and code 2023-10-09 10:12:51 +11:00
0x330a 20df73355d
fix: or should be and (#1334) 2023-10-06 13:56:47 +11:00
0x330a 84bc1dcb6b
Update session libsodium (#1326)
* chore: update dependencies for the libsodium dependency and replace aar

* fix: update gradle dependencies to work with integration tests

* fix: test

* refactor: make it use espresso wait

* refactor: bring compose dependencies back to stable releases
2023-10-04 15:17:27 +11:00
0x330a 77a18e337b
Maybe fix oob in media preview adapter cursor adapter (#1324)
* fix: maybe fix oob exception?

* fix: attachment download was broken again

* refactor: wrap in an if and log warning in the bad state
2023-10-04 15:17:07 +11:00
0x330a 443ddfa370
Merge remote-tracking branch 'upstream/master' into dev 2023-09-28 14:01:58 +10:00
Andrew e124d442ef
Ignore unknown fields in push V2 json 2023-09-22 10:02:39 +09:30
Andrew 698b853716
Make playDebug the default variant 2023-09-21 20:52:57 +09:30
andrew 0cd0ac9c75 Ignore unknown fields in push V2 json 2023-09-21 19:44:54 +09:30
Kee Jefferys 2093cbc5e4
Update issue templates (#1253)
Remove old templates and replace with templates which are being used on iOS and desktop
2023-09-19 15:16:38 +10:00
Andrew 984c3763b6
Remove unused longmessage package 2023-09-07 17:09:00 +09:30
Andrew 833b30fc14
Increase activity_home_outdated_client_config text size to 11sp 2023-09-07 17:05:28 +09:30
0x330a 380d6694ea
fix: treat self sends correctly when no contact information for ourself (#1318) 2023-09-07 16:54:38 +10:00
Andrew b4eb54ee89
Fix post notifications permission request
This is not available on old APIs
2023-09-05 14:59:39 +09:30
andrew 303aacb2e3 Fix post notifications permission request 2023-09-04 14:49:28 +09:30
hjubb 99e5ed3db7 Merge remote-tracking branch 'upstream/dev' 2023-09-01 13:50:33 +10:00
0x330a 29275cef51
build: update build number (#1310) 2023-09-01 13:42:23 +10:00
0x330a 4daa3e6923
feat: add huawei build folder to gitignore (#1308) 2023-08-29 14:43:48 +10:00
Andrew 99dca1cda7
Use separate keys for huawei shared prefs 2023-08-29 13:50:41 +09:30
andrew 0e0cbf112b Use separate keys for huawei shared prefs 2023-08-28 11:18:51 +09:30
0x330a 2466d9b4c0
[SES-1002] Synced blind requests (#1303)
* feat: update config to use blinded-msg-requests pr

* feat: add block community message requests bool to protos

* feat: add everything needed for recipientDB to have blocked community requests potentially

* feat: add db migrations

* feat: add sending community block flags and preference options

* feat: add parsing block request flag

* fix: open group message requests were broken anyway

* fix: delete all encoded open group inbox ID bs, fix privacy settings using user config as privacy store

* feat: initial creation sets flag, rename to match libsession implementation value

* fix: recipient blinded checks from open group message for blocking community requests on blinded ID version of recipient, use correct (inverted) values from before for checking polling and empty states etc

* fix: pr comments for view model factory context ref, simplified user config object check for category in PrivacySettingsPreferenceFragment

* fix: pr comments

* fix: migrate some dependencies and functionality out of VM into repository to remove content resolver and context dependecy so tests pass again

* refactor: better naming for hidesInputBar and add more tests for expected recipient view states

* fix: use contact information as opposed to active conversations

* fix: PR comments
2023-08-28 09:51:48 +10:00
Andrew f6345c86ce
[SES-469] Update push notifications
- Utilise new push endpoint
- Add huawei support
2023-08-23 11:42:19 +09:30
andrew 7861eb25c2 Improve param naming for isPushEnabled 2023-08-17 22:36:05 +09:30
andrew 296c5d743f Limit scope of huawei dependencies 2023-08-17 22:34:13 +09:30
andrew ae9d3810e1 Fix website flavor TokenFetcher 2023-08-17 14:02:22 +09:30
andrew 550955f530 Add huawei strings 2023-08-16 20:54:07 +09:30
andrew 62cd0f68f0 Improve huawei guarding 2023-08-16 19:23:13 +09:30
andrew d308f381d9 Disable fcm preference while register request is in flight 2023-08-16 13:27:28 +09:30
andrew 0aa5dc7969 Convert NotificationPreferenceFragment to Kotlin 2023-08-16 13:25:45 +09:30
andrew 7c8882e1f3 Fix project reference in task 2023-08-15 18:42:49 +09:30
andrew b09b6836d4 Add huawei info to BUILDING.md 2023-08-15 14:52:40 +09:30
andrew 4c8f38df72 Require command line arg to include huawei dependencies 2023-08-15 13:51:21 +09:30
andrew b987ba719b Remove unused longmessage package 2023-08-15 10:29:20 +09:30
andrew ed7ce36402 Cleanup gradle 2023-08-14 10:31:22 +09:30
andrew 1d9fb13809 Move plugin to huawei flavor 2023-08-11 19:29:31 +09:30
andrew 9899b37f43 Move huawei manifest metadata to huawei flavor 2023-08-11 17:45:56 +09:30
andrew 77100231d2 Fix logs 2023-08-11 17:37:35 +09:30
andrew 16177d5cb1 Fix logs 2023-08-11 17:36:55 +09:30
andrew 9813b526f0 Merge branch 'dev' into huawei-3 2023-08-10 13:18:59 +09:30
Andrew c9417b2fec
Fix Dialog#button default (#1299) 2023-08-10 13:17:28 +09:30
andrew 309293df63 Improve logs in SingleRecipientNotificationBuilder 2023-08-10 10:36:52 +09:30
Andrew 34fc6ee6cb
Remove contactshare package (#1288) 2023-08-09 14:12:07 +09:30
andrew e1e5c5937b Use live endpoint for legacy groups 2023-08-09 12:42:53 +09:30
andrew e60c05cee0 Fix Huawei message parsing 2023-08-09 12:18:22 +09:30
andrew 5a5b2f593f Refactor to accept Huawei token from getToken() and/or onNewToken() 2023-08-09 10:08:42 +09:30
Andrew 2f42fe9d0d
Remove unused classes (#1289) 2023-08-08 14:40:36 +09:30
andrew c8dcfbf32c Merge branch 'dev' into huawei-3 2023-08-07 23:09:54 +09:30
Andrew 9cf99480d6
Fix shareLogs() canceled by early dismiss() (#1295) 2023-08-07 23:01:03 +09:30
andrew d6380c5e63 Fix website flavor 2023-08-07 09:50:51 +09:30
andrew 24f7bb2b45 Move files 2023-08-06 22:39:49 +09:30
andrew bcf925c132 Cleanup 2023-08-06 22:22:39 +09:30
andrew a27f81db30 Fix Huawei push notifications 2023-08-04 13:36:09 +09:30
andrew cc6f880665 ... 2023-08-03 14:14:55 +09:30
andrew 41d24ef2c3 ... 2023-08-02 14:55:31 +09:30
andrew 7ee9b14247 Fix PNModeActivity DI 2023-08-02 10:41:50 +09:30
Andrew cd1a52399e Increase activity_home_outdated_client_config text size to 11sp 2023-07-31 22:18:34 +09:30
andrew d1e22ca369 Fix 2023-07-27 13:53:33 +09:30
andrew 7be1f092f9 More Huawei stuff 2023-07-26 22:09:24 +09:30
andrew 4738c9b4f9 Fix v2 2023-07-26 14:48:15 +09:30
andrew d3ea4e2e30 Fix 2023-07-26 14:36:06 +09:30
Andrew 55216875ac Connect Huawei push notifications 2023-07-26 10:48:20 +09:30
andrew 34990b13d3 ... 2023-07-25 14:49:41 +09:30
andrew 01e9d15872 Add Huawei flavor 2023-07-25 11:38:06 +09:30
andrew 58cda9ba4a Fix website 2023-07-25 10:32:41 +09:30
andrew 002793baed Merge branch 'dev' into add-unregister 2023-07-24 09:52:11 +09:30
Andrew d39cf2754c
[SES-557] Update MessageDetailActivity with Compose 2023-07-21 12:17:52 +09:30
andrew fbb2172739 Fix buttons 2023-07-21 11:50:42 +09:30
andrew d0415c5bf1 Merge branch 'dev' into comp 2023-07-21 10:42:13 +09:30
Andrew 9af6dc9265
[SES-205] Fix message clipping 2023-07-20 14:02:31 +09:30
Andrew fbf448f889
[SES-206, SES-359] Enable reply with voice and fix layout 2023-07-20 14:01:12 +09:30
andrew b25f0e6d27 Move voice below quote in message view 2023-07-20 13:22:52 +09:30
andrew 47d1c657f3 Enable reply with attachment 2023-07-20 13:21:58 +09:30
andrew fc8a92998c Fix message clipping 2023-07-19 15:04:30 +09:30
andrew 452db6dfa3 Make reply safer 2023-07-18 13:36:08 +09:30
andrew 9a84f6c67b Merge branch 'dev' into comp 2023-07-18 12:04:56 +09:30
hjubb 1bb3939930 Merge branch 'dev' 2023-07-14 18:47:39 +10:00
hjubb e7608763a0 fix: tests pass with forcing config 2023-07-14 18:47:10 +10:00
hjubb d4ab49ebbb build: submodule build command update 2023-07-14 18:31:25 +10:00
hjubb 9523953bd9 build: update build number 2023-07-14 18:29:47 +10:00
hjubb 976500e8f9 Merge branch 'dev' 2023-07-14 18:29:38 +10:00
0x330a ac18f1cbfe
Integrate shared libsession-util library (#1096)
* feat: add some config db basics and DI for it, make the user profile optional, start looking at integrate building from initial dump

* update: get latest util library submodule update

* refactor: fix compile for refactored API

* refactor: naming consistent with library

* feat: add in config storage and injection to common places, managing lifecycle of native instances

* refactor: config database changes, new protos, adding in support for config base namespace queries

* refactor: config query and store use the same format as other platforms

* feat: add batch snode calls and try to poll from all the config namespaces

* fix: add optional namespace in signature and params

* feat: add raw requests for modifying expiry and getting expiries

* feat: add some base config migration logic, start implementing wrappers for conversation and expiry types

* chore: update libsession base

* feat: start integrating conversation wrapper functions

* feat: add basic conversation info volatile types and implementations, start working on tests

* feat: more common library wrapper implementation and test

* fix: tests and compile issues

* fix: fix tests, don't use iterables

* feat: add all iterators and tests

* feat: add in more config factory for volatile

* feat: update request responses and their appropriate processing

* feat: add storage with hashes and some basic profile update logic in config factory probably move that somewhere else

* feat: adding config sync functionality, refactoring jobs to execute in suspend context to do some nice coroutine execution

* refactor: moving some properties around so we have access in libsession

* feat: expand on the config sync job, finish basic implementation to test against

* feat: add forced config sync

* feat: syncs the user profile stuff for now, and errors back to placeholder instead of unknown recipient

* feat: add basic message read logic for synchronizing last reads, need to modify the query to use the last seen instead of the unread count in a subquery possibly for thread display record

* feat: add broken unreads everywhere

* fix: unreads work now for incoming messages, need to sync conv volatile properly still

* feat: batching poll responses properly and handling groups properly

* fix: replace the mark read receiver (from notifications) to use the new set last seen mark read logic

* feat: update to the group list branch

* fix: compile errors from updating library to use latest branch, now requires cmake 3.22.1

* fix: fix the contact tests

* fix: getters weren't getters properly in the config factory, fixed new onboarding from configs

* feat: add the last seen

* feat: start adding user groups wrapper objects

* refactor: add more else branches for unimplemented types

* feat: buffer the last read when in conversation

* feat: add basic contact logic for setting local contact state. Need to implement handling properly

* refactor: trying to just include blocked status for now in updating contacts

* fix: add some more contact syncing: nicknames, approved statuses, blocked statuses

* feat: start implementing hashes in shared lib and refactoring

* feat: start to implement group list info classes and wrappers and refactor to use library based hashes

* feat: incorporate hashes from library, more wrapper for user groups and serialization from c++

* feat: adding more serialization changes for community base info and user groups LGC

* feat: adding more serialize deserialize to legacy closed groups

* feat: finish serial/deserial helper

* feat: just implement deserialize community info

* refactor: refactor tests and wrappers to use less pointers, finish implementing user groups API

* feat: finish latest wrappers fix tests and continue building default generation functions. refactor defaults to be used if no stored data blob in DB

* feat: more usergroup functionality, storage functionality for checking pinned status, adding pinned status for NTS/contacts, move community info parse full url to base community, add StorageProtocol logic for group info

* feat: adding user groups to the list of user configs, refactorign some of the config factory to fetch the user configs easier. Add handling for polling user group namespace

* feat: implement the default user config list

* feat: add user group config handling

* chore: extra missed existing group

* refactor: use existing lookup for objects in wrappers so they don't overwrite missing values

* feat: add contacts expiry serialization/deserialization, more LGC, timestamps to add closed group encryption info (for latest tracking)

* refactor: change how expiration timer works for contacts, set the expiration timer for those conversations in handling contact configs

* feat: add expiration updates via config for contacts as well

* feat: add almost all group editing cases, need to hook into the thread deletion for groups in the user groups

* feat: open group joining should work now

* feat: add groups to configs for push

* fix: handling user group updates bug fix for closed groups instead of all groups

* fix: open group sync persistence

* feat: add in activity finish if recipient no longer exists (deleted thread) from sync

* feat: support avatar removal from shared library

* feat: support thread deletion and refactoring a lot of getOrCreateThread references to go via storage or assume they are correctly set to hook into the contact and volatile creation during thread creation

* fix: database update not deleting in certain circumstances, storage persisting and removing the volatile convo info for thread deletion / creation, NTS hidden getter values in shared library

* refactor: make update listener visibility package

* refactor: update kotlin

* feat: update dependencies and support outdated config messages, refactor config factory to return null configs if new configs not supported

* feat: update shared library to use priority only, fix compile errors, fix group member sync problem

* fix: compile error

* fix: profile avatar fixes for local user now that we aren't setting local user profile key

* Revert "fix: profile avatar fixes for local user now that we aren't setting local user profile key"

This reverts commit 3f569e3403.

* refactor: let the local number update recipient details in profile manager

* fix: don't recreate thread after leaving

* fix: fix up the duplicate thread creation in the message receive handler

* fix: fix the placeholder rendering on new messages, add in extra context logging for adding contacts and preventing new thread creation on new messages of various types

* feat: add test theme for xml layout previews

* feat: add shortened hex for session IDs throughout, replace nullable getName with null in underlying contacts for individual contacts, build shared lib with release mode, remove todo, fix broken unit test

* feat: setup android unit tests for verifying storage behaviours and state of shared configs

* feat: adding dependencies to try and get android tests working, fixing bug with initial config not syncing properly

* fix: remove hilt testing, add spy on app context storage field instead, update libsession-util to fixed sodium cmake branch

* refactor: use PR version of libsession-util to test cmake build

* fix: new build on normal repo

* feat: new libsession util commit

* refactor: remove the old custom build libsodium stuff from cmake

* feat: update libsession module

* fix: add legacy config subscription to the home activity to enable showing banner at any time

* fix: pinned status for communities and groups, group last read time being set to snodeapi.now on finish joining

* fix: some open group volatile convo fix for last read timer being set. Need to investigate further

* fix: prevent blocking local number

* fix: adding in more checks for open group inbox recipients before being saved to the shared configs. Prevent sending typing indicator for blocked users

* fix: add blocked check for read receipt and updating expiring messages

* fix: another contact recipient config library call removed for non-standard IDs

* fix: another ID check

* fix: don't process thread creation for user is sender && recipient (sync message) for message request responses

* refactor: mark as read on open and use less buffer time

* fix: finally fix the darn unread count issue by

* fix: removing debug logs, adding failure error handling logs for expiry message updater, properly using the message thread ID created for the expiring messages. Process the non-thread messages properly with await in BatchMessageReceiveJob

* fix: checking the last read open to message and make sure that scroll behaviour matches expected, fix the config sync job not deleting ALL old hashes only latest

* refactor: try to add a retry logic to config sync job in case of snode failure

* build: update submodule

* fix: remove user notifications for leaving group to prevent synced device issues, don't create thread in messages for new closed groups, includei nactive groups in the deletion queries for merging group configs

* feat: use blinded message count for banner also

* refactor: remove some logging, don't use blinded conversations in the list

* fix: don't set the read flag in update notifications, some roundabout logic for first loads and scrolling to last known positions

* refactor: merge changes, re-add the group check in unapproved messages

* fix: re-poll on fail in case that was breaking anything

* fix: pinning groups and notifying list listeners in threadDb.setPinned

* feat: add in TTL extension subrequest and builder, enable extending TTLs for all latest config messages in poll as subrequest

* feat: add block to the delete all message requests, only if they're not open group inbox contacts

* refactor: disable edit text for non contacts

* refactor: let the user display name return "You" for local user

* fix: prevent NTS self create thread on user view bind

* refactor: remove populate public key cache if needed call which seems unnecessary at that point, maybe UserView refs have changed since 2020

* refactor: use just first visible instead of completely visible, merge message sender changes

* fix: prevent block of users in delete all

* fix: self sync sync message failures for default values

* feat: update libsession-util, adjust docs, update mms and sms to use message sent timestamp instead of -1 for last read in the thread

* fix: some compile issues in tests and some TODOs for things to do before merge

* fix: handle recyclerview scrolled on scroll to first unread if it's the first load

* fix: added more migration code for deleting unnecessary threads and groups, fixed a post-migration last seen issue on last item (current read is now), comment out actual network sync while testing migrations

* feat: adding a force new configs flag and logic for timestamp handling / forced configs, fix issue with handling legacy messages

* refactor: re-add the sending of configs

* fix: don't add contacts if they don't exist in the profile manager

* [wip]
fix: trying to consolidate prof pic and key properly

* feat: add logs and fix compile issue with a themes.xml entry, add removing profile picture into logic for profile manager

* fix: force has sent for local user, only prevent setting last seen for open group recipients, allow empty user pics to trigger config sync in settings

* fix: nts threads

* fix: open group avatar loop for open groups we have left

* feat: add a wrapper hash to track home diff util changes for wrapper contact recipient info, add test for dirty state in double set

* feat: add a dump in there as well

* refactor: more test code refactor

* fix: update last seen if later than current

* fix: open group threads and avatar downloads

* fix: add max size and maybe fix the non-200 sub requests for batches (for 421s in particular)

* fix: open group comparison issues potentially, have to update some more outgoing message open group flags for visibility of details etc

* Updated to the latest libSession-util

* Updated logic to delete legacy groups when kicked/left

* Added the legacy group 'joined_at' value

* Replaced incorrect character in JNI

* Fixed an issue where the group keyPair was getting encoded incorrectly

* Updated the code to ignore outdated legacy group control message changes

* Updated the code to ignore messages invalidated by the config

* [Review] Updated the poller to process config messages before standard

* Cleaned up the outdated message logic

* Fixed inverted config dropping flags

* Fixed an issue where the joining a community would read all messages

Stopped using a reversed RecyclerView in all cases (caused the unread issue)
Updated the logic to jump to the newly sent message when sending a message (to be consistent with other platforms)
Updated the logic to refresh the DB unread count when the cursor receives an update

* Updated the conversation to highlight the first unread message on open

* Fixed a couple of bugs with the highlighting

* Fixed a bug where the user profile picture wasn't downloading correctly

* feat: add all namespaces to delete all messages request and signature verification data

* fix: merge namespace hashes for signature returned and

* fix: import correct scroll to bottom

* build: update version code and name

* fix: initial contact generation fix for existing blinded contacts

* fix: initial convo generation fix for existing blinded convos (?)

* fix: conversation unread not doing a check for standard ID prefix

* fix: thread ID not being created for legacy config messages

* fix: don't treat 404 as bad snode

* fix: don't add retrieve profile job if we have one for that address

* build: update build code

* fix: reduce attempts for downloading image, invert unreachable type check

* fix: attempting to fix preventing message processing if group thread is not active for closed groups and initial contact dump only allows conversations with thread, may need further optimisations though

* feat: Added an unread marker and search result focus highlighting

* fix: empty set in appropriate places for current closed groups

* build: update build version code

* fix: fix the notifications and request at appropriate time

* refactor: remove debug logging for thread create and delete

* build: update build number

* fix: new community doesn't break persisting config if the .add request fails

* build: trying to track down broken retrieve avatar job

* feat: update to latest libsession dev

* fix: maybe fix avatar download for new messages

* fix: 404s causing snode errors and trying to retrieve avatars that have already 404'd a lot

* fix: closed group creation sets thread date to formation timestamp

* build: update version code

* build: update version code

* build: remove debuggable release build

* fix: use new permissions for external attachments

* build: update version code

* chore: remove debug logs

* fix: tests and main thread blocking db fetch for path status view

* wip: trying to track down failure to mark conversation as read in delayed group add

* wip: add more logs for initial last Read sync of communities

* wip: maybe the volatile is being updated with 0 on batch message receive?

* fix: maybe syncing read statuses are working now

* chore: remove debug logs

* build: update build number

* fix: trying to improve performance

* fix: add close to banner

* refactor: hide seed reminder in preview

* build: update build number

* fix: maybe requires update thread no matter what

* fix: message request banner shows again

* fix: android tests work again and permissions

* fix: blocked contacts click handler being overridden by something

* Revert "fix: blocked contacts click handler being overridden by something"

This reverts commit 608572fc42.

* build: update build number

* refactor: remove unused dependencies and update minor for sqlcipher

* fix: actually do insert contact, because otherwise name doesn't get set properly

* fix: maybe fix scroll to bottom issue

* build: update build number

* fix: the message time and jump to message queries are more optimized

* fix: maybe fix the last seen issues

* build: update build number

* fix: pfp broken closed groups why

* fix: add admins and members as member list instead of just members

* fix: exclude lgc without membership > 1 and inactive explicitly

* fix: submodule update

* fix: compiles with removal of iterator erase

* fix: unread indicator updates properly in ConversationActivityV2

* fix: unread notifications clear and altered if any notifications exist (prevents clearing read notifications in conversation or on home screen)

* refactor: profile pictures kinda broken

* build: update build number

* refactor: remove full hash from log

* fix: isPinned threadDB call

* refactor: use mutex in all libsession native calls, change timestamp

* refactor: add basic support for blinded v2 prefixes

---------

Co-authored-by: Morgan Pretty <morgan.t.pretty@gmail.com>
2023-07-14 18:27:13 +10:00
Andrew 96ec733517
Use consistent dialog style 2023-07-13 15:39:01 +09:30
andrew adfa94614b Merge branch 'dev' into utilise-dialog-dsl 2023-07-13 12:12:01 +09:30
andrew c3ef4d6e7b Merge branch 'dev' into utilise-dialog-dsl 2023-07-13 11:11:58 +09:30
andrew d83532b6af Use Channel and Flow to avoid duplicate events 2023-07-11 13:28:44 +09:30
andrew 1845b60dac Refactor slide out of MessageDetailsActivity 2023-07-11 12:28:04 +09:30
andrew 172f85ae4f Fix some edge cases in fileDetails 2023-07-11 01:30:03 +09:30
andrew fb68aaede6 Cleanup Components imports 2023-07-11 01:29:44 +09:30
andrew 09b321530d Add strings 2023-07-10 16:09:38 +09:30
andrew a1e8ad2c37 Cleanup ViewModel 2023-07-10 13:01:46 +09:30
andrew 821327569e Cleanup Themes 2023-07-10 01:05:09 +09:30
andrew b26c98af68 Hide Resend if no error 2023-07-10 00:35:13 +09:30
andrew 0824713ac5 Fix theming and polish 2023-07-10 00:12:42 +09:30
andrew bbc9cdfeeb Cleanup ViewModel 2023-07-09 23:31:43 +09:30
andrew d8b85768d2 Fix sms messages 2023-07-07 10:46:43 +09:30
andrew e5b19d4ea4 Fix incorrect recipient used as threadRecipient to launch MediaPreview 2023-07-06 22:39:54 +09:30
andrew fc87ae18f5 Fix message padding 2023-07-06 15:48:31 +09:30
andrew 1d1977ca7a Make expand button clickable 2023-07-06 15:32:51 +09:30
andrew c417b37236 Hide thumbnails 2023-07-06 15:23:58 +09:30
andrew 8be1e8e87e Wire up voiceMessageView 2023-07-06 15:20:45 +09:30
andrew 68684bb839 Refactor VisibleMessageContentView#onContentClick 2023-07-06 13:09:00 +09:30
andrew efb5b27191 Fix divider color 2023-07-06 12:03:33 +09:30
andrew 8d66d948ca Polish 2023-07-04 22:10:48 +09:30
andrew d6b1440217 Refactor MessageDetailsViewModel 2023-07-04 15:27:00 +09:30
andrew 7cd2bd0e0d Cleanup 2023-07-04 15:11:40 +09:30
andrew 6209ae68a8 Cleanup 2023-07-04 14:56:35 +09:30
andrew ee0141f82d Refactor to Components 2023-07-04 14:09:40 +09:30
andrew f82ed7718d Cleanup CarouselButton 2023-07-04 13:26:26 +09:30
Andrew 7da3e4f022
Fix blocked contacts button (#1264) 2023-07-04 11:39:49 +09:30
andrew 26aed783e8 Fix Divider padding 2023-07-04 11:34:20 +09:30
andrew 6890f5c448 Add image click listener 2023-07-04 09:31:14 +09:30
andrew d719660030 Add expand button 2023-07-03 21:25:59 +09:30
andrew 0fcd997290 Fix divider color 2023-07-03 20:40:16 +09:30
andrew 70e63a23bc Fix indicator colors 2023-07-03 20:12:23 +09:30
andrew 0ec93e4b36 Refactor CarouselButtons 2023-07-03 18:30:15 +09:30
andrew 1d29b5465f Refactor prev and next buttons 2023-07-03 17:41:30 +09:30
andrew db4ff94084 Add prev and next buttons to carousel 2023-07-03 17:19:33 +09:30
andrew 1902d4755c Add pager indicator 2023-07-03 13:00:11 +09:30
Stefan Junker ba4143c298
feat: increase voice recording limit to 5m (#1201) 2023-07-03 11:43:02 +10:00
andrew 1303979cdf Add image attachments 2023-07-03 10:35:50 +09:30
andrew 4decce9dde Add images 2023-07-01 17:41:33 +09:30
andrew d44dbe089f Wire up buttons 2023-06-30 22:49:23 +09:30
andrew 351b259449 Add error 2023-06-30 13:25:16 +09:30
andrew 19b2c5ef97 Fix LiveDataTestUtil 2023-06-30 12:32:56 +09:30
andrew f68c01b2ee Populate profile pic 2023-06-30 11:35:22 +09:30
andrew 92fae3d6bf Add ProfilePictureView to MessageDetailActivity 2023-06-30 11:07:27 +09:30
andrew 876e12c411 Refactor ProfilePictureView 2023-06-30 11:01:57 +09:30
andrew 6d596226b3 Add user info 2023-06-30 10:33:56 +09:30
Andrew c039eb89bc
Fix debug screen security default (#1256) 2023-06-30 10:05:32 +09:30
andrew 676c29ca60 Add click listeners 2023-06-30 09:48:48 +09:30
andrew ac476f4382 Add icons and respect nullability 2023-06-29 22:11:15 +09:30
andrew d5b3d9bcf9 Improve theming 2023-06-29 19:14:47 +09:30
andrew 0c2682fe47 Improve compose theming 2023-06-29 17:11:11 +09:30
andrew fc108b34db Create Message Details screen 2023-06-29 11:37:55 +09:30
andrew ab8b2c42b9 Add jetpack compose 2023-06-28 10:34:48 +09:30
andrew 97297508f4 Simplify ExpirationDialog 2023-06-26 10:58:08 +09:30
andrew 20eac67761 simplify 2023-06-23 13:01:11 +09:30
andrew 4ee8dc712e Fix + button in rationale dialog 2023-06-23 11:42:19 +09:30
andrew b46b52ace4 Merge branch 'dev' into utilise-dialog-dsl 2023-06-23 11:14:27 +09:30
andrew 8be088ad56 Reinstate v1 PNServerJob 2023-06-23 10:29:49 +09:30
andrew dc7602a1d3 Check fcm is enabled before modifying group sub 2023-06-21 12:37:10 +09:30
andrew e3f60eb5f2 Fix individual group subs 2023-06-21 10:32:29 +09:30
andrew 42cfce0c3e Refactor v1 and v2 2023-06-21 10:01:35 +09:30
andrew 0e0ab9151e cleanup 2023-06-20 22:34:10 +09:30
andrew 5c9dc36460 Merge branch 'dev' into add-unregister 2023-06-20 12:31:37 +09:30
Andrew b9f24bc4bd
Add button downstates to appearance settings screen (#1224) 2023-06-20 12:04:24 +09:30
Andrew 92a6447b8a
Add button downstates throughout (#1220) 2023-06-19 11:51:14 +09:30
andrew 667af27bfb Utilise TokenManager and ExpiryManager 2023-06-16 10:45:38 +09:30
andrew 153aa4ceaa Reinstate push v1 2023-06-14 18:46:28 +09:30
andrew 440a5a942d Merge branch 'dev' into utilise-dialog-dsl 2023-06-14 11:03:27 +09:30
Andrew cc015c45bd
Add 50 dp buffer to isScrolledToBottom (#1228) 2023-06-14 10:34:49 +09:30
andrew 288b70bb14 Store legacy fcm token to reduce unregister api calls 2023-06-13 13:35:38 +09:30
andrew b2a1b5fe46 Add token to FCM logs 2023-06-10 11:44:43 +09:30
andrew be4d742e84 Unregister v1 push 2023-06-09 18:10:06 +09:30
andrew 01d80ae54b cleanup PushNotificationAPI 2023-06-09 15:56:24 +09:30
andrew 3f6229f841 Remove PushNotificationManager 2023-06-09 14:50:45 +09:30
andrew ba6eca2443 Add FirebasePushManager#unregister 2023-06-09 09:52:11 +09:30
Morgan Pretty 300c3a6605
Merge pull request #1243 from oxen-io/dev
Release 1.16.9
2023-06-08 17:40:00 +10:00
Morgan Pretty f0486061b1 build: increment build number 2023-06-08 17:38:52 +10:00
Morgan Pretty 026b994664
Merge pull request #1238 from oxen-io/fix/modal-backgrounds
Fix broken modal styling from 1.16.8
2023-06-08 17:31:27 +10:00
Morgan Pretty a957e78aac
Merge pull request #1241 from mpretty-cyro/fix/open-group-polling-bugs
Fixed a few issues with the OpenGroupPoller
2023-06-08 16:53:40 +10:00
Morgan Pretty 80104f6db8 [SES-627] Fixed an issue where the DocumentView could run off the screen 2023-06-08 16:33:34 +10:00
Morgan Pretty 7699e47f7b Responded to PR comments 2023-06-07 15:02:32 +10:00
Morgan Pretty 6f28b41b53
Merge pull request #1240 from KeeJef/master
Update Session Android screenshot
2023-06-07 13:38:47 +10:00
Morgan Pretty 11c1fd382d Fixed a few issues with the OpenGroupPoller
Fixed an issue where the admin/moderator status wasn't getting stored if set before joining a community
Fixed an issue where multiple pollers for the same server could run at the same time when joining multiple rooms within the same app run (very noticeable when restoring/linking)
2023-06-07 13:31:22 +10:00
Kee Jefferys fef1fbd57c
Update Session Android screenshot 2023-06-07 11:18:26 +10:00
andrew 1efda68334 Merge branch 'dev' into utilise-dialog-dsl 2023-06-06 19:51:50 +09:30
Morgan Pretty 22a30f1907 Fixed the MediaSendFragment progress dialog 2023-06-06 18:29:02 +10:00
Morgan Pretty 082c087105 Added the custom dialog windowBackground back (fix permission dialogs) 2023-06-06 17:30:10 +10:00
Morgan Pretty 9ce89087a5 Updated the dialog dim amount to match the style guide 2023-06-06 16:57:27 +10:00
Morgan Pretty cea65f3e45 Fixed an issue where a bunch of dialog backgrounds were missing 2023-06-06 16:34:23 +10:00
Morgan Pretty 429b496a22
Merge pull request #1237 from oxen-io/dev
Release 1.16.8
2023-06-06 09:32:18 +10:00
Morgan Pretty 6edf0d46f8 build: increment build number 2023-06-06 09:30:43 +10:00
Morgan Pretty 31608111b3 Merge remote-tracking branch 'upstream/master' into dev 2023-06-06 09:08:45 +10:00
Morgan Pretty 7f7ab63d15
Merge pull request #1164 from RyanRory/bluetooth-manager-crash
Fix crash & ANRs
2023-06-06 09:00:49 +10:00
Morgan Pretty f1686ea260
Merge pull request #1236 from bemusementpark/fix-dialog-theme-background
Fix dialog theme to set windowColor and not background of every view
2023-06-06 08:53:40 +10:00
andrew e5db7fc886 Fix dialog theme so it sets window color and not background of every child view 2023-06-05 19:15:42 +09:30
Morgan Pretty 9d5fa1239c Commented out the plus button on the replace profile modal 2023-06-05 17:15:05 +10:00
Morgan Pretty 346bdc1774 Merge remote-tracking branch 'upstream/dev' into bluetooth-manager-crash 2023-06-05 16:05:20 +10:00
Morgan Pretty 6c14ed26e2
Merge pull request #1229 from bemusementpark/fix-long-message
[SES-571] Fix long message not visible on react
2023-06-05 16:04:52 +10:00
Morgan Pretty f21100eff7 [SES-569] Merge branch 'fix-pic' into bluetooth-manager-crash 2023-06-05 16:02:11 +10:00
andrew a6cfe5817d Change unset profile pic in dialog 2023-06-05 14:58:56 +09:30
Morgan Pretty 6b006b9d91
Merge pull request #1235 from bemusementpark/fix-colorControlHighlight
Fix back button background in light themes
2023-06-05 14:44:51 +10:00
Morgan Pretty c92ef09d09 [SES-570] Fixed an issue where quotes weren't using their quoted message media 2023-06-05 14:31:41 +10:00
andrew e99133fd8b Fix back button background in light themes 2023-06-05 13:13:49 +09:30
andrew 09b241ba67 Fix dialog#view(layoutRes) 2023-06-05 11:11:59 +09:30
Morgan Pretty 1980113e41 [SES-566] Fixed an issue where the blocked state wasn't syncing 2023-06-05 09:39:31 +10:00
andrew 94b48d5fb9 Utilise dialog dsl in DeleteMediaPreviewDialog 2023-06-02 22:36:15 +09:30
andrew 975f9fc4d1 Utilise dialog dsl in DeleteMediaDialog 2023-06-02 22:05:34 +09:30
andrew 033eabbc53 Utilise dialog dsl in SettingsDialog 2023-06-02 19:19:43 +09:30
andrew 4641512644 Utilise dialog dsl in RationaleDialog 2023-06-02 19:11:58 +09:30
Morgan Pretty 2b7bd7417e [SES-567] Fixed a couple of issues where the Group poller would be stopped during creation 2023-06-02 17:00:20 +10:00
andrew cbe90e7bfc Cleanup ConversationReactionOverlay 2023-06-02 15:46:28 +09:30
andrew 6d528d0e92 Fix long message not visible on react 2023-06-02 15:35:35 +09:30
andrew 72c07f4b99 Utilise dialog dsl in PrivacySettingsPreferenceFragment 2023-06-01 00:07:15 +09:30
andrew 80f21aeb41 Convert PrivacySettingsPreferenceFragment to Kotlin 2023-05-31 22:23:46 +09:30
andrew c5f821add0 Utilise dialog dsl in ExpirationDialog 2023-05-31 21:58:38 +09:30
andrew 50271685af Utilise dialog dsl in MuteDialog 2023-05-31 18:05:23 +09:30
andrew 623cfde8b1 Utilise dialog dsl in CallToggleListener 2023-05-31 13:07:28 +09:30
andrew 391735dc28 Convert CallToggleListener to Kotlin 2023-05-31 13:02:33 +09:30
andrew 4e92c40210 Remove BaseDialog and utilise dialog DSL 2023-05-31 12:43:51 +09:30
andrew 1d8d678047 Make kotlin code use dialog dsl 2023-05-30 21:39:03 +09:30
andrew 4ee68cbbb1 Utilise dialog dsl 2023-05-30 16:49:07 +09:30
andrew 1b88d4f950 Utilise dialog dsl 2023-05-30 16:49:01 +09:30
Morgan Pretty 9b7fb3dd86 Removed the JobDatabase since it's no longer used 2023-05-30 13:31:36 +10:00
Morgan Pretty 8ce6e997aa Fixed a potential crash after changing a property to be nullable 2023-05-30 12:50:51 +10:00
Morgan Pretty b7744f4f2d Addressed PR feedback 2023-05-30 12:43:51 +10:00
Morgan Pretty c77d465438 Merge remote-tracking branch 'upstream/dev' into bluetooth-manager-crash
# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt
#	app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt
2023-05-30 12:25:13 +10:00
Morgan Pretty 0c2a635d03 Merge remote-tracking branch 'upstream/dev' into bluetooth-manager-crash 2023-05-30 12:24:32 +10:00
Morgan Pretty 10af2815ac
Merge pull request #1176 from bemusementpark/sync-everything
Synchronize usage of Cipher
2023-05-30 12:22:28 +10:00
Andrew e8d26222b9
Fix scroll to bottom button visibility (#1219) 2023-05-30 11:44:20 +09:30
andrew 0af713317a Merge branch 'dev' into sync-everything 2023-05-30 11:34:34 +09:30
Morgan Pretty 22ed2dd8aa
Merge pull request #1205 from bemusementpark/disable-unblock
Disable unblock button
2023-05-30 10:30:51 +10:00
Morgan Pretty d541f395a5
Merge pull request #1213 from bemusementpark/fix-id-divider-color
Fix ID divider color
2023-05-30 10:29:57 +10:00
andrew 03aa19aae4 . 2023-05-29 18:02:20 +09:30
Andrew 5519f17775
Truncate session id when displayed as username (#1215) 2023-05-25 10:58:52 +09:30
Andrew 7d31af9eb0
Fix isScrolledToBottom (#1217) 2023-05-25 10:57:05 +09:30
Andrew 331d523c45
Improve notification content dialog (#1211) 2023-05-24 18:03:27 +09:30
Andrew 47d5b242ca
Fix share button color (#1212) 2023-05-24 18:00:32 +09:30
Andrew 1b231bfff3
Fix dialog button style (#1214) 2023-05-24 13:06:26 +09:30
0x330a 95bb9ee441
refactor: remove some unnecessary code for legacy PN registration 2023-05-24 11:56:38 +10:00
Andrew 945df19aef
Fix radio button selection transition (#1209) 2023-05-24 11:23:57 +09:30
Andrew 95a165aa05
Fix TabLayout ripple (#1210) 2023-05-24 11:23:19 +09:30
andrew 55dd62240a Fix size & space 2023-05-23 18:49:28 +09:30
andrew ff124b8edc Fix ID divider color 2023-05-23 18:41:04 +09:30
andrew a295dfb248 Cleanup 2023-05-23 14:56:03 +09:30
andrew 8b39c4e56a polish 2023-05-22 22:20:52 +09:30
andrew 76466e57de Fix ripple 2023-05-22 17:03:26 +09:30
andrew 4f2ef7f2af Add SelectableItem<> 2023-05-22 15:46:12 +09:30
andrew 8ef6ec2125 . 2023-05-22 14:57:36 +09:30
Andrew d505109aa3
Merge branch 'dev' into disable-unblock 2023-05-22 14:55:55 +09:30
Andrew a8e77659b3
Add remove avatar 2023-05-22 14:53:58 +09:30
Andrew 8f55ac93f8
Fix accept button color 2023-05-22 14:53:05 +09:30
Andrew 6fc7b3591e
Fix ripple color 2023-05-22 14:52:27 +09:30
andrew 30d748e147 Disable unblock button 2023-05-19 23:44:07 +09:30
Jason Rhinelander 46acd7878d
New SPNS subscription and notifications
Finishes the WIP for subscribing to push notifications and handling the
new-style pushes we get.
2023-05-18 18:03:00 -03:00
andrew 235b94a905 Fix ripple color 2023-05-18 19:20:44 +09:30
Andrew f64fe4b652
Fix device notification settings preference color 2023-05-17 22:34:46 +09:30
Andrew c31d4d6c31
Fix dialog button color 2023-05-17 16:34:08 +09:30
andrew 8c5ff1f944 Fix dialog button color 2023-05-17 14:50:20 +09:30
andrew a334b8912a Fix device notification settings preference color 2023-05-17 14:39:47 +09:30
Andrew ab3caf8ab5
Fix message entrance coordinates 2023-05-12 09:44:14 +09:30
Andrew 44cb8fec2c
Remove reply from context menu when you can't write 2023-05-11 22:59:53 +09:30
Andrew e6dc5f2128
Fix links not working when message is partially offscreen 2023-05-11 22:58:12 +09:30
Andrew fef2948f58
Fix scroll to bottom button position when input not visible 2023-05-11 22:57:08 +09:30
Andrew 57476cd56e
Fix scroll to bottom button always visible if last item is taller than RecyclerView 2023-05-11 19:48:10 +09:30
Andrew ba9dab33c5
Synchronize Cipher in KeystoreHelper 2023-05-11 19:47:49 +09:30
Andrew aadba75038
Add sync status message 2023-05-11 19:46:09 +09:30
Andrew 84004d2fdb
Swallow exceptions in ScreenshotObserver 2023-05-11 19:45:43 +09:30
Andrew 910ab8874e
Remove laid out check before drawToBitmap 2023-05-11 19:45:10 +09:30
ryanzhao a7e0bd5366 Merge branch 'dev' into bluetooth-manager-crash 2023-05-11 12:16:52 +10:00
ryanzhao 88e788a406 clean 2023-05-11 12:16:37 +10:00
Andrew 8dbabec4e7
Fix send after approval message (#1178)
* Fix send after approval message

* Fix logic

* Utilise isLocalNumber
2023-05-11 10:02:38 +10:00
Andrew 2b00729df3
Fix message request button overlap (#1188) 2023-05-11 10:01:05 +10:00
Andrew ba3566f7e8
Fix approval text position when input bar is gone (#1181) 2023-05-11 10:00:39 +10:00
ryanzhao 89545f0406 further clean up 2023-05-10 15:36:10 +10:00
andrew d20c27d6d3 Remove laid out check before drawToBitmap 2023-05-09 22:26:36 +09:30
andrew 4469d9754a Fix bubble entrance coordinates 2023-05-09 15:10:03 +09:30
andrew c8fa2d8d6e Remove reply from context menu when you can't write 2023-05-09 14:51:28 +09:30
ryanzhao fa71ea1850 make RetrieveProfileAvatarJob work 2023-05-09 14:14:49 +10:00
andrew 8d38d1c0fb Fix links not working when message is partially offscreen 2023-05-09 13:29:25 +09:30
Ryan Zhao b494088c3d WIP: further refactor on old jobs 2023-05-08 17:12:20 +10:00
andrew b6667b83ce Fix scroll to bottom button position when input not visible 2023-05-08 14:43:08 +09:30
andrew f0715f16e0 Fix scroll to bottom button always visible if last item is taller than RecyclerView 2023-05-08 14:09:01 +09:30
Ryan Zhao 2ceb9e2bf4 clean up old job manager 2023-05-08 12:29:21 +10:00
Ryan Zhao 2b48b52df0 remove unused jobs and wrap up old job refactoring 2023-05-08 11:18:33 +10:00
andrew ec2abffdcc Remove logs 2023-05-08 10:33:39 +09:30
andrew e9a15941ae Fix colors 2023-05-05 17:47:36 +09:30
ryanzhao 375815c719 WIP: refactor on jobs using old job table 2023-05-05 16:51:44 +10:00
andrew 6a5d97a0f0 Fix something 2023-05-05 12:37:46 +09:30
andrew 9e6d1e27fc Add comment 2023-05-05 12:33:50 +09:30
andrew a9078c8d08 ...and the rest 2023-05-05 12:32:54 +09:30
andrew a152250a60 Add comments 2023-05-05 12:18:39 +09:30
andrew 24741fcc22 Synchronize Cipher in KeystoreHelper 2023-05-05 12:10:46 +09:30
andrew d3ce899a80 Synchronize all Cipher#doFinal 2023-05-05 12:07:19 +09:30
andrew 45eb3549f6 Add sync status message 2023-05-04 17:01:45 +09:30
ryanzhao d868021f0a move job running status from database to memory 2023-05-04 15:30:09 +10:00
ryanzhao 7a14c3f8be Merge branch 'dev' into bluetooth-manager-crash 2023-05-02 09:20:41 +10:00
Andrew 99cb10f5be
Add send approval message (#1157) 2023-05-01 17:19:19 +10:00
Andrew 83b6002a27
Fix emoji misalignment (#1155) 2023-05-01 17:19:13 +10:00
Andrew a934c5c2e2
Fix missing media progress (#1151) 2023-05-01 17:19:01 +10:00
Andrew 70aab2994b
Refactor MmsDatabase (#1169) 2023-05-01 17:16:48 +10:00
Ryan Zhao 2630c97a4e add where clause for marking all jobs pending to reduce the time for writing to db 2023-05-01 17:01:57 +10:00
ryanzhao bd4f451513 Merge branch 'dev' into bluetooth-manager-crash 2023-04-27 14:38:09 +10:00
andrew 58e532f0ec Swallow screenshot exceptions 2023-04-26 16:31:28 +09:30
Andrew Gallasch ffef98ecc9
Fix LandingActivity textColor (#1159) 2023-04-26 14:59:32 +10:00
Andrew Gallasch 5d552a7f93
Remove unnecessary api version checks in WindowUtil (#1163) 2023-04-26 14:56:00 +10:00
Andrew Gallasch 716fc1f4fa
Add SafeViewPager to fix touch exception in MediaPreviewActivity (#1166) 2023-04-26 14:54:54 +10:00
Andrew Gallasch 7d33177e06
Fix status and nav bar colors (#1165) 2023-04-26 14:48:34 +10:00
Ryan Zhao c7bbdb778b minor refactor 2023-04-24 15:50:53 +10:00
Ryan Zhao 51856138e3 refactor on SignalAudioManager 2023-04-24 15:38:06 +10:00
0x330a d2e80c3157
feat: re-add bencode utility and fix tests to use bytearray instead of assuming utf-8 encoding for strings 2023-04-21 13:56:08 +10:00
0x330a 7762d534bb
feat: add no op push manager for de-googled 2023-04-20 17:25:03 +10:00
0x330a 8d4f2445f2
feat: add support for firebase and split out google services as a dependency for only the play version of the app. Add support for requests in new pn server 2023-04-20 17:12:38 +10:00
0x330a 2246a5d9ce feat: introduce new pns models and wire kotlinx serialization to apply in libsession 2023-04-19 23:07:36 +10:00
Andrew Gallasch 63d442584c
Fix empty message behind media message (#1150) 2023-04-13 16:54:03 +10:00
SzBenedek2006 7b0c014791
Fix hungarian translations (#1140) 2023-04-13 10:40:32 +10:00
hjubb e1ff2bf988 build: update build number 2023-04-05 16:32:16 +10:00
0x330a b25eb9af8e
Use new seed node certificates (#1144)
* fix: use new seed node certificates

* refactor: don't need to include subdomains actually
2023-04-05 15:58:56 +10:00
wafflesvsfrankie 9ad73139b6
Add accessibility tags (#1054)
* adding accessibility id to new conversation button

* adding accessibility ids and strings for testing

* updating id tags for new conversation buttons

* accessibility tags for create contact test

* adding ids for message requests, config message, requests banner and conversation view

* adding more tags to settings page

* adding tags to different resolutions for landing page

* updating display name in settings to include accessibility id

* found some stashed changes which i forgot about

* more stashed changes

* adding tags to layout sw400dp

* closed group testing, delete/unsend message testing and selecting contacts tag

* adding tags for message body, selecting contacts for group creation, deleted message config, unsend message modal, and trash icon

* added tags for disappearing messages menu option, time selector, clock icon, confirm of change (ok)and control message

* add test for block user

* docs: Adding in accessibility ID's for Appium testing

* accessibility tags for conversation options, profile picture/settings and block options

* Add content descriptions for better accessibility

* Add more content descriptions

* Add timer icon content description

* Update profile picture content description

* Adding accessibility ids to new conversation creation screen and to message notification settings

* fix: content descriptions in their correct places and prevent a crash

* build: update build number

* build: update build number

* Adding back in FrameLayout, making changes as request

* Fixed viewPager move

* Fixing changes as requested by Jubb

* Adding content descriptions to mentions list, voice message settings, link preview permissions, closed group menu and slow mode notifications option

* Adds content descriptions to blocked contacts heading in conversations settings, individual contacts in blocked contacts page and to empty message requests folder

* Adds content descriptions to empty message request folder and text input box for username on settings page

---------

Co-authored-by: charles <charles@oxen.io>
Co-authored-by: hjubb <hjubb@users.noreply.github.com>
2023-04-04 11:33:17 +10:00
hjubb cfdc3dc24d build: update build number 2023-04-03 17:12:41 +10:00
hjubb 2a2f90276d Merge branch 'master' into dev 2023-04-03 09:58:16 +10:00
Morgan Pretty eb739bdc9b
ANR Defensive Coding (#1132)
* Made a number of changes to try and improve background ANRs

Added some more logs to the BatchMessageReceiveJob (to make it easier to track a specific job)
Shifted the ConversationActivity adapter initialisation to run on a background thread to reduce the hang when opening a conversation
Updated the ConversationViewModel to cache the recipient and openGroup values to avoid accessing the database unnecessarily
Updated the code to just stop all current closed group pollers instead of fetching a list to stop
Updated the PN registration to be triggered in an AsyncTask
Updated the call code to unregister a couple of additional receivers
Updated the background poller so it waits for 15 mins before running and doesn't replace the existing scheduler (allows for PNs to trigger explicit background polling)
Fixed an issue where we were sending push notifications which were too large and likely to fail as a result (non-pre-offer call messages)
Fixed an issue where a failing Open Group poller could prevent the background poller from receiving and processing DMs

* Updated to a more coroutine-y convention
2023-03-31 13:24:36 +11:00
Morgan Pretty 5e28af2be4
Updated the code to use the network offset time everywhere relevant (#1111)
* Updated the code to use the network offset time everywhere relevant

* Updated the SnodeAPI.nowWithOffset to use @JvmStatic
2023-03-31 10:11:30 +11:00
Ian Macdonald 405b8c7d28
Allow user display names of up to 64 bytes. (#981) 2023-03-16 10:38:49 +11:00
Float-hu a062e1c2b2
Update zh-rCN strings.xml (#1129) 2023-03-16 10:18:18 +11:00
Morgan Pretty 391418ae1e
Merge pull request #1107 from mpretty-cyro/fix/group-avatar-download-job-duplication
Fixed a few issues related to the GroupAvatarDownloadJob
2023-02-17 10:16:22 +11:00
Morgan Pretty c8846bc31a
Merge pull request #1118 from mpretty-cyro/feature/add-copy-to-long-press-menu
Add ‘Copy community URL’ to the long-press menu
2023-02-15 15:40:47 +11:00
Morgan Pretty 5bd188387b
Merge pull request #1112 from mpretty-cyro/fix/link-preview-layout-issues
Fixed a few layout issues with the LinkPreviewView
2023-02-15 15:39:37 +11:00
Morgan Pretty 7df7412a77
Merge pull request #1108 from mpretty-cyro/fix/invalid-pn-server-job-crash
Fix failing PNServerJob issue
2023-02-15 15:39:14 +11:00
Morgan Pretty 5949a158d5
Merge pull request #1117 from mpretty-cyro/feature/update-giphy-warning-copy
Updated the copy for the Giphy warning prompt
2023-02-15 10:18:42 +11:00
Morgan Pretty cc7a80900f Added copy session id and community url to the conversation long press menu 2023-02-14 16:43:55 +11:00
Morgan Pretty da0d38f389 Updated the copy for the Giphy warning prompt 2023-02-14 16:17:25 +11:00
Morgan Pretty 4df530d341 Code review changes 2023-02-14 14:10:47 +11:00
Qian Hong 7e7c016bc4
Update APK url to Oxen repo (#1114) 2023-02-14 14:05:28 +11:00
Qian Hong 47066a320c
fix: Increase contrast between selected messages and window background. Fixes #1110 (Plan B) (#1116) 2023-02-13 18:26:55 +11:00
Morgan Pretty bbedad5ebb Fixed a few layout issues with the LinkPreviewView 2023-02-10 14:33:19 +11:00
Morgan Pretty 9fd68d27f8 Fixed a few issues with the GroupAvatarDownloadJob
Added a method to remove the Group avatar
Fixed an issue where the recipient diffing on the HomeActivity wasn't working properly
Fixed an issue where the GroupAvatarDownloadJob could be scheduled even when there was already one scheduled
2023-02-10 10:05:28 +11:00
Morgan Pretty 97458a4baa Added a missing constructor 2023-02-08 14:04:02 +11:00
Morgan Pretty cd3b8f3571 Fixed a few issues related to the GroupAvatarDownloadJob
Added logic to avoid scheduling a GroupAvatarDownloadJob if there is already an existing one with the same parameters
Added logic to avoid trying to download an old avatar for a group if there is a new one
Added logic to prevent an old GroupAvatarDownloadJob from overriding a valid avatar with an old one
2023-02-08 13:42:35 +11:00
hjubb 48799db21c build: update build number 2023-02-06 16:03:49 +11:00
hjubb 378fb40984 Merge branch 'dev' 2023-02-06 16:03:19 +11:00
0x330a 395ada62ff
Update French translations (#1103)
* chore: update french translations file from crowdin

* chore: update french translations libsession file from crowdin

* chore: update french non-region translations file from crowdin

* chore: update french non-region libsession translations file from crowdin
2023-02-06 16:01:16 +11:00
Morgan Pretty 40f315af8e
Merge pull request #1102 from mpretty-cyro/fix/non-blocking-migration-and-extras
Fixed a few bugs, added logging and removed some old code
2023-02-06 16:00:36 +11:00
Morgan Pretty 50989cb2ee Increased file upload limits to 10Mb 2023-02-06 14:22:26 +11:00
Morgan Pretty 0256735135 Fixed a few bugs, added logging and removed some old code
Added the ability to copy the sessionId of open group URL from the conversation menu
Added additional logging to the BatchMessageReceiveJob to make future debugging easier
Removed the OpenGroupMigrator
Updated the JobQueue logging to provide more insight
Fixed an issue where the database migrations weren't blocking which could result in failing/crashing SQL queries
Fixed an issue where the new database file wouldn't be removed if a migration error was thrown
Fixed an issue where the new database could exist in an invalid state and the app wouldn't attempt to remigrate
Fixed an incorrectly throw exception in the PassphrasePromptActivity
2023-02-03 13:33:52 +11:00
hjubb 4e38b75f57 build: update build number 2023-01-30 17:02:11 +11:00
Morgan Pretty cf916a5762
Merge pull request #1092 from mpretty-cyro/fix/qa-issues
Fixing issues uncovered during QA
2023-01-30 10:10:37 +11:00
Morgan Pretty 7ffe48b5ed Fixed an issue where the message bubble could be sized incorrectly 2023-01-27 15:19:02 +11:00
Morgan Pretty 0a391b8c88
Merge pull request #1090 from mpretty-cyro/fix/cursor-issues
Fixing issues found during QA
2023-01-27 10:21:21 +11:00
Morgan Pretty 5d7d141d0c Added a missing parameter to the SMS & MSS projections 2023-01-25 17:19:23 +11:00
Morgan Pretty 081d1c4e93
Merge pull request #1089 from mpretty-cyro/fix/build-issue
Fixed a build error in the MockDataGenerator
2023-01-24 15:56:45 +11:00
Morgan Pretty 30615be029 Fixed a build error in the MockDataGenerator 2023-01-24 15:56:14 +11:00
Morgan Pretty abf733fbdb
Merge pull request #1082 from mpretty-cyro/feature/unread-mention-indicator
Added the unread mention indicator to the conversation list
2023-01-24 15:53:54 +11:00
Morgan Pretty 87f1d708b1 Merge remote-tracking branch 'upstream/dev' into feature/unread-mention-indicator
# Conflicts:
#	app/src/main/res/values/themes.xml
2023-01-24 15:44:22 +11:00
Morgan Pretty f8c700793a
Merge pull request #1012 from ceokot/pinned-message-icon-color
fix: Use text color secondary for conversation pinned icon
2023-01-24 15:42:51 +11:00
Morgan Pretty 05838faaf0 Updated the unread count logic to recalculate correctly 2023-01-24 15:41:44 +11:00
Morgan Pretty fff0e5a32d Merge remote-tracking branch 'upstream/dev' into pinned-message-icon-color
# Conflicts:
#	app/src/main/res/values/themes.xml
2023-01-24 15:11:09 +11:00
Morgan Pretty 2711a6dd5f
Merge pull request #994 from ceokot/message-request-response
Add profile data to message request responses
2023-01-24 14:53:09 +11:00
Morgan Pretty 1ce9e47c51
Merge pull request #991 from ceokot/groups-push-message-sender-name
fix: Show message sender in push notifications for groups
2023-01-24 14:52:50 +11:00
Morgan Pretty 63debb34e6
Merge pull request #990 from ceokot/message-request-fixes
fix: Disable typing and message requests in read-only open groups
2023-01-24 14:52:41 +11:00
Morgan Pretty ce3aa980aa Merge remote-tracking branch 'upstream/dev' into message-request-fixes
# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
#	libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt
2023-01-24 14:48:54 +11:00
Morgan Pretty 251df065f8 Merge remote-tracking branch 'upstream/dev' into message-request-response
# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
2023-01-24 14:44:56 +11:00
Morgan Pretty e44ae140e0 Merge remote-tracking branch 'upstream/dev' into feature/unread-mention-indicator
# Conflicts:
#	app/src/main/res/values/themes.xml
2023-01-24 14:24:33 +11:00
Morgan Pretty beabc1c686
Merge pull request #1079 from mpretty-cyro/feature/read_status_updates
Added the updated delivery status UI
2023-01-24 14:20:56 +11:00
Morgan Pretty bc20811431 Merge remote-tracking branch 'upstream/dev' into feature/read_status_updates
# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
#	app/src/main/res/values/strings.xml
2023-01-24 14:10:14 +11:00
Morgan Pretty dc9458f313 Merge remote-tracking branch 'upstream/dev' into feature/unread-mention-indicator
# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
#	libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt
2023-01-24 14:02:08 +11:00
Morgan Pretty 23dca5b38d Merge remote-tracking branch 'upstream/dev' into feature/unread-mention-indicator 2023-01-24 14:00:07 +11:00
Morgan Pretty 025989f928
Merge pull request #1069 from mpretty-cyro/feature/performance-improvements
Performance improvements
2023-01-24 13:59:32 +11:00
Morgan Pretty bdac7f2ea3
Merge pull request #1087 from 0x330a/fix/expire-old-call-messages
Prevent very old messages (15 minutes ago) being processed to prevent endless crashes in some cases
2023-01-24 13:59:19 +11:00
hjubb f0e67fd86a Merge branch 'master' into dev 2023-01-24 13:41:53 +11:00
Morgan Pretty e0785c4854 Added in fixes and defensive coding for the most frequent crashes
Fixed a crash which could occur when dealing will calls in the background on Android 12 and newer
Fixed a crash when we don't have permission to check the current call state
Fixed a crash when the ScreenshotObserver receives an invalid Uri (just prevent the specific case)
2023-01-24 13:31:56 +11:00
0x330a 5861623369
fix: may have been preventing new closed group message on multi-device (#1081) 2023-01-23 17:20:07 +11:00
Morgan Pretty ebe8479e4c Resolved PR comments 2023-01-23 16:14:50 +11:00
0x330a 86b065203f
fix: prevent very old messages (15 minutes ago) being processed to prevent endless crashes in some cases 2023-01-23 15:43:43 +11:00
Morgan Pretty a6f09c6fef Added some defensive coding to help prevent incorrect message statuses 2023-01-23 12:42:10 +11:00
Morgan Pretty 8a4a9623cc Fixed an edge case where an OpenGroup might not download it's image 2023-01-20 17:42:46 +11:00
Morgan Pretty 0ed5c5825d Cleaned up some of the error logging 2023-01-20 15:24:14 +11:00
Morgan Pretty afa42daab1 Updated the 'scrollToBottom' behaviour to be more efficient 2023-01-20 09:19:29 +11:00
Morgan Pretty 810430e806 Fixed a couple of issues with the OpenGroupDeleteJob
Updated the OpenGroupDispatcher to have a thread limit of 8 (was previously unlimited which could result in the app getting flooded with threads under certain conditions)
Updated the OpenGroupDeleteJob to do bulk deletions (instead of individual message deletions)
Updated the OpenGroupDeleteJob to catch and report failures (wasn't previously happening)
2023-01-20 09:02:59 +11:00
Morgan Pretty ce8e5c596e Fixed the message sorting for a couple of database queries 2023-01-19 15:24:50 +11:00
Morgan Pretty 694ca79958 Added the unread mention indicator to the conversation list
Fixed the unread indicator colours to match correct theming designs
Fixed a bug where the unread count could be incorrect when receiving UnsendRequests within the same poll
Added a couple missing theme colours
2023-01-19 15:24:50 +11:00
Morgan Pretty cae15a200d Added temporary support for downgrading and notify the user upon failure 2023-01-19 15:24:09 +11:00
Morgan Pretty a2fcb3195d General cleanup
Fixed a bug where open groups were incorrectly displaying closed group avatar images
Removed some commented out code
2023-01-16 16:46:24 +11:00
Morgan Pretty f4fdfd7410 Added the updated delivery status UI 2023-01-16 14:48:38 +11:00
Morgan Pretty cc5c63b211 Fixed a couple of issues from the rebase, removed an unneeded text clear 2023-01-13 16:16:52 +11:00
Morgan Pretty 70f0dad36e Fixed a few bugs and some optimisations
Updated a number of nested layout components to be included instead of inflated
Added a couple of optimisations to the EmojiTextView
Fixed an issue where long conversation titles could squish the unread count
Fixed an issue where the typing indicator wasn't working on the home screen
2023-01-13 15:56:14 +11:00
Morgan Pretty 693c3a9656 Fixed a few cases where we were using the write access for read operations 2023-01-13 15:56:14 +11:00
Morgan Pretty f9ff3feb29 Refactored code to avoid passing lifecycleCoroutineScope as a parameter 2023-01-13 15:56:14 +11:00
Morgan Pretty afdf730eaa Added a couple of minor UI optimisations 2023-01-13 15:56:14 +11:00
Morgan Pretty 3e68bdc2f8 Fixed an issue introduced by the last commit with OpenGroup initialisation 2023-01-13 15:56:14 +11:00
Morgan Pretty 5afd647686 Tweaked some open group handling and a couple of onboarding issues
Updated the OpenGroup adding and polling logic to reduce duplicate API calls
Updated the BackgroundGroupAddJob to start a GroupAvatarDownloadJob instead of running the download itself (to appear to run faster)
Defaulted OpenGroups to use blinded auth when no server capabilities are present
Fixed an issue where the background poller could be started even though the onboarding hadn't been completed
Fixed an issue where the database could get into an invalid state if the app was restarted during onboarding
2023-01-13 15:56:14 +11:00
Morgan Pretty d0a4bac83e Shifted the creation of AttachmentDownloadJobs to the IO thread 2023-01-13 15:56:14 +11:00
Morgan Pretty a1b052ef82 Added indexes to the Reactions database 2023-01-13 15:56:13 +11:00
Morgan Pretty e7b6ddacbb Shifted a number of db writes when opening conversations to the IO thread so they don't block 2023-01-13 15:55:31 +11:00
Morgan Pretty c0bef51fe0 Fixed a couple of bugs where the HomeDiffUtil could incorrectly detect differences 2023-01-13 15:55:31 +11:00
Morgan Pretty d68d26cd5d Added the MockDataGenerator to simplify db testing 2023-01-10 10:41:53 +11:00
Morgan Pretty 5abc3119cb Fixed an issue where clearing device data would create an invalid DB state 2023-01-06 15:41:26 +11:00
Morgan Pretty e6fe38587b Fixed an issue where database migrations were broken 2023-01-06 10:36:56 +11:00
Morgan Pretty 12205e72b6 Another update was released last week 2023-01-06 09:05:29 +11:00
Morgan Pretty 1a28fd2a9e Added code to migrate from SQLCipher 3 to 4 2023-01-05 16:56:52 +11:00
hjubb df8a6d739a build: release v1.16.3 (3235) 2022-12-19 11:57:22 +11:00
0x330a cdd2559839
Paged conversation recycler, update compile sdk version 31 (#1049)
* Update build tools

* Update appcompat version

* Update dependencies

* feat: add paging into conversation recycler and queries to fetch data off-thread

* refactor: wip for updating paged results and bucketing messages / fetching enough to display

* fix: currently works for scrolling and possibly refreshing? need scroll to message and auto scroll down on insert (at bottom)

* fix: search and scrolling to X message works now

* build: increase version code and name

* fix: re-add refresh, remove the outdated comment

* refactor: lets see if 25 size pages increases performance 👀

* feat: add in some equals overrides for mms records to refresh if media has finished DLing

* feat: add scroll to bottom for new messages if we are at the end of the chat

* build: update build numbers

* fix: update AGP and fix compile errors for sdk version 31

* feat: add log for loki-avatar and loki-fs on upload types and responses

* feat: increase build number to match latest installed version

* feat: changing props and permission checks for call service

* fix: possible service exception when no call ID remote foreground service not terminated

* revert: google services version

* fix: re-add paging dependency

* feat: adding new last seen function and figuring out the last seen for recycler adapter

* build: update version names and codes for deploy

* refactor: undo the new adapter and query changes to use previous cursor logic. revert this commit to enable new paged adapter

* fix: use author's address in typist equality and hashcode for set inclusion

* refactor: refactor the select contacts activity

* refactor: refactor the select contacts activity

* build: update version code

* fix: hide all other bound views if deleted

* refactor: change voice message tint, upgrade build number

* fix: message detail showing up properly

* revert: realise copy public key is actually not allowed if open group participant

* fix: copy session ID, message detail activity support re-enabled

* build: update build version code

* build: remove version name

* build: update build code

* feat: google services version minimum compatible

* fix: selection for re-created objects not properly highlighting

* fix: foreground CENTER_INSIDE instead of just CENTER for scaletype

* build: update version code

* fix: don't show error if no error

* build: update version code

* fix: clear error messages if any on successful send

Co-authored-by: charles <charles@oxen.io>
2022-12-19 11:29:05 +11:00
hjubb bda50d263c Merge branch 'master' into dev 2022-12-19 11:06:42 +11:00
Ninad Bhuiyan 95298bb9e3
Fix voice message duration view (#948)
The seconds for a voice message duration, and also its decreasing duration when played, were just the total duration of the audio. So, a 3 minute audio would appear as 3:180 at the audio's right, and 2:170 for example when said audio is being played.

I added a modulo operator for 60 after the time millisecond to seconds conversion, just before setting it as viewable text, same for the progress function.
2022-12-19 10:53:03 +11:00
0x330a d9a815a729
Add adaptive monochrome icon based on the current ic_launcher_foreground (#1037)
* fix: Selected message theme colours and crash fixes (#1025)

* Fixed a bug where message requests could incorrectly appear in the thread list (#1009)

* New app theming (#1010)

* feat: start new app theming feature

* feat: add some theming colours

* refactor: start refactoring themes and colours to use dynamic attributes

* feat: adding more colours and switching over default colours to be theme based instead of hard-coded or day/night specific

* refactor: take a look at ocean light and logo colour

* feat: global search colours for light and dark ocean

* feat: more styling

* feat: adding themes to conversation activity and refactoring the base theme to apply over the top of the activity's theme so it retains noActionBar etc

* feat: add dynamic accent color

* docs: add todo for changing how accent colour is applied

* feat: update new theming to use override primary style so that the regular colorAccent attribute can be used in existing layouts

* feat: coordinating styles across layouts, fixing up pinned icons and naming for conversation list items

* refactor: re-styling layouts to match new themes and attributes. Need to figure out action mode close button

* refactor: remove @color/text and replace with ?android:textColorPrimary to override in themes

* refactor: add context theme wrapper to bottom sheet dialog that references accent color

* fix: input bar bug fix and preference activity themes

* refactor: new settings menu options

* fix: crash for PNModeActivity.kt

refactor: move ordering in seed dialog to match designs, copy changes to match new settings menu

* feat: add new appearance settings activity

* refactor: title and VM changes

* fix: correct override

* feat: add theme appearance screen UI features and start VM implementation. re-add legacy theme utils to get default for migration

* fix: compile errors and missing themes from emoji features

* refactor: remove background shape alteration and old bottom sheet styles, re-add the theme mode attr

* feat: appearance screen wired up, just need to refresh theme

* feat: add theme state recreation and fix match system settings option

* refactor: add bottom margin

* feat: explore custom preference category

* feat: add the customized session theme for CorrectedPreferenceFragment

* feat: replace AppProtectionPreferenceFragment to extend ListSummaryPreferenceFragment

* refactor: change drawable style and remove explicit dividers

* refactor: remove divider in CorrectedPreferenceFragment

* feat: add theme state check on resume, might be jarring currently

* feat: add preference divider elements for settings menu

* refactor: settings menu redesigns

* refactor: change led preference to integer and refactor TextSecurePreferences.kt

* feat: add scroll parcel to save/restore hierarchy on restart with appearance changes

* feat: add the conversations blocked contacts and refactor preference order and copy

* feat: add blocked contacts activity, basic layout and vm

* feat: add unblock DB functions and storage protocol, start working on the DB query state flow, might have to just implement recipient on modified listener

* feat: add blocked contacts and notif recipient listeners

* feat: add recipient db reader

* feat: add blocked contact interactions and fix a theming crash for notifications

* feat: introduce better equals and hashcode implementations to recipient, replace home diff util content check with hashcode-based comparison

* feat: add settings menu vectors

* fix: preview compile error

* refactor: migrating settings menu to new designs

* feat: help menu

* refactor: simplify link opening

* refactor: remove space

* feat: refactor preferences and start theming for light mode options

* refactor: fixing dark and light modes with dialogs

* refactor: popup dialogs use proper themes now

* refactor: alert dialogs and media edit fragments use attribute references

* refactor: use input bar button attribute instead color control normal in vector tint

* refactor: transparency, dialog fixes, notification fix

* refactor: attrs and styles for buttons

* fix: use prominent button color on the outline button's border

* fix: fix the trash

* refactor: remove the appearance

* refactor: avatar placeholder generation, chips and element border styles

* refactor: use colors instead of style references

* refactor: theming changes to match designs and feedback

* refactor: the titles are bold and the categories are tertiary coloured now

* fix: appearance settings match preferences, search bottom bar uses themed attributes

* refactor: increase setting button height

* Update clear all data dialog

* Update seed dialog

* refactor: more qa feedback changes

* feat: add new TLs and fa-rIR TLs

* Update notification content dialog

* Fix message requests clear all button text color

* feat: re-add screenshot observer

* refactor: make send tint accent color

* feat: add unread background differences

* fix: change unread count indicator

* build: upgrade build numbers

* Fix message requests popupmenu background color

* fix: crash from attr reference in color attribute

* build: upgrade build number

* fix: message bubbles, thumbnail backgrounds, search bar visibility with input bar, attachment buttons

* fix: tertiary text for keyboard page search view

* fix: emoji overflow colour differences

* fix: reaction pill dialog background is now correct colour

* Add style to reactions tab layout

* fix: appearance activity reverting primary color at correct time

* fix: show call privacy warning every time instead of just once

* fix: gradient background(?) and audio autoplay disable

* fix: crash in all media containing documents

* fix: reaction dialog heading fixes

* Add style to reactions tab layout

* fix: remove gradient backgrounds

* fix: adding new reaction normal text attribute to try correct the tab layout

* fix: ocean dark unread/read colours

* build; update build number

* build: update build number

* Fix ocean light theme pin color

* Update classic dark unread indicator text color

* Reduce group name text size on join community screen

* Restore tab indicator color on join community and all media screens

* Home adapter rename

* Updated the HomeViewModel to cancel old executors when new ones are added (#1)

* Updated the HomeViewModel to cancel old executors when new ones are added

* Removed a seemingly unneeded update

* Explicitly remove observers when the HomeActivity is paused (#2)

Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>

* Play fixes (#1017)

* fix: Prevent crash from recipient modification listener

* fix: Add message selection highlight color

* Handle crash from fingerprint auth

* Update message selection highlight colors

* Handle call answer/hangup processing appropriately

* Add proguard rule for ContextMenuList

* Handle more calls intents

Co-authored-by: charles <charles@oxen.io>

Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>

* release: increment build number

* feat: add adaptive monochrome icon

Co-authored-by: ceokot <ceokot@users.noreply.github.com>
Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
2022-12-14 11:34:31 +11:00
0x330a 6fbfc95ad2
Make selected message colours more visible, fix classic dark read indicator (#1031)
* fix: Selected message theme colours and crash fixes (#1025)

* Fixed a bug where message requests could incorrectly appear in the thread list (#1009)

* New app theming (#1010)

* feat: start new app theming feature

* feat: add some theming colours

* refactor: start refactoring themes and colours to use dynamic attributes

* feat: adding more colours and switching over default colours to be theme based instead of hard-coded or day/night specific

* refactor: take a look at ocean light and logo colour

* feat: global search colours for light and dark ocean

* feat: more styling

* feat: adding themes to conversation activity and refactoring the base theme to apply over the top of the activity's theme so it retains noActionBar etc

* feat: add dynamic accent color

* docs: add todo for changing how accent colour is applied

* feat: update new theming to use override primary style so that the regular colorAccent attribute can be used in existing layouts

* feat: coordinating styles across layouts, fixing up pinned icons and naming for conversation list items

* refactor: re-styling layouts to match new themes and attributes. Need to figure out action mode close button

* refactor: remove @color/text and replace with ?android:textColorPrimary to override in themes

* refactor: add context theme wrapper to bottom sheet dialog that references accent color

* fix: input bar bug fix and preference activity themes

* refactor: new settings menu options

* fix: crash for PNModeActivity.kt

refactor: move ordering in seed dialog to match designs, copy changes to match new settings menu

* feat: add new appearance settings activity

* refactor: title and VM changes

* fix: correct override

* feat: add theme appearance screen UI features and start VM implementation. re-add legacy theme utils to get default for migration

* fix: compile errors and missing themes from emoji features

* refactor: remove background shape alteration and old bottom sheet styles, re-add the theme mode attr

* feat: appearance screen wired up, just need to refresh theme

* feat: add theme state recreation and fix match system settings option

* refactor: add bottom margin

* feat: explore custom preference category

* feat: add the customized session theme for CorrectedPreferenceFragment

* feat: replace AppProtectionPreferenceFragment to extend ListSummaryPreferenceFragment

* refactor: change drawable style and remove explicit dividers

* refactor: remove divider in CorrectedPreferenceFragment

* feat: add theme state check on resume, might be jarring currently

* feat: add preference divider elements for settings menu

* refactor: settings menu redesigns

* refactor: change led preference to integer and refactor TextSecurePreferences.kt

* feat: add scroll parcel to save/restore hierarchy on restart with appearance changes

* feat: add the conversations blocked contacts and refactor preference order and copy

* feat: add blocked contacts activity, basic layout and vm

* feat: add unblock DB functions and storage protocol, start working on the DB query state flow, might have to just implement recipient on modified listener

* feat: add blocked contacts and notif recipient listeners

* feat: add recipient db reader

* feat: add blocked contact interactions and fix a theming crash for notifications

* feat: introduce better equals and hashcode implementations to recipient, replace home diff util content check with hashcode-based comparison

* feat: add settings menu vectors

* fix: preview compile error

* refactor: migrating settings menu to new designs

* feat: help menu

* refactor: simplify link opening

* refactor: remove space

* feat: refactor preferences and start theming for light mode options

* refactor: fixing dark and light modes with dialogs

* refactor: popup dialogs use proper themes now

* refactor: alert dialogs and media edit fragments use attribute references

* refactor: use input bar button attribute instead color control normal in vector tint

* refactor: transparency, dialog fixes, notification fix

* refactor: attrs and styles for buttons

* fix: use prominent button color on the outline button's border

* fix: fix the trash

* refactor: remove the appearance

* refactor: avatar placeholder generation, chips and element border styles

* refactor: use colors instead of style references

* refactor: theming changes to match designs and feedback

* refactor: the titles are bold and the categories are tertiary coloured now

* fix: appearance settings match preferences, search bottom bar uses themed attributes

* refactor: increase setting button height

* Update clear all data dialog

* Update seed dialog

* refactor: more qa feedback changes

* feat: add new TLs and fa-rIR TLs

* Update notification content dialog

* Fix message requests clear all button text color

* feat: re-add screenshot observer

* refactor: make send tint accent color

* feat: add unread background differences

* fix: change unread count indicator

* build: upgrade build numbers

* Fix message requests popupmenu background color

* fix: crash from attr reference in color attribute

* build: upgrade build number

* fix: message bubbles, thumbnail backgrounds, search bar visibility with input bar, attachment buttons

* fix: tertiary text for keyboard page search view

* fix: emoji overflow colour differences

* fix: reaction pill dialog background is now correct colour

* Add style to reactions tab layout

* fix: appearance activity reverting primary color at correct time

* fix: show call privacy warning every time instead of just once

* fix: gradient background(?) and audio autoplay disable

* fix: crash in all media containing documents

* fix: reaction dialog heading fixes

* Add style to reactions tab layout

* fix: remove gradient backgrounds

* fix: adding new reaction normal text attribute to try correct the tab layout

* fix: ocean dark unread/read colours

* build; update build number

* build: update build number

* Fix ocean light theme pin color

* Update classic dark unread indicator text color

* Reduce group name text size on join community screen

* Restore tab indicator color on join community and all media screens

* Home adapter rename

* Updated the HomeViewModel to cancel old executors when new ones are added (#1)

* Updated the HomeViewModel to cancel old executors when new ones are added

* Removed a seemingly unneeded update

* Explicitly remove observers when the HomeActivity is paused (#2)

Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>

* Play fixes (#1017)

* fix: Prevent crash from recipient modification listener

* fix: Add message selection highlight color

* Handle crash from fingerprint auth

* Update message selection highlight colors

* Handle call answer/hangup processing appropriately

* Add proguard rule for ContextMenuList

* Handle more calls intents

Co-authored-by: charles <charles@oxen.io>

Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>

* release: increment build number

* fix: make selected colours more visible, make read indicator more visible in dark mode

* refactor: revert ocean dark selected

Co-authored-by: ceokot <ceokot@users.noreply.github.com>
Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
2022-12-14 11:34:18 +11:00
0x330a 7fcd754f00
fix: don't use themed text color primary for the media send fragment (#1038) 2022-12-07 14:05:11 +11:00
charles 752c25d627 Handle nullable profile key 2022-12-05 11:27:55 +11:00
charles 76fff8bc74 release: increment build number 2022-10-24 15:53:14 +11:00
ceokot 427b2d7b5f
fix: Selected message theme colours and crash fixes (#1025)
* Fixed a bug where message requests could incorrectly appear in the thread list (#1009)

* New app theming (#1010)

* feat: start new app theming feature

* feat: add some theming colours

* refactor: start refactoring themes and colours to use dynamic attributes

* feat: adding more colours and switching over default colours to be theme based instead of hard-coded or day/night specific

* refactor: take a look at ocean light and logo colour

* feat: global search colours for light and dark ocean

* feat: more styling

* feat: adding themes to conversation activity and refactoring the base theme to apply over the top of the activity's theme so it retains noActionBar etc

* feat: add dynamic accent color

* docs: add todo for changing how accent colour is applied

* feat: update new theming to use override primary style so that the regular colorAccent attribute can be used in existing layouts

* feat: coordinating styles across layouts, fixing up pinned icons and naming for conversation list items

* refactor: re-styling layouts to match new themes and attributes. Need to figure out action mode close button

* refactor: remove @color/text and replace with ?android:textColorPrimary to override in themes

* refactor: add context theme wrapper to bottom sheet dialog that references accent color

* fix: input bar bug fix and preference activity themes

* refactor: new settings menu options

* fix: crash for PNModeActivity.kt

refactor: move ordering in seed dialog to match designs, copy changes to match new settings menu

* feat: add new appearance settings activity

* refactor: title and VM changes

* fix: correct override

* feat: add theme appearance screen UI features and start VM implementation. re-add legacy theme utils to get default for migration

* fix: compile errors and missing themes from emoji features

* refactor: remove background shape alteration and old bottom sheet styles, re-add the theme mode attr

* feat: appearance screen wired up, just need to refresh theme

* feat: add theme state recreation and fix match system settings option

* refactor: add bottom margin

* feat: explore custom preference category

* feat: add the customized session theme for CorrectedPreferenceFragment

* feat: replace AppProtectionPreferenceFragment to extend ListSummaryPreferenceFragment

* refactor: change drawable style and remove explicit dividers

* refactor: remove divider in CorrectedPreferenceFragment

* feat: add theme state check on resume, might be jarring currently

* feat: add preference divider elements for settings menu

* refactor: settings menu redesigns

* refactor: change led preference to integer and refactor TextSecurePreferences.kt

* feat: add scroll parcel to save/restore hierarchy on restart with appearance changes

* feat: add the conversations blocked contacts and refactor preference order and copy

* feat: add blocked contacts activity, basic layout and vm

* feat: add unblock DB functions and storage protocol, start working on the DB query state flow, might have to just implement recipient on modified listener

* feat: add blocked contacts and notif recipient listeners

* feat: add recipient db reader

* feat: add blocked contact interactions and fix a theming crash for notifications

* feat: introduce better equals and hashcode implementations to recipient, replace home diff util content check with hashcode-based comparison

* feat: add settings menu vectors

* fix: preview compile error

* refactor: migrating settings menu to new designs

* feat: help menu

* refactor: simplify link opening

* refactor: remove space

* feat: refactor preferences and start theming for light mode options

* refactor: fixing dark and light modes with dialogs

* refactor: popup dialogs use proper themes now

* refactor: alert dialogs and media edit fragments use attribute references

* refactor: use input bar button attribute instead color control normal in vector tint

* refactor: transparency, dialog fixes, notification fix

* refactor: attrs and styles for buttons

* fix: use prominent button color on the outline button's border

* fix: fix the trash

* refactor: remove the appearance

* refactor: avatar placeholder generation, chips and element border styles

* refactor: use colors instead of style references

* refactor: theming changes to match designs and feedback

* refactor: the titles are bold and the categories are tertiary coloured now

* fix: appearance settings match preferences, search bottom bar uses themed attributes

* refactor: increase setting button height

* Update clear all data dialog

* Update seed dialog

* refactor: more qa feedback changes

* feat: add new TLs and fa-rIR TLs

* Update notification content dialog

* Fix message requests clear all button text color

* feat: re-add screenshot observer

* refactor: make send tint accent color

* feat: add unread background differences

* fix: change unread count indicator

* build: upgrade build numbers

* Fix message requests popupmenu background color

* fix: crash from attr reference in color attribute

* build: upgrade build number

* fix: message bubbles, thumbnail backgrounds, search bar visibility with input bar, attachment buttons

* fix: tertiary text for keyboard page search view

* fix: emoji overflow colour differences

* fix: reaction pill dialog background is now correct colour

* Add style to reactions tab layout

* fix: appearance activity reverting primary color at correct time

* fix: show call privacy warning every time instead of just once

* fix: gradient background(?) and audio autoplay disable

* fix: crash in all media containing documents

* fix: reaction dialog heading fixes

* Add style to reactions tab layout

* fix: remove gradient backgrounds

* fix: adding new reaction normal text attribute to try correct the tab layout

* fix: ocean dark unread/read colours

* build; update build number

* build: update build number

* Fix ocean light theme pin color

* Update classic dark unread indicator text color

* Reduce group name text size on join community screen

* Restore tab indicator color on join community and all media screens

* Home adapter rename

* Updated the HomeViewModel to cancel old executors when new ones are added (#1)

* Updated the HomeViewModel to cancel old executors when new ones are added

* Removed a seemingly unneeded update

* Explicitly remove observers when the HomeActivity is paused (#2)

Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>

* Play fixes (#1017)

* fix: Prevent crash from recipient modification listener

* fix: Add message selection highlight color

* Handle crash from fingerprint auth

* Update message selection highlight colors

* Handle call answer/hangup processing appropriately

* Add proguard rule for ContextMenuList

* Handle more calls intents

Co-authored-by: charles <charles@oxen.io>

Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
2022-10-24 15:45:26 +11:00
ceokot 1dbcffe40b
Play fixes (#1017)
* fix: Prevent crash from recipient modification listener

* fix: Add message selection highlight color

* Handle crash from fingerprint auth

* Update message selection highlight colors

* Handle call answer/hangup processing appropriately

* Add proguard rule for ContextMenuList

* Handle more calls intents

Co-authored-by: charles <charles@oxen.io>
2022-10-24 14:24:57 +11:00
charles 10d0134269 fix: Use text color secondary for conversation pinned icon 2022-10-19 13:15:05 +11:00
charles db61d693c4 Merge branch 'u-master' into u-dev 2022-10-19 11:06:59 +11:00
charles 2216c99dcd release: increment build number 2022-10-18 18:05:41 +11:00
ceokot 3018937ad9
New app theming (#1011)
* Fixed a bug where message requests could incorrectly appear in the thread list (#1009)

* New app theming (#1010)

* feat: start new app theming feature

* feat: add some theming colours

* refactor: start refactoring themes and colours to use dynamic attributes

* feat: adding more colours and switching over default colours to be theme based instead of hard-coded or day/night specific

* refactor: take a look at ocean light and logo colour

* feat: global search colours for light and dark ocean

* feat: more styling

* feat: adding themes to conversation activity and refactoring the base theme to apply over the top of the activity's theme so it retains noActionBar etc

* feat: add dynamic accent color

* docs: add todo for changing how accent colour is applied

* feat: update new theming to use override primary style so that the regular colorAccent attribute can be used in existing layouts

* feat: coordinating styles across layouts, fixing up pinned icons and naming for conversation list items

* refactor: re-styling layouts to match new themes and attributes. Need to figure out action mode close button

* refactor: remove @color/text and replace with ?android:textColorPrimary to override in themes

* refactor: add context theme wrapper to bottom sheet dialog that references accent color

* fix: input bar bug fix and preference activity themes

* refactor: new settings menu options

* fix: crash for PNModeActivity.kt

refactor: move ordering in seed dialog to match designs, copy changes to match new settings menu

* feat: add new appearance settings activity

* refactor: title and VM changes

* fix: correct override

* feat: add theme appearance screen UI features and start VM implementation. re-add legacy theme utils to get default for migration

* fix: compile errors and missing themes from emoji features

* refactor: remove background shape alteration and old bottom sheet styles, re-add the theme mode attr

* feat: appearance screen wired up, just need to refresh theme

* feat: add theme state recreation and fix match system settings option

* refactor: add bottom margin

* feat: explore custom preference category

* feat: add the customized session theme for CorrectedPreferenceFragment

* feat: replace AppProtectionPreferenceFragment to extend ListSummaryPreferenceFragment

* refactor: change drawable style and remove explicit dividers

* refactor: remove divider in CorrectedPreferenceFragment

* feat: add theme state check on resume, might be jarring currently

* feat: add preference divider elements for settings menu

* refactor: settings menu redesigns

* refactor: change led preference to integer and refactor TextSecurePreferences.kt

* feat: add scroll parcel to save/restore hierarchy on restart with appearance changes

* feat: add the conversations blocked contacts and refactor preference order and copy

* feat: add blocked contacts activity, basic layout and vm

* feat: add unblock DB functions and storage protocol, start working on the DB query state flow, might have to just implement recipient on modified listener

* feat: add blocked contacts and notif recipient listeners

* feat: add recipient db reader

* feat: add blocked contact interactions and fix a theming crash for notifications

* feat: introduce better equals and hashcode implementations to recipient, replace home diff util content check with hashcode-based comparison

* feat: add settings menu vectors

* fix: preview compile error

* refactor: migrating settings menu to new designs

* feat: help menu

* refactor: simplify link opening

* refactor: remove space

* feat: refactor preferences and start theming for light mode options

* refactor: fixing dark and light modes with dialogs

* refactor: popup dialogs use proper themes now

* refactor: alert dialogs and media edit fragments use attribute references

* refactor: use input bar button attribute instead color control normal in vector tint

* refactor: transparency, dialog fixes, notification fix

* refactor: attrs and styles for buttons

* fix: use prominent button color on the outline button's border

* fix: fix the trash

* refactor: remove the appearance

* refactor: avatar placeholder generation, chips and element border styles

* refactor: use colors instead of style references

* refactor: theming changes to match designs and feedback

* refactor: the titles are bold and the categories are tertiary coloured now

* fix: appearance settings match preferences, search bottom bar uses themed attributes

* refactor: increase setting button height

* Update clear all data dialog

* Update seed dialog

* refactor: more qa feedback changes

* feat: add new TLs and fa-rIR TLs

* Update notification content dialog

* Fix message requests clear all button text color

* feat: re-add screenshot observer

* refactor: make send tint accent color

* feat: add unread background differences

* fix: change unread count indicator

* build: upgrade build numbers

* Fix message requests popupmenu background color

* fix: crash from attr reference in color attribute

* build: upgrade build number

* fix: message bubbles, thumbnail backgrounds, search bar visibility with input bar, attachment buttons

* fix: tertiary text for keyboard page search view

* fix: emoji overflow colour differences

* fix: reaction pill dialog background is now correct colour

* Add style to reactions tab layout

* fix: appearance activity reverting primary color at correct time

* fix: show call privacy warning every time instead of just once

* fix: gradient background(?) and audio autoplay disable

* fix: crash in all media containing documents

* fix: reaction dialog heading fixes

* Add style to reactions tab layout

* fix: remove gradient backgrounds

* fix: adding new reaction normal text attribute to try correct the tab layout

* fix: ocean dark unread/read colours

* build; update build number

* build: update build number

* Fix ocean light theme pin color

* Update classic dark unread indicator text color

* Reduce group name text size on join community screen

* Restore tab indicator color on join community and all media screens

* Home adapter rename

* Updated the HomeViewModel to cancel old executors when new ones are added (#1)

* Updated the HomeViewModel to cancel old executors when new ones are added

* Removed a seemingly unneeded update

* Explicitly remove observers when the HomeActivity is paused (#2)

Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>

Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
2022-10-18 17:59:47 +11:00
ceokot 38cc42a162
New app theming (#1010)
* feat: start new app theming feature

* feat: add some theming colours

* refactor: start refactoring themes and colours to use dynamic attributes

* feat: adding more colours and switching over default colours to be theme based instead of hard-coded or day/night specific

* refactor: take a look at ocean light and logo colour

* feat: global search colours for light and dark ocean

* feat: more styling

* feat: adding themes to conversation activity and refactoring the base theme to apply over the top of the activity's theme so it retains noActionBar etc

* feat: add dynamic accent color

* docs: add todo for changing how accent colour is applied

* feat: update new theming to use override primary style so that the regular colorAccent attribute can be used in existing layouts

* feat: coordinating styles across layouts, fixing up pinned icons and naming for conversation list items

* refactor: re-styling layouts to match new themes and attributes. Need to figure out action mode close button

* refactor: remove @color/text and replace with ?android:textColorPrimary to override in themes

* refactor: add context theme wrapper to bottom sheet dialog that references accent color

* fix: input bar bug fix and preference activity themes

* refactor: new settings menu options

* fix: crash for PNModeActivity.kt

refactor: move ordering in seed dialog to match designs, copy changes to match new settings menu

* feat: add new appearance settings activity

* refactor: title and VM changes

* fix: correct override

* feat: add theme appearance screen UI features and start VM implementation. re-add legacy theme utils to get default for migration

* fix: compile errors and missing themes from emoji features

* refactor: remove background shape alteration and old bottom sheet styles, re-add the theme mode attr

* feat: appearance screen wired up, just need to refresh theme

* feat: add theme state recreation and fix match system settings option

* refactor: add bottom margin

* feat: explore custom preference category

* feat: add the customized session theme for CorrectedPreferenceFragment

* feat: replace AppProtectionPreferenceFragment to extend ListSummaryPreferenceFragment

* refactor: change drawable style and remove explicit dividers

* refactor: remove divider in CorrectedPreferenceFragment

* feat: add theme state check on resume, might be jarring currently

* feat: add preference divider elements for settings menu

* refactor: settings menu redesigns

* refactor: change led preference to integer and refactor TextSecurePreferences.kt

* feat: add scroll parcel to save/restore hierarchy on restart with appearance changes

* feat: add the conversations blocked contacts and refactor preference order and copy

* feat: add blocked contacts activity, basic layout and vm

* feat: add unblock DB functions and storage protocol, start working on the DB query state flow, might have to just implement recipient on modified listener

* feat: add blocked contacts and notif recipient listeners

* feat: add recipient db reader

* feat: add blocked contact interactions and fix a theming crash for notifications

* feat: introduce better equals and hashcode implementations to recipient, replace home diff util content check with hashcode-based comparison

* feat: add settings menu vectors

* fix: preview compile error

* refactor: migrating settings menu to new designs

* feat: help menu

* refactor: simplify link opening

* refactor: remove space

* feat: refactor preferences and start theming for light mode options

* refactor: fixing dark and light modes with dialogs

* refactor: popup dialogs use proper themes now

* refactor: alert dialogs and media edit fragments use attribute references

* refactor: use input bar button attribute instead color control normal in vector tint

* refactor: transparency, dialog fixes, notification fix

* refactor: attrs and styles for buttons

* fix: use prominent button color on the outline button's border

* fix: fix the trash

* refactor: remove the appearance

* refactor: avatar placeholder generation, chips and element border styles

* refactor: use colors instead of style references

* refactor: theming changes to match designs and feedback

* refactor: the titles are bold and the categories are tertiary coloured now

* fix: appearance settings match preferences, search bottom bar uses themed attributes

* refactor: increase setting button height

* Update clear all data dialog

* Update seed dialog

* refactor: more qa feedback changes

* feat: add new TLs and fa-rIR TLs

* Update notification content dialog

* Fix message requests clear all button text color

* feat: re-add screenshot observer

* refactor: make send tint accent color

* feat: add unread background differences

* fix: change unread count indicator

* build: upgrade build numbers

* Fix message requests popupmenu background color

* fix: crash from attr reference in color attribute

* build: upgrade build number

* fix: message bubbles, thumbnail backgrounds, search bar visibility with input bar, attachment buttons

* fix: tertiary text for keyboard page search view

* fix: emoji overflow colour differences

* fix: reaction pill dialog background is now correct colour

* Add style to reactions tab layout

* fix: appearance activity reverting primary color at correct time

* fix: show call privacy warning every time instead of just once

* fix: gradient background(?) and audio autoplay disable

* fix: crash in all media containing documents

* fix: reaction dialog heading fixes

* Add style to reactions tab layout

* fix: remove gradient backgrounds

* fix: adding new reaction normal text attribute to try correct the tab layout

* fix: ocean dark unread/read colours

* build; update build number

* build: update build number

* Fix ocean light theme pin color

* Update classic dark unread indicator text color

* Reduce group name text size on join community screen

* Restore tab indicator color on join community and all media screens

* Home adapter rename

* Updated the HomeViewModel to cancel old executors when new ones are added (#1)

* Updated the HomeViewModel to cancel old executors when new ones are added

* Removed a seemingly unneeded update

* Explicitly remove observers when the HomeActivity is paused (#2)

Co-authored-by: jubb <hjubb@users.noreply.github.com>
Co-authored-by: charles <charles@oxen.io>
Co-authored-by: Morgan Pretty <mpretty-cyro@users.noreply.github.com>
2022-10-18 17:45:16 +11:00
Morgan Pretty aecb40cce9
Fixed a bug where message requests could incorrectly appear in the thread list (#1009) 2022-10-18 15:57:32 +11:00
Harris 7a773016da
New app theming (#913)
* feat: start new app theming feature

* feat: add some theming colours

* refactor: start refactoring themes and colours to use dynamic attributes

* feat: adding more colours and switching over default colours to be theme based instead of hard-coded or day/night specific

* refactor: take a look at ocean light and logo colour

* feat: global search colours for light and dark ocean

* feat: more styling

* feat: adding themes to conversation activity and refactoring the base theme to apply over the top of the activity's theme so it retains noActionBar etc

* feat: add dynamic accent color

* docs: add todo for changing how accent colour is applied

* feat: update new theming to use override primary style so that the regular colorAccent attribute can be used in existing layouts

* feat: coordinating styles across layouts, fixing up pinned icons and naming for conversation list items

* refactor: re-styling layouts to match new themes and attributes. Need to figure out action mode close button

* refactor: remove @color/text and replace with ?android:textColorPrimary to override in themes

* refactor: add context theme wrapper to bottom sheet dialog that references accent color

* fix: input bar bug fix and preference activity themes

* refactor: new settings menu options

* fix: crash for PNModeActivity.kt

refactor: move ordering in seed dialog to match designs, copy changes to match new settings menu

* feat: add new appearance settings activity

* refactor: title and VM changes

* fix: correct override

* feat: add theme appearance screen UI features and start VM implementation. re-add legacy theme utils to get default for migration

* fix: compile errors and missing themes from emoji features

* refactor: remove background shape alteration and old bottom sheet styles, re-add the theme mode attr

* feat: appearance screen wired up, just need to refresh theme

* feat: add theme state recreation and fix match system settings option

* refactor: add bottom margin

* feat: explore custom preference category

* feat: add the customized session theme for CorrectedPreferenceFragment

* feat: replace AppProtectionPreferenceFragment to extend ListSummaryPreferenceFragment

* refactor: change drawable style and remove explicit dividers

* refactor: remove divider in CorrectedPreferenceFragment

* feat: add theme state check on resume, might be jarring currently

* feat: add preference divider elements for settings menu

* refactor: settings menu redesigns

* refactor: change led preference to integer and refactor TextSecurePreferences.kt

* feat: add scroll parcel to save/restore hierarchy on restart with appearance changes

* feat: add the conversations blocked contacts and refactor preference order and copy

* feat: add blocked contacts activity, basic layout and vm

* feat: add unblock DB functions and storage protocol, start working on the DB query state flow, might have to just implement recipient on modified listener

* feat: add blocked contacts and notif recipient listeners

* feat: add recipient db reader

* feat: add blocked contact interactions and fix a theming crash for notifications

* feat: introduce better equals and hashcode implementations to recipient, replace home diff util content check with hashcode-based comparison

* feat: add settings menu vectors

* fix: preview compile error

* refactor: migrating settings menu to new designs

* feat: help menu

* refactor: simplify link opening

* refactor: remove space

* feat: refactor preferences and start theming for light mode options

* refactor: fixing dark and light modes with dialogs

* refactor: popup dialogs use proper themes now

* refactor: alert dialogs and media edit fragments use attribute references

* refactor: use input bar button attribute instead color control normal in vector tint

* refactor: transparency, dialog fixes, notification fix

* refactor: attrs and styles for buttons

* fix: use prominent button color on the outline button's border

* fix: fix the trash

* refactor: remove the appearance

* refactor: avatar placeholder generation, chips and element border styles

* refactor: use colors instead of style references

* refactor: theming changes to match designs and feedback

* refactor: the titles are bold and the categories are tertiary coloured now

* fix: appearance settings match preferences, search bottom bar uses themed attributes

* refactor: increase setting button height

* Update clear all data dialog

* Update seed dialog

* refactor: more qa feedback changes

* feat: add new TLs and fa-rIR TLs

* Update notification content dialog

* Fix message requests clear all button text color

* feat: re-add screenshot observer

* refactor: make send tint accent color

* feat: add unread background differences

* fix: change unread count indicator

* build: upgrade build numbers

* Fix message requests popupmenu background color

* fix: crash from attr reference in color attribute

* build: upgrade build number

* fix: message bubbles, thumbnail backgrounds, search bar visibility with input bar, attachment buttons

* fix: tertiary text for keyboard page search view

* fix: emoji overflow colour differences

* fix: reaction pill dialog background is now correct colour

* Add style to reactions tab layout

* fix: appearance activity reverting primary color at correct time

* fix: show call privacy warning every time instead of just once

* fix: gradient background(?) and audio autoplay disable

* fix: crash in all media containing documents

* fix: reaction dialog heading fixes

* Add style to reactions tab layout

* fix: remove gradient backgrounds

* fix: adding new reaction normal text attribute to try correct the tab layout

* fix: ocean dark unread/read colours

* build; update build number

* build: update build number

Co-authored-by: charles <charles@oxen.io>
2022-10-12 17:05:55 +11:00
jubb 92075aed32 Merge branch 'master' into dev 2022-10-10 09:59:40 +11:00
charles eb74f901c1 Persist profile data 2022-10-05 10:05:26 +11:00
charles 140877f4e3 Refactor 2022-10-04 18:31:20 +11:00
charles 1f7edadc59 Add profile data to message request responses 2022-10-04 15:46:32 +11:00
charles c537da6acd fix: Show message sender in push notifications for groups 2022-10-03 16:31:29 +11:00
charles 99f70e5c21 Move open group query into click handler 2022-10-03 13:42:47 +11:00
charles ffa280bc1b fix: Disable typing and message requests in read-only open groups 2022-10-03 13:15:14 +11:00
Harris 54efc36a8b
fix: don't use RTL layout for header image (#982) 2022-09-30 16:55:29 +10:00
ceokot fbd1721eaf
Menu redesign (#958)
* feat: Menu redesign

* Add bottomsheet

* Handle default peek height

* Smooth out setting peek height

* Move contacts prep to util

* Dialog layout tweaks

* Contact grouping tweaks

* Add new message dialog

* Add public key input delegate

* Add create group dialog

* Add join community dialog

* Handle dialog back navigation

* Enter community url tab tweaks

* Scan QR code tab refactor

* Scan qr code refactor

* Direct and community tabs refactor

* Add session id copy context menu item

* Set dialog background colours

* Set full dialog background colour

* Minor tweaks

* Add closed group contact search

* Cleanup

* Add content descriptions

* Resize community chips

* Fix new conversation screen paddings

* Fix fade in/out of join community screen

* Prevent creating conversation with empty public key

* Resize and position create group loader

* Fix back nav after creating direct message conversation

* Fix inter-screen transitions

* Fix new conversation background colours

* Fix background colours

* Rename contact list header for clarity

* Bug fixes

* Enable scrolling of Enter Session ID tab of the new message dialog

* Minor refactor

* Switch to child fragment manager

* Fix member search on create group screen

Co-authored-by: charles <charles@oxen.io>
2022-09-30 13:32:07 +10:00
jubb 42b2271336 build: update version code 2022-09-26 11:53:00 +10:00
Harris d2dc86de88
Fix moderator race condition (#975)
* fix: race condition in clear all / add new members

* fix: max width emoji text view on smaller devices

* fix: add extra widths and reduce the original again

* fix: quoted messages for blinded servers, remove unused add group member function

* fix: quote sending and resending using blinded IDs in quotes

* build: update build number
2022-09-26 11:44:59 +10:00
jubb 3fcd972c2a build: increment version code 2022-09-13 17:53:23 +10:00
Harris b6106d5506
Use streams and collectors supported below API 24 in StaticEmojiPageModel (#966)
* fix: use annimon stream instead of java util

* refactor: convert remaining java.til stream and functions into annimon equivalent

* refactor: remove a .stream() reference

* fix: possible future NPE in ConversationReactionOverlay
2022-09-13 17:52:09 +10:00
jubb c09e4e4907 build: update build numbers 2022-09-13 17:02:44 +10:00
jubb 87c1ede75c fix: compile issue from merge 2022-09-13 16:31:08 +10:00
jubb 2c3a949bb3 Merge branch 'master' into dev 2022-09-13 16:26:49 +10:00
Harris bd51cbf6fd
Remove open group guidelines on official open groups (#933)
* refactor: remove binding references and code that handles official open groups

* refactor: remove open group guidelines activity and layouts
2022-09-13 15:55:24 +10:00
Harris 9f8ed4daf2
Replace default action message request behavior (#927)
* refactor: add a block action and change default message request behavior to decline/delete

* refactor: move log calls, add block & delete menu option

* refactor: migrate some more actions into ConversationActivityV2.kt and only show message request incoming menu if it's an incoming message request

* refactor: change block behaviour to be in the message request actions

* refactor: use block user copy

* refactor: parameters for ConversationMenuHelper interface cleaned up
2022-09-13 15:06:46 +10:00
Harris aa43ab2a2e
Display Note to self in conversation instead of your own Session ID (#929)
* fix: display title bar in conversation as "Note to self" if it's the local user's session ID

* docs: remove ID blinding comment as it's your own user and probably won't ever me a conversation with your blinded ID

* fix: compile issue for missing recipient local var

* refactor: use same logic for recipient modified listener
2022-09-13 15:06:20 +10:00
Harris 919bb01d58
Make the message trimming behaviour consistent across platform (#936)
* refactor: remove old trim behaviour and create new defaults

* refactor: add in trimming by before time

* refactor: remove old trim thread job, remove message processor scheduling trim thread

* refactor: remove spacing

* refactor: enable trimming by default

* refactor: remove trim now option in chat preference
2022-09-13 15:01:15 +10:00
Harris bdc4f5aebe
Fix blinded ID moderator checks in emoji reaction popup (#964)
* fix: emoji popup now uses blinded ID as well

* fix: add conversation menu handling for ban and delete all actions, as well as add placeholder icons
2022-09-13 14:17:47 +10:00
Harris 29124f36b6
Fix improperly caching `notifyType` on `Recipient`s (#965)
* fix: perform clear member roles before setting new roles to clear out old members

* fix: hopefully add somewhere notifyType wasn't being carried over

fixes #945
2022-09-13 14:15:35 +10:00
Harris 7d186c198e
Fix emoji notifications (#960)
* fix: don't notify on group threads

* fix: flip boolean and compile issue

* build: update build number
2022-09-07 14:41:39 +10:00
Harris 361ff8370b
Fix conversation toolbar extras (#961)
* fix: remove official open group banner, set block banner under toolbar

* fix: remove shade visibility in preview
2022-09-07 13:41:59 +10:00
Harris b1918f07e1
fix: remove in-thread notifications (#962) 2022-09-07 13:41:43 +10:00
jubb 16d4519d7e build: bump build numbers 2022-09-05 11:30:50 +10:00
Harris ebcae1f284
fix: emoji author parsing and adding message senders to storage (#957) 2022-09-05 11:29:47 +10:00
Harris 2520287aff release: bump version code 2022-09-04 21:10:42 +10:00
Harris 2bd078a441 Merge branch 'dev'
# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
#	libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt
#	libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt
2022-09-04 21:10:08 +10:00
ceokot 16ca97d2d3
Add emoji reacts support (#889)
* feat: Add emoji reacts support

* Remove message multi-selection

* Add emoji reaction model

* Add emoji reaction panel

* Blur reacts panel background

* Show emoji keyboard

* Add emoji sprites

* Update reaction proto

* Emoji database updates

* Emoji database refactor

* Emoji reaction persistence

* Optimize reactions retrieval

* Fix emoji group query

* Display emojis

* Fix emoji persistence

* Cleanup

* Persistence refactor

* Add reactions bottom sheet

* Cleanup

* Ui tweaks

* React with any emoji

* Show emoji react notifications

* Remove reaction

* Show reactions modal on long press

* Click to react (+1) with an emoji

* Click to react with an emoji

* Enable emoji expand/collapse

* fix: some compile issues from merge conflicts

* fix: compile issues merging quote and media message UI

* fix: xml IDs and adding in legacy is selected for future inclusion

* Fix view constraints

* Fix merge issue

* Add message selection option in conversation context menu

* Add sogs emoji integration

* Handle sogs emoji reactions

* Enable sending/deleting sogs emojis

* fix: improve the visible message layout

* fix: add file IDs to request parameters for message send (#940)

* Fix open group polling from seqno instead of last hash (#939)

* fix: reset seqno to get recent messages from open groups

* build: upgrade build numbers

* fix: actually run the migration

* Using StringBuilder to construct request url

* Fix reaction filter

* fix: is_mms added in second projection query

* Update default emojis

* fix: include legacy and new open groups in server ID tracking (#941)

* feat: add hidden moderator and admin roles, separated as they may be used independently in future (#942)

* Cleanup

* Fix view constraints

* Add reactions capability check

* Fix reactions alignment

* Ui fixes

* Display reactions list

* feat: add formatted count strings

* fix: account for negatives and add tests

* Migrate old official open group locations for polling and adding (#932)

* feat: adding in first part of open group migrations and tests for migration logic / helpers

* feat: test code and migration logic for open groups in the case of no conflicts

* feat: add in extra test cases and refactor code for migrator

* refactor: migrate open group join URLs and references to server in adding new open groups to catch legacy and re-write it

* refactor: joining open groups using OpenGroupUrlParser.kt now

* fix: add in compile issues for renamed OpenGroupApi.kt from OpenGroupV2

* fix: prevent duplicates of http/https for new open group DNS and prevent adding new groups based on public key

* fix: room and server swapped parameters

* fix: replace default server for config messages

* fix: actually using public key to de-dupe didn't work for rooms

* build: bump version code and name

* Display reactions list on open groups for moderators

* Ui tweaks

* Ui tweaks for moderation

* Refactor

* fix: compile issue

* fix: de-duping joined queries in the get X from cursor

* Restore import

* fix: colouring the reaction overlay scrubber

* fix: highlight colour, show reaction count if 1 or above

* Cleanup

* fix: light mode accent

* fix: light / dark mode themeing in reactions dialog fragment

* Emoji notification blinded id check

* fix: show reaction list correctly and pass isUserModerator to bind methods

* fix: remove unnecessary places for the moderator

* fix: X button for removing own react not showing up properly

* feat: add clear all header view

* fix: migrate the clear all to the correct location

* fix: use display instead of base

* Truncate emoji sender ids

* feat: add notify thread function in thread db

* Notify threads on reaction received

* fix: design fixes for the reaction list

* fix: emoji reactions bottom sheet dialog UI designs

* feat: add unsupported emoji reaction

* fix: crash and doing vector properly

* Fix reaction database queries

* Fix background open group adder job

* Show new open group reactions

* Fetch a maximum of 5 reactors

* Handle open group reactions polling conflicts

* Add count to user reaction

* Show number of additional reactors

* fix: unreads set same as the unread query

* fix: design changes

* fix: update dependency to improve flexboxlayout behaviour, design consistencies

* Add select message icon and update long press menu items order and wording

* Fix crash on reactors dialog

* fix: colours and backgrounds to match designs

* fix: add header in recipient item

* fix: margins

* fix: alignments and layout issues for emoji reactions view

* feat: add overflow previews and logic for overflow

* Dim action bar

* Add emoji search

* Search index fix

* Set count for 1:1 and closed group reactions when inserting in local database

* Use on screen toolbar to allow overlaying

* Show/hide scroll to bottom button

* feat: add extended properties so it doesn't collapse on re-bind

* Cleanup

* feat: prevent keeping extended on rebinding if we get a new message ID

* fix: long press works on devices now, fix release lint issue and crash for emoji search DBs from emoji builds

* Display message timestamp

* Fix modal items alignment

* fix: sort order and emoji count in compareTo

* Scale down really large messages to fit

* Prevent closed group crash

* Fix reaction author

Co-authored-by: charles <charles@oxen.io>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
2022-09-04 21:03:32 +10:00
Harris d6d0c52745
Migrate old official open group locations for polling and adding (#932)
* feat: adding in first part of open group migrations and tests for migration logic / helpers

* feat: test code and migration logic for open groups in the case of no conflicts

* feat: add in extra test cases and refactor code for migrator

* refactor: migrate open group join URLs and references to server in adding new open groups to catch legacy and re-write it

* refactor: joining open groups using OpenGroupUrlParser.kt now

* fix: add in compile issues for renamed OpenGroupApi.kt from OpenGroupV2

* fix: prevent duplicates of http/https for new open group DNS and prevent adding new groups based on public key

* fix: room and server swapped parameters

* fix: replace default server for config messages

* fix: actually using public key to de-dupe didn't work for rooms

* build: bump version code and name
2022-08-22 09:27:35 +10:00
ceokot 2bfc8215d4
Fix quote retrieval crash (#944)
Co-authored-by: charles <charles@oxen.io>
2022-08-18 16:25:08 +10:00
Harris 5469f232a0
feat: add hidden moderator and admin roles, separated as they may be used independently in future (#942) 2022-08-17 11:06:02 +10:00
Harris e7ca53ff72
fix: include legacy and new open groups in server ID tracking (#941) 2022-08-17 11:03:12 +10:00
Harris c65feba683
Fix open group polling from seqno instead of last hash (#939)
* fix: reset seqno to get recent messages from open groups

* build: upgrade build numbers

* fix: actually run the migration
2022-08-15 17:02:51 +10:00
Harris 7dcb566a57
fix: add file IDs to request parameters for message send (#940) 2022-08-15 16:54:15 +10:00
jubb 25eff4fece Merge remote-tracking branch 'upstream/master' into dev 2022-08-12 10:36:38 +10:00
jubb 865a69c49f build: update build number 2022-08-11 15:45:02 +10:00
jubb 6c07121d7a fix: crash for join open group dialog and v4 file upload in FS and open group 2022-08-11 15:39:37 +10:00
jubb 997389b940 Merge branch 'master' into dev 2022-08-11 09:54:32 +10:00
jubb 2f80fac57e build: update build version code 2022-08-11 09:54:19 +10:00
jubb 3944d5d1df Merge branch 'master' into dev 2022-08-11 09:50:39 +10:00
jubb 44fc0fc2dd fix: only fetch latest messages on first join 2022-08-11 09:50:10 +10:00
jubb 00f28b7360 build: update version code 2022-08-10 19:04:09 +10:00
jubb 1ba204855c fix: db migration update 2022-08-10 19:03:46 +10:00
jubb 4007875f07 build: update build number and version code 2022-08-10 18:19:26 +10:00
ceokot bee287bb7e
Add Session Id blinding (#862)
* feat: Add Session Id blinding

Including modified version of lazysodium-android to expose missing libsodium functions, we could build from a fork which we still need to setup.

* Add v4 onion request handling

* Update SOGS signature construction

* Fix SOGS signature construction

* Update onion request

* Update signature data

* Keep path prefixes for v4 endpoints

* Update SOGS signature message

* Rename to remove api version suffix

* Update onion response parsing

* Refactor file download paths

* Implement request batching

* Refactor batch response handling

* Handle batch endpoint responses

* Update batch endpoint responses

* Update attachment download handling

* Handle file downloads

* Handle inbox messages

* Fix issue with file downloads

* Preserve image bytearray encoding

* Refactor

* Open group message requests

* Check id blinding in user detail bottom sheet rather

* Message validation refactor

* Cache last inbox/outbox server ids

* Update message encryption/decryption

* Refactor

* Refactor

* Bypass user details bottom sheet in open groups for blinded session ids

* Fix capabilities call auth

* Refactor

* Revert default server details

* Update sodium dependency to forked repo

* Fix attachment upload

* Revert "Update sodium dependency to forked repo"

This reverts commit c7db9529f9.

* Add signed sodium lib

* Update contact id truncation and mention logic

* Open group inbox messaging fix

* Refactor

* Update blinded id check

* Fix open group message sends

* Fix crash on open group direct message send

* Direct message refactor

* Direct message encrypt/decrypt fixes

* Use updated curve25519 version

* Updated lazysodium dependency

* Update encryption/decryption calls

* Handle direct message parse errors

* Minor refactor

* Existing chat refactor

* Update encryption & decryption parameters

* Fix authenticated ciphertext size

* Set direct message sync target

* Update direct message thread lookup

* Add blinded id mapping table

* Add blinded id mapping table

* Update threads after sends

* Update open group message timestamp handling

* Filter unblinded contacts

* Format blinded id mentions

* Add message deleted field

* Hide open group inbox id

* Update message request response handling

* Update message request response sender handling

* Fix mentions of blinded ids

* Handle open group poll failure

* fix: add log for failed open group onion request, add decoding body for blinding required error at destination

* fix: change the error check

* Persist group members

* Reschedule polling after capabilities update

* Retry on other exceptions

* Minor refactor

* Open group profile fix

* Group member db schema update

* Fix ban request key

* Update ban response type

* Ban endpoint updates

* Ban endpoint updates

* Delete messages

Co-authored-by: charles <charles@oxen.io>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
2022-08-10 18:17:48 +10:00
Harris d53752713e
Align quote behaviour, move the media message outside of text bubble to simplify layouts (#935)
* refactor: remove text from quote model

* refactor: add docs for TODOs where quote text should be refactored

* refactor: remove the references to stored text in the quote and get the quote text from referenced DB lookup

* refactor: drop the quote data from DB

* fix: turns out we can't drop columns using this version of sqlite

* fix: fixing an attachment download bug, fixing up UI issues with quotes and body text

* feat: split off the message attachment UI from message bubble

* refactor: replace media thumbnails with new designs

* refactor: add debug drawing to troubleshoot swipe gesture

* fix: fix the swipe to reply gesture drawing
2022-08-08 15:16:33 +10:00
jubb b1e954084c build: update build number and version 2022-07-19 17:15:54 +10:00
Harris 2ea7f638d8
fix: uninitialized exception for lateinit guardSnode in certain cases, replace with a nullable version for error handling (#926) 2022-07-19 14:32:05 +10:00
Harris d3e2ef0b40
fix: remove declared phone state permission (#919) 2022-07-19 14:31:58 +10:00
Harris 344e7f333e
Fix notification release issues (#925)
* fix: background polling issue for 1on1 messages, update the HomeDiffUtil.kt to include more cases to compare content equality, add synchronized ConversationNotificationDebouncer.kt and reduce the debouncer time

* fix: replace updateNotification to be thread-specific to fix unread behaviour and currently visible thread behaviour

* refactor: remove trimmed text limits
2022-07-19 14:31:50 +10:00
jubb ba60e8a8ee fix: fallback on biometric key failures and retry creating key, fix up notification issues 2022-06-21 16:17:01 +10:00
1063 changed files with 37949 additions and 27868 deletions

View File

@ -1,45 +0,0 @@
<!-- 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.
Before we begin, please note that this tracker is only for issues. It is not for questions, comments, or feature requests.
If you are looking for support, please file an issue or email team@oxen.io.
Let's begin with a checklist: Replace the empty checkboxes [ ] below with checked ones [x] accordingly. -->
- [ ] I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
- [ ] I have searched open and closed issues for duplicates
- [ ] I am submitting a bug report for existing functionality that does not work as intended
- [ ] This isn't a feature request or a discussion topic
----------------------------------------
### Bug description
Describe here the issue that you are experiencing.
### Steps to reproduce
- using hyphens as bullet points
- list the steps
- that reproduce the bug
**Actual result:**
Describe here what happens after you run the steps above (i.e. the buggy behaviour)
**Expected result:**
Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour)
### Screenshots
<!-- you can drag and drop images below -->
### Device info
<!-- replace the examples with your info -->
**Device:** Manufacturer Model XVI
**Android version:** 0.0.0
**Session version:** 0.0.0
### Link to debug log

View File

@ -1,34 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Code of conduct**
- [ ] I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
**Describe the bug**
A clear and concise description of what the bug is.
**To reproduce**
Steps to reproduce the behavior:
**Screenshots or logs**
If applicable, add screenshots or logs to help explain your problem.
**Smartphone (please complete the following information):**
- Device: [e.g. Samsung Galaxy S8]
- OS: [e.g. Android Pie]
- Version of Session or latest commit hash
**Additional context**
Add any other context about the problem here.

74
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,74 @@
name: 🐞 Bug Report
description: Create a report to help us improve
title: "[BUG] <title>"
labels: [bug]
body:
- type: checkboxes
attributes:
label: Code of conduct
description: I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I have read and agree to adhere to the [Code of Conduct](https://github.com/oxen-io/session-android/blob/master/CODE_OF_CONDUCT.md)
required: true
- type: checkboxes
attributes:
label: Self-training on how to write a bug report
description: High quality bug reports can help the team save time and improve the chance of getting your issue fixed. Please read [how to write a bug report](https://www.browserstack.com/guide/how-to-write-a-bug-report) before submitting your issue.
options:
- label: I have learned [how to write a bug report](https://www.browserstack.com/guide/how-to-write-a-bug-report)
required: true
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: false
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: input
attributes:
label: Android Version
description: What version of Android are you running?
placeholder: ex. Android 11
validations:
required: false
- type: input
attributes:
label: Session Version
description: What version of Session are you running? (This can be found at the bottom of the app settings)
placeholder: ex. 1.17.0 (3425)
validations:
required: false
- type: textarea
attributes:
label: Anything else?
description: |
Add any other context about the problem here.
Tip: You can attach screenshots or log files to help explain your problem by clicking this area to highlight it and then dragging files in.
validations:
required: false

View File

@ -0,0 +1,26 @@
name: 🚀 Feature request
description: Suggest an idea for Session
title: '[Feature] <title>'
labels: [feature-request]
body:
- type: checkboxes
attributes:
label: Is there an existing request for feature?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: What feature would you like?
description: |
A clear and concise description of the feature you would like added to Session
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Add any other context or screenshots about the feature request here
validations:
required: false

3
.gitignore vendored
View File

@ -15,4 +15,5 @@ signing.properties
ffpr
*.sh
pkcs11.password
play
app/play
app/huawei

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "libsession-util/libsession-util"]
path = libsession-util/libsession-util
url = https://github.com/oxen-io/libsession-util.git

View File

@ -32,6 +32,13 @@ Setting up a development environment and building from Android Studio
4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes".
5. Default config options should be good enough.
6. Project initialization and building should proceed.
7. Clone submodules with `git submodule update --init --recursive`
If you would like to build the Huawei Flavor with Huawei HMS push notifications you will need to pass 'huawei' as a command line arg to include the required dependencies.
e.g. `./gradlew assembleHuaweiDebug -Phuawei`
If you are building in Android Studio then add `-Phuawei` to `Preferences > Build, Execution, Deployment > Gradle-Android Compiler > Command-line Options`
Contributing code
-----------------

View File

@ -4,13 +4,13 @@
Add the [F-Droid repo](https://fdroid.getsession.org/)
[Download the APK from here](https://github.com/loki-project/session-android/releases/latest)
[Download the APK from here](https://github.com/oxen-io/session-android/releases/latest)
## Summary
Session integrates directly with [Oxen Service Nodes](https://docs.oxen.io/about-the-oxen-blockchain/oxen-service-nodes), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
<img src="https://i.imgur.com/dO9f7Hg.jpg" width="320" />
<img src="https://i.imgur.com/wcdAGBh.png" width="320" />
## Want to contribute? Found a bug or have a feature request?

View File

@ -1,24 +1,29 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
classpath files('libs/gradle-witness.jar')
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
classpath "com.google.gms:google-services:4.3.10"
classpath "com.google.gms:google-services:$googleServicesVersion"
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
}
}
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'witness'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'kotlinx-serialization'
apply plugin: 'dagger.hilt.android.plugin'
@ -26,31 +31,210 @@ configurations.all {
exclude module: "commons-logging"
}
def canonicalVersionCode = 360
def canonicalVersionName = "1.17.5"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4,
'universal' : 5]
android {
compileSdkVersion androidCompileSdkVersion
namespace 'network.loki.messenger'
useLibrary 'org.apache.http.legacy'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
}
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.4.7'
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion androidMinimumSdkVersion
targetSdkVersion androidTargetSdkVersion
multiDexEnabled = true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "session")
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
resConfigs autoResConfig()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}
sourceSets {
String sharedTestDir = 'src/sharedTest/java'
test.java.srcDirs += sharedTestDir
androidTest.java.srcDirs += sharedTestDir
}
buildTypes {
release {
minifyEnabled false
}
debug {
isDefault true
minifyEnabled false
}
}
flavorDimensions "distribution"
productFlavors {
play {
isDefault true
dimension "distribution"
apply plugin: 'com.google.gms.google-services'
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"'
}
huawei {
dimension "distribution"
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.HUAWEI"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"_HUAWEI\"'
}
website {
dimension "distribution"
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"'
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
lintOptions {
abortOnError true
baseline file("lint-baseline.xml")
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
buildFeatures {
dataBinding true
viewBinding true
}
def huaweiEnabled = project.properties['huawei'] != null
applicationVariants.configureEach { variant ->
if (variant.flavorName == 'huawei') {
variant.getPreBuildProvider().configure { task ->
task.doFirst {
if (!huaweiEnabled) {
def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See BUILDING.md'
logger.error(message)
throw new GradleException(message)
}
}
}
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.2.1'
implementation("com.google.dagger:hilt-android:2.46.1")
kapt("com.google.dagger:hilt-android-compiler:2.44")
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "com.google.android.material:material:$materialVersion"
implementation 'com.google.android:flexbox:2.0.1'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.3.3'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.exifinterface:exifinterface:1.3.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
implementation 'androidx.activity:activity-ktx:1.2.2'
implementation 'androidx.fragment:fragment-ktx:1.3.2'
implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.work:work-runtime-ktx:2.4.0"
implementation ("com.google.firebase:firebase-messaging:18.0.0") {
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
implementation 'androidx.activity:activity-ktx:1.5.1'
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.work:work-runtime-ktx:2.7.1"
playImplementation ("com.google.firebase:firebase-messaging:18.0.0") {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0'
@ -90,23 +274,17 @@ dependencies {
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.freemarker'
}
implementation 'androidx.sqlite:sqlite-ktx:2.3.1'
implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'
implementation project(":libsignal")
implementation project(":libsession")
implementation project(":libsession-util")
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
implementation "org.whispersystems:curve25519-java:$curve25519Version"
implementation 'com.goterl:lazysodium-android:5.0.2@aar'
implementation "net.java.dev.jna:jna:5.8.0@aar"
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
implementation project(":liblazysodium")
implementation "net.java.dev.jna:jna:5.12.1@aar"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
@ -115,183 +293,58 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion"
implementation "com.github.lelloman:android-identicons:v11"
implementation "com.prof.rssparser:rssparser:2.0.4"
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
implementation "com.github.ybq:Android-SpinKit:1.4.0"
implementation "com.opencsv:opencsv:4.6"
testImplementation 'junit:junit:4.12'
testImplementation "junit:junit:$junitVersion"
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation "org.mockito:mockito-inline:4.0.0"
testImplementation "org.mockito:mockito-inline:4.10.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.3.0'
testImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "org.mockito:mockito-android:4.10.0"
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "androidx.test:core:$testCoreVersion"
testImplementation "androidx.arch.core:core-testing:2.2.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Core library
androidTestImplementation 'androidx.test:core:1.4.0'
androidTestImplementation "androidx.test:core:$testCoreVersion"
androidTestImplementation('com.adevinta.android:barista:4.2.0') {
exclude group: 'org.jetbrains.kotlin'
}
// AndroidJUnitRunner and JUnit Rules
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
// Assertions
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.ext:truth:1.4.0'
androidTestImplementation 'com.google.truth:truth:1.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.ext:truth:1.5.0'
androidTestImplementation 'com.google.truth:truth:1.1.3'
// Espresso dependencies
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
androidTestUtil 'androidx.test:orchestrator:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
androidTestUtil 'androidx.test:orchestrator:1.4.2'
testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'org.robolectric:shadows-multidex:4.4'
}
def canonicalVersionCode = 286
def canonicalVersionName = "1.13.5"
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
implementation 'androidx.compose.ui:ui:1.5.2'
implementation 'androidx.compose.ui:ui-tooling:1.5.2'
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
implementation "androidx.compose.runtime:runtime-livedata:1.5.2"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4,
'universal' : 5]
android {
compileSdkVersion androidCompileSdkVersion
buildToolsVersion '29.0.3'
useLibrary 'org.apache.http.legacy'
dexOptions {
javaMaxHeapSize "4g"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
}
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion androidMinimumSdkVersion
targetSdkVersion androidCompileSdkVersion
multiDexEnabled = true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "session")
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
resConfigs autoResConfig()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}
sourceSets {
String sharedTestDir = 'src/sharedTest/java'
test.java.srcDirs += sharedTestDir
androidTest.java.srcDirs += sharedTestDir
}
buildTypes {
release {
minifyEnabled false
}
debug {
minifyEnabled false
}
}
flavorDimensions "distribution"
productFlavors {
play {
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
}
website {
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
lintOptions {
abortOnError true
baseline file("lint-baseline.xml")
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
buildFeatures {
dataBinding true
viewBinding true
}
implementation 'androidx.compose.foundation:foundation-layout:1.5.2'
implementation 'androidx.compose.material:material:1.5.2'
}
static def getLastCommitTimestamp() {
@ -312,3 +365,8 @@ def autoResConfig() {
.collect { matcher -> matcher.group(1) }
.sort()
}
// Allow references to generated code
kapt {
correctErrorTypes = true
}

View File

@ -2,6 +2,7 @@
-keepattributes SourceFile,LineNumberTable
-keep class org.whispersystems.** { *; }
-keep class org.thoughtcrime.securesms.** { *; }
-keep class org.thoughtcrime.securesms.components.menu.** { *; }
-keep class org.session.** { *; }
-keepclassmembers class ** {
public void onEvent*(**);

View File

@ -1,5 +1,6 @@
package network.loki.messenger
import android.Manifest
import android.app.Instrumentation
import android.content.ClipboardManager
import android.content.Context
@ -21,8 +22,8 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import com.adevinta.android.barista.interaction.PermissionGranter
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
import network.loki.messenger.util.NewConversationButtonDrawableMatcher.Companion.newConversationButtonWithDrawable
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
@ -39,7 +40,6 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
import org.thoughtcrime.securesms.home.HomeActivity
import org.thoughtcrime.securesms.mms.GlideApp
@RunWith(AndroidJUnit4::class)
@LargeTest
class HomeActivityTests {
@ -87,11 +87,13 @@ class HomeActivityTests {
}
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
onView(withId(R.id.registerButton)).perform(ViewActions.click())
// allow notification permission
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
}
private fun goToMyChat() {
onView(newConversationButtonWithDrawable(R.drawable.ic_plus)).perform(ViewActions.click())
onView(newConversationButtonWithDrawable(R.drawable.ic_message)).perform(ViewActions.click())
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
// new chat
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
onView(withId(R.id.copyButton)).perform(ViewActions.click())
@ -102,6 +104,7 @@ class HomeActivityTests {
copied = clipboardManager.primaryClip!!.getItemAt(0).text.toString()
}
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied))
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
}
@ -155,6 +158,7 @@ class HomeActivityTests {
val dialogPromptText = InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.dialog_open_url_explanation, amazonPuny)
onView(isRoot()).perform(waitFor(1000)) // no other way for this to work apparently
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
}

View File

@ -0,0 +1,102 @@
package network.loki.messenger
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argThat
import org.mockito.kotlin.eq
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
@SmallTest
class LibSessionTests {
private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
private var fakeHashI = 0
private val nextFakeHash: String
get() = "fakehash${fakeHashI++}"
private fun maybeGetUserInfo(): Pair<ByteArray, String>? {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
val prefs = appContext.prefs
val localUserPublicKey = prefs.getLocalNumber()
val secretKey = with(appContext) {
val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
edKey.secretKey.asBytes
}
return if (localUserPublicKey == null || secretKey == null) null
else secretKey to localUserPublicKey
}
private fun buildContactMessage(contactList: List<Contact>): ByteArray {
val (key,_) = maybeGetUserInfo()!!
val contacts = Contacts.Companion.newInstance(key)
contactList.forEach { contact ->
contacts.set(contact)
}
return contacts.push().config
}
private fun fakePollNewConfig(configBase: ConfigBase, toMerge: ByteArray) {
configBase.merge(nextFakeHash to toMerge)
MessagingModuleConfiguration.shared.configFactory.persist(configBase, System.currentTimeMillis())
}
@Before
fun setupUser() {
PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext).edit {
putBoolean(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, true).apply()
}
val newBytes = randomSeedBytes().toByteArray()
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
val kp = KeyPairUtilities.generate(newBytes)
KeyPairUtilities.store(context, kp.seed, kp.ed25519KeyPair, kp.x25519KeyPair)
val registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(context, registrationID)
TextSecurePreferences.setLocalNumber(context, kp.x25519KeyPair.hexEncodedPublicKey)
TextSecurePreferences.setRestorationTime(context, 0)
TextSecurePreferences.setHasViewedSeed(context, false)
}
@Test
fun migration_one_to_ones() {
val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
val storageSpy = spy(app.storage)
app.storage = storageSpy
val newContactId = randomSessionId()
val singleContact = Contact(
id = newContactId,
approved = true,
expiryMode = ExpiryMode.NONE
)
val newContactMerge = buildContactMessage(listOf(singleContact))
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
fakePollNewConfig(contacts, newContactMerge)
verify(storageSpy).addLibSessionContacts(argThat {
first().let { it.id == newContactId && it.approved } && size == 1
})
verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
}
}

View File

@ -0,0 +1,166 @@
package network.loki.messenger
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.toHexString
@RunWith(AndroidJUnit4::class)
class SodiumUtilitiesTest {
private val publicKey: String = "88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"
private val privateKey: String = "30d796c1ddb4dc455fd998a98aa275c247494a9a7bde9c1fee86ae45cd585241"
private val edKeySeed: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9"
private val edPublicKey: String = "bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
private val edSecretKey: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
private val blindedPublicKey: String = "98932d4bccbe595a8789d7eb1629cefc483a0eaddc7e20e8fe5c771efafd9af5"
private val serverPublicKey: String = "c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d"
private val edKeyPair = KeyPair(Key.fromHexString(edPublicKey), Key.fromHexString(edSecretKey))
@Test
fun generateBlindingFactorSuccess() {
val result = SodiumUtilities.generateBlindingFactor(serverPublicKey)
assertThat(result?.toHexString(), equalTo("84e3eb75028a9b73fec031b7448e322a68ca6485fad81ab1bead56f759ebeb0f"))
}
@Test
fun generateBlindingFactorFailure() {
val result = SodiumUtilities.generateBlindingFactor("Test")
assertNull(result?.toHexString())
}
@Test
fun blindedKeyPairSuccess() {
val result = SodiumUtilities.blindedKeyPair(serverPublicKey, edKeyPair)!!
assertThat(result.publicKey.asHexString.lowercase(), equalTo(blindedPublicKey))
assertThat(result.secretKey.asHexString.take(64).lowercase(), equalTo("16663322d6b684e1c9dcc02b9e8642c3affd3bc431a9ea9e63dbbac88ce7a305"))
}
@Test
fun blindedKeyPairFailurePublicKeyLength() {
val result = SodiumUtilities.blindedKeyPair(
serverPublicKey,
KeyPair(Key.fromHexString(edPublicKey.take(4)), Key.fromHexString(edKeySeed))
)
assertNull(result)
}
@Test
fun blindedKeyPairFailureSecretKeyLength() {
val result = SodiumUtilities.blindedKeyPair(
serverPublicKey,
KeyPair(Key.fromHexString(edPublicKey), Key.fromHexString(edSecretKey.take(4)))
)
assertNull(result)
}
@Test
fun blindedKeyPairFailureBlindingFactor() {
val result = SodiumUtilities.blindedKeyPair("Test", edKeyPair)
assertNull(result)
}
@Test
fun sogsSignature() {
val expectedSignature = "dcc086abdd2a740d9260b008fb37e12aa0ff47bd2bd9e177bbbec37fd46705a9072ce747bda66c788c3775cdd7ad60ad15a478e0886779aad5d795fd7bf8350d"
val result = SodiumUtilities.sogsSignature(
"TestMessage".toByteArray(),
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed("44d82cc15c0a5056825cae7520b6b52d000a23eb0c5ed94c4be2d9dc41d2d409"),
Hex.fromStringCondensed("0bb7815abb6ba5142865895f3e5286c0527ba4d31dbb75c53ce95e91ffe025a2")
)
assertThat(result?.toHexString(), equalTo(expectedSignature))
}
@Test
fun combineKeysSuccess() {
val result = SodiumUtilities.combineKeys(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(edPublicKey)
)
assertThat(result?.toHexString(), equalTo("1159b5d0fcfba21228eb2121a0f59712fa8276fc6e5547ff519685a40b9819e6"))
}
@Test
fun combineKeysFailure() {
val result = SodiumUtilities.combineKeys(
SodiumUtilities.generatePrivateKeyScalar(Hex.fromStringCondensed(edSecretKey))!!,
Hex.fromStringCondensed(publicKey)
)
assertNull(result?.toHexString())
}
@Test
fun sharedBlindedEncryptionKeySuccess() {
val result = SodiumUtilities.sharedBlindedEncryptionKey(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(blindedPublicKey),
Hex.fromStringCondensed(publicKey),
Hex.fromStringCondensed(blindedPublicKey)
)
assertThat(result?.toHexString(), equalTo("388ee09e4c356b91f1cce5cc0aa0cf59e8e8cade69af61685d09c2d2731bc99e"))
}
@Test
fun sharedBlindedEncryptionKeyFailure() {
val result = SodiumUtilities.sharedBlindedEncryptionKey(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(publicKey),
Hex.fromStringCondensed(edPublicKey),
Hex.fromStringCondensed(publicKey)
)
assertNull(result?.toHexString())
}
@Test
fun sessionIdSuccess() {
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
assertTrue(result)
}
@Test
fun sessionIdFailureInvalidSessionId() {
val result = SodiumUtilities.sessionId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
assertFalse(result)
}
@Test
fun sessionIdFailureInvalidBlindedId() {
val result = SodiumUtilities.sessionId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
assertFalse(result)
}
@Test
fun sessionIdFailureBlindingFactor() {
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", "Test")
assertFalse(result)
}
}

View File

@ -5,24 +5,6 @@ import androidx.annotation.DrawableRes
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
import org.thoughtcrime.securesms.home.NewConversationButtonSetView
class NewConversationButtonDrawableMatcher(@DrawableRes private val expectedId: Int): TypeSafeMatcher<View>() {
companion object {
@JvmStatic fun newConversationButtonWithDrawable(@DrawableRes expectedId: Int) = NewConversationButtonDrawableMatcher(expectedId)
}
override fun describeTo(description: Description?) {
description?.appendText("with drawable on button with resource id: $expectedId")
}
override fun matchesSafely(item: View): Boolean {
if (item !is NewConversationButtonSetView.Button) return false
return item.getIconID() == expectedId
}
}
class InputBarButtonDrawableMatcher(@DrawableRes private val expectedId: Int): TypeSafeMatcher<View>() {

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:node="merge">
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="appid=107205081">
</meta-data>
<meta-data
android:name="com.huawei.hms.client.cpid"
android:value="cpid=30061000024605000">
</meta-data>
<service
android:name="org.thoughtcrime.securesms.notifications.HuaweiPushService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -0,0 +1,96 @@
{
"agcgw":{
"backurl":"connect-dre.hispace.hicloud.com",
"url":"connect-dre.dbankcloud.cn",
"websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com",
"websocketurl":"connect-ws-dre.hispace.dbankcloud.cn"
},
"agcgw_all":{
"CN":"connect-drcn.dbankcloud.cn",
"CN_back":"connect-drcn.hispace.hicloud.com",
"DE":"connect-dre.dbankcloud.cn",
"DE_back":"connect-dre.hispace.hicloud.com",
"RU":"connect-drru.hispace.dbankcloud.ru",
"RU_back":"connect-drru.hispace.dbankcloud.cn",
"SG":"connect-dra.dbankcloud.cn",
"SG_back":"connect-dra.hispace.hicloud.com"
},
"websocketgw_all":{
"CN":"connect-ws-drcn.hispace.dbankcloud.cn",
"CN_back":"connect-ws-drcn.hispace.dbankcloud.com",
"DE":"connect-ws-dre.hispace.dbankcloud.cn",
"DE_back":"connect-ws-dre.hispace.dbankcloud.com",
"RU":"connect-ws-drru.hispace.dbankcloud.ru",
"RU_back":"connect-ws-drru.hispace.dbankcloud.cn",
"SG":"connect-ws-dra.hispace.dbankcloud.cn",
"SG_back":"connect-ws-dra.hispace.dbankcloud.com"
},
"client":{
"cp_id":"890061000023000573",
"product_id":"99536292102532562",
"client_id":"954244311350791232",
"client_secret":"555999202D718B6744DAD2E923B386DC17F3F4E29F5105CE0D061EED328DADEE",
"project_id":"99536292102532562",
"app_id":"107205081",
"api_key":"DAEDABeddLEqUy0QRwa1THLwRA0OqrSuyci/HjNvVSmsdWsXRM2U2hRaCyqfvGYH1IFOKrauArssz/WPMLRHCYxliWf+DTj9bDwlWA==",
"package_name":"network.loki.messenger"
},
"oauth_client":{
"client_id":"107205081",
"client_type":1
},
"app_info":{
"app_id":"107205081",
"package_name":"network.loki.messenger"
},
"service":{
"analytics":{
"collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
"collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
"collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
"collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
"collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
"resource_id":"p1",
"channel_id":""
},
"edukit":{
"edu_url":"edukit.edu.cloud.huawei.com.cn",
"dh_url":"edukit.edu.cloud.huawei.com.cn"
},
"search":{
"url":"https://search-dre.cloud.huawei.com"
},
"cloudstorage":{
"storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia",
"storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru",
"storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru",
"storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu",
"storage_url_de":"https://ops-dre.agcstorage.link",
"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn",
"storage_url_sg":"https://ops-dra.agcstorage.link",
"storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn",
"storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn"
},
"ml":{
"mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
}
},
"region":"DE",
"configuration_version":"3.0",
"appInfos":[
{
"package_name":"network.loki.messenger",
"client":{
"app_id":"107205081"
},
"app_info":{
"package_name":"network.loki.messenger",
"app_id":"107205081"
},
"oauth_client":{
"client_type":1,
"client_id":"107205081"
}
}
]
}

View File

@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.notifications
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class HuaweiBindingModule {
@Binds
abstract fun bindTokenFetcher(tokenFetcher: HuaweiTokenFetcher): TokenFetcher
}

View File

@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.notifications
import android.os.Bundle
import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import org.json.JSONException
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import java.lang.Exception
import javax.inject.Inject
private val TAG = HuaweiPushService::class.java.simpleName
@AndroidEntryPoint
class HuaweiPushService: HmsMessageService() {
@Inject lateinit var pushRegistry: PushRegistry
@Inject lateinit var pushReceiver: PushReceiver
override fun onMessageReceived(message: RemoteMessage?) {
Log.d(TAG, "onMessageReceived")
message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPush) ?:
pushReceiver.onPush(message?.data?.let(Base64::decode))
}
override fun onNewToken(token: String?) {
pushRegistry.register(token)
}
override fun onNewToken(token: String?, bundle: Bundle?) {
Log.d(TAG, "New HCM token: $token.")
pushRegistry.register(token)
}
override fun onDeletedMessages() {
Log.d(TAG, "onDeletedMessages")
pushRegistry.refresh(false)
}
}

View File

@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import com.huawei.hms.aaid.HmsInstanceId
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.session.libsignal.utilities.Log
import javax.inject.Inject
import javax.inject.Singleton
private const val APP_ID = "107205081"
private const val TOKEN_SCOPE = "HCM"
@Singleton
class HuaweiTokenFetcher @Inject constructor(
@ApplicationContext private val context: Context,
private val pushRegistry: Lazy<PushRegistry>,
): TokenFetcher {
override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run {
// https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/push-basic-capability#h2-1576218800370
// getToken may return an empty string, if so HuaweiPushService#onNewToken will be called.
withContext(Dispatchers.IO) { getToken(APP_ID, TOKEN_SCOPE) }
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="preferences_notifications_strategy_category_fast_mode_summary">You\'ll be notified of new messages reliably and immediately using Huaweis notification servers.</string>
<string name="activity_pn_mode_fast_mode_explanation">You\'ll be notified of new messages reliably and immediately using Huaweis notification servers.</string>
</resources>

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="network.loki.messenger">
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" />
@ -30,11 +29,18 @@
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
@ -53,7 +59,6 @@
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<queries>
@ -142,22 +147,15 @@
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.dms.CreatePrivateChatActivity"
android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.groups.CreateClosedGroupActivity"
android:screenOrientation="portrait" />
android:theme="@style/Theme.Session.DayNight.FlatActionBar"
android:label="@string/blocked_contacts_title"
/>
<activity
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
android:label="@string/activity_edit_closed_group_title"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.groups.JoinPublicChatActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.onboarding.SeedActivity"
android:screenOrientation="portrait" />
@ -174,8 +172,15 @@
<activity
android:name="org.thoughtcrime.securesms.preferences.ChatSettingsActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity"
android:label="@string/activity_help_settings_title"
android:screenOrientation="portrait" />
<activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity"
android:screenOrientation="portrait"/>
<activity
android:exported="true"
android:name="org.thoughtcrime.securesms.ShareActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:excludeFromRecents="true"
@ -222,7 +227,7 @@
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
android:screenOrientation="portrait"
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
android:theme="@style/Theme.Session.DayNight.FlatActionBar">
android:theme="@style/Theme.Session.DayNight.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
@ -230,16 +235,8 @@
<activity
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight">
android:theme="@style/Theme.Session.DayNight">
</activity>
<activity
android:name="org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight" />
<activity
android:name="org.thoughtcrime.securesms.longmessage.LongMessageActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight" />
<activity
android:name="org.thoughtcrime.securesms.DatabaseUpgradeActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@ -253,14 +250,14 @@
<activity
android:name="org.thoughtcrime.securesms.giph.ui.GiphyActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.TextSecure.DayNight.NoActionBar"
android:theme="@style/Theme.Session.DayNight.NoActionBar"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name="org.thoughtcrime.securesms.mediasend.MediaSendActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight.NoActionBar"
android:theme="@style/Theme.Session.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden" />
<activity
android:name="org.thoughtcrime.securesms.MediaPreviewActivity"
@ -311,22 +308,19 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
</activity>
<service
android:name="org.thoughtcrime.securesms.notifications.PushNotificationService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
android:foregroundServiceType="microphone"
android:exported="false" />
<service
android:name="org.thoughtcrime.securesms.service.KeyCachingService"
android:enabled="true"
android:exported="false" />
android:exported="false" android:foregroundServiceType="specialUse">
<!-- <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"-->
<!-- android:value="@string/preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint"/>-->
</service>
<service
android:name="org.thoughtcrime.securesms.service.DirectShareService"
android:exported="true"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
<intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" />
@ -399,58 +393,54 @@
android:name="org.thoughtcrime.securesms.database.DatabaseContentProviders$StickerPack"
android:authorities="network.loki.securesms.database.stickerpack"
android:exported="false" />
<provider
android:name="org.thoughtcrime.securesms.database.DatabaseContentProviders$Recipient"
android:authorities="network.loki.securesms.database.recipient"
android:exported="false" />
<receiver android:name="org.thoughtcrime.securesms.service.BootReceiver">
<receiver android:name="org.thoughtcrime.securesms.service.BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="network.loki.securesms.RESTART" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener">
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.notifications.LocaleChangedReceiver">
<receiver android:name="org.thoughtcrime.securesms.notifications.LocaleChangedReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver">
<receiver android:name="org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver"
android:exported="true">
<intent-filter>
<action android:name="network.loki.securesms.DELETE_NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name="org.thoughtcrime.securesms.service.PanicResponderListener"
android:exported="true">
android:exported="false">
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" />
</intent-filter>
</receiver>
<receiver
android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver"
android:enabled="true">
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name="org.thoughtcrime.securesms.jobmanager.JobSchedulerScheduler$SystemService"
android:enabled="@bool/enable_job_service"
android:permission="android.permission.BIND_JOB_SERVICE"
tools:targetApi="26" />
<service
android:name="org.thoughtcrime.securesms.jobmanager.KeepAliveService"
android:enabled="@bool/enable_alarm_manager" />
<receiver
android:name="org.thoughtcrime.securesms.jobmanager.AlarmManagerScheduler$RetryReceiver"
android:enabled="@bool/enable_alarm_manager" /> <!-- Probably don't need this one -->
<uses-library
android:name="com.sec.android.app.multiwindow"
android:required="false" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
package org.thoughtcrime.securesms
import android.util.Log
import nl.komponents.kovenant.Kovenant
import nl.komponents.kovenant.jvm.asDispatcher
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
import java.util.concurrent.Executors

View File

@ -40,6 +40,8 @@ import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPol
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
import org.session.libsession.utilities.Device;
import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
@ -47,40 +49,41 @@ import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.WindowDebouncer;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsignal.utilities.HTTP;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.Storage;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
import org.thoughtcrime.securesms.dependencies.AppComponent;
import org.thoughtcrime.securesms.dependencies.ConfigFactory;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.groups.OpenGroupManager;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobs.FastJobStorage;
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.FcmUtils;
import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
import org.thoughtcrime.securesms.notifications.PushRegistry;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.UiModeUtilities;
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory;
@ -89,12 +92,16 @@ import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.concurrent.Executors;
import javax.inject.Inject;
@ -103,6 +110,8 @@ import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit;
import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.libsession_util.ConfigBase;
import network.loki.messenger.libsession_util.UserProfile;
/**
* Will be called once when the TextSecure process is created.
@ -113,7 +122,7 @@ import network.loki.messenger.BuildConfig;
* @author Moxie Marlinspike
*/
@HiltAndroidApp
public class ApplicationContext extends Application implements DefaultLifecycleObserver {
public class ApplicationContext extends Application implements DefaultLifecycleObserver, ConfigFactoryUpdateListener {
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
@ -122,7 +131,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private ExpiringMessageManager expiringMessageManager;
private TypingStatusRepository typingStatusRepository;
private TypingStatusSender typingStatusSender;
private JobManager jobManager;
private ReadReceiptManager readReceiptManager;
private ProfileManager profileManager;
public MessageNotifier messageNotifier = null;
@ -135,10 +143,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private PersistentLogger persistentLogger;
@Inject LokiAPIDatabase lokiAPIDatabase;
@Inject Storage storage;
@Inject public Storage storage;
@Inject Device device;
@Inject MessageDataProvider messageDataProvider;
@Inject JobDatabase jobDatabase;
@Inject TextSecurePreferences textSecurePreferences;
@Inject PushRegistry pushRegistry;
@Inject ConfigFactory configFactory;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@ -156,6 +166,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return (ApplicationContext) context.getApplicationContext();
}
public TextSecurePreferences getPrefs() {
return EntryPoints.get(getApplicationContext(), AppComponent.class).getPrefs();
}
public DatabaseComponent getDatabaseComponent() {
return EntryPoints.get(getApplicationContext(), DatabaseComponent.class);
}
@ -182,15 +196,30 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return this.persistentLogger;
}
@Override
public void notifyUpdates(@NonNull ConfigBase forConfigObject) {
// forward to the config factory / storage ig
if (forConfigObject instanceof UserProfile && !textSecurePreferences.getConfigurationMessageSynced()) {
textSecurePreferences.setConfigurationMessageSynced(true);
}
storage.notifyConfigUpdates(forConfigObject);
}
@Override
public void onCreate() {
TextSecurePreferences.setPushSuffix(BuildConfig.PUSH_KEY_SUFFIX);
DatabaseModule.init(this);
MessagingModuleConfiguration.configure(this);
super.onCreate();
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
messagingModuleConfiguration = new MessagingModuleConfiguration(
this,
storage,
device,
messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
configFactory
);
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()");
startKovenant();
@ -204,11 +233,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
broadcaster = new Broadcaster(this);
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
SnodeModule.Companion.configure(apiDB, broadcaster);
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey != null) {
registerForFCMIfNeeded(false);
}
UiModeUtilities.setupUiModeToUserSelected(this);
initializeExpiringMessageManager();
initializeTypingStatusRepository();
initializeTypingStatusSender();
@ -216,10 +240,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
initializeProfileManager();
initializePeriodicTasks();
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
initializeJobManager();
initializeWebRtc();
initializeBlobProvider();
resubmitProfilePictureIfNeeded();
loadEmojiSearchIndexIfNeeded();
EmojiSource.refresh();
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
}
@Override
@ -228,6 +256,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
Log.i(TAG, "App is now visible.");
KeyCachingService.onAppForegrounded(this);
// If the user account hasn't been created or onboarding wasn't finished then don't start
// the pollers
if (TextSecurePreferences.getLocalNumber(this) == null || !TextSecurePreferences.hasSeenWelcomeScreen(this)) {
return;
}
ThreadUtils.queue(()->{
if (poller != null) {
poller.setCaughtUp(false);
@ -248,7 +282,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
if (poller != null) {
poller.stopIfNeeded();
}
ClosedGroupPollerV2.getShared().stop();
ClosedGroupPollerV2.getShared().stopAll();
}
@Override
@ -262,10 +296,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
LocaleParser.Companion.configure(new LocaleParseHelper());
}
public JobManager getJobManager() {
return jobManager;
}
public ExpiringMessageManager getExpiringMessageManager() {
return expiringMessageManager;
}
@ -328,16 +358,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
}
private void initializeJobManager() {
this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
.setDataSerializer(new JsonDataSerializer())
.setJobFactories(JobManagerFactories.getJobFactories(this))
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
.setJobStorage(new FastJobStorage(jobDatabase))
.build());
}
private void initializeExpiringMessageManager() {
this.expiringMessageManager = new ExpiringMessageManager(this);
}
@ -351,7 +371,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
private void initializeProfileManager() {
this.profileManager = new ProfileManager();
this.profileManager = new ProfileManager(this, configFactory);
}
private void initializeTypingStatusSender() {
@ -360,10 +380,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private void initializePeriodicTasks() {
BackgroundPollWorker.schedulePeriodic(this);
if (BuildConfig.PLAY_STORE_DISABLED) {
UpdateApkRefreshListener.schedule(this);
}
}
private void initializeWebRtc() {
@ -414,29 +430,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
private static class ProviderInitializationException extends RuntimeException { }
public void registerForFCMIfNeeded(final Boolean force) {
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
if (force && firebaseInstanceIdJob != null) {
firebaseInstanceIdJob.cancel(null);
}
firebaseInstanceIdJob = FcmUtils.getFcmInstanceId(task->{
if (!task.isSuccessful()) {
Log.w("Loki", "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.getException());
return Unit.INSTANCE;
}
String token = task.getResult().getToken();
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return Unit.INSTANCE;
if (TextSecurePreferences.isUsingFCM(this)) {
LokiPushNotificationManager.register(token, userPublicKey, this, force);
} else {
LokiPushNotificationManager.unregister(token, this);
}
return Unit.INSTANCE;
});
}
private void setUpPollingIfNeeded() {
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return;
@ -444,7 +437,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.setUserPublicKey(userPublicKey);
return;
}
poller = new Poller();
poller = new Poller(configFactory, new Timer());
}
public void startPollingIfNeeded() {
@ -465,6 +458,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
ThreadUtils.queue(() -> {
// Don't generate a new profile key here; we do that when the user changes their profile picture
Log.d("Loki-Avatar", "Uploading Avatar Started");
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
try {
// Read the file into a byte array
@ -481,33 +475,46 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
// Update the last profile picture upload date
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
Log.d("Loki-Avatar", "Uploading Avatar Finished");
return Unit.INSTANCE;
});
} catch (Exception exception) {
// Do nothing
Log.e("Loki-Avatar", "Uploading avatar failed", exception);
}
});
}
private void loadEmojiSearchIndexIfNeeded() {
Executors.newSingleThreadExecutor().execute(() -> {
EmojiSearchDatabase emojiSearchDb = getDatabaseComponent().emojiSearchDatabase();
if (emojiSearchDb.query("face", 1).isEmpty()) {
try (InputStream inputStream = getAssets().open("emoji/emoji_search_index.json")) {
List<EmojiSearchData> searchIndex = Arrays.asList(JsonUtil.fromJson(inputStream, EmojiSearchData[].class));
emojiSearchDb.setSearchIndex(searchIndex);
} catch (IOException e) {
Log.e("Loki", "Failed to load emoji search index");
}
}
});
}
public void clearAllData(boolean isMigratingToV2KeyPair) {
String token = TextSecurePreferences.getFCMToken(this);
if (token != null && !token.isEmpty()) {
LokiPushNotificationManager.unregister(token, this);
}
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
firebaseInstanceIdJob.cancel(null);
}
String displayName = TextSecurePreferences.getProfileName(this);
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
boolean isUsingFCM = TextSecurePreferences.isPushEnabled(this);
TextSecurePreferences.clearAll(this);
if (isMigratingToV2KeyPair) {
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
TextSecurePreferences.setPushEnabled(this, isUsingFCM);
TextSecurePreferences.setProfileName(this, displayName);
}
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
if (!deleteDatabase("signal.db")) {
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
Log.d("Loki", "Failed to delete database.");
}
configFactory.keyPairChanged();
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
}

View File

@ -1,44 +1,110 @@
package org.thoughtcrime.securesms;
import static android.os.Build.VERSION.SDK_INT;
import static org.session.libsession.utilities.TextSecurePreferences.SELECTED_ACCENT_COLOR;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.thoughtcrime.securesms.conversation.v2.WindowUtil;
import org.thoughtcrime.securesms.util.ActivityUtilitiesKt;
import org.thoughtcrime.securesms.util.ThemeState;
import org.thoughtcrime.securesms.util.UiModeUtilities;
import network.loki.messenger.R;
public abstract class BaseActionBarActivity extends AppCompatActivity {
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
public ThemeState currentThemeState;
private TextSecurePreferences getPreferences() {
ApplicationContext appContext = (ApplicationContext) getApplicationContext();
return appContext.textSecurePreferences;
}
@StyleRes
public int getDesiredTheme() {
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
int userSelectedTheme = themeState.getTheme();
if (themeState.getFollowSystem()) {
// do light or dark based on the selected theme
boolean isDayUi = UiModeUtilities.isDayUiMode(this);
if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) {
return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark;
} else {
return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark;
}
} else {
return userSelectedTheme;
}
}
@StyleRes @Nullable
public Integer getAccentTheme() {
if (!getPreferences().hasPreference(SELECTED_ACCENT_COLOR)) return null;
ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences());
return themeState.getAccentStyle();
}
@Override
public Resources.Theme getTheme() {
// New themes
Resources.Theme modifiedTheme = super.getTheme();
modifiedTheme.applyStyle(getDesiredTheme(), true);
Integer accentTheme = getAccentTheme();
if (accentTheme != null) {
modifiedTheme.applyStyle(accentTheme, true);
}
currentThemeState = ActivityUtilitiesKt.themeState(getPreferences());
return modifiedTheme;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
}
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
initializeScreenshotSecurity();
initializeScreenshotSecurity(true);
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
String name = getResources().getString(R.string.app_name);
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground);
int color = getResources().getColor(R.color.app_icon_background);
setTaskDescription(new ActivityManager.TaskDescription(name, icon, color));
if (!currentThemeState.equals(ActivityUtilitiesKt.themeState(getPreferences()))) {
recreate();
}
// apply lightStatusBar manually as API 26 does not update properly via applyTheme
// https://issuetracker.google.com/issues/65883460?pli=1
if (SDK_INT >= 26 && SDK_INT <= 27) WindowUtil.setLightStatusBarFromTheme(this);
if (SDK_INT == 27) WindowUtil.setLightNavigationBarFromTheme(this);
}
@Override
protected void onPause() {
super.onPause();
initializeScreenshotSecurity(false);
}
@Override
@ -49,11 +115,15 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
return true;
}
private void initializeScreenshotSecurity() {
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
private void initializeScreenshotSecurity(boolean isResume) {
if (!isResume) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
}

View File

@ -1,39 +0,0 @@
package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsignal.utilities.guava.Optional;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.recipients.Recipient;
import java.util.Locale;
import java.util.Set;
public interface BindableConversationItem extends Unbindable {
void bind(@NonNull MessageRecord messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseHighlight);
MessageRecord getMessageRecord();
void setEventListener(@Nullable EventListener listener);
interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms);
}
}

View File

@ -0,0 +1,28 @@
package org.thoughtcrime.securesms
import android.content.Context
import network.loki.messenger.R
class DeleteMediaDialog {
companion object {
@JvmStatic
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
iconAttribute(R.attr.dialog_alert_icon)
title(
context.resources.getQuantityString(
R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
recordCount,
recordCount
)
)
text(
context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
recordCount,
recordCount
)
)
button(R.string.delete) { doDelete.run() }
cancelButton()
}
}
}

View File

@ -0,0 +1,19 @@
package org.thoughtcrime.securesms
import android.content.Context
import network.loki.messenger.R
class DeleteMediaPreviewDialog {
companion object {
@JvmStatic
fun show(context: Context, doDelete: Runnable) {
context.showSessionDialog {
iconAttribute(R.attr.dialog_alert_icon)
title(R.string.MediaPreviewActivity_media_delete_confirmation_title)
text(R.string.MediaPreviewActivity_media_delete_confirmation_message)
button(R.string.delete) { doDelete.run() }
cancelButton()
}
}
}
}

View File

@ -0,0 +1,16 @@
package org.thoughtcrime.securesms
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import network.loki.messenger.BuildConfig
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DeviceModule {
@Provides
@Singleton
fun provides() = BuildConfig.DEVICE
}

View File

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

View File

@ -0,0 +1,51 @@
package org.thoughtcrime.securesms
import android.content.Context
import android.view.LayoutInflater
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import cn.carbswang.android.numberpickerview.library.NumberPickerView
import network.loki.messenger.R
import org.session.libsession.utilities.ExpirationUtil
fun Context.showExpirationDialog(
expiration: Int,
onExpirationTime: (Int) -> Unit
): AlertDialog {
val view = LayoutInflater.from(this).inflate(R.layout.expiration_dialog, null)
val numberPickerView = view.findViewById<NumberPickerView>(R.id.expiration_number_picker)
fun updateText(index: Int) {
view.findViewById<TextView>(R.id.expiration_details).text = when (index) {
0 -> getString(R.string.ExpirationDialog_your_messages_will_not_expire)
else -> getString(
R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen,
numberPickerView.displayedValues[index]
)
}
}
val expirationTimes = resources.getIntArray(R.array.expiration_times)
val expirationDisplayValues = expirationTimes
.map { ExpirationUtil.getExpirationDisplayValue(this, it) }
.toTypedArray()
val selectedIndex = expirationTimes.run { indexOfFirst { it >= expiration }.coerceIn(indices) }
numberPickerView.apply {
displayedValues = expirationDisplayValues
minValue = 0
maxValue = expirationTimes.lastIndex
setOnValueChangedListener { _, _, index -> updateText(index) }
value = selectedIndex
}
updateText(selectedIndex)
return showSessionDialog {
title(getString(R.string.ExpirationDialog_disappearing_messages))
view(view)
okButton { onExpirationTime(numberPickerView.let { expirationTimes[it.value] }) }
cancelButton()
}
}

View File

@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
if (slide != null) {
thumbnailView.setImageResource(glideRequests, slide, false, false);
thumbnailView.setImageResource(glideRequests, slide, false, null);
}
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));

View File

@ -54,6 +54,7 @@ import com.google.android.material.tabs.TabLayout;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.snode.SnodeAPI;
import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MediaDatabase;
@ -75,6 +76,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import kotlin.Unit;
import network.loki.messenger.R;
/**
@ -317,9 +319,9 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
@SuppressWarnings("CodeBlock2Expr")
@SuppressLint({"InlinedApi", "StaticFieldLeak"})
private void handleSaveMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
final Context context = getContext();
final Context context = requireContext();
SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> {
SaveAttachmentTask.showWarningDialog(context, mediaRecords.size(), () -> {
Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
@ -361,53 +363,39 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
}.execute();
})
.execute();
}, mediaRecords.size());
return Unit.INSTANCE;
});
}
private void sendMediaSavedNotificationIfNeeded() {
if (recipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
MessageSender.send(message, recipient.getAddress());
}
@SuppressLint("StaticFieldLeak")
private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
int recordCount = mediaRecords.size();
Resources res = getContext().getResources();
String confirmTitle = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
recordCount,
recordCount);
String confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
recordCount,
recordCount);
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setTitle(confirmTitle);
builder.setMessage(confirmMessage);
builder.setCancelable(true);
builder.setPositiveButton(R.string.delete, (dialogInterface, i) -> {
new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(getContext(),
R.string.MediaOverviewActivity_Media_delete_progress_title,
R.string.MediaOverviewActivity_Media_delete_progress_message)
{
@Override
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
if (records == null || records.length == 0) {
return null;
}
for (MediaDatabase.MediaRecord record : records) {
AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
}
DeleteMediaDialog.show(
requireContext(),
recordCount,
() -> new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(
requireContext(),
R.string.MediaOverviewActivity_Media_delete_progress_title,
R.string.MediaOverviewActivity_Media_delete_progress_message) {
@Override
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
if (records == null || records.length == 0) {
return null;
}
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()]));
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
for (MediaDatabase.MediaRecord record : records) {
AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
}
return null;
}
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()])));
}
private void handleSelectAllMedia() {

View File

@ -47,7 +47,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Pair;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
@ -60,6 +59,7 @@ import androidx.viewpager.widget.ViewPager;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
import org.session.libsession.snode.SnodeAPI;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
@ -84,6 +84,7 @@ import java.io.IOException;
import java.util.Locale;
import java.util.WeakHashMap;
import kotlin.Unit;
import network.loki.messenger.R;
/**
@ -145,6 +146,10 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
};
public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) {
return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread());
}
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
Intent previewIntent = null;
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
@ -415,7 +420,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem == null) return;
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
SaveAttachmentTask.showWarningDialog(this, 1, () -> {
Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P)
@ -423,7 +428,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
saveTask.executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR,
new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
@ -432,12 +437,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
})
.execute();
return Unit.INSTANCE;
});
}
private void sendMediaSavedNotificationIfNeeded() {
if (conversationRecipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
MessageSender.send(message, conversationRecipient.getAddress());
}
@ -448,29 +454,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
builder.setCancelable(true);
builder.setPositiveButton(R.string.delete, (dialogInterface, which) -> {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
if (mediaItem.attachment == null) {
return null;
}
AttachmentUtil.deleteAttachment(MediaPreviewActivity.this.getApplicationContext(),
mediaItem.attachment);
return null;
}
}.execute();
DeleteMediaPreviewDialog.show(this, () -> {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
DatabaseAttachment attachment = mediaItem.attachment;
if (attachment != null) {
AttachmentUtil.deleteAttachment(getApplicationContext(), attachment);
}
return null;
}
}.execute();
finish();
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
@Override
@ -530,18 +527,21 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
if (data != null) {
@SuppressWarnings("ConstantConditions")
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
viewModel.setCursor(this, data.first, leftIsRecent);
int item = restartItem >= 0 ? restartItem : data.second;
mediaPager.setCurrentItem(item);
if (restartItem >= 0 || data.second >= 0) {
int item = restartItem >= 0 ? restartItem : data.second;
mediaPager.setCurrentItem(item);
if (item == 0) {
viewPagerListener.onPageSelected(0);
if (item == 0) {
viewPagerListener.onPageSelected(0);
}
} else {
Log.w(TAG, "one of restartItem "+restartItem+" and data.second "+data.second+" would cause OOB exception");
}
}
}

View File

@ -0,0 +1,11 @@
package org.thoughtcrime.securesms
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.Slide
data class MediaPreviewArgs(
val slide: Slide,
val mmsRecord: MmsMessageRecord?,
val thread: Recipient?,
)

View File

@ -1,111 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.contacts.UserView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.Conversions;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
private final Context context;
private final GlideRequests glideRequests;
private final MessageRecord record;
private final List<RecipientDeliveryStatus> members;
private final boolean isPushGroup;
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> members,
boolean isPushGroup)
{
this.context = context;
this.glideRequests = glideRequests;
this.record = record;
this.isPushGroup = isPushGroup;
this.members = members;
}
@Override
public int getCount() {
return members.size();
}
@Override
public Object getItem(int position) {
return members.get(position);
}
@Override
public long getItemId(int position) {
try {
return Conversions.byteArrayToLong(MessageDigest.getInstance("SHA1").digest(members.get(position).recipient.getAddress().serialize().getBytes()));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
UserView result = new UserView(context);
Recipient recipient = members.get(position).getRecipient();
result.setOpenGroupThreadID(record.getThreadId());
result.bind(recipient, glideRequests, UserView.ActionIndicator.None, false);
return result;
}
@Override
public void onMovedToScrapHeap(View view) {
((UserView)view).unbind();
}
static class RecipientDeliveryStatus {
enum Status {
UNKNOWN, PENDING, SENT, DELIVERED, READ
}
private final Recipient recipient;
private final Status deliveryStatus;
private final boolean isUnidentified;
private final long timestamp;
RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) {
this.recipient = recipient;
this.deliveryStatus = deliveryStatus;
this.isUnidentified = isUnidentified;
this.timestamp = timestamp;
}
Status getDeliveryStatus() {
return deliveryStatus;
}
boolean isUnidentified() {
return isUnidentified;
}
public long getTimestamp() {
return timestamp;
}
public Recipient getRecipient() {
return recipient;
}
}
}

View File

@ -1,56 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.DialogInterface;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import java.util.concurrent.TimeUnit;
import network.loki.messenger.R;
public class MuteDialog extends AlertDialog {
protected MuteDialog(Context context) {
super(context);
}
protected MuteDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
}
protected MuteDialog(Context context, int theme) {
super(context, theme);
}
public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.MuteDialog_mute_notifications);
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, final int which) {
final long muteUntil;
switch (which) {
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2); break;
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
case 4: muteUntil = Long.MAX_VALUE; break;
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
}
listener.onMuted(muteUntil);
}
});
builder.show();
}
public interface MuteSelectionListener {
public void onMuted(long until);
}
}

View File

@ -0,0 +1,27 @@
package org.thoughtcrime.securesms
import android.content.Context
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import network.loki.messenger.R
import java.util.concurrent.TimeUnit
fun showMuteDialog(
context: Context,
onMuteDuration: (Long) -> Unit
): AlertDialog = context.showSessionDialog {
title(R.string.MuteDialog_mute_notifications)
items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
onMuteDuration(Option.values()[it].getTime())
}
}
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)),
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration })
}

View File

@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.crypto.BiometricSecretProvider;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
import java.security.InvalidKeyException;
import java.security.Signature;
import network.loki.messenger.R;
@ -68,6 +69,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
private boolean authenticated;
private boolean failure;
private boolean hasSignatureObject = true;
private KeyCachingService keyCachingService;
@ -146,12 +148,11 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
// Finish and proceed with the next intent.
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
if (nextIntent != null) {
startActivity(nextIntent);
// try {
// startActivity(nextIntent);
// } catch (java.lang.SecurityException e) {
// Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.");
// }
try {
startActivity(nextIntent);
} catch (java.lang.SecurityException e) {
Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.", e);
}
}
finish();
}
@ -205,7 +206,22 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
Log.i(TAG, "Listening for fingerprints...");
fingerprintCancellationSignal = new CancellationSignal();
fingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(biometricSecretProvider.getOrCreateBiometricSignature(this)), 0, fingerprintCancellationSignal, fingerprintListener, null);
Signature signature;
try {
signature = biometricSecretProvider.getOrCreateBiometricSignature(this);
hasSignatureObject = true;
} catch (Exception e) {
signature = null;
hasSignatureObject = false;
Log.e(TAG, "Error getting / creating signature", e);
}
fingerprintManager.authenticate(
signature == null ? null : new FingerprintManagerCompat.CryptoObject(signature),
0,
fingerprintCancellationSignal,
fingerprintListener,
null
);
} else {
Log.i(TAG, "firing intent...");
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Session", "");
@ -230,8 +246,22 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
Log.i(TAG, "onAuthenticationSucceeded");
if (result.getCryptoObject() == null || result.getCryptoObject().getSignature() == null) {
// authentication failed
onAuthenticationFailed();
if (hasSignatureObject) {
// authentication failed
onAuthenticationFailed();
} else {
fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
handleAuthenticated();
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
}
}).start();
}
return;
}
// Signature object now successfully unlocked

View File

@ -9,13 +9,14 @@ import android.os.Bundle;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.onboarding.LandingActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.session.libsession.utilities.TextSecurePreferences;
import java.util.Locale;
@ -168,7 +169,13 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
};
IntentFilter filter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
registerReceiver(clearKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
ContextCompat.registerReceiver(
this,
clearKeyReceiver, filter,
KeyCachingService.KEY_PERMISSION,
null,
ContextCompat.RECEIVER_NOT_EXPORTED
);
}
private void removeClearKeyReceiver(Context context) {

View File

@ -0,0 +1,146 @@
package org.thoughtcrime.securesms
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.Button
import android.widget.LinearLayout
import android.widget.LinearLayout.VERTICAL
import android.widget.TextView
import androidx.annotation.AttrRes
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.setMargins
import androidx.core.view.setPadding
import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment
import network.loki.messenger.R
import org.thoughtcrime.securesms.util.toPx
@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class DialogDsl
@DialogDsl
class SessionDialogBuilder(val context: Context) {
private val dp20 = toPx(20, context.resources)
private val dp40 = toPx(40, context.resources)
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
private var dialog: AlertDialog? = null
private fun dismiss() = dialog?.dismiss()
private val topView = LinearLayout(context).apply { orientation = VERTICAL }
.also(dialogBuilder::setCustomTitle)
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
private val buttonLayout = LinearLayout(context)
private val root = LinearLayout(context).apply { orientation = VERTICAL }
.also(dialogBuilder::setView)
.apply {
addView(contentView)
addView(buttonLayout)
}
fun title(@StringRes id: Int) = title(context.getString(id))
fun title(text: CharSequence?) = title(text?.toString())
fun title(text: String?) {
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20) }
}
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
text(text, style) {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
.apply { updateMargins(dp40, 0, dp40, dp20) }
}
}
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
text ?: return
TextView(context, null, 0, style)
.apply {
setText(text)
textAlignment = View.TEXT_ALIGNMENT_CENTER
modify()
}.let(topView::addView)
}
fun view(view: View) = contentView.addView(view)
fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
fun iconAttribute(@AttrRes icon: Int): AlertDialog.Builder = dialogBuilder.setIconAttribute(icon)
fun singleChoiceItems(
options: Collection<String>,
currentSelected: Int = 0,
onSelect: (Int) -> Unit
) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect)
fun singleChoiceItems(
options: Array<String>,
currentSelected: Int = 0,
onSelect: (Int) -> Unit
): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems(
options,
currentSelected
) { dialog, it -> onSelect(it); dialog.dismiss() }
fun items(
options: Array<String>,
onSelect: (Int) -> Unit
): AlertDialog.Builder = dialogBuilder.setItems(
options,
) { dialog, it -> onSelect(it); dialog.dismiss() }
fun destructiveButton(
@StringRes text: Int,
@StringRes contentDescription: Int,
listener: () -> Unit = {}
) = button(
text,
contentDescription,
R.style.Widget_Session_Button_Dialog_DestructiveText,
) { listener() }
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() }
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button) { listener() }
fun button(
@StringRes text: Int,
@StringRes contentDescriptionRes: Int = text,
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
dismiss: Boolean = true,
listener: (() -> Unit) = {}
) = Button(context, null, 0, style).apply {
setText(text)
contentDescription = resources.getString(contentDescriptionRes)
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f)
.apply { setMargins(toPx(20, resources)) }
setOnClickListener {
listener.invoke()
if (dismiss) dismiss()
}
}.let(buttonLayout::addView)
fun create(): AlertDialog = dialogBuilder.create().also { dialog = it }
fun show(): AlertDialog = dialogBuilder.show().also { dialog = it }
}
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(this).apply { build() }.show()
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(requireContext()).apply { build() }.show()
fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(requireContext()).apply { build() }.create()

View File

@ -1,5 +0,0 @@
package org.thoughtcrime.securesms;
public interface Unbindable {
public void unbind();
}

View File

@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.animation;
import android.animation.Animator;
public abstract class AnimationCompleteListener implements Animator.AnimatorListener {
@Override
public final void onAnimationStart(Animator animation) {}
@Override
public abstract void onAnimationEnd(Animator animation);
@Override
public final void onAnimationCancel(Animator animation) {}
@Override
public final void onAnimationRepeat(Animator animation) {}
}

View File

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.animation;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import androidx.annotation.NonNull;
public class ResizeAnimation extends Animation {
private final View target;
private final int targetWidthPx;
private final int targetHeightPx;
private int startWidth;
private int startHeight;
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
this.target = target;
this.targetWidthPx = targetWidthPx;
this.targetHeightPx = targetHeightPx;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
ViewGroup.LayoutParams params = target.getLayoutParams();
params.width = newWidth;
params.height = newHeight;
target.setLayoutParams(params);
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
this.startWidth = width;
this.startHeight = height;
}
@Override
public boolean willChangeBounds() {
return true;
}
}

View File

@ -74,10 +74,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value)
}
override fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? {
override fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>? {
val messagingDatabase = DatabaseComponent.get(context).mmsSmsDatabase()
val message = messagingDatabase.getMessageFor(timestamp, author)
return if (message != null) Pair(message.id, message.isMms) else null
return if (message != null) Triple(message.id, message.isMms, message.body) else null
}
override fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment> {
@ -176,6 +176,11 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return messageDB.getMessageID(serverId, threadId)
}
override fun getMessageIDs(serverIds: List<Long>, threadId: Long): Pair<List<Long>, List<Long>> {
val messageDB = DatabaseComponent.get(context).lokiMessageDatabase()
return messageDB.getMessageIDs(serverIds, threadId)
}
override fun deleteMessage(messageID: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
@ -184,16 +189,27 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID)
}
override fun updateMessageAsDeleted(timestamp: Long, author: String) {
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs)
}
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = Address.fromSerialized(author)
val message = database.getMessageFor(timestamp, address) ?: return
val message = database.getMessageFor(timestamp, address) ?: return null
val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase()
else DatabaseComponent.get(context).smsDatabase()
messagingDatabase.markAsDeleted(message.id, message.isRead)
messagingDatabase.markAsDeleted(message.id, message.isRead, message.hasMention)
if (message.isOutgoing) {
messagingDatabase.deleteMessage(message.id)
}
return message.id
}
override fun getServerHashForMessage(messageID: Long): String? {

View File

@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.attachments
import android.content.Context
import android.database.ContentObserver
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer
private const val TAG = "ScreenshotObserver"
class ScreenshotObserver(private val context: Context, handler: Handler, private val screenshotTriggered: ()->Unit): ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
uri ?: return
// There is an odd bug where we can get notified for changes to 'content://media/external'
// directly which is a protected folder, this code is to prevent that crash
if (uri.scheme == "content" && uri.host == "media" && uri.path == "/external") { return }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryRelativeDataColumn(uri)
} else {
queryDataColumn(uri)
}
}
private val cache = mutableSetOf<Int>()
private fun queryDataColumn(uri: Uri) {
val projection = arrayOf(
MediaStore.Images.Media.DATA
)
try {
context.contentResolver.query(
uri,
projection,
null,
null,
null
)?.use { cursor ->
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
while (cursor.moveToNext()) {
val path = cursor.getString(dataColumn)
if (path.contains("screenshot", true)) {
if (cache.add(uri.hashCode())) {
screenshotTriggered()
}
}
}
}
} catch (e: SecurityException) {
Log.e(TAG, e)
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun queryRelativeDataColumn(uri: Uri) {
val projection = arrayOf(
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.RELATIVE_PATH
)
try {
context.contentResolver.query(
uri,
projection,
null,
null,
null
)?.use { cursor ->
val relativePathColumn =
cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH)
val displayNameColumn =
cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
while (cursor.moveToNext()) {
val name = cursor.getString(displayNameColumn)
val relativePath = cursor.getString(relativePathColumn)
if (name.contains("screenshot", true) or
relativePath.contains("screenshot", true)) {
if (cache.add(uri.hashCode())) {
screenshotTriggered()
}
}
}
}
} catch (e: IllegalStateException) {
Log.e(TAG, e)
}
}
}

View File

@ -1,104 +0,0 @@
package org.thoughtcrime.securesms.backup;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.util.BackupDirSelector;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.session.libsession.utilities.Util;
import java.io.IOException;
import network.loki.messenger.R;
public class BackupDialog {
private static final String TAG = "BackupDialog";
public static void showEnableBackupDialog(
@NonNull Context context,
@NonNull SwitchPreferenceCompat preference,
@NonNull BackupDirSelector backupDirSelector) {
String[] password = BackupUtil.generateBackupPassphrase();
String passwordSt = Util.join(password, "");
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_enable_local_backups)
.setView(R.layout.backup_enable_dialog)
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(created -> {
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(v -> {
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
if (confirmationCheckBox.isChecked()) {
backupDirSelector.selectBackupDir(true, uri -> {
try {
BackupUtil.enableBackups(context, passwordSt);
} catch (IOException e) {
Log.e(TAG, "Failed to activate backups.", e);
Toast.makeText(context,
context.getString(R.string.dialog_backup_activation_failed),
Toast.LENGTH_LONG)
.show();
return;
}
preference.setChecked(true);
created.dismiss();
});
} else {
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
}
});
});
dialog.show();
CheckBox checkBox = dialog.findViewById(R.id.confirmation_check);
TextView textView = dialog.findViewById(R.id.confirmation_text);
((TextView)dialog.findViewById(R.id.code_first)).setText(password[0]);
((TextView)dialog.findViewById(R.id.code_second)).setText(password[1]);
((TextView)dialog.findViewById(R.id.code_third)).setText(password[2]);
((TextView)dialog.findViewById(R.id.code_fourth)).setText(password[3]);
((TextView)dialog.findViewById(R.id.code_fifth)).setText(password[4]);
((TextView)dialog.findViewById(R.id.code_sixth)).setText(password[5]);
textView.setOnClickListener(v -> checkBox.toggle());
dialog.findViewById(R.id.number_table).setOnClickListener(v -> {
((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", passwordSt));
Toast.makeText(context, R.string.BackupDialog_copied_to_clipboard, Toast.LENGTH_SHORT).show();
});
}
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_delete_backups)
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
BackupUtil.disableBackups(context, true);
preference.setChecked(false);
})
.create()
.show();
}
}

View File

@ -1,14 +0,0 @@
package org.thoughtcrime.securesms.backup
data class BackupEvent constructor(val type: Type, val count: Int, val exception: Exception?) {
enum class Type {
PROGRESS, FINISHED
}
companion object {
@JvmStatic fun createProgress(count: Int) = BackupEvent(Type.PROGRESS, count, null)
@JvmStatic fun createFinished() = BackupEvent(Type.FINISHED, 0, null)
@JvmStatic fun createFinished(e: Exception?) = BackupEvent(Type.FINISHED, 0, e)
}
}

View File

@ -1,47 +0,0 @@
package org.thoughtcrime.securesms.backup;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
import org.session.libsignal.utilities.Log;
import org.session.libsession.utilities.TextSecurePreferences;
/**
* Allows the getting and setting of the backup passphrase, which is stored encrypted on API >= 23.
*/
public class BackupPassphrase {
private static final String TAG = BackupPassphrase.class.getSimpleName();
public static @Nullable String get(@NonNull Context context) {
String passphrase = TextSecurePreferences.getBackupPassphrase(context);
String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
if (Build.VERSION.SDK_INT < 23 || (passphrase == null && encryptedPassphrase == null)) {
return passphrase;
}
if (encryptedPassphrase == null) {
Log.i(TAG, "Migrating to encrypted passphrase.");
set(context, passphrase);
encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
}
KeyStoreHelper.SealedData data = KeyStoreHelper.SealedData.fromString(encryptedPassphrase);
return new String(KeyStoreHelper.unseal(data));
}
public static void set(@NonNull Context context, @Nullable String passphrase) {
if (passphrase == null || Build.VERSION.SDK_INT < 23) {
TextSecurePreferences.setBackupPassphrase(context, passphrase);
TextSecurePreferences.setEncryptedBackupPassphrase(context, null);
} else {
KeyStoreHelper.SealedData encryptedPassphrase = KeyStoreHelper.seal(passphrase.getBytes());
TextSecurePreferences.setEncryptedBackupPassphrase(context, encryptedPassphrase.serialize());
TextSecurePreferences.setBackupPassphrase(context, null);
}
}
}

View File

@ -1,101 +0,0 @@
package org.thoughtcrime.securesms.backup
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.preference.PreferenceManager
import android.preference.PreferenceManager.getDefaultSharedPreferencesName
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_BOOLEAN
import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_INT
import java.util.*
object BackupPreferences {
// region Backup related
fun getBackupRecords(context: Context): List<BackupProtos.SharedPreference> {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val prefsFileName: String
prefsFileName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
getDefaultSharedPreferencesName(context)
} else {
context.packageName + "_preferences"
}
val prefList: LinkedList<BackupProtos.SharedPreference> = LinkedList<BackupProtos.SharedPreference>()
addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_REGISTRATION_ID_PREF)
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_NUMBER_PREF)
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_NAME_PREF)
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_URL_PREF)
addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_ID_PREF)
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_KEY_PREF)
addBackupEntryBoolean(prefList, preferences, prefsFileName, TextSecurePreferences.IS_USING_FCM)
return prefList
}
private fun addBackupEntryString(
outPrefList: MutableList<BackupProtos.SharedPreference>,
prefs: SharedPreferences,
prefFileName: String,
prefKey: String,
) {
val value = prefs.getString(prefKey, null)
if (value == null) {
logBackupEntry(prefKey, false)
return
}
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefFileName)
.setKey(prefKey)
.setValue(value)
.build())
logBackupEntry(prefKey, true)
}
private fun addBackupEntryInt(
outPrefList: MutableList<BackupProtos.SharedPreference>,
prefs: SharedPreferences,
prefFileName: String,
prefKey: String,
) {
val value = prefs.getInt(prefKey, -1)
if (value == -1) {
logBackupEntry(prefKey, false)
return
}
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefFileName)
.setKey(PREF_PREFIX_TYPE_INT + prefKey) // The prefix denotes the type of the preference.
.setValue(value.toString())
.build())
logBackupEntry(prefKey, true)
}
private fun addBackupEntryBoolean(
outPrefList: MutableList<BackupProtos.SharedPreference>,
prefs: SharedPreferences,
prefFileName: String,
prefKey: String,
) {
if (!prefs.contains(prefKey)) {
logBackupEntry(prefKey, false)
return
}
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefFileName)
.setKey(PREF_PREFIX_TYPE_BOOLEAN + prefKey) // The prefix denotes the type of the preference.
.setValue(prefs.getBoolean(prefKey, false).toString())
.build())
logBackupEntry(prefKey, true)
}
private fun logBackupEntry(prefName: String, wasIncluded: Boolean) {
val sb = StringBuilder()
sb.append("Backup preference ")
sb.append(if (wasIncluded) "+ " else "- ")
sb.append('\"').append(prefName).append("\" ")
if (!wasIncluded) {
sb.append("(is empty and not included)")
}
Log.d("Loki", sb.toString())
} // endregion
}

View File

@ -1,206 +0,0 @@
package org.thoughtcrime.securesms.backup
import android.app.Activity
import android.app.Application
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ClickableSpan
import android.text.style.StyleSpan
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.android.gms.common.util.Strings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.home.HomeActivity
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.util.show
class BackupRestoreActivity : BaseActionBarActivity() {
companion object {
private const val TAG = "BackupRestoreActivity"
}
private val viewModel by viewModels<BackupRestoreViewModel>()
private val fileSelectionResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK && result.data != null && result.data!!.data != null) {
viewModel.backupFile.value = result.data!!.data!!
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpActionBarSessionLogo()
// val viewBinding = DataBindingUtil.setContentView<ActivityBackupRestoreBinding>(this, R.layout.activity_backup_restore)
// viewBinding.lifecycleOwner = this
// viewBinding.viewModel = viewModel
// viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() }
// viewBinding.buttonSelectFile.setOnClickListener {
// fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
// //FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly
// // and the backup files are unavailable for selection.
//// type = BackupUtil.BACKUP_FILE_MIME_TYPE
// type = "*/*"
// })
// }
// viewBinding.backupCode.addTextChangedListener { text -> viewModel.backupPassphrase.value = text.toString() }
// Focus passphrase text edit when backup file is selected.
// viewModel.backupFile.observe(this, { backupFile ->
// if (backupFile != null) viewBinding.backupCode.post {
// viewBinding.backupCode.requestFocus()
// (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
// .showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT)
// }
// })
// React to backup import result.
viewModel.backupImportResult.observe(this) { result ->
if (result != null) when (result) {
BackupRestoreViewModel.BackupRestoreResult.SUCCESS -> {
val intent = Intent(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
this.show(intent)
}
BackupRestoreViewModel.BackupRestoreResult.FAILURE_VERSION_DOWNGRADE ->
Toast.makeText(this, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
BackupRestoreViewModel.BackupRestoreResult.FAILURE_UNKNOWN ->
Toast.makeText(this, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
}
}
//region Legal info views
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
openURL("https://getsession.org/terms-of-service/")
}
}, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
openURL("https://getsession.org/privacy-policy/")
}
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
// viewBinding.termsTextView.text = termsExplanation
//endregion
}
private fun openURL(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
}
}
}
class BackupRestoreViewModel(application: Application): AndroidViewModel(application) {
companion object {
private const val TAG = "BackupRestoreViewModel"
@JvmStatic
fun uriToFileName(view: View, fileUri: Uri?): String? {
fileUri ?: return null
view.context.contentResolver.query(fileUri, null, null, null, null).use {
val nameIndex = it!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
it.moveToFirst()
return it.getString(nameIndex)
}
}
@JvmStatic
fun validateData(fileUri: Uri?, passphrase: String?): Boolean {
return fileUri != null &&
!Strings.isEmptyOrWhitespace(passphrase) &&
passphrase!!.length == BackupUtil.BACKUP_PASSPHRASE_LENGTH
}
}
val backupFile = MutableLiveData<Uri>(null)
val backupPassphrase = MutableLiveData<String>(null)
val processingBackupFile = MutableLiveData<Boolean>(false)
val backupImportResult = MutableLiveData<BackupRestoreResult>(null)
fun tryRestoreBackup() = viewModelScope.launch {
if (processingBackupFile.value == true) return@launch
if (backupImportResult.value == BackupRestoreResult.SUCCESS) return@launch
if (!validateData(backupFile.value, backupPassphrase.value)) return@launch
val context = getApplication<Application>()
val backupFile = backupFile.value!!
val passphrase = backupPassphrase.value!!
val result: BackupRestoreResult
processingBackupFile.value = true
withContext(Dispatchers.IO) {
result = try {
val database = DatabaseComponent.get(context).openHelper().readableDatabase
FullBackupImporter.importFromUri(
context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database,
backupFile,
passphrase
)
DatabaseFactory.upgradeRestored(context, database)
NotificationChannels.restoreContactNotificationChannels(context)
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
TextSecurePreferences.setHasViewedSeed(context, true)
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
BackupRestoreResult.SUCCESS
} catch (e: DatabaseDowngradeException) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
BackupRestoreResult.FAILURE_VERSION_DOWNGRADE
} catch (e: Exception) {
Log.w(TAG, e)
BackupRestoreResult.FAILURE_UNKNOWN
}
}
processingBackupFile.value = false
backupImportResult.value = result
}
enum class BackupRestoreResult {
SUCCESS, FAILURE_VERSION_DOWNGRADE, FAILURE_UNKNOWN
}
}

View File

@ -1,447 +0,0 @@
package org.thoughtcrime.securesms.backup
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.text.TextUtils
import androidx.annotation.WorkerThread
import com.annimon.stream.function.Consumer
import com.annimon.stream.function.Predicate
import com.google.protobuf.ByteString
import net.sqlcipher.database.SQLiteDatabase
import org.greenrobot.eventbus.EventBus
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.utilities.Conversions
import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
import org.thoughtcrime.securesms.backup.BackupProtos.Header
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
import org.thoughtcrime.securesms.backup.BackupProtos.Sticker
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.PushDatabase
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.util.BackupUtil
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.Flushable
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.LinkedList
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.Mac
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object FullBackupExporter {
private val TAG = FullBackupExporter::class.java.simpleName
@JvmStatic
@WorkerThread
@Throws(IOException::class)
fun export(context: Context,
attachmentSecret: AttachmentSecret,
input: SQLiteDatabase,
fileUri: Uri,
passphrase: String) {
val baseOutputStream = context.contentResolver.openOutputStream(fileUri)
?: throw IOException("Cannot open an output stream for the file URI: $fileUri")
var count = 0
try {
BackupFrameOutputStream(baseOutputStream, passphrase).use { outputStream ->
outputStream.writeDatabaseVersion(input.version)
val tables = exportSchema(input, outputStream)
for (table in tables) if (shouldExportTable(table)) {
count = when (table) {
SmsDatabase.TABLE_NAME, MmsDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0
},
null,
count)
}
GroupReceiptDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID)))
},
null,
count)
}
AttachmentDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)))
},
{ cursor: Cursor ->
exportAttachment(attachmentSecret, cursor, outputStream)
},
count)
}
else -> {
exportTable(table, input, outputStream, null, null, count)
}
}
}
for (preference in BackupUtil.getBackupRecords(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writePreferenceEntry(preference)
}
for (preference in BackupPreferences.getBackupRecords(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writePreferenceEntry(preference)
}
for (avatar in AvatarHelper.getAvatarFiles(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writeAvatar(avatar.name, FileInputStream(avatar), avatar.length())
}
outputStream.writeEnd()
}
EventBus.getDefault().post(BackupEvent.createFinished())
} catch (e: Exception) {
Log.e(TAG, "Failed to make full backup.", e)
EventBus.getDefault().post(BackupEvent.createFinished(e))
throw e
}
}
private inline fun shouldExportTable(table: String): Boolean {
return table != PushDatabase.TABLE_NAME &&
table != LokiBackupFilesDatabase.TABLE_NAME &&
table != LokiAPIDatabase.openGroupProfilePictureTable &&
table != JobDatabase.Jobs.TABLE_NAME &&
table != JobDatabase.Constraints.TABLE_NAME &&
table != JobDatabase.Dependencies.TABLE_NAME &&
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
!table.startsWith("sqlite_")
}
@Throws(IOException::class)
private fun exportSchema(input: SQLiteDatabase, outputStream: BackupFrameOutputStream): List<String> {
val tables: MutableList<String> = LinkedList()
input.rawQuery("SELECT sql, name, type FROM sqlite_master", null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val sql = cursor.getString(0)
val name = cursor.getString(1)
val type = cursor.getString(2)
if (sql != null) {
val isSmsFtsSecretTable = name != null && name != SearchDatabase.SMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME)
val isMmsFtsSecretTable = name != null && name != SearchDatabase.MMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME)
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
if ("table" == type) {
tables.add(name)
}
outputStream.writeSql(SqlStatement.newBuilder().setStatement(cursor.getString(0)).build())
}
}
}
}
return tables
}
@Throws(IOException::class)
private fun exportTable(table: String,
input: SQLiteDatabase,
outputStream: BackupFrameOutputStream,
predicate: Predicate<Cursor>?,
postProcess: Consumer<Cursor>?,
count: Int): Int {
var count = count
val template = "INSERT INTO $table VALUES "
input.rawQuery("SELECT * FROM $table", null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
if (predicate != null && !predicate.test(cursor)) continue
val statement = StringBuilder(template)
val statementBuilder = SqlStatement.newBuilder()
statement.append('(')
for (i in 0 until cursor.columnCount) {
statement.append('?')
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_STRING -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setStringParamter(cursor.getString(i)))
}
Cursor.FIELD_TYPE_FLOAT -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setDoubleParameter(cursor.getDouble(i)))
}
Cursor.FIELD_TYPE_INTEGER -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setIntegerParameter(cursor.getLong(i)))
}
Cursor.FIELD_TYPE_BLOB -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))))
}
Cursor.FIELD_TYPE_NULL -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setNullparameter(true))
}
else -> {
throw AssertionError("unknown type?" + cursor.getType(i))
}
}
if (i < cursor.columnCount - 1) {
statement.append(',')
}
}
statement.append(')')
outputStream.writeSql(statementBuilder.setStatement(statement.toString()).build())
postProcess?.accept(cursor)
}
}
return count
}
private fun exportAttachment(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
try {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID))
val uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))
var size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE))
val data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA))
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM))
if (!TextUtils.isEmpty(data) && size <= 0) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data)
}
if (!TextUtils.isEmpty(data) && size > 0) {
val inputStream: InputStream = if (random != null && random.size == 32) {
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
} else {
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
}
outputStream.writeAttachment(AttachmentId(rowId, uniqueId), inputStream, size)
}
} catch (e: IOException) {
Log.w(TAG, e)
}
}
@Throws(IOException::class)
private fun calculateVeryOldStreamLength(attachmentSecret: AttachmentSecret, random: ByteArray?, data: String): Long {
var result: Long = 0
val inputStream: InputStream = if (random != null && random.size == 32) {
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
} else {
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
}
var read: Int
val buffer = ByteArray(8192)
while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) {
result += read.toLong()
}
return result
}
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
val where = MmsSmsColumns.ID + " = ?"
val args = arrayOf(mmsId.toString())
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return mmsCursor.getLong(0) == 0L
}
}
return false
}
private class BackupFrameOutputStream : Closeable, Flushable {
private val outputStream: OutputStream
private var cipher: Cipher
private var mac: Mac
private val cipherKey: ByteArray
private val macKey: ByteArray
private val iv: ByteArray
private var counter: Int = 0
constructor(outputStream: OutputStream, passphrase: String) : super() {
try {
val salt = Util.getSecretBytes(32)
val key = BackupUtil.computeBackupKey(passphrase, salt)
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
val split = ByteUtil.split(derived, 32, 32)
cipherKey = split[0]
macKey = split[1]
cipher = Cipher.getInstance("AES/CTR/NoPadding")
mac = Mac.getInstance("HmacSHA256")
this.outputStream = outputStream
iv = Util.getSecretBytes(16)
counter = Conversions.byteArrayToInt(iv)
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
val header = BackupFrame.newBuilder().setHeader(Header.newBuilder()
.setIv(ByteString.copyFrom(iv))
.setSalt(ByteString.copyFrom(salt)))
.build().toByteArray()
outputStream.write(Conversions.intToByteArray(header.size))
outputStream.write(header)
} catch (e: Exception) {
when (e) {
is NoSuchAlgorithmException,
is NoSuchPaddingException,
is InvalidKeyException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
fun writeSql(statement: SqlStatement) {
write(outputStream, BackupFrame.newBuilder().setStatement(statement).build())
}
@Throws(IOException::class)
fun writePreferenceEntry(preference: SharedPreference?) {
write(outputStream, BackupFrame.newBuilder().setPreference(preference).build())
}
@Throws(IOException::class)
fun writeAvatar(avatarName: String, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setAvatar(Avatar.newBuilder()
.setName(avatarName)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeAttachment(attachmentId: AttachmentId, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setAttachment(Attachment.newBuilder()
.setRowId(attachmentId.rowId)
.setAttachmentId(attachmentId.uniqueId)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeSticker(rowId: Long, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setSticker(Sticker.newBuilder()
.setRowId(rowId)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeDatabaseVersion(version: Int) {
write(outputStream, BackupFrame.newBuilder()
.setVersion(DatabaseVersion.newBuilder().setVersion(version))
.build())
}
@Throws(IOException::class)
fun writeEnd() {
write(outputStream, BackupFrame.newBuilder().setEnd(true).build())
}
@Throws(IOException::class)
private fun writeStream(inputStream: InputStream) {
try {
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
mac.update(iv)
val buffer = ByteArray(8192)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
val ciphertext = cipher.update(buffer, 0, read)
if (ciphertext != null) {
outputStream.write(ciphertext)
mac.update(ciphertext)
}
}
val remainder = cipher.doFinal()
outputStream.write(remainder)
mac.update(remainder)
val attachmentDigest = mac.doFinal()
outputStream.write(attachmentDigest, 0, 10)
} catch (e: Exception) {
when (e) {
is InvalidKeyException,
is InvalidAlgorithmParameterException,
is IllegalBlockSizeException,
is BadPaddingException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
private fun write(out: OutputStream, frame: BackupFrame) {
try {
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
val frameCiphertext = cipher.doFinal(frame.toByteArray())
val frameMac = mac.doFinal(frameCiphertext)
val length = Conversions.intToByteArray(frameCiphertext.size + 10)
out.write(length)
out.write(frameCiphertext)
out.write(frameMac, 0, 10)
} catch (e: Exception) {
when (e) {
is InvalidKeyException,
is InvalidAlgorithmParameterException,
is IllegalBlockSizeException,
is BadPaddingException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
override fun flush() {
outputStream.flush()
}
@Throws(IOException::class)
override fun close() {
outputStream.close()
}
}
}

View File

@ -1,352 +0,0 @@
package org.thoughtcrime.securesms.backup
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import androidx.annotation.WorkerThread
import net.sqlcipher.database.SQLiteDatabase
import org.greenrobot.eventbus.EventBus
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Conversions
import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.BackupUtil
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.LinkedList
import java.util.Locale
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.Mac
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object FullBackupImporter {
/**
* Because BackupProtos.SharedPreference was made only to serialize string values,
* we use these 3-char prefixes to explicitly cast the values before inserting to a preference file.
*/
const val PREF_PREFIX_TYPE_INT = "i__"
const val PREF_PREFIX_TYPE_BOOLEAN = "b__"
private val TAG = FullBackupImporter::class.java.simpleName
@JvmStatic
@WorkerThread
@Throws(IOException::class)
fun importFromUri(context: Context,
attachmentSecret: AttachmentSecret,
db: SQLiteDatabase,
fileUri: Uri,
passphrase: String) {
val baseInputStream = context.contentResolver.openInputStream(fileUri)
?: throw IOException("Cannot open an input stream for the file URI: $fileUri")
var count = 0
try {
BackupRecordInputStream(baseInputStream, passphrase).use { inputStream ->
db.beginTransaction()
dropAllTables(db)
var frame: BackupFrame
while (!inputStream.readFrame().also { frame = it }.end) {
if (count++ % 100 == 0) EventBus.getDefault().post(BackupEvent.createProgress(count))
when {
frame.hasVersion() -> processVersion(db, frame.version)
frame.hasStatement() -> processStatement(db, frame.statement)
frame.hasPreference() -> processPreference(context, frame.preference)
frame.hasAttachment() -> processAttachment(context, attachmentSecret, db, frame.attachment, inputStream)
frame.hasAvatar() -> processAvatar(context, frame.avatar, inputStream)
}
}
trimEntriesForExpiredMessages(context, db)
db.setTransactionSuccessful()
}
} finally {
if (db.inTransaction()) {
db.endTransaction()
}
}
EventBus.getDefault().post(BackupEvent.createFinished())
}
@Throws(IOException::class)
private fun processVersion(db: SQLiteDatabase, version: DatabaseVersion) {
if (version.version > db.version) {
throw DatabaseDowngradeException(db.version, version.version)
}
db.version = version.version
}
private fun processStatement(db: SQLiteDatabase, statement: SqlStatement) {
val isForSmsFtsSecretTable = statement.statement.contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_")
val isForMmsFtsSecretTable = statement.statement.contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_")
val isForSqliteSecretTable = statement.statement.toLowerCase(Locale.ENGLISH).startsWith("create table sqlite_")
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForSqliteSecretTable) {
Log.i(TAG, "Ignoring import for statement: " + statement.statement)
return
}
val parameters: MutableList<Any?> = LinkedList()
for (parameter in statement.parametersList) {
when {
parameter.hasStringParamter() -> parameters.add(parameter.stringParamter)
parameter.hasDoubleParameter() -> parameters.add(parameter.doubleParameter)
parameter.hasIntegerParameter() -> parameters.add(parameter.integerParameter)
parameter.hasBlobParameter() -> parameters.add(parameter.blobParameter.toByteArray())
parameter.hasNullparameter() -> parameters.add(null)
}
}
if (parameters.size > 0) {
db.execSQL(statement.statement, parameters.toTypedArray())
} else {
db.execSQL(statement.statement)
}
}
@Throws(IOException::class)
private fun processAttachment(context: Context, attachmentSecret: AttachmentSecret,
db: SQLiteDatabase, attachment: Attachment,
inputStream: BackupRecordInputStream) {
val partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE)
val dataFile = File.createTempFile("part", ".mms", partsDirectory)
val output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false)
inputStream.readAttachmentTo(output.second, attachment.length)
val contentValues = ContentValues()
contentValues.put(AttachmentDatabase.DATA, dataFile.absolutePath)
contentValues.put(AttachmentDatabase.THUMBNAIL, null as String?)
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first)
db.update(AttachmentDatabase.TABLE_NAME, contentValues,
"${AttachmentDatabase.ROW_ID} = ? AND ${AttachmentDatabase.UNIQUE_ID} = ?",
arrayOf(attachment.rowId.toString(), attachment.attachmentId.toString()))
}
@Throws(IOException::class)
private fun processAvatar(context: Context, avatar: Avatar, inputStream: BackupRecordInputStream) {
inputStream.readAttachmentTo(FileOutputStream(
AvatarHelper.getAvatarFile(context, Address.fromExternal(context, avatar.name))), avatar.length)
}
@SuppressLint("ApplySharedPref")
private fun processPreference(context: Context, preference: SharedPreference) {
val preferences = context.getSharedPreferences(preference.file, 0)
val key = preference.key
val value = preference.value
// See the comment next to PREF_PREFIX_TYPE_* constants.
when {
key.startsWith(PREF_PREFIX_TYPE_INT) ->
preferences.edit().putInt(
key.substring(PREF_PREFIX_TYPE_INT.length),
value.toInt()
).commit()
key.startsWith(PREF_PREFIX_TYPE_BOOLEAN) ->
preferences.edit().putBoolean(
key.substring(PREF_PREFIX_TYPE_BOOLEAN.length),
value.toBoolean()
).commit()
else ->
preferences.edit().putString(key, value).commit()
}
}
private fun dropAllTables(db: SQLiteDatabase) {
db.rawQuery("SELECT name, type FROM sqlite_master", null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val name = cursor.getString(0)
val type = cursor.getString(1)
if ("table" == type && !name.startsWith("sqlite_")) {
db.execSQL("DROP TABLE IF EXISTS $name")
}
}
}
}
private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
val trimmedCondition = " NOT IN (SELECT ${MmsSmsColumns.ID} FROM ${MmsDatabase.TABLE_NAME})"
db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
val where = AttachmentDatabase.MMS_ID + trimmedCondition
db.query(AttachmentDatabase.TABLE_NAME, columns, where, null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
DatabaseComponent.get(context).attachmentDatabase()
.deleteAttachment(AttachmentId(cursor.getLong(0), cursor.getLong(1)))
}
}
db.query(ThreadDatabase.TABLE_NAME, arrayOf(ThreadDatabase.ID),
ThreadDatabase.EXPIRES_IN + " > 0", null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
DatabaseComponent.get(context).threadDatabase().update(cursor.getLong(0), false)
}
}
}
private class BackupRecordInputStream : Closeable {
private val inputStream: InputStream
private val cipher: Cipher
private val mac: Mac
private val cipherKey: ByteArray
private val macKey: ByteArray
private val iv: ByteArray
private var counter = 0
@Throws(IOException::class)
constructor(inputStream: InputStream, passphrase: String) : super() {
try {
this.inputStream = inputStream
val headerLengthBytes = ByteArray(4)
Util.readFully(this.inputStream, headerLengthBytes)
val headerLength = Conversions.byteArrayToInt(headerLengthBytes)
val headerFrame = ByteArray(headerLength)
Util.readFully(this.inputStream, headerFrame)
val frame = BackupFrame.parseFrom(headerFrame)
if (!frame.hasHeader()) {
throw IOException("Backup stream does not start with header!")
}
val header = frame.header
iv = header.iv.toByteArray()
if (iv.size != 16) {
throw IOException("Invalid IV length!")
}
val key = BackupUtil.computeBackupKey(passphrase, if (header.hasSalt()) header.salt.toByteArray() else null)
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
val split = ByteUtil.split(derived, 32, 32)
cipherKey = split[0]
macKey = split[1]
cipher = Cipher.getInstance("AES/CTR/NoPadding")
mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
counter = Conversions.byteArrayToInt(iv)
} catch (e: Exception) {
when (e) {
is NoSuchAlgorithmException,
is NoSuchPaddingException,
is InvalidKeyException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
fun readFrame(): BackupFrame {
return readFrame(inputStream)
}
@Throws(IOException::class)
fun readAttachmentTo(out: OutputStream, length: Int) {
var length = length
try {
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
mac.update(iv)
val buffer = ByteArray(8192)
while (length > 0) {
val read = inputStream.read(buffer, 0, Math.min(buffer.size, length))
if (read == -1) throw IOException("File ended early!")
mac.update(buffer, 0, read)
val plaintext = cipher.update(buffer, 0, read)
if (plaintext != null) {
out.write(plaintext, 0, plaintext.size)
}
length -= read
}
val plaintext = cipher.doFinal()
if (plaintext != null) {
out.write(plaintext, 0, plaintext.size)
}
out.close()
val ourMac = ByteUtil.trim(mac.doFinal(), 10)
val theirMac = ByteArray(10)
try {
Util.readFully(inputStream, theirMac)
} catch (e: IOException) {
throw IOException(e)
}
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw IOException("Bad MAC")
}
} catch (e: Exception) {
when (e) {
is InvalidKeyException,
is InvalidAlgorithmParameterException,
is IllegalBlockSizeException,
is BadPaddingException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
private fun readFrame(`in`: InputStream?): BackupFrame {
return try {
val length = ByteArray(4)
Util.readFully(`in`, length)
val frame = ByteArray(Conversions.byteArrayToInt(length))
Util.readFully(`in`, frame)
val theirMac = ByteArray(10)
System.arraycopy(frame, frame.size - 10, theirMac, 0, theirMac.size)
mac.update(frame, 0, frame.size - 10)
val ourMac = ByteUtil.trim(mac.doFinal(), 10)
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw IOException("Bad MAC")
}
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
val plaintext = cipher.doFinal(frame, 0, frame.size - 10)
BackupFrame.parseFrom(plaintext)
} catch (e: Exception) {
when (e) {
is InvalidKeyException,
is InvalidAlgorithmParameterException,
is IllegalBlockSizeException,
is BadPaddingException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
override fun close() {
inputStream.close()
}
}
class DatabaseDowngradeException internal constructor(currentVersion: Int, backupVersion: Int) :
IOException("Tried to import a backup with version $backupVersion into a database with version $currentVersion")
}

View File

@ -249,17 +249,12 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
viewModel.callState.collect { state ->
Log.d("Loki", "Consuming view model state $state")
when (state) {
CALL_RINGING -> {
if (wantsToAnswer) {
answerCall()
wantsToAnswer = false
}
}
CALL_OUTGOING -> {
}
CALL_CONNECTED -> {
CALL_RINGING -> if (wantsToAnswer) {
answerCall()
wantsToAnswer = false
}
CALL_CONNECTED -> wantsToAnswer = false
else -> {}
}
updateControls(state)
}

View File

@ -1,153 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.utilities.Stub;
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import java.util.List;
import network.loki.messenger.R;
public class AlbumThumbnailView extends FrameLayout {
private @Nullable SlideClickListener thumbnailClickListener;
private @Nullable SlidesClickedListener downloadClickListener;
private int currentSizeClass;
private ViewGroup albumCellContainer;
private Stub<TransferControlView> transferControls;
private final SlideClickListener defaultThumbnailClickListener = (v, slide) -> {
if (thumbnailClickListener != null) {
thumbnailClickListener.onClick(v, slide);
}
};
private final OnLongClickListener defaultLongClickListener = v -> this.performLongClick();
public AlbumThumbnailView(@NonNull Context context) {
super(context);
initialize();
}
public AlbumThumbnailView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
private void initialize() {
inflate(getContext(), R.layout.album_thumbnail_view, this);
albumCellContainer = findViewById(R.id.albumCellContainer);
transferControls = new Stub<>(findViewById(R.id.albumTransferControlsStub));
}
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
if (slides.size() < 2) {
throw new IllegalStateException("Provided less than two slides.");
}
if (showControls) {
transferControls.get().setShowDownloadText(true);
transferControls.get().setSlides(slides);
transferControls.get().setDownloadClickListener(v -> {
if (downloadClickListener != null) {
downloadClickListener.onClick(v, slides);
}
});
} else {
if (transferControls.resolved()) {
transferControls.get().setVisibility(GONE);
}
}
int sizeClass = Math.min(slides.size(), 6);
if (sizeClass != currentSizeClass) {
inflateLayout(sizeClass);
currentSizeClass = sizeClass;
}
showSlides(glideRequests, slides);
}
public void setCellBackgroundColor(@ColorInt int color) {
ViewGroup cellRoot = findViewById(R.id.album_thumbnail_root);
if (cellRoot != null) {
for (int i = 0; i < cellRoot.getChildCount(); i++) {
cellRoot.getChildAt(i).setBackgroundColor(color);
}
}
}
public void setThumbnailClickListener(@Nullable SlideClickListener listener) {
thumbnailClickListener = listener;
}
public void setDownloadClickListener(@Nullable SlidesClickedListener listener) {
downloadClickListener = listener;
}
private void inflateLayout(int sizeClass) {
albumCellContainer.removeAllViews();
switch (sizeClass) {
case 2:
inflate(getContext(), R.layout.album_thumbnail_2, albumCellContainer);
break;
case 3:
inflate(getContext(), R.layout.album_thumbnail_3, albumCellContainer);
break;
case 4:
inflate(getContext(), R.layout.album_thumbnail_4, albumCellContainer);
break;
case 5:
inflate(getContext(), R.layout.album_thumbnail_5, albumCellContainer);
break;
default:
inflate(getContext(), R.layout.album_thumbnail_many, albumCellContainer);
break;
}
}
private void showSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides) {
setSlide(glideRequests, slides.get(0), R.id.album_cell_1);
setSlide(glideRequests, slides.get(1), R.id.album_cell_2);
if (slides.size() >= 3) {
setSlide(glideRequests, slides.get(2), R.id.album_cell_3);
}
if (slides.size() >= 4) {
setSlide(glideRequests, slides.get(3), R.id.album_cell_4);
}
if (slides.size() >= 5) {
setSlide(glideRequests, slides.get(4), R.id.album_cell_5);
}
if (slides.size() > 5) {
TextView text = findViewById(R.id.album_cell_overflow_text);
text.setText(getContext().getString(R.string.AlbumThumbnailView_plus, slides.size() - 5));
}
}
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) {
}
}

View File

@ -13,6 +13,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.snode.SnodeAPI;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -106,7 +107,7 @@ public class ConversationItemFooter extends LinearLayout {
messageRecord.getExpiresIn());
this.timerView.startAnimation();
if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= System.currentTimeMillis()) {
if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= SnodeAPI.getNowWithOffset()) {
ApplicationContext.getInstance(getContext()).getExpiringMessageManager().checkSchedule();
}
} else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {

View File

@ -1,158 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.utilities.ThemeUtil;
import java.util.List;
import network.loki.messenger.R;
public class ConversationItemThumbnail extends FrameLayout {
private ThumbnailView thumbnail;
private AlbumThumbnailView album;
private ImageView shade;
private ConversationItemFooter footer;
private CornerMask cornerMask;
private Outliner outliner;
private boolean borderless;
public ConversationItemThumbnail(Context context) {
super(context);
init(null);
}
public ConversationItemThumbnail(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public ConversationItemThumbnail(final Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
private void init(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.conversation_item_thumbnail, this);
this.thumbnail = findViewById(R.id.conversation_thumbnail_image);
this.album = findViewById(R.id.conversation_thumbnail_album);
this.shade = findViewById(R.id.conversation_thumbnail_shade);
this.footer = findViewById(R.id.conversation_thumbnail_footer);
this.cornerMask = new CornerMask(this);
this.outliner = new Outliner();
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
typedArray.recycle();
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (!borderless) {
cornerMask.mask(canvas);
if (album.getVisibility() != VISIBLE) {
outliner.draw(canvas);
}
}
}
@Override
public void setFocusable(boolean focusable) {
thumbnail.setFocusable(focusable);
album.setFocusable(focusable);
}
@Override
public void setClickable(boolean clickable) {
thumbnail.setClickable(clickable);
album.setClickable(clickable);
}
@Override
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
thumbnail.setOnLongClickListener(l);
album.setOnLongClickListener(l);
}
public void showShade(boolean show) {
shade.setVisibility(show ? VISIBLE : GONE);
forceLayout();
}
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
}
public void setBorderless(boolean borderless) {
this.borderless = borderless;
}
public ConversationItemFooter getFooter() {
return footer;
}
@UiThread
public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides,
boolean showControls, boolean isPreview)
{
if (slides.size() == 1) {
thumbnail.setVisibility(VISIBLE);
album.setVisibility(GONE);
Slide slide = slides.get(0);
Attachment attachment = slide.asAttachment();
thumbnail.setImageResource(glideRequests, slide, showControls, isPreview, attachment.getWidth(), attachment.getHeight());
thumbnail.setLoadIndicatorVisibile(slide.isInProgress());
setTouchDelegate(thumbnail.getTouchDelegate());
} else {
thumbnail.setVisibility(GONE);
album.setVisibility(VISIBLE);
album.setSlides(glideRequests, slides, showControls);
setTouchDelegate(album.getTouchDelegate());
}
}
public void setConversationColor(@ColorInt int color) {
if (album.getVisibility() == VISIBLE) {
album.setCellBackgroundColor(color);
}
}
public void setThumbnailClickListener(SlideClickListener listener) {
thumbnail.setThumbnailClickListener(listener);
album.setThumbnailClickListener(listener);
}
public void setDownloadClickListener(SlidesClickedListener listener) {
thumbnail.setDownloadClickListener(listener);
album.setDownloadClickListener(listener);
}
}

View File

@ -4,30 +4,48 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.widget.ImageView;
import com.bumptech.glide.request.target.BitmapImageViewTarget;
import org.session.libsignal.utilities.SettableFuture;
import java.lang.ref.WeakReference;
public class GlideBitmapListeningTarget extends BitmapImageViewTarget {
private final SettableFuture<Boolean> loaded;
private final WeakReference<View> loadingView;
public GlideBitmapListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
public GlideBitmapListeningTarget(@NonNull ImageView view, @Nullable View loadingView, @NonNull SettableFuture<Boolean> loaded) {
super(view);
this.loaded = loaded;
this.loadingView = new WeakReference<View>(loadingView);
}
@Override
protected void setResource(@Nullable Bitmap resource) {
super.setResource(resource);
loaded.set(true);
View loadingViewInstance = loadingView.get();
if (loadingViewInstance != null) {
loadingViewInstance.setVisibility(View.GONE);
}
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
loaded.set(true);
View loadingViewInstance = loadingView.get();
if (loadingViewInstance != null) {
loadingViewInstance.setVisibility(View.GONE);
}
}
}

View File

@ -3,30 +3,48 @@ package org.thoughtcrime.securesms.components;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import android.widget.ImageView;
import com.bumptech.glide.request.target.DrawableImageViewTarget;
import org.session.libsignal.utilities.SettableFuture;
import java.lang.ref.WeakReference;
public class GlideDrawableListeningTarget extends DrawableImageViewTarget {
private final SettableFuture<Boolean> loaded;
private final WeakReference<View> loadingView;
public GlideDrawableListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
public GlideDrawableListeningTarget(@NonNull ImageView view, @Nullable View loadingView, @NonNull SettableFuture<Boolean> loaded) {
super(view);
this.loaded = loaded;
this.loadingView = new WeakReference<View>(loadingView);
}
@Override
protected void setResource(@Nullable Drawable resource) {
super.setResource(resource);
loaded.set(true);
View loadingViewInstance = loadingView.get();
if (loadingViewInstance != null) {
loadingViewInstance.setVisibility(View.GONE);
}
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
loaded.set(true);
View loadingViewInstance = loadingView.get();
if (loadingViewInstance != null) {
loadingViewInstance.setVisibility(View.GONE);
}
}
}

View File

@ -1,70 +0,0 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewSeparatorBinding
import org.thoughtcrime.securesms.util.toPx
import org.session.libsession.utilities.ThemeUtil
class LabeledSeparatorView : RelativeLayout {
private lateinit var binding: ViewSeparatorBinding
private val path = Path()
private val paint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.STROKE
result.color = ThemeUtil.getThemedColor(context, R.attr.dividerHorizontal)
result.strokeWidth = toPx(1, resources).toFloat()
result.isAntiAlias = true
result
}
// region Lifecycle
constructor(context: Context) : super(context) {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setUpViewHierarchy()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
setUpViewHierarchy()
}
private fun setUpViewHierarchy() {
binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context))
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(binding.root, layoutParams)
setWillNotDraw(false)
}
// endregion
// region Updating
override fun onDraw(c: Canvas) {
super.onDraw(c)
val w = width.toFloat()
val h = height.toFloat()
val hMargin = toPx(16, resources).toFloat()
path.reset()
path.moveTo(0.0f, h / 2)
path.lineTo(binding.titleTextView.left - hMargin, h / 2)
path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
path.moveTo(binding.titleTextView.right + hMargin, h / 2)
path.lineTo(w, h / 2)
path.close()
c.drawPath(path, paint)
}
// endregion
}

View File

@ -1,158 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import network.loki.messenger.R;
import okhttp3.HttpUrl;
public class LinkPreviewView extends FrameLayout {
private static final int TYPE_CONVERSATION = 0;
private static final int TYPE_COMPOSE = 1;
private ViewGroup container;
private OutlinedThumbnailView thumbnail;
private TextView title;
private TextView site;
private View divider;
private View closeButton;
private View spinner;
private int type;
private int defaultRadius;
private CornerMask cornerMask;
private Outliner outliner;
private CloseClickedListener closeClickedListener;
public LinkPreviewView(Context context) {
super(context);
init(null);
}
public LinkPreviewView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
private void init(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.link_preview, this);
container = findViewById(R.id.linkpreview_container);
thumbnail = findViewById(R.id.linkpreview_thumbnail);
title = findViewById(R.id.linkpreview_title);
site = findViewById(R.id.linkpreview_site);
divider = findViewById(R.id.linkpreview_divider);
spinner = findViewById(R.id.linkpreview_progress_wheel);
closeButton = findViewById(R.id.linkpreview_close);
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
cornerMask = new CornerMask(this);
outliner = new Outliner();
outliner.setColor(getResources().getColor(R.color.transparent));
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
type = typedArray.getInt(R.styleable.LinkPreviewView_linkpreview_type, 0);
typedArray.recycle();
}
if (type == TYPE_COMPOSE) {
container.setBackgroundColor(Color.TRANSPARENT);
container.setPadding(0, 0, 0, 0);
divider.setVisibility(VISIBLE);
closeButton.setOnClickListener(v -> {
if (closeClickedListener != null) {
closeClickedListener.onCloseClicked();
}
});
}
setWillNotDraw(false);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (type == TYPE_COMPOSE) return;
cornerMask.mask(canvas);
outliner.draw(canvas);
}
public void setLoading() {
title.setVisibility(GONE);
site.setVisibility(GONE);
thumbnail.setVisibility(GONE);
spinner.setVisibility(VISIBLE);
closeButton.setVisibility(GONE);
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) {
setLinkPreview(glideRequests, linkPreview, showThumbnail);
if (showCloseButton) {
closeButton.setVisibility(VISIBLE);
} else {
closeButton.setVisibility(GONE);
}
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
title.setVisibility(VISIBLE);
site.setVisibility(VISIBLE);
thumbnail.setVisibility(VISIBLE);
spinner.setVisibility(GONE);
closeButton.setVisibility(VISIBLE);
title.setText(linkPreview.getTitle());
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
if (url != null) {
site.setText(url.topPrivateDomain());
}
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
thumbnail.setVisibility(VISIBLE);
thumbnail.setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
thumbnail.showDownloadText(false);
} else {
thumbnail.setVisibility(GONE);
}
}
public void setCorners(int topLeft, int topRight) {
cornerMask.setRadii(topLeft, topRight, 0, 0);
outliner.setRadii(topLeft, topRight, 0, 0);
thumbnail.setCorners(topLeft, defaultRadius, defaultRadius, defaultRadius);
postInvalidate();
}
public void setCloseClickedListener(@Nullable CloseClickedListener closeClickedListener) {
this.closeClickedListener = closeClickedListener;
}
public void setDownloadClickedListener(SlidesClickedListener listener) {
thumbnail.setDownloadClickListener(listener);
}
public interface CloseClickedListener {
void onCloseClicked();
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
import kotlin.math.sign
/**
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
*
* This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/
class NestedScrollableHost : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}

View File

@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import org.session.libsession.utilities.ThemeUtil;
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
import network.loki.messenger.R;
public class OutlinedThumbnailView extends ThumbnailView {
private CornerMask cornerMask;
private Outliner outliner;
public OutlinedThumbnailView(Context context) {
super(context);
init();
}
public OutlinedThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
cornerMask = new CornerMask(this);
outliner = new Outliner();
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
setWillNotDraw(false);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
cornerMask.mask(canvas);
outliner.draw(canvas);
}
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
postInvalidate();
}
}

View File

@ -1,50 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import androidx.annotation.ColorInt;
public class Outliner {
private final float[] radii = new float[8];
private final Path corners = new Path();
private final RectF bounds = new RectF();
private final Paint outlinePaint = new Paint();
{
outlinePaint.setStyle(Paint.Style.STROKE);
outlinePaint.setStrokeWidth(1f);
outlinePaint.setAntiAlias(true);
}
public void setColor(@ColorInt int color) {
outlinePaint.setColor(color);
}
public void draw(Canvas canvas) {
final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
bounds.left = halfStrokeWidth;
bounds.top = halfStrokeWidth;
bounds.right = canvas.getWidth() - halfStrokeWidth;
bounds.bottom = canvas.getHeight() - halfStrokeWidth;
corners.reset();
corners.addRoundRect(bounds, radii, Path.Direction.CW);
canvas.drawPath(corners, outlinePaint);
}
public void setRadius(int radius) {
setRadii(radius, radius, radius, radius);
}
public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) {
radii[0] = radii[1] = topLeft;
radii[2] = radii[3] = topRight;
radii[4] = radii[5] = bottomRight;
radii[6] = radii[7] = bottomLeft;
}
}

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
@ -15,15 +16,17 @@ import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.avatars.ResourceContactPhoto
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
class ProfilePictureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
lateinit var glide: GlideRequests
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
private val glide: GlideRequests = GlideApp.with(this)
var publicKey: String? = null
var displayName: String? = null
var additionalPublicKey: String? = null
@ -31,32 +34,49 @@ class ProfilePictureView @JvmOverloads constructor(
var isLarge = false
private val profilePicturesCache = mutableMapOf<String, String?>()
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
// endregion
constructor(context: Context, sender: Recipient): this(context) {
update(sender)
}
// region Updating
fun update(recipient: Recipient) {
fun getUserDisplayName(publicKey: String): String {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
fun isOpenGroupWithProfilePicture(recipient: Recipient): Boolean {
return recipient.isOpenGroupRecipient && recipient.groupAvatarId != null
}
if (recipient.isGroupRecipient && !isOpenGroupWithProfilePicture(recipient)) {
if (recipient.isClosedGroupRecipient) {
val members = DatabaseComponent.get(context).groupDatabase()
.getGroupMemberAddresses(recipient.address.toGroupString(), true)
.sorted()
.take(2)
.toMutableList()
val pk = members.getOrNull(0)?.serialize() ?: ""
publicKey = pk
displayName = getUserDisplayName(pk)
val apk = members.getOrNull(1)?.serialize() ?: ""
additionalPublicKey = apk
additionalDisplayName = getUserDisplayName(apk)
if (members.size <= 1) {
publicKey = ""
displayName = ""
additionalPublicKey = ""
additionalDisplayName = ""
} else {
val pk = members.getOrNull(0)?.serialize() ?: ""
publicKey = pk
displayName = getUserDisplayName(pk)
val apk = members.getOrNull(1)?.serialize() ?: ""
additionalPublicKey = apk
additionalDisplayName = getUserDisplayName(apk)
}
} else if(recipient.isOpenGroupInboxRecipient) {
val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
this.publicKey = publicKey
displayName = getUserDisplayName(publicKey)
additionalPublicKey = null
} else {
val publicKey = recipient.address.toString()
this.publicKey = publicKey
@ -67,7 +87,6 @@ class ProfilePictureView @JvmOverloads constructor(
}
fun update() {
if (!this::glide.isInitialized) return
val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey
if (additionalPublicKey != null) {
@ -101,26 +120,37 @@ class ProfilePictureView @JvmOverloads constructor(
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
val signalProfilePicture = recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
glide.clear(imageView)
glide.load(signalProfilePicture)
.placeholder(unknownRecipientDrawable)
.centerCrop()
.error(unknownRecipientDrawable)
.error(glide.load(placeholder))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imageView)
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
glide.clear(imageView)
glide.load(unknownOpenGroupDrawable)
.centerCrop()
.circleCrop()
.into(imageView)
} else {
glide.clear(imageView)
glide.load(placeholder)
.placeholder(unknownRecipientDrawable)
.centerCrop()
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
}
profilePicturesCache[publicKey] = recipient.profileAvatar
} else {
imageView.setImageDrawable(null)
glide.load(unknownRecipientDrawable)
.centerCrop()
.into(imageView)
}
}

View File

@ -1,304 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.session.libsession.messaging.contacts.Contact;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.database.SessionContactDatabase;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.util.UiModeUtilities;
import java.util.List;
import network.loki.messenger.R;
public class QuoteView extends FrameLayout implements RecipientModifiedListener {
private static final String TAG = QuoteView.class.getSimpleName();
private static final int MESSAGE_TYPE_PREVIEW = 0;
private static final int MESSAGE_TYPE_OUTGOING = 1;
private static final int MESSAGE_TYPE_INCOMING = 2;
private ViewGroup mainView;
private ViewGroup footerView;
private TextView authorView;
private TextView bodyView;
private ImageView quoteBarView;
private ImageView thumbnailView;
private View attachmentVideoOverlayView;
private ViewGroup attachmentContainerView;
private TextView attachmentNameView;
private ImageView dismissView;
private long id;
private Recipient author;
private String body;
private Recipient conversationRecipient;
private TextView mediaDescriptionText;
private TextView missingLinkText;
private SlideDeck attachments;
private int messageType;
private int largeCornerRadius;
private int smallCornerRadius;
private CornerMask cornerMask;
public QuoteView(Context context) {
super(context);
initialize(null);
}
public QuoteView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(attrs);
}
public QuoteView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public QuoteView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(attrs);
}
private void initialize(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.quote_view, this);
this.mainView = findViewById(R.id.quote_main);
this.footerView = findViewById(R.id.quote_missing_footer);
this.authorView = findViewById(R.id.quote_author);
this.bodyView = findViewById(R.id.quote_text);
this.quoteBarView = findViewById(R.id.quote_bar);
this.thumbnailView = findViewById(R.id.quote_thumbnail);
this.attachmentVideoOverlayView = findViewById(R.id.quote_video_overlay);
this.attachmentContainerView = findViewById(R.id.quote_attachment_container);
this.attachmentNameView = findViewById(R.id.quote_attachment_name);
this.dismissView = findViewById(R.id.quote_dismiss);
this.mediaDescriptionText = findViewById(R.id.media_type);
this.missingLinkText = findViewById(R.id.quote_missing_text);
this.largeCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom);
this.smallCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom);
cornerMask = new CornerMask(this);
cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
int primaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorPrimary, Color.BLACK);
int secondaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorSecondary, Color.BLACK);
messageType = typedArray.getInt(R.styleable.QuoteView_message_type, 0);
typedArray.recycle();
dismissView.setVisibility(messageType == MESSAGE_TYPE_PREVIEW ? VISIBLE : GONE);
authorView.setTextColor(primaryColor);
bodyView.setTextColor(primaryColor);
attachmentNameView.setTextColor(primaryColor);
mediaDescriptionText.setTextColor(secondaryColor);
missingLinkText.setTextColor(primaryColor);
if (messageType == MESSAGE_TYPE_PREVIEW) {
int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
cornerMask.setTopLeftRadius(radius);
cornerMask.setTopRightRadius(radius);
}
}
dismissView.setOnClickListener(view -> setVisibility(GONE));
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
cornerMask.mask(canvas);
}
public void setQuote(GlideRequests glideRequests,
long id,
@NonNull Recipient author,
@Nullable String body,
boolean originalMissing,
@NonNull SlideDeck attachments,
@NonNull Recipient conversationRecipient)
{
if (this.author != null) this.author.removeListener(this);
this.id = id;
this.author = author;
this.body = body;
this.attachments = attachments;
this.conversationRecipient = conversationRecipient;
author.addListener(this);
setQuoteAuthor(author);
setQuoteText(body, attachments);
setQuoteAttachment(glideRequests, attachments);
setQuoteMissingFooter(originalMissing);
}
public void setTopCornerSizes(boolean topLeftLarge, boolean topRightLarge) {
cornerMask.setTopLeftRadius(topLeftLarge ? largeCornerRadius : smallCornerRadius);
cornerMask.setTopRightRadius(topRightLarge ? largeCornerRadius : smallCornerRadius);
}
public void dismiss() {
if (this.author != null) this.author.removeListener(this);
this.id = 0;
this.author = null;
this.body = null;
setVisibility(GONE);
}
@Override
public void onModified(Recipient recipient) {
Util.runOnMain(() -> {
if (recipient == author) {
setQuoteAuthor(recipient);
}
});
}
private void setQuoteAuthor(@NonNull Recipient author) {
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress().serialize());
String quoteeDisplayName;
String senderHexEncodedPublicKey = author.getAddress().serialize();
if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
quoteeDisplayName = TextSecurePreferences.getProfileName(getContext());
} else {
SessionContactDatabase contactDB = DatabaseComponent.get(getContext()).sessionContactDatabase();
Contact contact = contactDB.getContactWithSessionID(senderHexEncodedPublicKey);
if (contact != null) {
Contact.ContactContext context = (this.conversationRecipient.isOpenGroupRecipient()) ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR;
quoteeDisplayName = contact.displayName(context);
} else {
quoteeDisplayName = senderHexEncodedPublicKey;
}
}
authorView.setText(isOwnNumber ? getContext().getString(R.string.QuoteView_you) : quoteeDisplayName);
// We use the raw color resource because Android 4.x was struggling with tints here
int colorID = UiModeUtilities.isDayUiMode(getContext()) ? R.color.black : R.color.accent;
quoteBarView.setImageResource(colorID);
mainView.setBackgroundColor(ThemeUtil.getThemedColor(getContext(),
outgoing ? R.attr.message_received_background_color : R.attr.message_sent_background_color));
}
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
bodyView.setVisibility(VISIBLE);
bodyView.setText(body == null ? "" : body);
mediaDescriptionText.setVisibility(GONE);
return;
}
bodyView.setVisibility(GONE);
mediaDescriptionText.setVisibility(VISIBLE);
List<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
// Given that most types have images, we specifically check images last
if (!audioSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_audio);
} else if (!documentSlides.isEmpty()) {
mediaDescriptionText.setVisibility(GONE);
} else if (!videoSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_video);
} else if (!imageSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_photo);
}
}
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo()).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
attachmentVideoOverlayView.setVisibility(GONE);
if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) {
thumbnailView.setVisibility(VISIBLE);
attachmentContainerView.setVisibility(GONE);
dismissView.setBackgroundResource(R.drawable.dismiss_background);
if (imageVideoSlides.get(0).hasVideo()) {
attachmentVideoOverlayView.setVisibility(VISIBLE);
}
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(thumbnailView);
} else if (!documentSlides.isEmpty()){
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(VISIBLE);
attachmentNameView.setText(documentSlides.get(0).getFileName().or(""));
} else {
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(GONE);
dismissView.setBackgroundDrawable(null);
}
if (ThemeUtil.isDarkTheme(getContext())) {
dismissView.setBackgroundResource(R.drawable.circle_alpha);
}
}
private void setQuoteMissingFooter(boolean missing) {
footerView.setVisibility(missing ? VISIBLE : GONE);
footerView.setBackgroundColor(getResources().getColor(R.color.quote_not_found_background));
}
public long getQuoteId() {
return id;
}
public Recipient getAuthor() {
return author;
}
public String getBody() {
return body;
}
public List<Attachment> getAttachments() {
return attachments.asAttachments();
}
}

View File

@ -0,0 +1,30 @@
package org.thoughtcrime.securesms.components
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
/**
* An extension of ViewPager to swallow erroneous multi-touch exceptions.
*
* @see https://stackoverflow.com/questions/6919292/pointerindex-out-of-range-android-multitouch
*/
class SafeViewPager @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean = try {
super.onTouchEvent(event)
} catch (e: IllegalArgumentException) {
false
}
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean = try {
super.onInterceptTouchEvent(event)
} catch (e: IllegalArgumentException) {
false
}
}

View File

@ -52,19 +52,4 @@ public class StickerView extends FrameLayout {
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
image.setOnLongClickListener(l);
}
public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
image.setImageResource(glideRequests, stickerSlide, showControls, false);
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
}
public void setThumbnailClickListener(@NonNull SlideClickListener listener) {
image.setThumbnailClickListener(listener);
}
public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
image.setDownloadClickListener(listener);
}
}

View File

@ -1,11 +1,10 @@
package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import androidx.preference.CheckBoxPreference;
import androidx.preference.Preference;
import android.util.AttributeSet;
import network.loki.messenger.R;
@ -18,7 +17,6 @@ public class SwitchPreferenceCompat extends CheckBoxPreference {
setLayoutRes();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setLayoutRes();

View File

@ -1,21 +1,30 @@
package org.thoughtcrime.securesms.components.emoji;
import android.net.Uri;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.conversation.v2.Util;
import java.util.LinkedList;
import java.util.List;
public class CompositeEmojiPageModel implements EmojiPageModel {
@AttrRes private final int iconAttr;
@NonNull private final EmojiPageModel[] models;
@AttrRes private final int iconAttr;
@NonNull private final List<EmojiPageModel> models;
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull EmojiPageModel... models) {
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List<EmojiPageModel> models) {
this.iconAttr = iconAttr;
this.models = models;
}
@Override
public String getKey() {
return Util.hasItems(models) ? models.get(0).getKey() : "";
}
public int getIconAttr() {
return iconAttr;
}
@ -44,7 +53,7 @@ public class CompositeEmojiPageModel implements EmojiPageModel {
}
@Override
public @Nullable String getSprite() {
public @Nullable Uri getSpriteUri() {
return null;
}

View File

@ -1,14 +1,27 @@
package org.thoughtcrime.securesms.components.emoji;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Emoji {
private final List<String> variations;
private final List<String> rawVariations;
public Emoji(String... variations) {
this.variations = Arrays.asList(variations);
this(Arrays.asList(variations), Collections.emptyList());
}
public Emoji(List<String> variations) {
this(variations, Collections.emptyList());
}
public Emoji(List<String> variations, List<String> rawVariations) {
this.variations = variations;
this.rawVariations = rawVariations;
}
public String getValue() {
@ -18,4 +31,15 @@ public class Emoji {
public List<String> getVariations() {
return variations;
}
public boolean hasMultipleVariations() {
return variations.size() > 1;
}
public @Nullable String getRawVariation(int variationIndex) {
if (rawVariations != null && variationIndex >= 0 && variationIndex < rawVariations.size()) {
return rawVariations.get(variationIndex);
}
return null;
}
}

View File

@ -1,20 +1,23 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import android.text.InputFilter;
import android.util.AttributeSet;
import network.loki.messenger.R;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.session.libsession.utilities.TextSecurePreferences;
import network.loki.messenger.R;
public class EmojiEditText extends AppCompatEditText {
private static final String TAG = EmojiEditText.class.getSimpleName();
private static final String TAG = Log.tag(EmojiEditText.class);
public EmojiEditText(Context context) {
this(context, null);
@ -26,8 +29,14 @@ public class EmojiEditText extends AppCompatEditText {
public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (!TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
setFilters(appendEmojiFilter(this.getFilters()));
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
boolean jumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
a.recycle();
if (!isInEditMode() && forceCustom) {
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
}
}
@ -45,7 +54,7 @@ public class EmojiEditText extends AppCompatEditText {
else super.invalidateDrawable(drawable);
}
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters) {
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
InputFilter[] result;
if (originalFilters != null) {
@ -55,7 +64,7 @@ public class EmojiEditText extends AppCompatEditText {
result = new InputFilter[1];
}
result[0] = new EmojiFilter(this);
result[0] = new EmojiFilter(this, jumboEmoji);
return result;
}

View File

@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.components.emoji;
import android.view.KeyEvent;
public interface EmojiEventListener {
void onEmojiSelected(String emoji);
void onKeyEvent(KeyEvent keyEvent);
}

View File

@ -8,9 +8,11 @@ import android.widget.TextView;
public class EmojiFilter implements InputFilter {
private TextView view;
private boolean jumboEmoji;
public EmojiFilter(TextView view) {
this.view = view;
public EmojiFilter(TextView view, boolean jumboEmoji) {
this.view = view;
this.jumboEmoji = jumboEmoji;
}
@Override
@ -19,7 +21,7 @@ public class EmojiFilter implements InputFilter {
char[] v = new char[end - start];
TextUtils.getChars(source, start, end, v, 0);
Spannable emojified = EmojiProvider.getInstance(view.getContext()).emojify(new String(v), view);
Spannable emojified = EmojiProvider.emojify(new String(v), view, jumboEmoji);
if (source instanceof Spanned && emojified != null) {
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);

View File

@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import network.loki.messenger.R;
public class EmojiImageView extends AppCompatImageView {
private final boolean forceJumboEmoji;
public EmojiImageView(Context context) {
this(context, null);
}
public EmojiImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public EmojiImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiImageView, 0, 0);
forceJumboEmoji = a.getBoolean(R.styleable.EmojiImageView_forceJumbo, false);
a.recycle();
}
public void setImageEmoji(CharSequence emoji) {
if (isInEditMode()) {
setImageResource(R.drawable.ic_emoji);
} else {
Drawable emojiDrawable = EmojiProvider.getEmojiDrawable(getContext(), emoji);
if (emojiDrawable == null) {
// fallback
setImageResource(R.drawable.ic_outline_disabled_by_default_24);
} else {
setImageDrawable(emojiDrawable);
}
}
}
}

View File

@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.components.emoji
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.appcompat.widget.AppCompatTextView
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiModel
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
import org.thoughtcrime.securesms.util.InsetItemDecoration
private val EDGE_LENGTH: Int = ViewUtil.dpToPx(6)
private val HORIZONTAL_INSET: Int = ViewUtil.dpToPx(6)
private val EMOJI_VERTICAL_INSET: Int = ViewUtil.dpToPx(5)
private val HEADER_VERTICAL_INSET: Int = ViewUtil.dpToPx(8)
/**
* Use super class to add insets to the emojis and use the [onDrawOver] to draw the variation
* hint if the emoji has more than one variation.
*/
class EmojiItemDecoration(private val allowVariations: Boolean, private val variationsDrawable: Drawable) : InsetItemDecoration(SetInset()) {
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(canvas, parent, state)
val adapter: EmojiPageViewGridAdapter? = parent.adapter as? EmojiPageViewGridAdapter
if (allowVariations && adapter != null) {
for (i in 0 until parent.childCount) {
val child: View = parent.getChildAt(i)
val position: Int = parent.getChildAdapterPosition(child)
if (position >= 0 && position <= adapter.itemCount) {
val model = adapter.currentList[position]
if (model is EmojiModel && model.emoji.hasMultipleVariations()) {
variationsDrawable.setBounds(child.right, child.bottom - EDGE_LENGTH, child.right + EDGE_LENGTH, child.bottom)
variationsDrawable.draw(canvas)
}
}
}
}
}
private class SetInset : InsetItemDecoration.SetInset() {
override fun setInset(outRect: Rect, view: View, parent: RecyclerView) {
val isHeader = view.javaClass == AppCompatTextView::class.java
outRect.left = HORIZONTAL_INSET
outRect.right = HORIZONTAL_INSET
outRect.top = if (isHeader) HEADER_VERTICAL_INSET else EMOJI_VERTICAL_INSET
outRect.bottom = if (isHeader) 0 else EMOJI_VERTICAL_INSET
}
}
}

View File

@ -136,8 +136,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener);
page.setModel(pages.get(position));
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, false);
container.addView(page);
return page;
}
@ -160,8 +159,4 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
}
}
public interface EmojiEventListener {
void onEmojiSelected(String emoji);
void onKeyEvent(KeyEvent keyEvent);
}
}

View File

@ -1,12 +1,15 @@
package org.thoughtcrime.securesms.components.emoji;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.util.List;
public interface EmojiPageModel {
String getKey();
int getIconAttr();
List<String> getEmoji();
List<Emoji> getDisplayEmoji();
boolean hasSpriteMap();
String getSprite();
@Nullable Uri getSpriteUri();
boolean isDynamic();
}

View File

@ -1,60 +1,136 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiHeader;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiNoResultsModel;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
import java.util.List;
import java.util.Optional;
import network.loki.messenger.R;
public class EmojiPageView extends FrameLayout implements VariationSelectorListener {
private static final String TAG = EmojiPageView.class.getSimpleName();
private EmojiPageModel model;
private EmojiPageViewGridAdapter adapter;
private RecyclerView recyclerView;
private GridLayoutManager layoutManager;
public class EmojiPageView extends RecyclerView implements VariationSelectorListener {
private AdapterFactory adapterFactory;
private LinearLayoutManager layoutManager;
private RecyclerView.OnItemTouchListener scrollDisabler;
private VariationSelectorListener variationSelectorListener;
private EmojiVariationSelectorPopup popup;
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public EmojiPageView(@NonNull Context context,
@NonNull EmojiKeyboardProvider.EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener)
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations)
{
super(context);
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
initialize(emojiSelectionListener, variationSelectorListener, allowVariations);
}
public EmojiPageView(@NonNull Context context,
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations,
@NonNull LinearLayoutManager layoutManager,
@LayoutRes int displayEmojiLayoutResId,
@LayoutRes int displayEmoticonLayoutResId)
{
super(context);
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, layoutManager, displayEmojiLayoutResId, displayEmoticonLayoutResId);
}
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations)
{
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, new GridLayoutManager(getContext(), 8), R.layout.emoji_display_item_grid, R.layout.emoji_text_display_item_grid);
}
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations,
@NonNull LinearLayoutManager layoutManager,
@LayoutRes int displayEmojiLayoutResId,
@LayoutRes int displayEmoticonLayoutResId)
{
this.variationSelectorListener = variationSelectorListener;
recyclerView = view.findViewById(R.id.emoji);
layoutManager = new GridLayoutManager(context, 8);
scrollDisabler = new ScrollDisabler();
popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
popup,
emojiSelectionListener,
this);
this.layoutManager = layoutManager;
this.scrollDisabler = new ScrollDisabler();
this.popup = new EmojiVariationSelectorPopup(getContext(), emojiSelectionListener);
this.adapterFactory = () -> new EmojiPageViewGridAdapter(popup,
emojiSelectionListener,
this,
allowVariations,
displayEmojiLayoutResId,
displayEmoticonLayoutResId);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
if (this.layoutManager instanceof GridLayoutManager) {
GridLayoutManager gridLayout = (GridLayoutManager) this.layoutManager;
gridLayout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (getAdapter() != null) {
Optional<MappingModel<?>> model = getAdapter().getModel(position);
if (model.isPresent() && (model.get() instanceof EmojiHeader || model.get() instanceof EmojiNoResultsModel)) {
return gridLayout.getSpanCount();
}
}
return 1;
}
});
}
setLayoutManager(layoutManager);
Drawable drawable = DrawableUtil.tint(ContextUtil.requireDrawable(getContext(), R.drawable.triangle_bottom_right_corner), ContextCompat.getColor(getContext(), R.color.signal_button_secondary_text_disabled));
addItemDecoration(new EmojiItemDecoration(allowVariations, drawable));
}
public void presentForEmojiKeyboard() {
setPadding(getPaddingLeft(),
getPaddingTop(),
getPaddingRight(),
getPaddingBottom() + ViewUtil.dpToPx(56));
setClipToPadding(false);
}
public void onSelected() {
if (model.isDynamic() && adapter != null) {
adapter.notifyDataSetChanged();
if (getAdapter() != null) {
getAdapter().notifyDataSetChanged();
}
}
public void setModel(EmojiPageModel model) {
this.model = model;
adapter.setEmoji(model.getDisplayEmoji());
public void setList(@NonNull List<MappingModel<?>> list, @Nullable Runnable commitCallback) {
EmojiPageViewGridAdapter adapter = adapterFactory.create();
setAdapter(adapter);
adapter.submitList(list, commitCallback);
}
@Override
@ -66,16 +142,21 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
layoutManager.setSpanCount(Math.max(w / idealWidth, 1));
if (layoutManager instanceof GridLayoutManager) {
int viewWidth = w - getPaddingStart() - getPaddingEnd();
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
int spanCount = Math.max(viewWidth / idealWidth, 1);
((GridLayoutManager) layoutManager).setSpanCount(spanCount);
}
}
@Override
public void onVariationSelectorStateChanged(boolean open) {
if (open) {
recyclerView.addOnItemTouchListener(scrollDisabler);
addOnItemTouchListener(scrollDisabler);
} else {
post(() -> recyclerView.removeOnItemTouchListener(scrollDisabler));
post(() -> removeOnItemTouchListener(scrollDisabler));
}
if (variationSelectorListener != null) {
@ -83,6 +164,32 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
}
}
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
setNestedScrollingEnabled(enabled);
}
public void smoothScrollToPositionTop(int position) {
int currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition();
boolean shortTrip = Math.abs(currentPosition - position) < 475;
if (shortTrip) {
RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) {
@Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
};
smoothScroller.setTargetPosition(position);
layoutManager.startSmoothScroll(smoothScroller);
} else {
layoutManager.scrollToPositionWithOffset(position, 0);
}
}
public @Nullable EmojiPageViewGridAdapter getAdapter() {
return (EmojiPageViewGridAdapter) super.getAdapter();
}
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
@ -95,4 +202,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
@Override
public void onRequestDisallowInterceptTouchEvent(boolean b) { }
}
private interface AdapterFactory {
EmojiPageViewGridAdapter create();
}
}

View File

@ -1,94 +1,40 @@
package org.thoughtcrime.securesms.components.emoji;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
import java.util.ArrayList;
import java.util.List;
public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWindow.OnDismissListener {
public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageViewGridAdapter.EmojiViewHolder> implements PopupWindow.OnDismissListener {
private final VariationSelectorListener variationSelectorListener;
private final List<Emoji> emojiList;
private final EmojiProvider emojiProvider;
private final EmojiVariationSelectorPopup popup;
private final VariationSelectorListener variationSelectorListener;
private final EmojiEventListener emojiEventListener;
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
@NonNull EmojiVariationSelectorPopup popup,
public EmojiPageViewGridAdapter(@NonNull EmojiVariationSelectorPopup popup,
@NonNull EmojiEventListener emojiEventListener,
@NonNull VariationSelectorListener variationSelectorListener)
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations,
@LayoutRes int displayEmojiLayoutResId,
@LayoutRes int displayEmoticonLayoutResId)
{
this.emojiList = new ArrayList<>();
this.emojiProvider = emojiProvider;
this.popup = popup;
this.emojiEventListener = emojiEventListener;
this.variationSelectorListener = variationSelectorListener;
popup.setOnDismissListener(this);
}
@NonNull
@Override
public EmojiViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new EmojiViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.emoji_display_item, viewGroup, false));
}
@Override
public void onBindViewHolder(@NonNull EmojiViewHolder viewHolder, int i) {
Emoji emoji = emojiList.get(i);
Drawable drawable = emojiProvider.getEmojiDrawable(emoji.getValue());
if (drawable != null) {
viewHolder.textView.setVisibility(View.GONE);
viewHolder.imageView.setVisibility(View.VISIBLE);
viewHolder.imageView.setImageDrawable(drawable);
} else {
viewHolder.textView.setVisibility(View.VISIBLE);
viewHolder.imageView.setVisibility(View.GONE);
viewHolder.textView.setEmoji(emoji.getValue());
}
viewHolder.itemView.setOnClickListener(v -> {
emojiEventListener.onEmojiSelected(emoji.getValue());
});
if (emoji.getVariations().size() > 1) {
viewHolder.itemView.setOnLongClickListener(v -> {
popup.dismiss();
popup.setVariations(emoji.getVariations());
popup.showAsDropDown(viewHolder.itemView, 0, -(2 * viewHolder.itemView.getHeight()));
variationSelectorListener.onVariationSelectorStateChanged(true);
return true;
});
viewHolder.hintCorner.setVisibility(View.VISIBLE);
} else {
viewHolder.itemView.setOnLongClickListener(null);
viewHolder.hintCorner.setVisibility(View.GONE);
}
}
@Override
public int getItemCount() {
return emojiList.size();
}
public void setEmoji(@NonNull List<Emoji> emojiList) {
this.emojiList.clear();
this.emojiList.addAll(emojiList);
notifyDataSetChanged();
registerFactory(EmojiHeader.class, new LayoutFactory<>(EmojiHeaderViewHolder::new, R.layout.emoji_grid_header));
registerFactory(EmojiModel.class, new LayoutFactory<>(v -> new EmojiViewHolder(v, emojiEventListener, variationSelectorListener, popup, allowVariations), displayEmojiLayoutResId));
registerFactory(EmojiTextModel.class, new LayoutFactory<>(v -> new EmojiTextViewHolder(v, emojiEventListener), displayEmoticonLayoutResId));
registerFactory(EmojiNoResultsModel.class, new LayoutFactory<>(MappingViewHolder.SimpleViewHolder::new, R.layout.emoji_grid_no_results));
}
@Override
@ -96,18 +42,196 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
variationSelectorListener.onVariationSelectorStateChanged(false);
}
static class EmojiViewHolder extends RecyclerView.ViewHolder {
public static class EmojiHeader implements MappingModel<EmojiHeader>, HasKey {
private final ImageView imageView;
private final AsciiEmojiView textView;
private final ImageView hintCorner;
private final String key;
private final int title;
public EmojiViewHolder(@NonNull View itemView) {
super(itemView);
this.imageView = itemView.findViewById(R.id.emoji_image);
this.textView = itemView.findViewById(R.id.emoji_text);
this.hintCorner = itemView.findViewById(R.id.emoji_variation_hint);
public EmojiHeader(@NonNull String key, int title) {
this.key = key;
this.title = title;
}
@Override
public @NonNull String getKey() {
return key;
}
@Override
public boolean areItemsTheSame(@NonNull EmojiHeader newItem) {
return title == newItem.title;
}
@Override
public boolean areContentsTheSame(@NonNull EmojiHeader newItem) {
return areItemsTheSame(newItem);
}
}
static class EmojiHeaderViewHolder extends MappingViewHolder<EmojiHeader> {
private final TextView title;
public EmojiHeaderViewHolder(@NonNull View itemView) {
super(itemView);
title = findViewById(R.id.emoji_grid_header_title);
}
@Override
public void bind(@NonNull EmojiHeader model) {
title.setText(model.title);
}
}
public static class EmojiModel implements MappingModel<EmojiModel>, HasKey {
private final String key;
private final Emoji emoji;
public EmojiModel(@NonNull String key, @NonNull Emoji emoji) {
this.key = key;
this.emoji = emoji;
}
@Override
public @NonNull String getKey() {
return key;
}
public @NonNull Emoji getEmoji() {
return emoji;
}
@Override
public boolean areItemsTheSame(@NonNull EmojiModel newItem) {
return newItem.emoji.getValue().equals(emoji.getValue());
}
@Override
public boolean areContentsTheSame(@NonNull EmojiModel newItem) {
return areItemsTheSame(newItem);
}
}
static class EmojiViewHolder extends MappingViewHolder<EmojiModel> {
private final EmojiVariationSelectorPopup popup;
private final VariationSelectorListener variationSelectorListener;
private final EmojiEventListener emojiEventListener;
private final boolean allowVariations;
private final ImageView imageView;
public EmojiViewHolder(@NonNull View itemView,
@NonNull EmojiEventListener emojiEventListener,
@NonNull VariationSelectorListener variationSelectorListener,
@NonNull EmojiVariationSelectorPopup popup,
boolean allowVariations)
{
super(itemView);
this.popup = popup;
this.variationSelectorListener = variationSelectorListener;
this.emojiEventListener = emojiEventListener;
this.allowVariations = allowVariations;
this.imageView = itemView.findViewById(R.id.emoji_image);
}
@Override
public void bind(@NonNull EmojiModel model) {
final Drawable drawable = EmojiProvider.getEmojiDrawable(imageView.getContext(), model.emoji.getValue());
if (drawable != null) {
imageView.setVisibility(View.VISIBLE);
imageView.setImageDrawable(drawable);
}
itemView.setOnClickListener(v -> {
emojiEventListener.onEmojiSelected(model.emoji.getValue());
});
if (allowVariations && model.emoji.hasMultipleVariations()) {
itemView.setOnLongClickListener(v -> {
popup.dismiss();
popup.setVariations(model.emoji.getVariations());
popup.showAsDropDown(itemView, 0, -(2 * itemView.getHeight()));
variationSelectorListener.onVariationSelectorStateChanged(true);
return true;
});
} else {
itemView.setOnLongClickListener(null);
}
}
}
public static class EmojiTextModel implements MappingModel<EmojiTextModel>, HasKey {
private final String key;
private final Emoji emoji;
public EmojiTextModel(@NonNull String key, @NonNull Emoji emoji) {
this.key = key;
this.emoji = emoji;
}
@Override
public @NonNull String getKey() {
return key;
}
public @NonNull Emoji getEmoji() {
return emoji;
}
@Override
public boolean areItemsTheSame(@NonNull EmojiTextModel newItem) {
return newItem.emoji.getValue().equals(emoji.getValue());
}
@Override
public boolean areContentsTheSame(@NonNull EmojiTextModel newItem) {
return areItemsTheSame(newItem);
}
}
static class EmojiTextViewHolder extends MappingViewHolder<EmojiTextModel> {
private final EmojiEventListener emojiEventListener;
private final AsciiEmojiView textView;
public EmojiTextViewHolder(@NonNull View itemView,
@NonNull EmojiEventListener emojiEventListener)
{
super(itemView);
this.emojiEventListener = emojiEventListener;
this.textView = itemView.findViewById(R.id.emoji_text);
}
@Override
public void bind(@NonNull EmojiTextModel model) {
textView.setEmoji(model.emoji.getValue());
itemView.setOnClickListener(v -> {
emojiEventListener.onEmojiSelected(model.emoji.getValue());
});
}
}
public static class EmojiNoResultsModel implements MappingModel<EmojiNoResultsModel> {
@Override
public boolean areItemsTheSame(@NonNull EmojiNoResultsModel newItem) {
return true;
}
@Override
public boolean areContentsTheSame(@NonNull EmojiNoResultsModel newItem) {
return true;
}
}
public interface HasKey {
@NonNull String getKey();
}
public interface VariationSelectorListener {

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