Compare commits

..

118 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] db98b41aa7 Use safe fragment handling for AllMediaActivity page changes
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/27346299-b840-4970-893f-f438f9b94981

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-11 20:49:33 +00:00
copilot-swe-agent[bot] 877545518b Lazy-create pager fragments in AllMediaActivity without tag lookup
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/27346299-b840-4970-893f-f438f9b94981

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-11 20:47:27 +00:00
copilot-swe-agent[bot] 0cff1066e1 Remove internal fragment tag lookup in AllMediaActivity pager
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/27346299-b840-4970-893f-f438f9b94981

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-11 20:45:32 +00:00
copilot-swe-agent[bot] 88f528e8b8 QrActivity: replace fragile fragment tag lookups with direct references
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/2ccd1bbb-e291-4d2d-b541-964679f4188e

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-11 17:20:41 +00:00
copilot-swe-agent[bot] ee3dcd38a6 Fix review issues: NO_POSITION and setCurrentItem animate param
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/de374ae1-3e38-498d-8896-624a25191eb3

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-11 14:16:37 +00:00
copilot-swe-agent[bot] 1af9e87af0 Migrate from ViewPager to ViewPager2
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/de374ae1-3e38-498d-8896-624a25191eb3

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-11 14:14:28 +00:00
adbenitez dcaa19b29d Merge remote-tracking branch 'upstream/main' 2026-05-05 21:01:48 +02:00
adb 11845da65f Merge pull request #4407 from deltachat/adb/issue-4406
add "mark all as read" to account menu
2026-05-05 18:03:46 +02:00
adbenitez 82262cb166 update changelog 2026-05-05 15:41:55 +02:00
adbenitez e85ae5247a add "mark all as read" to account menu 2026-05-05 15:40:06 +02:00
adb 40cdeee47a Merge pull request #4404 from deltachat/adb/issue-4396
don't set draft if user can't send
2026-05-05 15:09:37 +02:00
adb 5040fb4634 Merge branch 'main' into adb/issue-4396 2026-05-04 22:04:51 +02:00
adb 4313e8824b Merge pull request #4395 from deltachat/adb/issue-4200
allow to open message links via actions in TalkBack menu
2026-05-04 20:42:37 +02:00
B. Petersen 102c3fd82a make strings as deprecated 2026-05-04 20:14:19 +02:00
adbenitez dc31eee4bc Merge branch 'adb/issue-4200' of https://github.com/deltachat/deltachat-android into adb/issue-4200 2026-05-04 20:14:06 +02:00
adbenitez 875f0e5c86 remove unnecessary check 2026-05-04 20:13:37 +02:00
wchen342 2b36e2dc26 Hide webxdc attach button for channels (#4402) 2026-05-04 20:02:37 +02:00
adbenitez 6a9c3ab8ca update changelog 2026-05-04 19:57:10 +02:00
adbenitez 2ab544b18e don't set draft if user can't send 2026-05-04 19:55:13 +02:00
adbenitez 2630e22421 Merge remote-tracking branch 'upstream/main' 2026-05-04 19:20:04 +02:00
wchen342 1b7e32ab44 Get channels out of experimental (#4403)
* Get channels out of experimental

* Update CHANGELOG.md

Co-authored-by: adb <adb@merlinux.eu>
2026-05-04 18:28:51 +02:00
adb 8436909977 Merge branch 'main' into adb/issue-4200 2026-05-01 15:51:40 +02:00
adb 9dd44c24b3 Merge pull request #4394 from deltachat/adb/open-map-on-loc-streaming-enabled
open map if user clicks the "Location streaming enabled by Foo/you" message
2026-05-01 15:47:14 +02:00
adbenitez 5d0e85b30d update changelog 2026-05-01 15:41:06 +02:00
adbenitez 5193dfa412 allow to open message links via actions in TalkBack menu 2026-05-01 15:38:06 +02:00
adbenitez 96490e23f8 update changelog 2026-04-30 17:59:31 +02:00
adbenitez 7959b07d45 open map if user clicks the "Location streaming enabled by Foo/you" message 2026-04-30 17:57:35 +02:00
link2xt c4ae7927ad Setup zizmor workflow to check GitHub Actions workflows 2026-04-29 17:13:09 +00:00
wchen342 ba0515ebc5 Allow voice messages to be played continuously in a chat (#4376)
* Allow voice messages to be played continuously in a chat

---------

Co-authored-by: adb <adb@merlinux.eu>
2026-04-29 17:24:10 +02:00
adb 349bab9510 Merge pull request #4392 from deltachat/adb/issue-4391
change sticker sort order
2026-04-29 17:05:26 +02:00
adbenitez e26f803d1f update changelog 2026-04-29 17:01:36 +02:00
adbenitez 60ab7f2826 Merge remote-tracking branch 'upstream/main' 2026-04-29 16:48:31 +02:00
adbenitez 67d2c89bcc change sticker sort order 2026-04-29 15:58:11 +02:00
adb e511237e5e Merge pull request #4384 from deltachat/adb/issue-4345
add highlight to input panel buttons for keyboard navigation
2026-04-28 17:17:27 +02:00
adb f249fd6174 Merge pull request #4386 from deltachat/adb/issue-4342
add mapping.txt to symbols zip
2026-04-28 17:16:29 +02:00
B. Petersen 6db45c1911 add translator hints for new call strings 2026-04-28 17:01:47 +02:00
adb 06a1114744 Merge pull request #4388 from deltachat/adb/issue-4328
fix migration while upgrading from 1.10.1 and similar old versions
2026-04-28 16:54:19 +02:00
adb 3afff5625b Merge branch 'main' into adb/issue-4328 2026-04-28 16:40:53 +02:00
adbenitez cef8ddeedb update changelog 2026-04-28 16:32:33 +02:00
adbenitez 2f0ac973c0 fix migration from 1.10.1 and similar old versions 2026-04-28 16:28:25 +02:00
adbenitez 9465776715 add mapping.txt to symbols zip 2026-04-28 16:04:07 +02:00
B. Petersen 39685a575d feat: better wording for "Media Quality" option
this changes "Worse quality, small size" to "Worse quality, save data".

this makes it more clearer, what the advantage of that option is.

also, the recently added hint about sending as original also speaks about "data"
(https://github.com/deltachat/deltachat-android/pull/4259)
which is more known to the average user, knowing that of "mobile data" etc.
2026-04-28 15:51:37 +02:00
B. Petersen 8601736387 update translations 2026-04-28 15:49:37 +02:00
adbenitez 98d8572baf add highlight to input panel buttons for keyboard navigation 2026-04-28 15:16:36 +02:00
wchen342 917e764b6f Promote call FGS asap (#4377)
* Promote call FGS as soon as possible

* Add resource strings for call notification

* Launch with only phone call type on 14+
2026-04-27 20:34:51 +02:00
adb 52263150c8 Merge pull request #4374 from deltachat/adb/location-streaming-time-tweaks
allow to share location for 24 hours, remove 5 minutes
2026-04-27 17:27:12 +02:00
B. Petersen c2aba94b34 mark deprecated strings as such 2026-04-25 02:07:46 +02:00
adbenitez 1278dfd395 Merge remote-tracking branch 'upstream/main' 2026-04-24 19:55:16 +02:00
wchen342 44a688470f Merge pull request #4370 from deltachat/wch423/change-tag-names
Remove usages of `class.getSimpleName()`
2026-04-24 12:18:37 -04:00
wch423 e9a09d9c72 Correct tag name 2026-04-24 15:59:37 +02:00
wchen342 abc49a3b95 Update src/main/java/com/b44t/messenger/DcMsg.java
Co-authored-by: adb <adb@merlinux.eu>
2026-04-23 17:55:20 +02:00
wch423 cdd4e5eccd Code formatting 2026-04-23 17:55:18 +02:00
wch423 fa8ed2b881 Remove usages of class.getSimpleName() 2026-04-23 17:55:16 +02:00
B. Petersen 0731fe9447 fix warning string, make reuse of new explicit button strings 2026-04-23 15:57:42 +02:00
adbenitez d3567d9f0c fix arrays.xml 2026-04-23 14:51:48 +02:00
adbenitez 66ab1f8051 update changelog 2026-04-23 14:51:04 +02:00
adbenitez 872fd17f5e allow to share location for 24 hours, remove 5 minutes 2026-04-23 14:49:18 +02:00
B. Petersen af49018911 add strings from desktop, add some missing strings 2026-04-23 00:43:12 +02:00
adbenitez f870e9c8fd Merge remote-tracking branch 'upstream/main' 2026-04-22 21:48:58 +02:00
wchen342 d5982ba09f Merge pull request #4361 from deltachat/wch423/location-streaming
Add foreground service for location streaming
2026-04-18 17:30:56 -04:00
adbenitez 22d70d7bd4 Merge remote-tracking branch 'upstream/main' 2026-04-18 22:15:03 +02:00
adb 09bdc32a9c Merge pull request #4315 from deltachat/r10s/markfresh
feat: mark messages as "unread"
2026-04-18 21:09:18 +02:00
adb 7763ba17bf Merge branch 'main' into r10s/markfresh 2026-04-18 20:15:57 +02:00
adb 965204c46f Merge branch 'main' into wch423/location-streaming 2026-04-18 19:56:00 +02:00
adbenitez 815e4def6f Merge branch 'wch423/location-streaming' of https://github.com/deltachat/deltachat-android into wch423/location-streaming 2026-04-18 19:54:17 +02:00
adbenitez dc3ca8ff70 use "Location Streaming" for notification title and channel name 2026-04-18 19:48:39 +02:00
adb 3bccb3fb84 Apply suggestion from @adbenitez 2026-04-18 19:44:39 +02:00
adb 402d93b5a4 Merge pull request #4363 from deltachat/adb/allow-autoplay
allow webxdc to auto-play audio without requiring user gesture/touch
2026-04-18 17:10:48 +02:00
adbenitez 927dc46431 update changelog 2026-04-18 00:02:19 +02:00
adbenitez 87c32b1904 allow webxdc to auto-play audio without requiring user gesture/touch 2026-04-17 23:59:51 +02:00
adbenitez bb97213460 apply spotless 2026-04-17 23:52:22 +02:00
adbenitez 9ca9bf1acd remove unnecessary entry in src/main/res/xml/file_provider_paths.xml 2026-04-17 23:45:36 +02:00
wch423 885c902b14 Allow fallback to different providers 2026-04-17 21:11:56 +02:00
adbenitez dc4ee3f686 Merge remote-tracking branch 'upstream/main' 2026-04-17 20:52:30 +02:00
wch423 61bf5aaad4 Change how location sources work 2026-04-17 20:30:57 +02:00
link2xt 18595cbee1 Run nix flake update
This fixes some deprecation warnings.
I used it to build the app after update,
it still works.
2026-04-17 18:08:53 +00:00
wch423 5d76586ac1 Add foreground service for location streaming 2026-04-15 19:07:34 +02:00
adb dc78307df7 Merge pull request #4335 from deltachat/adb/re-enable-full-screen-intent-perm
get calls out of experimental
2026-04-14 18:23:39 +02:00
adbenitez 86a9cbfb45 update changelog 2026-04-14 18:19:52 +02:00
adb 623b20f713 Merge branch 'main' into adb/re-enable-full-screen-intent-perm 2026-04-14 18:12:05 +02:00
adb 5f4eae798f Merge pull request #4359 from deltachat/prep-2.49.0
prepare 2.49.0
2026-04-13 16:39:18 +02:00
adbenitez 4a8609822d prepare 2.49.0 2026-04-13 16:23:27 +02:00
adb 89ddc1e01f Merge pull request #4358 from deltachat/update-core-and-stuff-2026-04-13
Update core to 2.49.0
2026-04-13 16:20:44 +02:00
adbenitez 0ce42578fa update changelog 2026-04-13 15:44:42 +02:00
adbenitez faa7ad0a35 update translations 2026-04-13 15:44:33 +02:00
adbenitez 38e8ceb253 update RPC 2026-04-13 15:33:58 +02:00
adbenitez a32460f253 update deltachat-core-rust to 'chore(release): prepare for 2.49.0' of 'v2.49.0' 2026-04-13 15:29:06 +02:00
adbenitez 198268a4c3 Merge remote-tracking branch 'upstream/main' 2026-04-11 14:35:01 +02:00
adb 71158970ae Merge pull request #4357 from deltachat/wch423/external-cache-fix
Add fallback for getExternalCacheDir() in case primary storage is SD
2026-04-10 18:40:34 +02:00
wch423 1383b06e86 Add fallback for getExternalCacheDir() in case primary storage is SD card 2026-04-10 15:36:56 +02:00
B. Petersen 24165e311b deprecate sticker string, pointing to deeply nested profile folder 2026-04-09 17:34:14 +02:00
adb 5ec892db34 Merge pull request #4352 from deltachat/wch423/call-microphone-permission
Refine call permission checks and gates
2026-04-08 20:46:21 +02:00
wch423 caef7eda29 Add fixes for PiP caused bug and potential Android 16 problem with permission popups 2026-04-08 18:21:49 +02:00
wch423 cf53af4778 Refine permission checks and gates 2026-04-08 16:08:14 +02:00
adb 415e0c2b5f Merge branch 'main' into adb/re-enable-full-screen-intent-perm 2026-04-03 17:55:46 +02:00
adbenitez 9a22597473 delete zapstore 2026-04-03 17:18:08 +02:00
B. Petersen beb45af440 prefer 'mark noticed' over 'mark fresh' 2026-04-02 15:50:40 +02:00
adbenitez 87a21eb0f2 apply spotless 2026-04-02 15:41:41 +02:00
adbenitez dea51cd356 use RPC API instead of C API 2026-04-02 15:41:41 +02:00
B. Petersen ed540e5584 update CHANGLOG 2026-04-02 15:41:38 +02:00
B. Petersen 9f80c9f35f make spotless formatter happy 2026-04-02 15:40:32 +02:00
B. Petersen 5dec1b24cd option to mark chat fresh 2026-04-02 15:40:32 +02:00
adb acb4eb2ae1 Merge pull request #4334 from deltachat/adb/issue-4316
remove proxy switch from EditRelayActivity
2026-04-01 00:14:06 +02:00
wchen342 20c0354938 Merge pull request #4336 from deltachat/wch423/call-multidevice-incoming
Fix wrong states when an incoming call is answered on a second device
2026-03-31 17:36:30 -04:00
adbenitez eac112d602 update changelog 2026-03-31 23:09:29 +02:00
wch423 9b4f659f67 Fix crash when an incoming call is answered on a second device 2026-03-31 17:17:11 +02:00
adbenitez 0166d4e656 remove "Debug Calls" 2026-03-31 16:20:16 +02:00
adbenitez 92b3761d2d re-enable full intent permission 2026-03-31 15:59:22 +02:00
adbenitez 484cee21c6 apply Spotless 2026-03-31 15:24:03 +02:00
adbenitez 0e40318050 rmeove proxy switch from EditRelayActivity 2026-03-31 15:15:02 +02:00
wchen342 b2cc76ff2e Merge pull request #4333 from deltachat/wch423/call-mirror-self
Mirror self video during call
2026-03-30 13:24:01 -04:00
adb 96acaaf000 Merge pull request #4332 from deltachat/adb/remove-deprecated-stock-str-2026-03-30
remove deprecated stock strings 99 and 100
2026-03-30 18:44:31 +02:00
adbenitez 400e5ea671 Merge branch 'adb/remove-deprecated-stock-str-2026-03-30' of https://github.com/deltachat/deltachat-android into adb/remove-deprecated-stock-str-2026-03-30 2026-03-30 18:41:03 +02:00
adbenitez 4fac460926 remove unused strings 2026-03-30 18:40:44 +02:00
wch423 94a5631566 Fix camera iterating through multiple back cameras; Formatting code 2026-03-30 18:00:39 +02:00
wch423 ea91075107 Mirror self video during calls; Minor thread-safe fixes 2026-03-30 17:29:43 +02:00
adb d765d3ddeb Merge branch 'main' into adb/remove-deprecated-stock-str-2026-03-30 2026-03-30 16:01:06 +02:00
adbenitez 094fb1e2a4 remove deprecated stock strings 99 and 100 2026-03-30 15:47:35 +02:00
212 changed files with 4345 additions and 1432 deletions
+7 -1
View File
@@ -4,14 +4,20 @@ on:
workflows: ["Upload Preview APK"]
types: [completed]
permissions: {}
jobs:
artifacts-url-comments:
name: add artifact links to pull request
permissions:
pull-requests: write
actions: read
contents: read
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: add artifact links to pull request
uses: tonyhallett/artifacts-url-comments@v1.1.0
uses: tonyhallett/artifacts-url-comments@0965ff1a7ae03c5c1644d3c30f956effea4e05ef # v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+6 -3
View File
@@ -10,6 +10,8 @@ on:
- 'jni/dc_wrapper.c'
- 'scripts/ndk-make.sh'
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
@@ -17,14 +19,15 @@ jobs:
- uses: actions/checkout@v5
with:
submodules: recursive
persist-credentials: false
- uses: android-actions/setup-android@v3
- uses: nttld/setup-ndk@v1
- uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: jni/deltachat-core-rust
+5 -1
View File
@@ -5,6 +5,8 @@ on:
branches: [main]
pull_request:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -17,6 +19,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-java@v5
with:
java-version: 17
@@ -30,6 +34,6 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Check formatting
run: ./gradlew spotlessCheck
+7 -4
View File
@@ -6,6 +6,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
permissions: {}
jobs:
build:
name: Upload Preview APK
@@ -14,6 +16,7 @@ jobs:
- uses: actions/checkout@v5
with:
submodules: recursive
persist-credentials: false
- name: Validate Fastlane Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2
@@ -33,15 +36,15 @@ jobs:
${{ runner.os }}-gradle-
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- uses: android-actions/setup-android@v3
- uses: nttld/setup-ndk@v1
- uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: jni/deltachat-core-rust
-11
View File
@@ -72,14 +72,3 @@ jobs:
files: |
build/outputs/apk/foss/release/*.apk
build/outputs/mapping/fossRelease/mapping-*.txt
- name: Release on ZapStore
run: |
export CHECKSUM=6e2c7cf6da53c3f1a78b523a6aacd6316dce3d74ace6f859c2676729ee439990
curl -sL https://cdn.zapstore.dev/$CHECKSUM -o zapstore
if echo "$CHECKSUM zapstore" | sha256sum -c --status; then
chmod +x zapstore
SIGN_WITH=${{ secrets.NOSTR_KEY }} ./zapstore publish --indexer-mode
else
echo "ERROR: checksum doesn't match!"
fi
+26
View File
@@ -0,0 +1,26 @@
name: GitHub Actions Security Analysis with zizmor
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
permissions: {}
jobs:
zizmor:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+6
View File
@@ -0,0 +1,6 @@
rules:
unpinned-uses:
config:
policies:
actions/*: ref-pin
dependabot/*: ref-pin
+12
View File
@@ -243,3 +243,15 @@ $ANDROID_NDK_ROOT/ndk-stack --sym obj/local/armeabi-v7a --dump crash.txt > decod
`obj/local/armeabi-v7a` is the extracted path from `deltachat-gplay-release-X.X.X.apk-symbols.zip` file from https://download.delta.chat/android/symbols/
Replace `armeabi-v7a` by the correct architecture the logs come from (can be guessed by trial and error)
### Deobfuscating Java Stack Traces
Because the app uses code minification (ProGuard/R8), Java stack traces in crash reports are obfuscated.
To decode them, use `retrace` with the `mapping.txt` file that is included in the symbols zip:
```
retrace mapping.txt crash.txt > decoded-crash.txt
```
`mapping.txt` is extracted from the same `deltachat-gplay-release-X.X.X.apk-symbols.zip` file available at https://download.delta.chat/android/symbols/
+25
View File
@@ -2,7 +2,32 @@
## Unreleased
* Better incoming call system integration
* Calls are not experimental anymore and don't need to be manually enabled
* Channels are no longer experimental and are available by default
* Display a permanent notification when doing location streaming and get rid of dangerous "Access Location in Background" permission
* Autoplay all voice messages in a chat
* Allow to share location for 24 hours
* Allow mini-apps to play audio without user interaction
* Mark chats as unread (long tap a chat and select the corresponding option from the three-dot-menu)
* Add "Mark all as read" option to profile menu in the profile switcher
* Fix process of upgrading from a very old version of the app
* Show more recent added stickers at the top of the sticker picker
* Allow to open links in messages via actions in TalkBack menu
* Allow to open map if user clicks "Location streaming enabled" system message
* Fix: do not accidentally set draft in chats that don't allow sending messages
## v2.49.0
2026-04
* Fix file sharing to certain apps (e.g. Material Files, etc.)
* Fix problem with calls when microphone permission is not granted
* Fix taking pictures and videos in devices with SD cards
* Fix flipped orientation for some images
* Fix: avoid empty contact request chats when using invite links in a multi-device setup
* Remove proxy toggle from profile editing to avoid confusion
* Updated translations
* Update to core 2.49.0
## v2.48.0
2026-03
+5 -3
View File
@@ -34,8 +34,8 @@ android {
useLibrary 'org.apache.http.legacy'
defaultConfig {
versionCode 30000741
versionName "2.48.0"
versionCode 30000742
versionName "2.49.0"
applicationId "chat.delta.lite"
multiDexEnabled true
@@ -223,7 +223,7 @@ spotless {
}
format 'xml', {
target 'src/*/res/**/*.xml', 'src/*/AndroidManifest.xml'
targetExclude 'src/*/res/values*/strings.xml' // line-break changes invalidate translations
targetExclude 'src/*/res/values*/strings*.xml' // line-break changes invalidate translations
eclipseWtp('xml').configFile('spotless/eclipse-wtp-xml.prefs')
trimTrailingWhitespace()
endWithNewline()
@@ -279,6 +279,7 @@ dependencies {
implementation "androidx.media3:media3-session:$media3_version"
implementation "androidx.media3:media3-ui:$media3_version"
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'androidx.viewpager2:viewpager2:1.1.0'
implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24
implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1' // used as JSON library
@@ -307,6 +308,7 @@ dependencies {
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
gplayImplementation 'com.google.android.gms:play-services-location:21.3.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.assertj:assertj-core:3.27.3'
@@ -25,7 +25,7 @@ ArcaneChat is a Delta Chat client and was created with a focus on usability, goo
<li>Multiple color themes/skins</li>
<li>It is possible to disable profiles to completely disconnect them saving data/bandwidth</li>
<li>You can easily see the connection status of all your profiles in the profile switcher</li>
<li>Extra option to share location for 12 hours</li>
<li>Extra option to share location for 24 hours</li>
<li>Clicking on a message with a POI location, will open the POI on the map</li>
<li>Last-seen status of contacts is shown in your contact list, like in WhatsApp, Telegram, etc.</li>
<li>Videos are played in loop, useful for short GIF videos</li>
Generated
+15 -15
View File
@@ -7,11 +7,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1756239746,
"narHash": "sha256-0ibN685tT+u/Nbmbrrq9G3mRUzct2Votyv/a7Wwv26s=",
"lastModified": 1775939535,
"narHash": "sha256-Zq1U7Vhw5ctvhJQy+3ShqIv7Mplf3XkgJY+QJnhfUGQ=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "256631d162ec883b2341ee59621516e1f65f0f6b",
"rev": "d2e16192309bf3101e4998ffa62e914819dee077",
"type": "github"
},
"original": {
@@ -28,11 +28,11 @@
]
},
"locked": {
"lastModified": 1741473158,
"narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=",
"lastModified": 1768818222,
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
"owner": "numtide",
"repo": "devshell",
"rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0",
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
"type": "github"
},
"original": {
@@ -79,11 +79,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1756125398,
"narHash": "sha256-XexyKZpf46cMiO5Vbj+dWSAXOnr285GHsMch8FBoHbc=",
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3b9f00d7a7bf68acd4c4abb9d43695afb04e03a5",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"type": "github"
},
"original": {
@@ -95,11 +95,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1756159630,
"narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=",
"lastModified": 1775823930,
"narHash": "sha256-ALT447J7FcxP/97J01A/gp/hgdO5lXRsm+zLMt+gIjc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b",
"rev": "8c11f88bb9573a10a7d6bf87161ef08455ac70b9",
"type": "github"
},
"original": {
@@ -138,11 +138,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1763347184,
"narHash": "sha256-6QH8hpCYJxifvyHEYg+Da0BotUn03BwLIvYo3JAxuqQ=",
"lastModified": 1775877051,
"narHash": "sha256-wpSQm2PD/w4uRo2wb8utk0b5hOBkkg/CZ1xICY+qB7M=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "08895cce80433978d5bfd668efa41c5e24578cbd",
"rev": "08b4f3633471874c8894632ade1b78d75dbda002",
"type": "github"
},
"original": {
+1
View File
@@ -1393,6 +1393,7 @@ JNIEXPORT jint Java_com_b44t_messenger_DcMsg_getId(JNIEnv *env, jobject obj)
return dc_msg_get_id(get_dc_msg(env, obj));
}
JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getText(JNIEnv *env, jobject obj)
{
char* temp = dc_msg_get_text(get_dc_msg(env, obj));
+1
View File
@@ -34,6 +34,7 @@ cd ../..
SYMBOLS_ZIP="$APK-symbols.zip"
rm $SYMBOLS_ZIP
zip -r $SYMBOLS_ZIP obj
zip $SYMBOLS_ZIP build/outputs/mapping/gplayRelease/mapping.txt
ls -l $SYMBOLS_ZIP
rsync --progress $SYMBOLS_ZIP jekyll@download.delta.chat:/var/www/html/download/android/symbols/
@@ -39,7 +39,7 @@ public class EnterChatsBenchmark {
// PLEASE BACKUP YOUR ACCOUNT BEFORE RUNNING THIS!
// ==============================================================================================
private static final String TAG = EnterChatsBenchmark.class.getSimpleName();
private static final String TAG = "EnterChatsBenchmark";
@Rule
public ActivityScenarioRule<ConversationListActivity> activityRule =
-1
View File
@@ -5,7 +5,6 @@
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<application>
<service
@@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.geolocation;
import android.content.Context;
import android.util.Log;
/** Non-GMS, always uses the platform LocationManager. */
public final class LocationSourceFactory {
private static final String TAG = "LocationSourceFactory";
private LocationSourceFactory() {}
public static LocationSource create(Context context) {
Log.i(TAG, "Non-GMS build, Using platform LocationManager");
return new PlatformLocationSource();
}
}
@@ -0,0 +1,61 @@
package org.thoughtcrime.securesms.geolocation;
import android.content.Context;
import android.location.Location;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.Priority;
public class GmsLocationSource implements LocationSource {
private static final String TAG = "GmsLocationSource";
private static final long UPDATE_INTERVAL_MS = 3_000;
private static final long FASTEST_INTERVAL_MS = 1_000;
private FusedLocationProviderClient client;
private LocationCallback locationCallback;
@Override
public void startUpdates(@NonNull Context context, @NonNull Callback callback) {
client = LocationServices.getFusedLocationProviderClient(context);
LocationRequest request =
new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, UPDATE_INTERVAL_MS)
.setMinUpdateIntervalMillis(FASTEST_INTERVAL_MS)
.setMinUpdateDistanceMeters(0)
.setWaitForAccurateLocation(false)
.build();
locationCallback =
new LocationCallback() {
@Override
public void onLocationResult(@NonNull LocationResult result) {
Location loc = result.getLastLocation();
if (loc != null) {
callback.onLocationUpdate(loc);
}
}
};
try {
client.requestLocationUpdates(request, locationCallback, Looper.getMainLooper());
} catch (SecurityException e) {
Log.e(TAG, "Missing location permission", e);
}
}
@Override
public void stopUpdates() {
if (client != null && locationCallback != null) {
client.removeLocationUpdates(locationCallback);
client = null;
locationCallback = null;
}
}
}
@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.geolocation;
import android.content.Context;
import android.util.Log;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
/**
* Prefers FusedLocationProviderClient, falls back to platform LocationManager if Play Services are
* somehow unavailable.
*/
public final class LocationSourceFactory {
private static final String TAG = "LocationSourceFactory";
private LocationSourceFactory() {}
public static LocationSource create(Context context) {
if (isGmsAvailable(context)) {
Log.i(TAG, "Using FusedLocationProviderClient");
return new GmsLocationSource();
}
Log.i(TAG, "GMS unavailable, falling back to LocationManager");
return new PlatformLocationSource();
}
private static boolean isGmsAvailable(Context context) {
try {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
== ConnectionResult.SUCCESS;
} catch (Exception e) {
return false;
}
}
}
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.service.FetchForegroundService;
import org.thoughtcrime.securesms.util.Util;
public class FcmReceiveService extends FirebaseMessagingService {
private static final String TAG = FcmReceiveService.class.getSimpleName();
private static final String TAG = "FcmReceiveService";
private static final Object INIT_LOCK = new Object();
private static boolean initialized;
private static volatile boolean triedRegistering;
+9 -6
View File
@@ -49,6 +49,7 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- normal/instant permissions - adding them here is enough -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
@@ -70,6 +71,8 @@
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<!-- force compiling libs on older sdk than supported; runtime checks are required -->
@@ -80,7 +83,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
tools:replace="android:allowBackup"
android:allowBackup="false"
android:theme="@style/TextSecure.LightTheme"
android:largeHeap="true"
@@ -463,10 +465,6 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
</activity>
<service
android:name=".geolocation.LocationBackgroundService"
android:foregroundServiceType="location" />
<service
android:name=".service.GenericForegroundService"
android:foregroundServiceType="dataSync" />
@@ -475,6 +473,11 @@
android:name=".service.FetchForegroundService"
android:foregroundServiceType="dataSync" />
<service
android:name=".geolocation.LocationStreamingService"
android:foregroundServiceType="location"
android:exported="false" />
<service
android:name=".service.AudioPlaybackService"
android:foregroundServiceType="mediaPlayback"
@@ -488,7 +491,7 @@
android:name=".calls.CallService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="camera|microphone" />
android:foregroundServiceType="camera|microphone|phoneCall" />
<receiver
android:name=".notifications.MarkReadReceiver"
+1 -1
View File
@@ -1578,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
<li>
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
Delta Chats network connections and a reproducible mail server setup as
<a href="https://delta.chat/cs/serverguide">recommended on this site</a>.
<a href="https://delta.chat/serverguide">recommended on this site</a>.
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
</li>
+1 -1
View File
@@ -1468,7 +1468,7 @@ Weitere Informationen findest du in unserem Blogbeitrag über <a href="https://d
<p>Im April 2023 haben wir Sicherheits- und Datenschutzprobleme mit dem “In Chats geteilten Apps”-Feature behoben, die mit Fehlern beim Sandboxing, insbesondere mit Chromium zusammenhängen. Wir haben daraufhin eine unabhängige Sicherheitsprüfung von Cure53 durchführen lassen, und alle gefundenen Probleme wurden mit den im April 2023 veröffentlichten 1.36 Releases behoben. Siehe <a href="https://delta.chat/en/2023-05-22-webxdc-security">hier für die vollständige Hintergrundgeschichte</a>.</p>
</li>
<li>
<p>Im März 2023 analysierte <a href="https://cure53.de">Cure53</a> sowohl die Transportverschlüsselung von Delta Chats Netzwerkverbindungen als auch das reproduzierbare Mailserver-Setup wie <a href="https://delta.chat/de/serverguide">auf dieser Seite empfohlen</a>. Du kannst mehr über das Audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">in unserem Blog</a> lesen oder du liest den <a href="https://delta.chat/assets/blog/MER-01-report.pdf">vollständigen Bericht hier</a>.</p>
<p>Im März 2023 analysierte <a href="https://cure53.de">Cure53</a> sowohl die Transportverschlüsselung von Delta Chats Netzwerkverbindungen als auch das reproduzierbare Mailserver-Setup wie <a href="https://delta.chat/serverguide">auf dieser Seite empfohlen</a>. Du kannst mehr über das Audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">in unserem Blog</a> lesen oder du liest den <a href="https://delta.chat/assets/blog/MER-01-report.pdf">vollständigen Bericht hier</a>.</p>
</li>
<li>
<p>Im Jahr 2020 analysierte <a href="https://includesecurity.com">Include Security</a> Delta Chats Rust <a href="https://github.com/deltachat/deltachat-core-rust/">core</a>, <a href="https://github.com/async-email/async-imap">IMAP</a>,<a href="https://github.com/async-email/async-smtp">SMTP</a>, und <a href="https://github.com/async-email/async-native-tls">TLS</a> Bibliotheken.
+1 -1
View File
@@ -1578,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
<li>
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
Delta Chats network connections and a reproducible mail server setup as
<a href="https://delta.chat/en/serverguide">recommended on this site</a>.
<a href="https://delta.chat/serverguide">recommended on this site</a>.
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
</li>
+2 -3
View File
@@ -105,8 +105,7 @@
</h2>
<p>Delta Chat is a reliable, decentralized and secure instant messaging app,
available for mobile and desktop platforms.</p>
<p>Delta Chat es una aplicación de mensajería instantánea confiable, descentralizada y segura, disponible para dispositivos móviles y computadoras de escritorio.</p>
<ul>
<li>
@@ -1567,7 +1566,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
<li>
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
Delta Chats network connections and a reproducible mail server setup as
<a href="https://delta.chat/es/serverguide">recommended on this site</a>.
<a href="https://delta.chat/serverguide">recommended on this site</a>.
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
</li>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1483,7 +1483,7 @@ research paper published afterwards.</p>
Vous trouverez <a href="https://delta.chat/en/2023-05-22-webxdc-security">ici un article de fond complet à propos de la sécurité du chiffrement de bout-en-bout sur internet</a>.</p>
</li>
<li>
<p>Début 2023, <a href="https://cure53.de">Cure53</a> a analysé le chiffrement dacheminement des connexions réseau de Delta Chat et testé une configuration de serveur de courriel reproductible, telle que <a href="https://delta.chat/fr/serverguide">recommandée sur ce site</a>.
<p>Début 2023, <a href="https://cure53.de">Cure53</a> a analysé le chiffrement dacheminement des connexions réseau de Delta Chat et testé une configuration de serveur de courriel reproductible, telle que <a href="https://delta.chat/serverguide">recommandée sur ce site</a>.
Vous trouverez plus dinformations sur cet audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">sur notre blog</a> ou dans <a href="https://delta.chat/assets/blog/MER-01-report.pdf">le rapport complet ici</a>.</p>
</li>
<li>
+1 -1
View File
@@ -1578,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
<li>
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
Delta Chats network connections and a reproducible mail server setup as
<a href="https://delta.chat/id/serverguide">recommended on this site</a>.
<a href="https://delta.chat/serverguide">recommended on this site</a>.
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
</li>
+1 -1
View File
@@ -1566,7 +1566,7 @@ Vedi <a href="https://delta.chat/en/2023-05-22-webxdc-security">qui per la stori
<li>
<p>A partire dal 2023, <a href="https://cure53.de">Cure53</a> ha analizzato sia la crittografia del trasporto delle
Connessioni di rete di Delta Chat e una configurazione del server di posta riproducibile come
<a href="https://delta.chat/it/serverguide">consigliato su questo sito</a>.
<a href="https://delta.chat/serverguide">consigliato su questo sito</a>.
Puoi leggere ulteriori informazioni sullaudit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">sul nostro blog</a>
o leggere il <a href="https://delta.chat/assets/blog/MER-01-report.pdf">rapporto completo qui</a>.</p>
</li>
+1 -1
View File
@@ -1572,7 +1572,7 @@ Lees <a href="https://delta.chat/en/2023-05-22-webxdc-security">hier het volledi
<li>
<p>Aan het begin van 2023 heeft <a href="https://cure53.de">Cure53</a> de transportversleuteling van
Delta Chats netwerkverbindingen getest, evenals de e-mailserveropzet zoals
<a href="https://delta.chat/nl/serverguide">beschreven op onze site</a>.
<a href="https://delta.chat/serverguide">beschreven op onze site</a>.
Meer informatie over deze test is te lezen <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">op ons blog</a>
of in het <a href="https://delta.chat/assets/blog/MER-01-report.pdf">volledige verslag</a>.</p>
</li>
+1 -1
View File
@@ -1413,7 +1413,7 @@ od najnowszych do najstarszych:</p>
<p>W kwietniu 2023 r. naprawiliśmy problemy z bezpieczeństwem i prywatnością w funkcji „aplikacje internetowe udostępniane na czacie”, związane z awariami piaskownicy, szczególnie w przypadku Chromium. Następnie przeprowadziliśmy niezależny audyt bezpieczeństwa od Cure53 i wszystkie wykryte problemy zostały naprawione w aplikacji z serii 1.36 wydanej w kwietniu 2023 r. <a href="https://delta.chat/en/2023-05-22-webxdc-security">Pełną historię bezpieczeństwa end-to-end w sieci można znaleźć tutaj</a>.</p>
</li>
<li>
<p>W marcu 2023 r. firma <a href="https://cure53.de">Cure53</a> przeanalizowała zarówno szyfrowanie transportu połączeń sieciowych Delta Chat, jak i powtarzalną konfigurację serwera pocztowego zgodnie z <a href="https://delta.chat/pl/serverguide">zaleceniami na tej stronie</a>. Możesz przeczytać więcej o audycie <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">na naszym blogu</a> lub przeczytać pełny raport <a href="https://delta.chat/assets/blog/MER-01-report.pdf">tutaj</a>.</p>
<p>W marcu 2023 r. firma <a href="https://cure53.de">Cure53</a> przeanalizowała zarówno szyfrowanie transportu połączeń sieciowych Delta Chat, jak i powtarzalną konfigurację serwera pocztowego zgodnie z <a href="https://delta.chat/serverguide">zaleceniami na tej stronie</a>. Możesz przeczytać więcej o audycie <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">na naszym blogu</a> lub przeczytać pełny raport <a href="https://delta.chat/assets/blog/MER-01-report.pdf">tutaj</a>.</p>
</li>
<li>
<p>W 2020 r. firma <a href="https://includesecurity.com">Include Security</a> przeanalizowała biblioteki Rust <a href="https://github.com/deltachat/deltachat-core-rust/">core</a>, <a href="https://github.com/async-email/async-imap">IMAP</a>, <a href="https://github.com/async-email/async-smtp">SMTP</a> i <a href="https://github.com/async-email/async-native-tls">TLS</a> Delta Chat. Nie znalazła żadnych problemów krytycznych ani poważnych. W raporcie zwrócono uwagę na kilka słabych punktów o średniej wadze same w sobie nie stanowią zagrożenia dla użytkowników Delta Chat, ponieważ zależą od środowiska, w którym używany jest Delta Chat. Ze względu na użyteczność i kompatybilność nie możemy złagodzić wszystkich z nich i zdecydowaliśmy się przedstawić zalecenia dotyczące bezpieczeństwa zagrożonym użytkownikom. Pełny raport można przeczytać <a href="https://delta.chat/assets/blog/2020-second-security-review.pdf">tutaj</a>.</p>
+1 -1
View File
@@ -1573,7 +1573,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
<li>
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
Delta Chats network connections and a reproducible mail server setup as
<a href="https://delta.chat/pt/serverguide">recommended on this site</a>.
<a href="https://delta.chat/serverguide">recommended on this site</a>.
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
</li>
+4 -5
View File
@@ -1571,11 +1571,10 @@ Applied Cryptography в ETH Цюрихе и устранили все выявл
См. здесь <a href="https://delta.chat/en/2023-05-22-webxdc-security">полную информацию о безопасности сквозного шифрования в Интернете</a>.</p>
</li>
<li>
<p>В Марте 2023 года, <a href="https://cure53.de">Cure53</a> проанализировал как протокол защиты транспортного уровня
сетевого соединения Delta Chat, так и воспроизводимую установку почтового сервера,
<a href="https://delta.chat/ru/serverguide">рекомендуемую на этом сайте</a>.
Подробнее об аудите можно узнать <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">в нашем блоге</a>
или прочитать <a href="https://delta.chat/assets/blog/MER-01-report.pdf">полный отчёт здесь</a>.</p>
<p>В марте 2023 года компания <a href="https://cure53.de">Cure53</a> провела анализ шифрования транспортного уровня сетевых соединений Delta Chat, а также проверку воспроизводимой конфигурации почтового сервера,
<a href="https://delta.chat/serverguide">рекомендованной на этом сайте</a>.
Подробнее об аудите можно узнать <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">в нашем блоге</a>,
а <a href="https://delta.chat/assets/blog/MER-01-report.pdf">полный отчет доступен по этой ссылке</a>.</p>
</li>
<li>
<p>2020 год, <a href="https://includesecurity.com">Include Security</a> проанализировала Delta
+1 -1
View File
@@ -1578,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
<li>
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
Delta Chats network connections and a reproducible mail server setup as
<a href="https://delta.chat/sk/serverguide">recommended on this site</a>.
<a href="https://delta.chat/serverguide">recommended on this site</a>.
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
</li>
+1 -1
View File
@@ -1581,7 +1581,7 @@ Shihni <a href="https://delta.chat/en/2023-05-22-webxdc-security">këtu, për sh
<li>
<p>Në fillim të 2023-it, <a href="https://cure53.de">Cure53</a> analizoi qoftë fshehtëzimin
e transporteve për lidhje rrjeti të Delta Chat-it, qoftë një formësim të riprodhueshëm
shërbyesi poste si <a href="https://delta.chat/sq/serverguide">të rekomanduarin në këtë sajt</a>.
shërbyesi poste si <a href="https://delta.chat/serverguide">të rekomanduarin në këtë sajt</a>.
Mund të lexoni më tepër rreth auditimit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">në blogun tonë</a>,
ose të lexoni <a href="https://delta.chat/assets/blog/MER-01-report.pdf">raportin e plotë këtu</a>.</p>
</li>
+1 -1
View File
@@ -1391,7 +1391,7 @@ you need to look it up in the “keypairs” SQLite table of a profile backup ta
<p>Починаючи з 2023 року, ми виправили проблеми з безпекою та конфіденційністю у функції “веб застосунків, що поширені у чаті”, пов’язані зі збоями в роботі пісочниці особливо в Chromium. Згодом ми отримали незалежний аудит безпеки аудит безпеки від Cure53, і всі знайдені проблеми були виправлені в серії додатків 1.36, випущених у квітні 2023 року. Повну історію про наскрізну безпеку в Інтернеті дивіться <a href="https://delta.chat/en/2023-05-22-webxdc-security">тут</a>.</p>
</li>
<li>
<p>Починаючи з 2023 року <a href="https://cure53.de">Cure53</a> проаналізував транспортне шифрування мережевих з’єднань Delta Chat і відтворюване налаштування поштового сервера як <a href="https://delta.chat/uk/serverguide">рекомендовано на цьому сайті</a>. Ви можете прочитати більше про аудит <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">у нашому блозі</a> або прочитайте <a href="https://delta.chat/assets/blog/MER-01-report.pdf">повний звіт тут</a>.</p>
<p>Починаючи з 2023 року <a href="https://cure53.de">Cure53</a> проаналізував транспортне шифрування мережевих з’єднань Delta Chat і відтворюване налаштування поштового сервера як <a href="https://delta.chat/serverguide">рекомендовано на цьому сайті</a>. Ви можете прочитати більше про аудит <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">у нашому блозі</a> або прочитайте <a href="https://delta.chat/assets/blog/MER-01-report.pdf">повний звіт тут</a>.</p>
</li>
<li>
<p>У 2020 році <a href="https://includesecurity.com">Include Security</a> проаналізувала Rust-<a href="https://github.com/deltachat/deltachat-core-rust/">ядро</a> Delta Chat і бібліотеки <a href="https://github.com/async-email/async-imap">IMAP</a>, <a href="https://github.com/async-email/async-smtp">SMTP</a> та <a href="https://github.com/async-email/async-native-tls">TLS</a>. Він не виявив критичних або серйозних проблем. У звіті виявлено кілька слабких місць середнього ступеня тяжкості – вони самі по собі не становлять загрози для користувачів Delta Chat оскільки вони залежать від середовища, у якому використовується Delta Chat. З міркувань зручності використання та сумісності ми не можемо пом’якшити їх усі тому вирішили надати рекомендації щодо безпеки користувачам, яким загрожує небезпека. Ви можете прочитати <a href="https://delta.chat/assets/blog/2020-second-security-review.pdf">повний звіт тут</a>.</p>
+1 -1
View File
@@ -1557,7 +1557,7 @@ Delta Chat 长期接受第三方独立机构的安全审计与漏洞分析,
</li>
<li>
<p>2023 年 3 月,<a href="https://cure53.de">Cure53</a> 分析了 Delta Chat 网络连接的传输加密和一个可重现的邮件服务器设置,如
<a href="https://delta.chat/zh_CN/serverguide">本网站</a> 推荐的那样。
<a href="https://delta.chat/serverguide">本网站</a> 推荐的那样。
你可以在 <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">我们的博客</a> 上阅读有关审计的更多信息
,或在此处阅读 <a href="https://delta.chat/assets/blog/MER-01-report.pdf">完整报告</a></p>
</li>
+16 -2
View File
@@ -396,7 +396,7 @@ public class Rpc {
}
/**
* Gets messages to be processed by the bot and returns their IDs.
* (deprecated) Gets messages to be processed by the bot and returns their IDs.
* <p>
* Only messages with database ID higher than `last_msg_id` config value
* are returned. After processing the messages, the bot should
@@ -404,6 +404,13 @@ public class Rpc {
* or manually updating the value to avoid getting already
* processed messages.
* <p>
* Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
* even if it is not fully downloaded yet.
* The bot needs to wait for the message to be fully downloaded.
* Since this is usually not the desired behavior,
* bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
* event for getting notified about new messages.
* <p>
* [`markseen_msgs`]: Self::markseen_msgs
*/
public java.util.List<Integer> getNextMsgs(Integer accountId) throws RpcException {
@@ -411,7 +418,7 @@ public class Rpc {
}
/**
* Waits for messages to be processed by the bot and returns their IDs.
* (deprecated) Waits for messages to be processed by the bot and returns their IDs.
* <p>
* This function is similar to [`get_next_msgs`],
* but waits for internal new message notification before returning.
@@ -422,6 +429,13 @@ public class Rpc {
* To shutdown the bot, stopping I/O can be used to interrupt
* pending or next `wait_next_msgs` call.
* <p>
* Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
* even if it is not fully downloaded yet.
* The bot needs to wait for the message to be fully downloaded.
* Since this is usually not the desired behavior,
* bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
* event for getting notified about new messages.
* <p>
* [`get_next_msgs`]: Self::get_next_msgs
*/
public java.util.List<Integer> waitNextMsgs(Integer accountId) throws RpcException {
@@ -59,8 +59,6 @@ public class DcMsg {
public static final int DC_VIDEOCHATTYPE_UNKNOWN = 0;
public static final int DC_VIDEOCHATTYPE_BASICWEBRTC = 1;
private static final String TAG = DcMsg.class.getSimpleName();
public DcMsg(DcContext context, int viewtype) {
msgCPtr = context.createMsgCPtr(viewtype);
}
@@ -4,7 +4,6 @@ import android.content.ComponentName;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@@ -12,21 +11,23 @@ import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.ChatAudioQueueProvider;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
@@ -35,7 +36,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class AllMediaActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate {
private static final String TAG = AllMediaActivity.class.getSimpleName();
private static final String TAG = "AllMediaActivity";
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
@@ -63,7 +64,7 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
private final ArrayList<TabData> tabs = new ArrayList<>();
private Toolbar toolbar;
private TabLayout tabLayout;
private ViewPager viewPager;
private ViewPager2 viewPager;
private @Nullable MediaController mediaController;
private ListenableFuture<MediaController> mediaControllerFuture;
@@ -97,8 +98,20 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
isGlobalGallery() ? R.string.menu_all_media : R.string.apps_and_media);
}
this.tabLayout.setupWithViewPager(viewPager);
this.viewPager.setAdapter(new AllMediaPagerAdapter(getSupportFragmentManager()));
AllMediaPagerAdapter adapter = new AllMediaPagerAdapter(this);
this.viewPager.setAdapter(adapter);
this.viewPager.registerOnPageChangeCallback(
new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
adapter.onPageChanged(position);
}
});
new TabLayoutMediator(
this.tabLayout,
this.viewPager,
(tab, position) -> tab.setText(getString(tabs.get(position).title)))
.attach();
if (getIntent().getBooleanExtra(FORCE_GALLERY, false)) {
this.viewPager.setCurrentItem(1, false);
}
@@ -107,7 +120,9 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
int accountId = DcHelper.getAccounts(this).getSelectedAccount().getAccountId();
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
playbackViewModel.setQueueProvider(new ChatAudioQueueProvider(this, chatId, accountId));
initializeMediaController();
}
@@ -119,6 +134,7 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
mediaController = null;
playbackViewModel.setMediaController(null);
}
playbackViewModel.setQueueProvider(null);
super.onDestroy();
}
@@ -182,31 +198,16 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
return contactId == 0 && chatId == 0;
}
private class AllMediaPagerAdapter extends FragmentStatePagerAdapter {
private Object currentFragment = null;
private class AllMediaPagerAdapter extends FragmentStateAdapter {
private int currentPosition = -1;
AllMediaPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
super.setPrimaryItem(container, position, object);
if (currentFragment != null && currentFragment != object) {
ActionMode action = null;
if (currentFragment instanceof MessageSelectorFragment) {
action = ((MessageSelectorFragment) currentFragment).getActionMode();
}
if (action != null) {
action.finish();
}
}
currentFragment = object;
AllMediaPagerAdapter(FragmentActivity activity) {
super(activity);
}
@NonNull
@Override
public Fragment getItem(int position) {
public Fragment createFragment(int position) {
TabData data = tabs.get(position);
Fragment fragment;
Bundle args = new Bundle();
@@ -229,13 +230,24 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
}
@Override
public int getCount() {
public int getItemCount() {
return tabs.size();
}
@Override
public CharSequence getPageTitle(int position) {
return getString(tabs.get(position).title);
void onPageChanged(int newPosition) {
if (currentPosition != -1 && currentPosition != newPosition) {
for (Fragment fragment : getSupportFragmentManager().getFragments()) {
if (!(fragment instanceof MessageSelectorFragment)) {
continue;
}
ActionMode action = ((MessageSelectorFragment) fragment).getActionMode();
if (action != null) {
action.finish();
}
}
}
currentPosition = newPosition;
}
}
@@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.connect.KeepAliveService;
import org.thoughtcrime.securesms.connect.NetworkStateReceiver;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
import org.thoughtcrime.securesms.notifications.InChatSounds;
@@ -53,7 +52,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webxdc.WebxdcGarbageCollectionWorker;
public class ApplicationContext extends MultiDexApplication {
private static final String TAG = ApplicationContext.class.getSimpleName();
private static final String TAG = "ApplicationContext";
private static final Object initLock = new Object();
private static volatile boolean isInitialized = false;
@@ -61,7 +60,6 @@ public class ApplicationContext extends MultiDexApplication {
private Rpc rpc;
private DcContext dcContext;
private DcLocationManager dcLocationManager;
private DcEventCenter eventCenter;
private NotificationCenter notificationCenter;
private JobManager jobManager;
@@ -126,15 +124,6 @@ public class ApplicationContext extends MultiDexApplication {
}
}
/**
* Get DcLocationManager instance, waiting for initialization if necessary. This method is
* thread-safe and will block until initialization is complete.
*/
public DcLocationManager getLocationManager() {
ensureInitialized();
return dcLocationManager;
}
/**
* Get DcEventCenter instance, waiting for initialization if necessary. This method is thread-safe
* and will block until initialization is complete.
@@ -229,7 +218,7 @@ public class ApplicationContext extends MultiDexApplication {
Log.i(TAG, "DcAccounts created");
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
Log.i(TAG, "Rpc created");
AccountManager.getInstance().migrateToDcAccounts(this);
AccountManager.getInstance().migrateToDcAccounts(this, dcAccounts);
int[] allAccounts = dcAccounts.getAll();
Log.i(TAG, "Number of profiles: " + allAccounts.length);
@@ -282,7 +271,6 @@ public class ApplicationContext extends MultiDexApplication {
}
dcContext = dcAccounts.getSelectedAccount();
notificationCenter = new NotificationCenter(this);
dcLocationManager = new DcLocationManager(this, dcContext);
isInitialized = true;
initLock.notifyAll();
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public abstract class BaseActionBarActivity extends AppCompatActivity {
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
private static final String TAG = "BaseActionBarActivity";
protected DynamicTheme dynamicTheme = new DynamicTheme();
protected void onPreCreate() {
@@ -13,6 +13,7 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -26,6 +27,8 @@ import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.fragment.app.Fragment;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
@@ -44,6 +47,7 @@ import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.thoughtcrime.securesms.util.views.ProgressDialog;
public abstract class BaseConversationListFragment extends Fragment implements ActionMode.Callback {
private static final String TAG = "BaseConversationListFragment";
protected ActionMode actionMode;
protected PulsingFloatingActionButton fab;
@@ -170,6 +174,17 @@ public abstract class BaseConversationListFragment extends Fragment implements A
return false;
}
private boolean areSomeSelectedChatsFresh() {
DcContext dcContext = DcHelper.getContext(requireActivity());
final Set<Long> selectedChats = getListAdapter().getBatchSelections();
for (long chatId : selectedChats) {
if (dcContext.getFreshMsgCount((int) chatId) > 0) {
return true;
}
}
return false;
}
private void handlePinAllSelected() {
final DcContext dcContext = DcHelper.getContext(requireActivity());
final Set<Long> selectedConversations =
@@ -229,6 +244,26 @@ public abstract class BaseConversationListFragment extends Fragment implements A
}
}
private void handleMarkfreshSelected() {
final Set<Long> selectedConversations =
new HashSet<Long>(getListAdapter().getBatchSelections());
final Rpc rpc = DcHelper.getRpc(requireActivity());
try {
int accId = rpc.getSelectedAccountId();
for (long chatId : selectedConversations) {
rpc.markfreshChat(accId, (int) chatId);
}
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
if (actionMode != null) {
actionMode.finish();
actionMode = null;
}
}
@SuppressLint("StaticFieldLeak")
private void handleArchiveAllSelected() {
final DcContext dcContext = DcHelper.getContext(requireActivity());
@@ -409,6 +444,7 @@ public abstract class BaseConversationListFragment extends Fragment implements A
if (!isRelayingMessageContent(requireActivity())) {
final int selectedCount = getListAdapter().getBatchSelections().size();
menu.findItem(R.id.menu_add_to_home_screen).setVisible(selectedCount == 1);
MenuItem archiveItem = menu.findItem(R.id.menu_archive_selected);
if (offerToArchive()) {
archiveItem.setIcon(R.drawable.ic_archive_white_24dp);
@@ -417,6 +453,7 @@ public abstract class BaseConversationListFragment extends Fragment implements A
archiveItem.setIcon(R.drawable.ic_unarchive_white_24dp);
archiveItem.setTitle(R.string.menu_unarchive_chat);
}
MenuItem pinItem = menu.findItem(R.id.menu_pin_selected);
if (areSomeSelectedChatsUnpinned()) {
pinItem.setIcon(R.drawable.ic_pin_white);
@@ -425,12 +462,17 @@ public abstract class BaseConversationListFragment extends Fragment implements A
pinItem.setIcon(R.drawable.ic_unpin_white);
pinItem.setTitle(R.string.unpin_chat);
}
MenuItem muteItem = menu.findItem(R.id.menu_mute_selected);
if (areSomeSelectedChatsUnmuted()) {
muteItem.setTitle(R.string.menu_mute);
} else {
muteItem.setTitle(R.string.menu_unmute);
}
final boolean someAreFresh = areSomeSelectedChatsFresh();
menu.findItem(R.id.menu_marknoticed_selected).setVisible(someAreFresh);
menu.findItem(R.id.menu_markfresh_selected).setVisible(!someAreFresh);
}
}
@@ -484,6 +526,9 @@ public abstract class BaseConversationListFragment extends Fragment implements A
} else if (itemId == R.id.menu_marknoticed_selected) {
handleMarknoticedSelected();
return true;
} else if (itemId == R.id.menu_markfresh_selected) {
handleMarkfreshSelected();
return true;
} else if (itemId == R.id.menu_add_to_home_screen) {
handleAddToHomeScreen();
return true;
@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
*/
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
implements ContactSelectionListFragment.OnContactSelectedListener {
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
private static final String TAG = "ContactSelectionActivity";
protected ContactSelectionListFragment contactsFragment;
@@ -67,7 +67,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
*/
public class ContactSelectionListFragment extends Fragment
implements LoaderManager.LoaderCallbacks<DcContactsLoader.Ret>, DcEventCenter.DcEventDelegate {
private static final String TAG = ContactSelectionListFragment.class.getSimpleName();
private static final String TAG = "ContactSelectionListFragment";
public static final String MULTI_SELECT = "multi_select";
public static final String SELECT_UNENCRYPTED_EXTRA = "select_unencrypted_extra";
@@ -101,6 +101,7 @@ import org.thoughtcrime.securesms.components.ScaleStableImageView;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.components.audioplay.ChatAudioQueueProvider;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
@@ -148,7 +149,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
InputPanel.Listener,
InputPanel.MediaListener,
AudioView.OnActionListener {
private static final String TAG = ConversationActivity.class.getSimpleName();
private static final String TAG = "ConversationActivity";
public static final String ACCOUNT_ID_EXTRA = "account_id";
public static final String CHAT_ID_EXTRA = "chat_id";
@@ -223,7 +224,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeViews();
initializeResources();
int accountId = DcHelper.getAccounts(this).getSelectedAccount().getAccountId();
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
playbackViewModel.setQueueProvider(new ChatAudioQueueProvider(this, chatId, accountId));
initializeMediaController();
initializeSecurity(false, isDefaultSms)
@@ -413,6 +416,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
mediaController = null;
playbackViewModel.setMediaController(null);
}
playbackViewModel.setQueueProvider(null);
super.onDestroy();
}
@@ -536,8 +540,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
menu.findItem(R.id.menu_start_call)
.setVisible(
Prefs.isCallsEnabled(this)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& dcChat.canSend()
&& dcChat.isEncrypted()
&& !dcChat.isSelfTalk()
@@ -673,8 +676,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
public void setDraftText(String txt) {
composeText.setText(txt);
composeText.setSelection(composeText.getText().length());
try {
if (rpc.canSend(rpc.getSelectedAccountId(), chatId)) {
composeText.setText(txt);
composeText.setSelection(composeText.getText().length());
}
} catch (RpcException e) {
Log.e(TAG, "Rpc error", e);
}
}
public void hideSoftKeyboard() {
@@ -1120,6 +1129,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (chatId == DcChat.DC_CHAT_NO_CHAT)
throw new IllegalStateException("can't display a conversation for no chat.");
dcChat = DcHelper.getContext(context).getChat(chatId);
attachmentTypeSelector = null;
recipient = new Recipient(this, dcChat);
glideRequests = GlideApp.with(this);
@@ -1268,6 +1278,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DcContext dcContext = DcHelper.getContext(context);
final int currentChatId = dcChat.getId();
final boolean canSend = dcChat.canSend();
Util.runOnAnyBackgroundThread(
() -> {
DcMsg msg = null;
@@ -1390,7 +1401,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
} else {
dcContext.setDraft(currentChatId, msg);
// set or clear draft if user can't send in chat since they can't delete it otherwise
dcContext.setDraft(currentChatId, canSend ? msg : null);
}
future.set(currentChatId);
});
@@ -80,7 +80,7 @@ import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar;
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends MessageSelectorFragment {
private static final String TAG = ConversationFragment.class.getSimpleName();
private static final String TAG = "ConversationFragment";
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
@@ -944,6 +944,8 @@ public class ConversationFragment extends MessageSelectorFragment {
WebxdcActivity.openWebxdcActivity(
getContext(), messageRecord.getParent(), messageRecord.getWebxdcHref());
}
} else if (messageRecord.getInfoType() == DcMsg.DC_INFO_LOCATIONSTREAMING_ENABLED) {
WebxdcActivity.openMaps(getContext(), (int) chatId);
} else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_DESCRIPTION_CHANGED) {
Intent intent = new Intent(getContext(), ProfileActivity.class);
intent.putExtra(ProfileActivity.CHAT_ID_EXTRA, (int) chatId);
@@ -24,6 +24,7 @@ import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.os.Build;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@@ -36,6 +37,7 @@ import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.ViewCompat;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.CallInfo;
import chat.delta.rpc.types.CallState;
@@ -44,6 +46,7 @@ import chat.delta.rpc.types.VcardContact;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.thoughtcrime.securesms.calls.CallCoordinator;
@@ -72,6 +75,7 @@ import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Linkifier;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MarkdownUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -86,7 +90,7 @@ import org.thoughtcrime.securesms.util.views.Stub;
* @author Moxie Marlinspike
*/
public class ConversationItem extends BaseConversationItem {
private static final String TAG = ConversationItem.class.getSimpleName();
private static final String TAG = "ConversationItem";
private static final Rect SWIPE_RECT = new Rect();
@@ -120,6 +124,9 @@ public class ConversationItem extends BaseConversationItem {
private Stub<CallItemView> callViewStub;
private @Nullable EventListener eventListener;
// IDs of accessibility actions registered via ViewCompat.addAccessibilityAction
private final List<Integer> linkActionIds = new ArrayList<>();
private int measureCalls;
private int incomingBubbleColor;
@@ -424,6 +431,12 @@ public class ConversationItem extends BaseConversationItem {
bodyText.setClickable(false);
bodyText.setFocusable(false);
// Remove any link actions registered for the previous message binding
for (int id : linkActionIds) {
ViewCompat.removeAccessibilityAction(this, id);
}
linkActionIds.clear();
String subject = messageRecord.getSubject();
String text = messageRecord.getText();
@@ -433,12 +446,33 @@ public class ConversationItem extends BaseConversationItem {
if (messageRecord.getType() == DcMsg.DC_MSG_CALL || text.isEmpty()) {
bodyText.setVisibility(View.GONE);
} else {
Spannable spannable = (Spannable) MarkdownUtil.toMarkdown(context, text);
Spannable spannable = MarkdownUtil.toMarkdown(context, text);
if (batchSelected.isEmpty()) {
spannable = Linkifier.linkify(spannable);
Linkifier.linkify(spannable);
}
bodyText.setText(spannable);
bodyText.setVisibility(View.VISIBLE);
// Register a TalkBack "Actions" entry for each link in the message
Spanned spanned = (Spanned) spannable;
final TextView tv = bodyText;
for (LongClickCopySpan span :
spanned.getSpans(0, spanned.length(), LongClickCopySpan.class)) {
int start = spanned.getSpanStart(span);
int end = spanned.getSpanEnd(span);
if (start >= 0 && end > start && end <= spanned.length()) {
String linkText = spanned.subSequence(start, end).toString();
String label = context.getString(R.string.accessibility_link_action, linkText);
linkActionIds.add(
ViewCompat.addAccessibilityAction(
this,
label,
(v, args) -> {
span.onClick(tv);
return true;
}));
}
}
}
int downloadState = messageRecord.getDownloadState();
@@ -69,6 +69,7 @@ import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.connect.DirectShareUtil;
import org.thoughtcrime.securesms.geolocation.LocationStreamingService;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
@@ -90,7 +91,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class ConversationListActivity extends PassphraseRequiredActionBarActivity
implements ConversationListFragment.ConversationSelectedListener {
private static final String TAG = ConversationListActivity.class.getSimpleName();
private static final String TAG = "ConversationListActivity";
private static final String OPENPGP4FPR = "openpgp4fpr";
private static final String NDK_ARCH_WARNED = "ndk_arch_warned";
public static final String CLEAR_NOTIFICATIONS = "clear_notifications";
@@ -449,6 +450,9 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
refreshTitle();
invalidateOptionsMenu();
DirectShareUtil.triggerRefreshDirectShare(this);
if (DcHelper.getContext(this).isSendingLocationsToChat(0)) {
LocationStreamingService.ensureRunning(this);
}
}
@Override
@@ -688,7 +692,11 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
// Util.copy(inputStream, new FileOutputStream(outputFile));
// msg.setFile(outputFile, "image/jpeg");
msg.setText(getString(R.string.update_2_46ac, "https://arcanechat.me/#contribute", "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=AM1YCd_2hKuiSiTEb-2177On&a=arcanechat%40arcanechat.me&n=ArcaneChat&b=ArcaneChat+Channel"));
msg.setText(
getString(
R.string.update_2_46ac,
"https://arcanechat.me/#contribute",
"https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=AM1YCd_2hKuiSiTEb-2177On&a=arcanechat%40arcanechat.me&n=ArcaneChat&b=ArcaneChat+Channel"));
}
dcContext.addDeviceMsg(deviceMsgLabel, msg);
@@ -56,7 +56,7 @@ public class ConversationListFragment extends BaseConversationListFragment
public static final String ARCHIVE = "archive";
public static final String RELOAD_LIST = "reload_list";
private static final String TAG = ConversationListFragment.class.getSimpleName();
private static final String TAG = "ConversationListFragment";
private RecyclerView list;
private View emptyState;
@@ -42,7 +42,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
@SuppressLint("StaticFieldLeak")
public class CreateProfileActivity extends BaseActionBarActivity {
private static final String TAG = CreateProfileActivity.class.getSimpleName();
private static final String TAG = "CreateProfileActivity";
private static final int REQUEST_CODE_AVATAR = 1;
@@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class EphemeralMessagesDialog {
private static final String TAG = EphemeralMessagesDialog.class.getSimpleName();
private static final String TAG = "EphemeralMessagesDialog";
public static void show(
final Context context,
@@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
implements ItemClickListener {
private static final String TAG = GroupCreateActivity.class.getSimpleName();
private static final String TAG = "GroupCreateActivity";
public static final String EDIT_GROUP_CHAT_ID = "edit_group_chat_id";
public static final String CREATE_BROADCAST = "create_broadcast";
public static final String UNENCRYPTED = "unencrypted";
@@ -307,8 +307,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private boolean showGroupNameEmptyToast(String groupName) {
if (groupName == null) {
Toast.makeText(this, getString(R.string.group_please_enter_group_name), Toast.LENGTH_LONG)
.show();
Toast.makeText(this, getString(R.string.please_enter_chat_name), Toast.LENGTH_LONG).show();
return true;
}
return false;
@@ -65,7 +65,7 @@ import org.thoughtcrime.securesms.util.views.ProgressDialog;
public class InstantOnboardingActivity extends BaseActionBarActivity
implements DcEventCenter.DcEventDelegate {
private static final String TAG = InstantOnboardingActivity.class.getSimpleName();
private static final String TAG = "InstantOnboardingActivity";
private static final String DCACCOUNT = "dcaccount";
private static final String DCLOGIN = "dclogin";
private static final String INSTANCES_URL = "https://chatmail.at/relays";
@@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.util.FileProviderUtil;
public class LogViewActivity extends BaseActionBarActivity {
private static final String TAG = LogViewActivity.class.getSimpleName();
private static final String TAG = "LogViewActivity";
LogViewFragment logViewFragment;
@@ -33,14 +33,13 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMediaGalleryElement;
@@ -69,7 +68,7 @@ import org.thoughtcrime.securesms.util.Util;
public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
implements RecipientModifiedListener, LoaderManager.LoaderCallbacks<DcMediaGalleryElement> {
private static final String TAG = MediaPreviewActivity.class.getSimpleName();
private static final String TAG = "MediaPreviewActivity";
public static final String ACTIVITY_TITLE_EXTRA = "activity_title";
public static final String EDIT_AVATAR_CHAT_ID = "avatar_for_chat_id";
@@ -88,7 +87,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
@Nullable private DcMsg messageRecord;
private DcContext dcContext;
private MediaItem initialMedia;
private ViewPager mediaPager;
private ViewPager2 mediaPager;
private Recipient conversationRecipient;
private boolean leftIsRecent;
@@ -190,7 +189,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
private void initializeViews() {
mediaPager = findViewById(R.id.media_pager);
mediaPager.setOffscreenPageLimit(1);
mediaPager.addOnPageChangeListener(new ViewPagerListener());
mediaPager.registerOnPageChangeCallback(new ViewPagerListener());
}
private void initializeResources() {
@@ -261,7 +260,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
private int cleanupMedia() {
int restartItem = mediaPager.getCurrentItem();
mediaPager.removeAllViews();
mediaPager.setAdapter(null);
return restartItem;
@@ -483,11 +481,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
@SuppressWarnings("ConstantConditions")
DcMediaPagerAdapter adapter =
new DcMediaPagerAdapter(this, GlideApp.with(this), getWindow(), data, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
mediaPager.setAdapter(adapter);
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition());
else mediaPager.setCurrentItem(restartItem);
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition(), false);
else mediaPager.setCurrentItem(restartItem, false);
}
}
@@ -526,7 +524,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
}
}
private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter {
private static class SingleItemPagerAdapter extends RecyclerView.Adapter<SingleItemPagerAdapter.MediaViewHolder> implements MediaItemAdapter {
private final GlideRequests glideRequests;
private final Window window;
@@ -555,37 +553,29 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
}
@Override
public int getCount() {
public int getItemCount() {
return 1;
}
@NonNull
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new MediaViewHolder(inflater.inflate(R.layout.media_view_page, parent, false));
}
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
View itemView = inflater.inflate(R.layout.media_view_page, container, false);
MediaView mediaView = itemView.findViewById(R.id.media_view);
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
try {
mediaView.set(glideRequests, window, uri, name, mediaType, size, true);
holder.mediaView.set(glideRequests, window, uri, name, mediaType, size, true);
} catch (IOException e) {
Log.w(TAG, e);
}
container.addView(itemView);
return itemView;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
mediaView.cleanup();
container.removeView((FrameLayout) object);
public void onViewRecycled(@NonNull MediaViewHolder holder) {
super.onViewRecycled(holder);
holder.mediaView.cleanup();
}
@Override
@@ -595,9 +585,18 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
@Override
public void pause(int position) {}
static class MediaViewHolder extends RecyclerView.ViewHolder {
final MediaView mediaView;
MediaViewHolder(@NonNull View itemView) {
super(itemView);
mediaView = itemView.findViewById(R.id.media_view);
}
}
}
private static class DcMediaPagerAdapter extends PagerAdapter implements MediaItemAdapter {
private static class DcMediaPagerAdapter extends RecyclerView.Adapter<DcMediaPagerAdapter.MediaViewHolder> implements MediaItemAdapter {
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
@@ -630,21 +629,21 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
}
@Override
public int getCount() {
public int getItemCount() {
if (!active) return 0;
else return gallery.getCount();
}
@NonNull
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView =
LayoutInflater.from(context).inflate(R.layout.media_view_page, parent, false);
return new MediaViewHolder(itemView);
}
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
View itemView =
LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false);
MediaView mediaView = itemView.findViewById(R.id.media_view);
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
boolean autoplay = position == autoPlayPosition;
int cursorPosition = getCursorPosition(position);
@@ -656,7 +655,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
try {
//noinspection ConstantConditions
mediaView.set(
holder.mediaView.set(
glideRequests,
window,
Uri.fromFile(msg.getFileAsFile()),
@@ -668,19 +667,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
Log.w(TAG, e);
}
mediaViews.put(position, mediaView);
container.addView(itemView);
return itemView;
mediaViews.put(position, holder.mediaView);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
mediaView.cleanup();
mediaViews.remove(position);
container.removeView((FrameLayout) object);
public void onViewRecycled(@NonNull MediaViewHolder holder) {
super.onViewRecycled(holder);
int pos = holder.getBindingAdapterPosition();
if (pos != RecyclerView.NO_POSITION) {
mediaViews.remove(pos);
}
holder.mediaView.cleanup();
}
public MediaItem getMediaItemFor(int position) {
@@ -710,6 +707,15 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
if (leftIsRecent) return position;
else return gallery.getCount() - 1 - position;
}
static class MediaViewHolder extends RecyclerView.ViewHolder {
final MediaView mediaView;
MediaViewHolder(@NonNull View itemView) {
super(itemView);
mediaView = itemView.findViewById(R.id.media_view);
}
}
}
private static class MediaItem {
@@ -41,7 +41,7 @@ import org.thoughtcrime.securesms.qr.QrCodeHandler;
*/
public class NewConversationActivity extends ContactSelectionActivity {
private static final String TAG = NewConversationActivity.class.getSimpleName();
private static final String TAG = "NewConversationActivity";
@Override
public void onCreate(Bundle bundle, boolean ready) {
@@ -7,7 +7,7 @@ import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.GenericForegroundService;
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity {
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
private static final String TAG = "PassphraseRequiredActionBarActivity";
@Override
protected final void onCreate(Bundle savedInstanceState) {
@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.Util;
public class ProfileAdapter extends RecyclerView.Adapter {
private static final String TAG = ProfileAdapter.class.getSimpleName();
private static final String TAG = "ProfileAdapter";
public static final int ITEM_AVATAR = 10;
public static final int ITEM_DIVIDER = 20;
@@ -38,7 +38,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class ProfileFragment extends Fragment
implements ProfileAdapter.ItemClickListener, DcEventCenter.DcEventDelegate {
private static final String TAG = ProfileFragment.class.getSimpleName();
private static final String TAG = "ProfileFragment";
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
public class ResolveMediaTask extends AsyncTask<Uri, Void, Uri> {
private static final String TAG = ResolveMediaTask.class.getSimpleName();
private static final String TAG = "ResolveMediaTask";
interface OnMediaResolvedListener {
void onMediaResolved(Uri uri);
@@ -47,7 +47,7 @@ import org.thoughtcrime.securesms.util.ShareUtil;
*/
public class ShareActivity extends PassphraseRequiredActionBarActivity
implements ResolveMediaTask.OnMediaResolvedListener {
private static final String TAG = ShareActivity.class.getSimpleName();
private static final String TAG = "ShareActivity";
public static final String EXTRA_ACC_ID = "acc_id";
public static final String EXTRA_CHAT_ID = "chat_id";
@@ -18,22 +18,19 @@ public class ShareLocationDialog {
switch (which) {
default:
case 0:
shareLocationUnit = 5 * 60;
break;
case 1:
shareLocationUnit = 30 * 60;
break;
case 2:
case 1:
shareLocationUnit = 60 * 60;
break;
case 3:
case 2:
shareLocationUnit = 2 * 60 * 60;
break;
case 4:
case 3:
shareLocationUnit = 6 * 60 * 60;
break;
case 5:
shareLocationUnit = 12 * 60 * 60;
case 4:
shareLocationUnit = 24 * 60 * 60;
break;
}
@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class WebViewActivity extends PassphraseRequiredActionBarActivity
implements SearchView.OnQueryTextListener, WebView.FindListener {
private static final String TAG = WebViewActivity.class.getSimpleName();
private static final String TAG = "WebViewActivity";
// Regex to extract the host from a URL for IDN conversion.
private static final Pattern URL_PATTERN =
Pattern.compile("^((?:[a-zA-Z0-9]+://)?)([^/?#]+)(.*)$");
@@ -59,7 +59,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
private static final String TAG = WebxdcActivity.class.getSimpleName();
private static final String TAG = "WebxdcActivity";
private static final String EXTRA_ACCOUNT_ID = "accountId";
private static final String EXTRA_APP_MSG_ID = "appMessageId";
private static final String EXTRA_HIDE_ACTION_BAR = "hideActionBar";
@@ -240,6 +240,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setMediaPlaybackRequiresUserGesture(false);
webSettings.setAllowFileAccess(false);
webSettings.setBlockNetworkLoads(!internetAccess);
webSettings.setAllowContentAccess(false);
@@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
public class WebxdcStoreActivity extends PassphraseRequiredActionBarActivity {
private static final String TAG = WebxdcStoreActivity.class.getSimpleName();
private static final String TAG = "WebxdcStoreActivity";
private DcContext dcContext;
private Rpc rpc;
@@ -48,7 +48,7 @@ public class WelcomeActivity extends BaseActionBarActivity
implements DcEventCenter.DcEventDelegate {
public static final String BACKUP_QR_EXTRA = "backup_qr_extra";
public static final int PICK_BACKUP = 20574;
private static final String TAG = WelcomeActivity.class.getSimpleName();
private static final String TAG = "WelcomeActivity";
public static final String TMP_BACKUP_FILE = "tmp-backup-file";
private ProgressDialog progressDialog = null;
@@ -41,7 +41,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class AccountSelectionListFragment extends DialogFragment
implements DcEventCenter.DcEventDelegate {
private static final String TAG = AccountSelectionListFragment.class.getSimpleName();
private static final String TAG = "AccountSelectionListFragment";
private static final String ARG_SELECT_ONLY = "select_only";
private RecyclerView recyclerView;
private AccountSelectionListAdapter adapter;
@@ -157,6 +157,12 @@ public class AccountSelectionListFragment extends DialogFragment
onSetTag(accountId);
} else if (itemId == R.id.menu_move_to_top) {
onMoveToTop(accountId);
} else if (itemId == R.id.menu_mark_all_as_read) {
try {
DcHelper.getRpc(requireActivity()).marknoticedAllChats(accountId);
} catch (RpcException e) {
Log.e(TAG, "Error calling rpc.marknoticedAllChats()", e);
}
}
}
@@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.util.Prefs;
public class AudioCodec {
private static final String TAG = AudioCodec.class.getSimpleName();
private static final String TAG = "AudioCodec";
private static final int SAMPLE_RATE_BALANCED = 44100;
private static final int BIT_RATE_BALANCED = 32000;
@@ -196,7 +196,7 @@ public class AudioCodec {
setFinished();
}
},
AudioCodec.class.getSimpleName())
"AudioCodec")
.start();
}
@@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.util.Util;
public class AudioRecorder {
private static final String TAG = AudioRecorder.class.getSimpleName();
private static final String TAG = "AudioRecorder";
private static final ExecutorService executor = ThreadUtil.newDynamicSingleThreadedExecutor();
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.R;
/** Bottom sheet dialog for selecting audio output device */
@RequiresApi(Build.VERSION_CODES.O)
public class AudioDevicePickerDialog extends BottomSheetDialog {
private static final String TAG = AudioDevicePickerDialog.class.getSimpleName();
private static final String TAG = "AudioDevicePickerDialog";
public interface OnDeviceSelectedListener {
void onDeviceSelected(CallEndpointCompat endpoint);
@@ -51,8 +51,9 @@ import org.webrtc.VideoTrack;
@RequiresApi(api = Build.VERSION_CODES.O)
public class CallActivity extends AppCompatActivity {
private static final String TAG = CallActivity.class.getSimpleName();
private static final int PERMISSION_REQUEST_CODE = 1001;
private static final String TAG = "CallActivity";
private static final int MIC_PERMISSION_REQUEST_CODE = 1001;
private static final int CAMERA_PERMISSION_REQUEST_CODE = 1002;
public static final String ACTION_ANSWER_CALL = BuildConfig.APPLICATION_ID + ".ANSWER_CALL";
public static final String ACTION_DECLINE_CALL = BuildConfig.APPLICATION_ID + ".DECLINE_CALL";
@@ -94,10 +95,30 @@ public class CallActivity extends AppCompatActivity {
private PowerManager.WakeLock proximityWakeLock;
// States
private boolean awaitingPermissionResult = false;
private boolean pausedWhileAwaitingPermission = false;
private boolean intentHandled = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Destructive actions need nothing
String action = getIntent() != null ? getIntent().getAction() : null;
if (ACTION_DECLINE_CALL.equals(action)) {
Log.d(TAG, "Handling DECLINE_CALL action from notification");
CallCoordinator.getInstance(getApplication()).declineCall();
finish();
return;
}
if (ACTION_HANGUP_CALL.equals(action)) {
Log.d(TAG, "Handling HANGUP_CALL action from notification");
CallCoordinator.getInstance(getApplication()).hangUp();
finish();
return;
}
setContentView(R.layout.activity_call);
setupWindowFlags();
@@ -108,11 +129,6 @@ public class CallActivity extends AppCompatActivity {
initializeProximityWakeLock();
if (!hasRequiredPermissions()) {
requestRequiredPermissions();
return;
}
// PiP listener
addOnPictureInPictureModeChangedListener(
pipModeInfo -> {
@@ -139,47 +155,74 @@ public class CallActivity extends AppCompatActivity {
initializeViewModel();
// Intent handling needs permissions
if (!hasMicrophonePermission()) {
awaitingPermissionResult = true;
ActivityCompat.requestPermissions(
this, new String[] {Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE);
return;
}
if (shouldRequestCameraPermission()) {
awaitingPermissionResult = true;
ActivityCompat.requestPermissions(
this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
return;
}
handleIntents(getIntent());
intentHandled = true;
}
private void handleIntents(Intent intent) {
if (intent == null || viewModel == null) {
if (intent == null) {
return;
}
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
if (!coordinator.hasActiveCall()) {
Log.e(TAG, "No active call exists, cannot proceed");
Toast.makeText(this, "No active call", Toast.LENGTH_SHORT).show();
finish();
return;
}
String action = intent.getAction();
Log.d(TAG, "handleIntents: action=" + action);
// Handle notification actions
if (ACTION_ANSWER_CALL.equals(action)) {
Log.d(TAG, "Handling ANSWER_CALL action from notification");
viewModel.handleNotificationAnswer();
return;
}
// Destructive actions without ViewModel
if (ACTION_DECLINE_CALL.equals(action)) {
Log.d(TAG, "Handling DECLINE_CALL action from notification");
viewModel.handleNotificationDecline();
Log.d(TAG, "Handling DECLINE_CALL action");
if (viewModel != null) {
viewModel.handleNotificationDecline();
} else {
coordinator.declineCall();
}
finish();
return;
}
if (ACTION_HANGUP_CALL.equals(action)) {
Log.d(TAG, "Handling HANGUP_CALL action from notification");
viewModel.handleNotificationHangup();
Log.d(TAG, "Handling HANGUP_CALL action");
if (viewModel != null) {
viewModel.handleNotificationHangup();
} else {
coordinator.hangUp();
}
finish();
return;
}
if (viewModel == null) {
return;
}
if (!coordinator.hasActiveCall()) {
Log.e(TAG, "No active call exists, cannot proceed");
finish();
return;
}
if (ACTION_ANSWER_CALL.equals(action)) {
Log.d(TAG, "Handling ANSWER_CALL action from notification");
viewModel.handleNotificationAnswer();
return;
}
if (coordinator.hasOngoingCall()) {
Log.d(TAG, "Resuming existing call");
} else if (!coordinator.isIncomingCall()) {
@@ -373,6 +416,7 @@ public class CallActivity extends AppCompatActivity {
case INITIALIZING:
case PROMPTING_USER_ACCEPT:
case ENDED:
case ANSWERED_ELSEWHERE:
case ERROR:
default:
finish();
@@ -504,6 +548,8 @@ public class CallActivity extends AppCompatActivity {
viewModel.getVideoEnabled(), v -> videoConfigChanged.setValue(true));
videoConfigChanged.addSource(
viewModel.getRemoteVideoEnabled(), v -> videoConfigChanged.setValue(true));
videoConfigChanged.addSource(
viewModel.getIsFrontCamera(), v -> videoConfigChanged.setValue(true));
// Video layout
videoConfigChanged.observe(
@@ -578,6 +624,23 @@ public class CallActivity extends AppCompatActivity {
statusText.setText(R.string.call_reconnecting);
break;
case ANSWERED_ELSEWHERE:
statusText.setText(R.string.call_answered_elsewhere);
incomingCallPrompt.setVisibility(View.GONE);
bottomLayoutContainer.setVisibility(View.GONE);
callerIconContainer.setVisibility(View.GONE);
answerModeSelector.setVisibility(View.GONE);
new Handler(Looper.getMainLooper())
.postDelayed(
() -> {
if (!isFinishing()) {
finish();
}
},
1500);
break;
case ENDED:
statusText.setText(R.string.call_ended);
finish();
@@ -705,17 +768,21 @@ public class CallActivity extends AppCompatActivity {
VideoTrack localTrack = viewModel.getLocalVideoTrack().getValue();
VideoTrack remoteTrack = viewModel.getRemoteVideoTrack().getValue();
boolean isFront = Boolean.TRUE.equals(viewModel.getIsFrontCamera().getValue());
boolean showFullScreen = false;
if (state == CallViewModel.CallState.CONNECTED
&& remoteTrack != null
&& Boolean.TRUE.equals(remoteVideoEnabled)) {
remoteVideoView.setMirror(false);
remoteTrack.addSink(remoteVideoView);
showFullScreen = true;
} else if (!coordinator.isIncomingCall()
&& (state == CallViewModel.CallState.RINGING || state == CallViewModel.CallState.CONNECTING)
&& localTrack != null
&& Boolean.TRUE.equals(videoEnabled)) {
remoteVideoView.setMirror(isFront);
localTrack.addSink(remoteVideoView);
showFullScreen = true;
}
@@ -729,6 +796,7 @@ public class CallActivity extends AppCompatActivity {
&& !isInPictureInPictureMode();
if (showCorner) {
localVideoView.setMirror(isFront);
localTrack.addSink(localVideoView);
}
@@ -758,18 +826,46 @@ public class CallActivity extends AppCompatActivity {
// Permissions
private boolean hasRequiredPermissions() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED;
private boolean hasMicrophonePermission() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED;
}
private void requestRequiredPermissions() {
ActivityCompat.requestPermissions(
this,
new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
PERMISSION_REQUEST_CODE);
private boolean hasCameraPermission() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED;
}
private boolean shouldRequestCameraPermission() {
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
return coordinator.isStartsWithVideo() && !hasCameraPermission();
}
private void handleMicPermissionDenied() {
Toast.makeText(this, R.string.call_requires_mic_permission, Toast.LENGTH_LONG).show();
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
if (coordinator.hasActiveCall()
&& coordinator.isIncomingCall()
&& !coordinator.hasOngoingCall()) {
coordinator.declineCall();
}
finish();
}
private void proceedAfterPermissions() {
if (intentHandled) return;
if (shouldRequestCameraPermission()) {
awaitingPermissionResult = true;
ActivityCompat.requestPermissions(
this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
return;
}
handleIntents(getIntent());
intentHandled = true;
}
@Override
@@ -777,38 +873,34 @@ public class CallActivity extends AppCompatActivity {
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
boolean microphoneGranted = false;
boolean cameraGranted = false;
awaitingPermissionResult = false;
pausedWhileAwaitingPermission = false;
for (int i = 0; i < permissions.length; i++) {
if (permissions[i].equals(Manifest.permission.RECORD_AUDIO)) {
microphoneGranted = (grantResults[i] == PackageManager.PERMISSION_GRANTED);
} else if (permissions[i].equals(Manifest.permission.CAMERA)) {
cameraGranted = (grantResults[i] == PackageManager.PERMISSION_GRANTED);
}
}
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
if (!microphoneGranted) {
Toast.makeText(this, "Microphone permission is required for calls", Toast.LENGTH_LONG)
.show();
finish();
if (requestCode == MIC_PERMISSION_REQUEST_CODE) {
boolean micGranted =
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (!micGranted) {
handleMicPermissionDenied();
return;
}
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
} else if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
boolean cameraGranted =
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (!cameraGranted && coordinator.isStartsWithVideo()) {
if (!cameraGranted) {
Log.w(TAG, "Camera permission denied, switching to audio-only");
Toast.makeText(
this, "Starting audio-only call (camera permission denied)", Toast.LENGTH_SHORT)
.show();
coordinator.setStartsWithVideo(false);
}
initializeViewModel();
handleIntents(getIntent());
}
proceedAfterPermissions();
}
// Picture-in-Picture
@@ -817,6 +909,11 @@ public class CallActivity extends AppCompatActivity {
public void onUserLeaveHint() {
super.onUserLeaveHint();
// Do not finish activity when a permission request is pending
if (awaitingPermissionResult) {
return;
}
// Enter PiP mode when user presses home button during active call
if (viewModel != null) {
CallViewModel.CallState state = viewModel.getCallState().getValue();
@@ -833,6 +930,7 @@ public class CallActivity extends AppCompatActivity {
case INITIALIZING:
case PROMPTING_USER_ACCEPT:
case ENDED:
case ANSWERED_ELSEWHERE:
case ERROR:
default:
finish();
@@ -873,17 +971,51 @@ public class CallActivity extends AppCompatActivity {
protected void onPause() {
super.onPause();
if (awaitingPermissionResult) {
pausedWhileAwaitingPermission = true;
}
if (proximityWakeLock != null && proximityWakeLock.isHeld()) {
proximityWakeLock.release();
Log.d(TAG, "Proximity wake lock released in onDestroy");
}
}
@Override
protected void onResume() {
super.onResume();
// Fallback for Android 16 bug: onRequestPermissionsResult not called
if (awaitingPermissionResult && pausedWhileAwaitingPermission) {
Log.w(TAG, "Permission result callback not received, handling in onResume");
awaitingPermissionResult = false;
pausedWhileAwaitingPermission = false;
if (!hasMicrophonePermission()) {
handleMicPermissionDenied();
return;
}
// Mic was granted without callback
if (shouldRequestCameraPermission()) {
awaitingPermissionResult = true;
ActivityCompat.requestPermissions(
this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
return;
}
proceedAfterPermissions();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
detachAllTracks();
if (viewModel != null) {
detachAllTracks();
}
// Release video renderers
if (localVideoView != null) {
@@ -63,7 +63,7 @@ import org.webrtc.VideoTrack;
@RequiresApi(api = Build.VERSION_CODES.O)
public class CallCoordinator implements DcEventCenter.DcEventDelegate {
private static final String TAG = CallCoordinator.class.getSimpleName();
private static final String TAG = "CallCoordinator";
// Notification channels
private static final String CHANNEL_ID_INCOMING = "voip_incoming_calls";
@@ -95,6 +95,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
private final MutableLiveData<String> displayName = new MutableLiveData<>();
private final MutableLiveData<Icon> displayIcon = new MutableLiveData<>();
private final MutableLiveData<Boolean> outgoingCallPlaced = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> answeredElsewhere = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> isFrontCamera = new MutableLiveData<>(true);
// Audio Routing Support
private final MediatorLiveData<CallEndpointCompat> currentAudioEndpoint =
@@ -213,7 +215,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
calleeName = "Unknown";
}
showOrUpdateOngoingNotification("Calling " + calleeName + "...");
showOrUpdateOngoingNotification(
appContext.getString(R.string.calling_person, calleeName));
}
// Initialize call
@@ -317,6 +320,10 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return outgoingCallPlaced;
}
public LiveData<Boolean> getAnsweredElsewhere() {
return answeredElsewhere;
}
public LiveData<CallEndpointCompat> getCurrentAudioEndpoint() {
return currentAudioEndpoint;
}
@@ -325,6 +332,10 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return availableAudioEndpoints;
}
public LiveData<Boolean> getIsFrontCamera() {
return isFrontCamera;
}
// State Update Methods (CallService)
public void updateConnectionState(PeerConnection.PeerConnectionState state) {
@@ -359,6 +370,11 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
isRelayUsed.postValue(isRelay);
}
public void updateFrontCamera(boolean front) {
Log.d(TAG, "updateFrontCamera: " + front);
isFrontCamera.postValue(front);
}
public void reportError(String error) {
Log.e(TAG, "reportError: " + error);
errorMessage.postValue(error);
@@ -366,7 +382,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
// Delayed Media Initialization Support
public void startMediaCapture() {
public synchronized void startMediaCapture() {
Log.d(TAG, "startMediaCapture");
if (callService != null) {
callService.startMediaCapture();
@@ -375,7 +391,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
}
}
public void handleCallControlScopeAnswer() {
public synchronized void handleCallControlScopeAnswer() {
Log.d(TAG, "handleCallControlScopeAnswer");
if (!isIncomingCall) {
@@ -387,6 +403,15 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
callService.stopRingtone();
}
// Promote the service to foreground immediately. Waiting until onIncomingCallAccepted
// on executor pool thread is too late on stricter OEM.
//
// Do not cancel() but use showOrUpdateOngoingNotification to replace incoming
// notification without a gap.
String callerName = displayName.getValue();
if (callerName == null) callerName = "Unknown";
showOrUpdateOngoingNotification(appContext.getString(R.string.call_with, callerName));
// Notify Android system with CallControlScope
CallControlScope scope = activeCallControlScope;
if (scope != null) {
@@ -410,8 +435,6 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
}
});
}
notificationManager.cancel(NOTIFICATION_ID_CALL);
}
public void answerWebRTC() {
@@ -512,22 +535,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return;
}
// Check microphone and camera permissions
if (!hasMicrophonePermission()) {
Log.e(TAG, "Microphone permission not granted");
Intent intent = new Intent(appContext, CallActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
appContext.startActivity(intent);
notificationManager.cancel(NOTIFICATION_ID_CALL);
return;
}
if (startsWithVideo && !hasCameraPermission()) {
Log.w(TAG, "Camera permission not granted");
startsWithVideo = false;
}
// Launch CallActivity with answer action
// Launch CallActivity
Intent intent = new Intent(appContext, CallActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
appContext.startActivity(intent);
@@ -628,7 +636,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
cleanupCall(activeAccId, activeCallId);
}
public void setAudioEnabled(boolean enabled) {
public synchronized void setAudioEnabled(boolean enabled) {
Log.d(TAG, "setAudioEnabled: " + enabled);
localAudioEnabled.postValue(enabled);
@@ -640,7 +648,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
}
}
public void setVideoEnabled(boolean enabled) {
public synchronized void setVideoEnabled(boolean enabled) {
Log.d(TAG, "setVideoEnabled: " + enabled);
localVideoEnabled.postValue(enabled);
@@ -652,14 +660,14 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
}
}
public void switchCamera() {
public synchronized void switchCamera() {
Log.d(TAG, "switchCamera");
if (callService != null) {
callService.switchCamera();
}
}
public void startOutgoingCall() {
public synchronized void startOutgoingCall() {
Log.d(TAG, "startOutgoingCall");
if (callService != null) {
callService.startOutgoingCall();
@@ -846,7 +854,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
onIncomingCall(accId, callId, event.getData2Str(), hasVideo);
break;
case DcContext.DC_EVENT_INCOMING_CALL_ACCEPTED:
onIncomingCallAccepted(callId);
boolean fromThisDevice = event.getData2Int() != 0; // Data2 is from_this_device
onIncomingCallAccepted(callId, fromThisDevice);
break;
case DcContext.DC_EVENT_OUTGOING_CALL_ACCEPTED:
String answerSDP = event.getData2Str();
@@ -902,8 +911,13 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
startAndBindService();
}
private synchronized void onIncomingCallAccepted(int callId) {
Log.d(TAG, "onIncomingCallAccepted: callId=" + callId);
private synchronized void onIncomingCallAccepted(int callId, boolean fromThisDevice) {
Log.d(TAG, "onIncomingCallAccepted: callId=" + callId + ", fromThisDevice=" + fromThisDevice);
if (!fromThisDevice) {
onCallAnsweredOnOtherDevice();
return;
}
if (activeCallId == null || !activeCallId.equals(callId)) {
Log.w(TAG, "Accepted call ID doesn't match active call");
@@ -915,7 +929,53 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
callerName = "Unknown";
}
showOrUpdateOngoingNotification("Call with " + callerName);
showOrUpdateOngoingNotification(appContext.getString(R.string.call_with, callerName));
}
private synchronized void onCallAnsweredOnOtherDevice() {
Log.d(TAG, "Call was answered on another device");
if (!hasActiveCall()) {
Log.d(TAG, "No active call, ignoring");
return;
}
// Prevent notifyBackendCallEnded() from firing during WebRTC teardown.
// The call is still active on the other device.
hasNotifiedBackend = true;
if (callService != null) {
callService.stopRingtone();
}
notificationManager.cancel(NOTIFICATION_ID_CALL);
answeredElsewhere.postValue(true);
// Disconnect from Telecom CallControlScope
CallControlScope scope = activeCallControlScope;
if (scope != null) {
scope.disconnect(
new DisconnectCause(DisconnectCause.REMOTE),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
Log.d(TAG, "Disconnect (answered elsewhere) completed");
}
});
}
if (callService != null) {
callService.endCall();
}
cleanupCall(activeAccId, activeCallId);
}
private void onOutgoingCallAccepted(int callId, String answerSdp) {
@@ -956,7 +1016,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
if (calleeName == null) {
calleeName = "Unknown";
}
showOrUpdateOngoingNotification("Call with " + calleeName);
showOrUpdateOngoingNotification(appContext.getString(R.string.call_with, calleeName));
}
private synchronized void onCallEnded(int accId, int callId) {
@@ -1049,6 +1109,14 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return;
}
if (callService != null) {
try {
callService.stopForegroundAndDismiss();
} catch (Exception e) {
Log.w(TAG, "stopForegroundAndDismiss failed", e);
}
}
// Clear state
this.activeAccId = null;
this.activeCallId = null;
@@ -1125,6 +1193,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
private void resetLiveDataForNewCall() {
connectionState.postValue(PeerConnection.PeerConnectionState.NEW);
answeredElsewhere.postValue(false); // clearLiveData() must not reset answeredElsewhere
clearLiveData();
}
@@ -1150,7 +1219,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return;
}
// Check camera and microphone permissions
// Check microphone permission
if (!hasMicrophonePermission()) {
Log.e(TAG, "Microphone permission not granted");
@@ -1160,11 +1229,6 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return;
}
if (startsWithVideo && !hasCameraPermission()) {
Log.w(TAG, "Camera permission not granted, will start audio-only");
startsWithVideo = false;
}
resetLiveDataForNewCall();
this.activeCallId = -1; // Placeholder call ID for Intent
@@ -1310,10 +1374,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
}
// Check full screen intent permission on Android 14+
boolean canUseFullScreen = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
canUseFullScreen = canUseFullScreenIntent();
if (!canUseFullScreen) {
if (!canUseFullScreenIntent()) {
Log.w(TAG, "Full screen intent permission not granted, notification will appear normally");
}
}
@@ -1354,7 +1416,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Notification.Builder builder;
if (canUseFullScreen && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+, CallStyle
Person caller =
new Person.Builder().setName(callerName).setIcon(callerIcon).setImportant(true).build();
@@ -4,6 +4,7 @@ import android.app.Notification;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
@@ -13,7 +14,9 @@ import android.media.ToneGenerator;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -33,7 +36,7 @@ import org.webrtc.VideoTrack;
@RequiresApi(api = Build.VERSION_CODES.O)
public class CallService extends Service implements WebRTCClient.Callbacks {
private static final String TAG = CallService.class.getSimpleName();
private static final String TAG = "CallService";
private final IBinder binder = new LocalBinder();
@@ -142,17 +145,13 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
return;
}
// Check permissions
boolean hasMicrophone = callCoordinator.hasMicrophonePermission();
boolean hasCamera = callCoordinator.hasCameraPermission();
if (!hasMicrophone) {
if (!callCoordinator.hasMicrophonePermission()) {
Log.e(TAG, "Microphone permission not granted, cannot start call");
callCoordinator.reportError("Microphone permission is required for calls");
return;
}
boolean startsWithVideo = callCoordinator.isStartsWithVideo() && hasCamera;
boolean startsWithVideo = callCoordinator.isStartsWithVideo();
Log.d(TAG, "Creating media stream with video: " + startsWithVideo);
@@ -164,14 +163,18 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
webRTCClient.setLocalMediaStream(stream);
callCoordinator.updateFrontCamera(mediaStreamManager.isFrontCamera());
callCoordinator.setVideoEnabled(startsWithVideo);
if (!stream.videoTracks.isEmpty()) {
VideoTrack localTrack = stream.videoTracks.get(0);
callCoordinator.updateLocalVideoTrack(localTrack);
} else {
Log.w(TAG, "Camera unavailable, call will be audio-only");
callCoordinator.reportError("Camera unavailable, using audio only");
Log.w(TAG, "No video track in stream, call will be audio-only");
if (startsWithVideo) {
callCoordinator.reportError("Camera unavailable, using audio only");
}
callCoordinator.setVideoEnabled(false);
}
@@ -181,7 +184,9 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
@Override
public void onError(String error) {
Log.e(TAG, "Failed to setup media: " + error);
callCoordinator.reportError("Camera/microphone error: " + error);
if (startsWithVideo) {
callCoordinator.reportError("Camera/microphone error: " + error);
}
callCoordinator.setVideoEnabled(false);
}
});
@@ -410,7 +415,18 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
Log.d(TAG, "switchCamera");
if (mediaStreamManager != null) {
mediaStreamManager.switchCamera();
mediaStreamManager.switchCamera(
new MediaStreamManager.CameraSwitchCallback() {
@Override
public void onCameraSwitch(boolean isFrontCamera) {
callCoordinator.updateFrontCamera(isFrontCamera);
}
@Override
public void onError(String error) {
Log.e(TAG, "Camera switch failed: " + error);
}
});
}
}
@@ -419,6 +435,12 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
disposeWebRTC();
try {
stopForeground(STOP_FOREGROUND_REMOVE);
} catch (Exception e) {
Log.w(TAG, "stopForeground failed", e);
}
stopService();
}
@@ -481,13 +503,44 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
// Foreground Notification
public void startForegroundWithNotification(int id, Notification notification) {
Log.d(TAG, "Starting foreground with notification id: " + id);
startForeground(id, notification);
// Always run on main thread
if (Looper.myLooper() != Looper.getMainLooper()) {
new Handler(Looper.getMainLooper())
.post(() -> startForegroundWithNotification(id, notification));
return;
}
Log.d(TAG, "Starting call FGS with notification id: " + id);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
int types =
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
| ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
startForeground(id, notification, types);
} else {
startForeground(id, notification);
}
} catch (Exception e) {
Log.e(TAG, "startForeground failed", e);
if (callCoordinator != null) {
callCoordinator.reportError("Failed to activate call FGS: " + e.getMessage());
}
}
}
public void stopForegroundAndDismiss() {
Log.d(TAG, "Stopping foreground and dismissing notification");
stopForeground(STOP_FOREGROUND_REMOVE);
if (Looper.myLooper() != Looper.getMainLooper()) {
new Handler(Looper.getMainLooper()).post(this::stopForegroundAndDismiss);
return;
}
Log.d(TAG, "Stopping call FGS and dismissing notification");
try {
stopForeground(STOP_FOREGROUND_REMOVE);
} catch (Exception e) {
Log.w(TAG, "stopForeground failed", e);
}
}
// Cleanup
@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BitmapUtil;
public class CallUtil {
private static final String TAG = CallUtil.class.getSimpleName();
private static final String TAG = "CallUtil";
@RequiresApi(api = Build.VERSION_CODES.O)
public static void startAudioCall(Context context, int chatId) {
@@ -13,13 +13,14 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.Observer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.webrtc.PeerConnection;
import org.webrtc.VideoTrack;
@RequiresApi(api = Build.VERSION_CODES.O)
public class CallViewModel extends AndroidViewModel {
private static final String TAG = CallViewModel.class.getSimpleName();
private static final String TAG = "CallViewModel";
private final CallCoordinator callCoordinator;
@@ -36,8 +37,10 @@ public class CallViewModel extends AndroidViewModel {
private final LiveData<String> displayName;
private final LiveData<Icon> displayIcon;
private final LiveData<Boolean> outgoingCallPlaced;
private final LiveData<Boolean> answeredElsewhere;
private final LiveData<CallEndpointCompat> currentAudioEndpoint;
private final LiveData<List<CallEndpointCompat>> availableAudioEndpoints;
private final LiveData<Boolean> isFrontCamera;
// Translated from coordinator's connectionState
private final MediatorLiveData<CallState> callState;
@@ -46,7 +49,7 @@ public class CallViewModel extends AndroidViewModel {
private Observer<VideoTrack> answerCallObserver;
private Observer<VideoTrack> startOutgoingCallObserver;
private boolean hasCallEnded = false;
private final AtomicBoolean hasCallEnded = new AtomicBoolean(false);
// User-facing call states
public enum CallState {
@@ -56,6 +59,7 @@ public class CallViewModel extends AndroidViewModel {
CONNECTING,
CONNECTED,
RECONNECTING,
ANSWERED_ELSEWHERE,
ENDED,
ERROR
}
@@ -77,8 +81,10 @@ public class CallViewModel extends AndroidViewModel {
this.displayName = callCoordinator.getDisplayName();
this.displayIcon = callCoordinator.getDisplayIcon();
this.outgoingCallPlaced = callCoordinator.getOutgoingCallPlaced();
this.answeredElsewhere = callCoordinator.getAnsweredElsewhere();
this.currentAudioEndpoint = callCoordinator.getCurrentAudioEndpoint();
this.availableAudioEndpoints = callCoordinator.getAvailableAudioEndpoints();
this.isFrontCamera = callCoordinator.getIsFrontCamera();
this.callState = new MediatorLiveData<>(CallState.INITIALIZING);
@@ -105,6 +111,10 @@ public class CallViewModel extends AndroidViewModel {
callState.addSource(
callCoordinator.getConnectionState(),
state -> {
if (callState.getValue() == CallState.ANSWERED_ELSEWHERE) {
return;
}
CallState newState = translateConnectionState(state);
if (callState.getValue() != newState) {
@@ -113,9 +123,7 @@ public class CallViewModel extends AndroidViewModel {
if (state == PeerConnection.PeerConnectionState.FAILED
|| state == PeerConnection.PeerConnectionState.CLOSED) {
if (!hasCallEnded) {
hasCallEnded = true;
}
hasCallEnded.set(true);
}
});
@@ -128,6 +136,15 @@ public class CallViewModel extends AndroidViewModel {
}
}
});
callState.addSource(
answeredElsewhere,
value -> {
if (Boolean.TRUE.equals(value)) {
hasCallEnded.set(true);
callState.setValue(CallState.ANSWERED_ELSEWHERE);
}
});
}
private CallState translateConnectionState(PeerConnection.PeerConnectionState state) {
@@ -257,11 +274,10 @@ public class CallViewModel extends AndroidViewModel {
public void declineCall() {
Log.d(TAG, "declineCall");
if (hasCallEnded) {
if (!hasCallEnded.compareAndSet(false, true)) {
Log.w(TAG, "Call already ended");
return;
}
hasCallEnded = true;
callCoordinator.declineCall();
}
@@ -269,11 +285,10 @@ public class CallViewModel extends AndroidViewModel {
public void hangUp() {
Log.d(TAG, "hangUp");
if (hasCallEnded) {
if (!hasCallEnded.compareAndSet(false, true)) {
Log.w(TAG, "Call already ended");
return;
}
hasCallEnded = true;
callCoordinator.hangUp();
}
@@ -354,8 +369,7 @@ public class CallViewModel extends AndroidViewModel {
public void onCallDisconnected(DisconnectCause disconnectCause) {
Log.d(TAG, "onCallDisconnected callback from CallControlScope, cause: " + disconnectCause);
if (!hasCallEnded) {
hasCallEnded = true;
if (hasCallEnded.compareAndSet(false, true)) {
callState.postValue(CallState.ENDED);
}
}
@@ -414,6 +428,10 @@ public class CallViewModel extends AndroidViewModel {
return availableAudioEndpoints;
}
public LiveData<Boolean> getIsFrontCamera() {
return isFrontCamera;
}
// Notification Action Handlers
public void handleNotificationAnswer() {
@@ -1,9 +1,14 @@
package org.thoughtcrime.securesms.calls;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.EglUtils;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
@@ -19,7 +24,7 @@ import org.webrtc.VideoTrack;
public class MediaStreamManager {
private static final String TAG = MediaStreamManager.class.getSimpleName();
private static final String TAG = "MediaStreamManager";
private static final String STREAM_ID = "local_stream";
private static final String AUDIO_TRACK_ID = "audio_track";
private static final String VIDEO_TRACK_ID = "video_track";
@@ -31,6 +36,7 @@ public class MediaStreamManager {
private VideoSource videoSource;
private AudioSource audioSource;
private SurfaceTextureHelper surfaceTextureHelper;
private volatile boolean isFrontCamera = true;
public interface Callback {
void onMediaStreamReady(MediaStream stream);
@@ -38,6 +44,12 @@ public class MediaStreamManager {
void onError(String error);
}
public interface CameraSwitchCallback {
void onCameraSwitch(boolean isFrontCamera);
void onError(String error);
}
public MediaStreamManager(@NonNull Context context, PeerConnectionFactory peerConnectionFactory) {
this.context = context.getApplicationContext();
@@ -45,6 +57,7 @@ public class MediaStreamManager {
}
/** Create media stream with audio and optionally video */
@RequiresApi(api = Build.VERSION_CODES.M)
public void createMediaStream(Callback callback) {
try {
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream(STREAM_ID);
@@ -83,6 +96,12 @@ public class MediaStreamManager {
@Nullable
private VideoCapturer createVideoCapturer() {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "Camera permission not granted");
return null;
}
Camera2Enumerator enumerator = new Camera2Enumerator(context);
// Try front camera first
@@ -91,6 +110,7 @@ public class MediaStreamManager {
if (enumerator.isFrontFacing(deviceName)) {
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer != null) {
isFrontCamera = true;
return capturer;
}
}
@@ -100,6 +120,7 @@ public class MediaStreamManager {
for (String deviceName : deviceNames) {
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer != null) {
isFrontCamera = enumerator.isFrontFacing(deviceName);
return capturer;
}
}
@@ -107,12 +128,61 @@ public class MediaStreamManager {
return null;
}
public void switchCamera() {
if (videoCapturer instanceof CameraVideoCapturer) {
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
cameraVideoCapturer.switchCamera(null);
Log.d(TAG, "Camera switched");
public void switchCamera(@Nullable CameraSwitchCallback callback) {
if (!(videoCapturer instanceof CameraVideoCapturer)) {
Log.e(TAG, "switchCamera called but videoCapturer is not a CameraVideoCapturer");
return;
}
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
// Find the opposite-facing camera
Camera2Enumerator enumerator = new Camera2Enumerator(context);
String[] deviceNames = enumerator.getDeviceNames();
String targetCameraName = null;
for (String deviceName : deviceNames) {
boolean isTargetFront = !isFrontCamera;
boolean deviceIsFront = enumerator.isFrontFacing(deviceName);
if (deviceIsFront == isTargetFront) {
targetCameraName = deviceName;
break; // Take the first match
}
}
if (targetCameraName == null) {
Log.e(TAG, "No camera found with opposite facing direction");
if (callback != null) {
callback.onError("No opposite camera available");
}
return;
}
final String finalTargetCameraName = targetCameraName;
Log.d(TAG, "Switching to camera: " + finalTargetCameraName);
// Call with explicit camera name
cameraVideoCapturer.switchCamera(
new CameraVideoCapturer.CameraSwitchHandler() {
@Override
public void onCameraSwitchDone(boolean isFront) {
Log.d(TAG, "switchCamera SUCCESS, isFront=" + isFront);
isFrontCamera = isFront;
if (callback != null) callback.onCameraSwitch(isFront);
}
@Override
public void onCameraSwitchError(String errorDescription) {
Log.e(TAG, "switchCamera FAILED: " + errorDescription);
if (callback != null) callback.onError(errorDescription);
}
},
finalTargetCameraName);
}
public boolean isFrontCamera() {
return isFrontCamera;
}
/** Cleanup resources */
@@ -26,6 +26,7 @@ import androidx.loader.app.LoaderManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ViewUtil;
public class AttachmentTypeSelector extends PopupWindow {
@@ -45,7 +46,7 @@ public class AttachmentTypeSelector extends PopupWindow {
private final @NonNull ImageView imageButton;
private final @NonNull ImageView documentButton;
private final @NonNull ImageView contactButton;
// private final @NonNull ImageView cameraButton;
// private final @NonNull ImageView cameraButton;
private final @NonNull ImageView videoButton;
private final @NonNull ImageView locationButton;
private final @NonNull ImageView webxdcButton;
@@ -87,11 +88,13 @@ public class AttachmentTypeSelector extends PopupWindow {
this.webxdcButton.setOnClickListener(new PropagatingClickListener(ADD_WEBXDC));
this.recentRail.setListener(new RecentPhotoSelectedListener());
// disable location streaming button for now
// if (!Prefs.isLocationStreamingEnabled(context)) {
this.locationButton.setVisibility(View.GONE);
ViewUtil.findById(layout, R.id.location_button_label).setVisibility(View.GONE);
// }
if (!Prefs.isLocationStreamingEnabled(context)) {
ViewUtil.findById(layout, R.id.location_linear_layout).setVisibility(View.GONE);
}
if (DcHelper.getContext(context).getChat(chatId).isOutBroadcast()) {
ViewUtil.findById(layout, R.id.webxdc_linear_layout).setVisibility(View.GONE);
}
setLocationButtonImage(context);
@@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DateUtils;
public class CallItemView extends FrameLayout {
private static final String TAG = CallItemView.class.getSimpleName();
private static final String TAG = "CallItemView";
private final @NonNull ImageView icon;
private final @NonNull TextView title;
@@ -1,31 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;
import org.thoughtcrime.securesms.components.viewpager.HackyViewPager;
/** An implementation of {@link ViewPager} that disables swiping when the view is disabled. */
public class ControllableViewPager extends HackyViewPager {
public ControllableViewPager(@NonNull Context context) {
super(context);
}
public ControllableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return isEnabled() && super.onTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isEnabled() && super.onInterceptTouchEvent(ev);
}
}
@@ -43,7 +43,7 @@ public class InputPanel extends ConstraintLayout
KeyboardAwareLinearLayout.OnKeyboardShownListener,
MediaKeyboard.MediaKeyboardListener {
private static final String TAG = InputPanel.class.getSimpleName();
private static final String TAG = "InputPanel";
private static final int FADE_TIME = 150;
@@ -38,7 +38,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
* been opened and what its height would be.
*/
public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
private static final String TAG = KeyboardAwareLinearLayout.class.getSimpleName();
private static final String TAG = "KeyboardAwareLinearLayout";
private static final long KEYBOARD_DEBOUNCE = 150;
@@ -38,7 +38,7 @@ import org.thoughtcrime.securesms.util.Util;
public class QuoteView extends FrameLayout implements RecipientForeverObserver {
private static final String TAG = QuoteView.class.getSimpleName();
private static final String TAG = "QuoteView";
private static final int MESSAGE_TYPE_PREVIEW = 0;
@@ -30,7 +30,7 @@ public class ScaleStableImageView extends AppCompatImageView
implements KeyboardAwareLinearLayout.OnKeyboardShownListener,
KeyboardAwareLinearLayout.OnKeyboardHiddenListener {
private static final String TAG = ScaleStableImageView.class.getSimpleName();
private static final String TAG = "ScaleStableImageView";
private Drawable defaultDrawable;
private Drawable currentDrawable;
@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
public class SearchToolbar extends LinearLayout {
private static final String TAG = SearchToolbar.class.getSimpleName();
private static final String TAG = "SearchToolbar";
private float x, y;
private MenuItem searchItem;
private EditText searchText;
@@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.util.Util;
public class ThumbnailView extends FrameLayout {
private static final String TAG = ThumbnailView.class.getSimpleName();
private static final String TAG = "ThumbnailView";
private static final int WIDTH = 0;
private static final int HEIGHT = 1;
private static final int MIN_WIDTH = 0;
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.recipients.Recipient;
public class VcardView extends FrameLayout {
private static final String TAG = VcardView.class.getSimpleName();
private static final String TAG = "VcardView";
private final @NonNull AvatarView avatar;
private final @NonNull TextView name;
@@ -6,22 +6,26 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.session.MediaController;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AudioPlaybackViewModel extends ViewModel {
private static final String TAG = AudioPlaybackViewModel.class.getSimpleName();
private static final String TAG = "AudioPlaybackViewModel";
private static final int NON_MESSAGE_AUDIO_MSG_ID =
0; // Audios not attached to a message doesn't have message id.
@@ -34,6 +38,8 @@ public class AudioPlaybackViewModel extends ViewModel {
private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2);
private @Nullable MediaController mediaController;
private @Nullable ChatAudioQueueProvider queueProvider;
private @Nullable Player.Listener playerListener;
private final Handler handler;
private boolean isUserSeeking = false;
@@ -47,6 +53,11 @@ public class AudioPlaybackViewModel extends ViewModel {
}
public void setMediaController(@Nullable MediaController controller) {
if (this.mediaController != null && playerListener != null) {
this.mediaController.removeListener(playerListener);
}
playerListener = null;
this.mediaController = controller;
if (mediaController != null && mediaController.isPlaying()) {
startUpdateProgress();
@@ -59,30 +70,41 @@ public class AudioPlaybackViewModel extends ViewModel {
public void loadAudioAndPlay(int msgId, Uri audioUri) {
if (mediaController == null) return;
// Set media item if we have a different audio.
if (isDifferentAudio(msgId, audioUri)) {
updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0);
String mediaId = String.valueOf(msgId);
MediaItem mediaItem =
new MediaItem.Builder().setMediaId(String.valueOf(msgId)).setUri(audioUri).build();
mediaController.setMediaItem(mediaItem);
mediaController.prepare();
MediaItem current = mediaController.getCurrentMediaItem();
if (current != null && mediaId.equals(current.mediaId)) {
mediaController.play();
return;
}
play(msgId, audioUri);
updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0);
List<MediaItem> items = null;
int startIndex = -1;
if (queueProvider != null) {
items = queueProvider.buildAudioQueue();
startIndex = indexOfMediaId(items, mediaId);
}
if (startIndex < 0) {
items =
Collections.singletonList(
new MediaItem.Builder().setMediaId(mediaId).setUri(audioUri).build());
startIndex = 0;
}
mediaController.setMediaItems(items, startIndex, 0);
mediaController.prepare();
mediaController.play();
}
private boolean isSameAudio(int msgId, Uri audioUri) {
return !isDifferentAudio(msgId, audioUri);
}
private boolean isDifferentAudio(int msgId, Uri audioUri) {
AudioPlaybackState currentState = playbackState.getValue();
return currentState != null
&& (msgId != currentState.getMsgId()
|| currentState.getAudioUri() == null
|| currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri));
private static int indexOfMediaId(List<MediaItem> items, String mediaId) {
for (int i = 0; i < items.size(); i++) {
if (mediaId.equals(items.get(i).mediaId)) return i;
}
return -1;
}
public LiveData<Map<Integer, Long>> getDurations() {
@@ -138,46 +160,72 @@ public class AudioPlaybackViewModel extends ViewModel {
}
}
public void pause(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
public void pause(int msgId) {
if (isCurrentItem(msgId)) {
mediaController.pause();
}
}
public void play(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
public void play(int msgId) {
if (isCurrentItem(msgId)) {
mediaController.play();
}
}
public void seekTo(long position, int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
public void seekTo(long position, int msgId) {
if (isCurrentItem(msgId)) {
mediaController.seekTo(position);
}
}
public void stop(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
public void stop(int msgId) {
if (isCurrentItem(msgId)) {
mediaController.stop();
mediaController.clearMediaItems();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
private boolean isCurrentItem(int msgId) {
if (mediaController == null) return false;
MediaItem current = mediaController.getCurrentMediaItem();
return current != null && String.valueOf(msgId).equals(current.mediaId);
}
public void stopNonMessageAudioPlayback() {
stopByIds(NON_MESSAGE_AUDIO_MSG_ID);
}
// A special method for deleting message, where we only use message Ids
public void stopByIds(int... msgIds) {
AudioPlaybackState currentState = playbackState.getValue();
if (mediaController == null) return;
if (mediaController != null && currentState != null) {
AudioPlaybackState currentState = playbackState.getValue();
boolean stoppedCurrent = false;
if (currentState != null) {
for (int msgId : msgIds) {
if (msgId == currentState.getMsgId()) {
mediaController.stop();
mediaController.clearMediaItems();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
stoppedCurrent = true;
break;
}
}
}
if (!stoppedCurrent) {
Set<String> deletedMediaIds = new HashSet<>();
for (int msgId : msgIds) {
deletedMediaIds.add(String.valueOf(msgId));
}
for (int i = mediaController.getMediaItemCount() - 1; i >= 0; i--) {
MediaItem item = mediaController.getMediaItemAt(i);
if (deletedMediaIds.contains(item.mediaId)) {
mediaController.removeMediaItem(i);
}
}
}
@@ -191,10 +239,10 @@ public class AudioPlaybackViewModel extends ViewModel {
private void setupPlayerListener() {
if (mediaController == null) return;
mediaController.addListener(
playerListener =
new Player.Listener() {
@Override
public void onEvents(Player player, Player.Events events) {
public void onEvents(@NonNull Player player, @NonNull Player.Events events) {
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
if (player.isPlaying()) {
startUpdateProgress();
@@ -203,20 +251,40 @@ public class AudioPlaybackViewModel extends ViewModel {
}
updateCurrentState(false);
}
if (events.containsAny(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
updateCurrentState(true);
}
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
if (player.getPlaybackState() == Player.STATE_READY) {
updateCurrentState(false);
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
// This is to prevent automatically playing after the audio
// has been play to the end once, then user dragged the seek bar again
} else if (player.getPlaybackState() == Player.STATE_ENDED
&& !player.hasNextMediaItem()) {
mediaController.setPlayWhenReady(false);
}
}
if (events.containsAny(Player.EVENT_PLAYER_ERROR)) {
}
@Override
public void onPlayerError(@NonNull PlaybackException error) {
Log.w(
TAG,
"Playback error on msgId="
+ (mediaController.getCurrentMediaItem() != null
? mediaController.getCurrentMediaItem().mediaId
: "null"),
error);
if (mediaController.hasNextMediaItem()) {
mediaController.seekToNextMediaItem();
mediaController.prepare();
mediaController.play();
} else {
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0);
mediaController.clearMediaItems();
}
}
});
};
mediaController.addListener(playerListener);
}
private void updateCurrentState(boolean queryPlaying) {
@@ -285,6 +353,11 @@ public class AudioPlaybackViewModel extends ViewModel {
}
}
// Playing Queue
public void setQueueProvider(@Nullable ChatAudioQueueProvider provider) {
this.queueProvider = provider;
}
// Progress tracking
private final Runnable progressRunnable =
new Runnable() {
@@ -23,7 +23,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
public class AudioView extends FrameLayout {
private static final String TAG = AudioView.class.getSimpleName();
private static final String TAG = "AudioView";
private final @NonNull ImageView playPauseButton;
private final AnimatedVectorDrawableCompat playToPauseDrawable;
@@ -109,12 +109,12 @@ public class AudioView extends FrameLayout {
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) {
if (state != null && msgId == state.getMsgId()) {
// Same audio
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
viewModel.pause(msgId, audioUri);
viewModel.pause(msgId);
} else {
viewModel.play(msgId, audioUri);
viewModel.play(msgId);
}
} else {
// Different audio
@@ -145,7 +145,7 @@ public class AudioView extends FrameLayout {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
viewModel.setUserSeeking(false);
viewModel.seekTo(seekBar.getProgress(), msgId, audioUri);
viewModel.seekTo(seekBar.getProgress(), msgId);
}
});
@@ -305,7 +305,7 @@ public class AudioView extends FrameLayout {
if (audioUri == null || state == null) return;
// Check if this state is about this message
boolean isThisMessage = msgId == state.getMsgId() && audioUri.equals(state.getAudioUri());
boolean isThisMessage = msgId == state.getMsgId();
if (isThisMessage) {
updateUIForPlaybackState(state);
@@ -340,13 +340,9 @@ public class AudioView extends FrameLayout {
private void onDurationsChanged(Map<Integer, Long> durations) {
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
// When there is no playback happening, msgId can be -1 and audioUri is null
if (state != null
&& msgId >= 0
&& msgId == state.getMsgId()
&& audioUri != null
&& audioUri.equals(state.getAudioUri())) {
return; // Is playing this message
// When there is no playback happening, msgId can be -1
if (state != null && msgId >= 0 && msgId == state.getMsgId()) {
return;
}
Long duration = durations.get(msgId);
@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.media3.common.MediaItem;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import java.util.ArrayList;
import java.util.List;
import org.thoughtcrime.securesms.connect.DcHelper;
public class ChatAudioQueueProvider {
private final Context context;
private final int chatId;
private final int accountId;
public ChatAudioQueueProvider(@NonNull Context context, int chatId, int accountId) {
this.context = context.getApplicationContext();
this.chatId = chatId;
this.accountId = accountId;
}
@NonNull
public List<MediaItem> buildAudioQueue() {
DcContext dcContext = DcHelper.getContext(context);
int[] msgIds = dcContext.getChatMedia(chatId, DcMsg.DC_MSG_AUDIO, DcMsg.DC_MSG_VOICE, 0);
List<MediaItem> items = new ArrayList<>(msgIds.length);
for (int msgId : msgIds) {
String id = String.valueOf(msgId);
items.add(
new MediaItem.Builder()
.setMediaId(id)
.setUri(Uri.parse("dcmsg://" + accountId + "/" + id))
.build());
}
return items;
}
}
@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.util.ResUtil;
public class MediaKeyboard extends LinearLayout
implements InputView, Consumer<EmojiViewItem>, StickerPickerView.StickerPickerListener {
private static final String TAG = MediaKeyboard.class.getSimpleName();
private static final String TAG = "MediaKeyboard";
@Nullable private MediaKeyboardListener keyboardListener;
private EmojiPickerView emojiPicker;
@@ -71,7 +71,7 @@ public class StickerPickerView extends RecyclerView {
}
// Sort stickers just to provide consistent order
Collections.sort(stickerFiles, (f1, f2) -> f1.getName().compareToIgnoreCase(f2.getName()));
Collections.sort(stickerFiles, (f1, f2) -> f2.getName().compareToIgnoreCase(f1.getName()));
return stickerFiles;
}
@@ -23,7 +23,7 @@ import org.thoughtcrime.securesms.util.Prefs;
@SuppressLint("BatteryLife")
public class DozeReminder {
private static final String TAG = DozeReminder.class.getSimpleName();
private static final String TAG = "DozeReminder";
public static boolean isEligible(Context context) {
if (context == null) {
@@ -1,8 +1,8 @@
package org.thoughtcrime.securesms.components.viewpager;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager2.widget.ViewPager2;
public abstract class ExtendedOnPageChangedListener implements ViewPager.OnPageChangeListener {
public abstract class ExtendedOnPageChangedListener extends ViewPager2.OnPageChangeCallback {
private Integer currentPage = null;
@@ -1,41 +0,0 @@
package org.thoughtcrime.securesms.components.viewpager;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import androidx.viewpager.widget.ViewPager;
/**
* Hacky fix for http://code.google.com/p/android/issues/detail?id=18990
*
* <p>ScaleGestureDetector seems to mess up the touch events, which means that ViewGroups which make
* use of onInterceptTouchEvent throw a lot of IllegalArgumentException: pointerIndex out of range.
*
* <p>There's not much I can do in my code for now, but we can mask the result by just catching the
* problem and ignoring it.
*
* @author Chris Banes
*/
public class HackyViewPager extends ViewPager {
private static final String TAG = HackyViewPager.class.getSimpleName();
public HackyViewPager(Context context) {
super(context);
}
public HackyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
Log.w(TAG, e);
return false;
}
}
}
@@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.accounts.AccountSelectionListFragment;
public class AccountManager {
private static final String TAG = AccountManager.class.getSimpleName();
private static final String TAG = "AccountManager";
private static final String LAST_ACCOUNT_ID = "last_account_id";
private static AccountManager self;
@@ -38,7 +38,7 @@ public class AccountManager {
return self;
}
public void migrateToDcAccounts(ApplicationContext context) {
public void migrateToDcAccounts(Context context, DcAccounts dcAccounts) {
try {
int selectAccountId = 0;
@@ -49,8 +49,7 @@ public class AccountManager {
if (!file.isDirectory()
&& file.getName().startsWith("messenger")
&& file.getName().endsWith(".db")) {
int accountId =
ApplicationContext.getDcAccounts().migrateAccount(file.getAbsolutePath());
int accountId = dcAccounts.migrateAccount(file.getAbsolutePath());
if (accountId != 0) {
String selName =
PreferenceManager.getDefaultSharedPreferences(context)
@@ -67,7 +66,7 @@ public class AccountManager {
}
if (selectAccountId != 0) {
ApplicationContext.getDcAccounts().selectAccount(selectAccountId);
dcAccounts.selectAccount(selectAccountId);
}
} catch (Exception e) {
Log.e(TAG, "Error in migrateToDcAccounts()", e);
@@ -5,7 +5,6 @@ import androidx.annotation.NonNull;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import org.thoughtcrime.securesms.util.AsyncLoader;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
@@ -53,10 +52,7 @@ public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
}
if (query == null && addCreateGroupLinks) {
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_GROUP);
final boolean broadcastsEnabled = Prefs.isNewBroadcastAvailable(getContext());
if (broadcastsEnabled)
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST);
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST);
if (!dcContext.isChatmail()) {
additional_items =
@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.service.FetchForegroundService;
import org.thoughtcrime.securesms.util.Util;
public class DcEventCenter {
private static final String TAG = DcEventCenter.class.getSimpleName();
private static final String TAG = "DcEventCenter";
private @NonNull final Hashtable<Integer, ArrayList<DcEventDelegate>> currentAccountObservers =
new Hashtable<>();
private @NonNull final Hashtable<Integer, ArrayList<DcEventDelegate>> multiAccountObservers =
@@ -42,7 +42,7 @@ import org.thoughtcrime.securesms.util.Util;
public class DcHelper {
private static final String TAG = DcHelper.class.getSimpleName();
private static final String TAG = "DcHelper";
public static final String CONFIG_CONFIGURED_ADDRESS = "configured_addr";
public static final String CONFIG_DISPLAY_NAME = "displayname";
@@ -145,8 +145,6 @@ public class DcHelper {
dcContext.setStockTranslation(91, context.getString(R.string.devicemsg_self_deleted));
dcContext.setStockTranslation(97, context.getString(R.string.forwarded));
dcContext.setStockTranslation(98, context.getString(R.string.devicemsg_storage_exceeding));
dcContext.setStockTranslation(99, context.getString(R.string.n_bytes_message));
dcContext.setStockTranslation(100, context.getString(R.string.download_max_available_until));
dcContext.setStockTranslation(103, context.getString(R.string.incoming_messages));
dcContext.setStockTranslation(104, context.getString(R.string.outgoing_messages));
dcContext.setStockTranslation(107, context.getString(R.string.connectivity_connected));
@@ -227,6 +225,8 @@ public class DcHelper {
dcContext.setStockTranslation(201, context.getString(R.string.qrshow_join_channel_hint));
dcContext.setStockTranslation(202, context.getString(R.string.you_joined_the_channel));
dcContext.setStockTranslation(203, context.getString(R.string.secure_join_channel_started));
dcContext.setStockTranslation(204, context.getString(R.string.channel_name_changed));
dcContext.setStockTranslation(205, context.getString(R.string.channel_image_changed));
dcContext.setStockTranslation(210, context.getString(R.string.stats_msg_body));
dcContext.setStockTranslation(220, context.getString(R.string.proxy_enabled));
dcContext.setStockTranslation(221, context.getString(R.string.proxy_enabled_hint));

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