Compare commits

...

158 Commits

Author SHA1 Message Date
genofire aa8b9f338f fix usage directTLS of manuelle enter an address 2021-12-11 16:42:31 +01:00
genofire 658c1c58d5 Merge remote-tracking branch 'upstream/master' into develop 2021-11-25 21:20:44 +01:00
genofire 39792f0815 fix Language merge 2021-11-20 22:37:21 +01:00
Daniel Gultsch db834a1f07 indicate call reconnect in notification 2021-11-19 12:26:11 +01:00
Daniel Gultsch f8a94161db don't play tone going from connect->reconnect->connect 2021-11-19 12:25:27 +01:00
Daniel Gultsch 5d526a77e3 include uncertainty into shared geo uri 2021-11-18 11:24:10 +01:00
Daniel Gultsch a508a81553 externalize rtc config generation into seperate method 2021-11-17 11:33:15 +01:00
Daniel Gultsch 61fb38cd84 clean up some error handling error ICE restarts 2021-11-17 10:49:16 +01:00
Daniel Gultsch 1bf2d5dd8f video calls: leave full screen mode during reconnect 2021-11-16 22:01:48 +01:00
Daniel Gultsch 0a18c8613f assume credentials are the same for all contents when restarting ICE 2021-11-16 17:08:34 +01:00
Daniel Gultsch abb671616c synchronize setDescription calls 2021-11-16 15:17:12 +01:00
Daniel Gultsch 297a843b9c use implicit rollback (needed to be enabled on libwebrtc) 2021-11-16 13:17:10 +01:00
Daniel Gultsch 0698fa0d8c store peer dtls setup for later use in ice restart 2021-11-16 11:21:11 +01:00
Daniel Gultsch 70b5d8d81a set proper peer dtls setup on ice restart received 2021-11-15 21:49:31 +01:00
Daniel Gultsch 0a3947b8e3 terminate with application failure when failing to apply ICE restart
This is fairly unlikely to happen in practice
2021-11-15 17:18:45 +01:00
Daniel Gultsch 3f402b132b respond with tie-break to prevent ICE restart race 2021-11-15 13:03:04 +01:00
Daniel Gultsch 5b80c62a63 treat transport-info w/o candidates and changed credentials as offer 2021-11-14 18:22:18 +01:00
Daniel Gultsch 717c83753f delay discovered ice candidates until JingleRtpConnection is ready to receive
otherwise setLocalDescription and the arrival of first candidates might race
the rtpContentDescription being set
2021-11-11 21:02:17 +01:00
Daniel Gultsch b6dee6da6a reverse: webrtc: include oldState in onConnectionChange
turns out we don’t need it and a better way is for RtpConnection to keep track of *all*
states in the current generation
2021-11-11 17:05:36 +01:00
Daniel Gultsch 9c3f55bef2 use stopwatch to keep track of jingle rtp session duration 2021-11-11 16:52:18 +01:00
Daniel Gultsch 9843b72f6f always use Camera2Enumerator 2021-11-11 15:23:45 +01:00
Daniel Gultsch 61851e5f84 do not automacially hang up failed webrtc sessions 2021-11-11 14:40:15 +01:00
Daniel Gultsch 4ec0996dff webrtc: include oldState in onConnectionChange 2021-11-11 11:19:37 +01:00
genofire fa1363cea0 Merge tag '2.10.2' into develop 2021-11-11 00:28:16 +01:00
Daniel Gultsch fda45a7c86 use implicit descriptions for WebRTC
using the parameter-free form of setLocalDescription() solves some potential race conditions
that can occur - especially once we introduce restartIce()
2021-11-10 16:40:24 +01:00
Daniel Gultsch b5786787f0 bump libphone number library 2021-11-09 14:27:26 +01:00
Daniel Gultsch d4cbf2e11e take intent type into account when sharing with conversations 2021-11-07 11:35:00 +01:00
Daniel Gultsch 7d7e158fd7 code clean up for LocationProvider 2021-11-03 16:00:26 +01:00
Daniel Gultsch a8ff88398d version bump to 2.10.2 + changelog 2021-11-03 15:59:05 +01:00
Daniel Gultsch bae9fc45c2 make rtcpMux optional 2021-10-31 10:20:58 +01:00
Daniel Gultsch ba4a47204b fixed IndexOutOfBounds when rendering quotes 2021-10-31 10:20:34 +01:00
Daniel Gultsch 226eb739bd make custom 'xmpp' protocol in address book case insensitve
fixes #4215
2021-10-31 08:35:44 +01:00
Daniel Gultsch 869a135bab bump okhttp 2021-10-20 10:19:59 +02:00
Daniel Gultsch 7ddd60d314 bump jxmpp. fix crash in magic create when entering @ 2021-10-20 10:19:46 +02:00
Daniel Gultsch 2ca00265db bump speed dial version to something that uses AndroidX 2021-10-20 09:52:10 +02:00
Daniel Gultsch e0c4964cc2 bump gradle plugin version 2021-10-14 17:32:07 +02:00
Daniel Gultsch 3706981645 fix mime type detection in urls that have query params or an anchor 2021-10-14 17:30:55 +02:00
genofire a124b3df9a metadata: add changelog 2.10.1 2021-10-13 18:19:29 +02:00
genofire 5a7bae592d Merge tag '2.10.1' into develop 2021-10-13 18:14:50 +02:00
Daniel Gultsch 6d2e406ee5 attempt to parse Link header from https url scanned from welcome screen 2021-10-07 09:48:49 +02:00
Daniel Gultsch cc489ef7bf bump version code 2021-10-06 12:33:36 +02:00
Daniel Gultsch 495537d087 minor code cleanup in UriHandlerActivity 2021-10-06 12:18:58 +02:00
Daniel Gultsch 20e4d108d4 fixed regression of not handling jingle content map parsing failures 2021-10-05 15:43:05 +02:00
Daniel Gultsch d0af5a002e leave code comment about xmpp vs jabber vcard entry 2021-10-04 15:18:37 +02:00
Daniel Gultsch 86de21f6a8 allow encrypted backups. fixes #4190 2021-10-04 14:17:01 +02:00
Daniel Gultsch e664a27cd0 fix typo in action matcher 2021-10-03 18:51:18 +02:00
Daniel Gultsch 4a6df90f0c attempt to read both jabber and xmpp IM fields from address book 2021-10-03 17:26:24 +02:00
Daniel Gultsch fdaab1c27e remove unused import in favor of fqn 2021-10-03 17:01:51 +02:00
Daniel Gultsch f8c59a7b75 support imto://xmpp intents 2021-10-03 17:01:32 +02:00
Daniel Gultsch f182fe6697 use PM on direct reply if last message in notifacation stack is PM 2021-10-03 16:38:30 +02:00
Daniel Gultsch daf1bbfca5 bump version code 2021-10-02 19:49:18 +02:00
Daniel Gultsch b8eec6ae5b pulled translations from transifex 2021-10-02 16:59:39 +02:00
Daniel Gultsch 3ede2d00bd remove logging 2021-10-02 16:54:19 +02:00
Daniel Gultsch d2a387e82f correctly calculate socks destination 2021-10-02 16:44:36 +02:00
Daniel Gultsch da14f83a42 ensure all bytes are read in socks handshake. fixes #4188 2021-10-02 14:24:36 +02:00
Daniel Gultsch 586fff5485 Quicksy: theme choose country activity 2021-09-29 10:51:25 +02:00
Daniel Gultsch ea9b73c1fe Quicksy: fix drawables not being styled in enter phone number screen 2021-09-29 10:42:26 +02:00
Daniel Gultsch e791e19265 ignore non letters when parsing action from xmpp uri 2021-09-27 11:15:56 +02:00
Daniel Gultsch 3de8147b41 pulled translations from transifex 2021-09-27 10:48:04 +02:00
Daniel Gultsch b9ceb67104 version bump to 2.10.1 + changelog 2021-09-24 09:25:27 +02:00
Daniel Gultsch 90a0d36362 fix not recognizing message as download. fixes #4178 2021-09-24 09:15:21 +02:00
Daniel Gultsch bd4d939a29 backport requireActivity method 2021-09-21 11:55:37 +02:00
Daniel Gultsch 64a6edd3fb Revert "Migrate Fragments to AndroidX"
This reverts commit 231d97ea81.

Migrating to AndroidX Fragments seems to have some unforseen side effects
and no clear benefits
2021-09-21 11:41:35 +02:00
Daniel Gultsch 75c20a7a2b handle on-device contacts with unstable system uri
on device contacts (contacts not synced) have an unstable system uri.
For quicksy.im contacts we can identify the contact based on the phone number
instead.

fixes #4174
2021-09-21 10:20:23 +02:00
Daniel Gultsch d5994a8d65 add to address book should add phone number for Quicksy+quicksy.im
fixes #4165
2021-09-21 10:19:09 +02:00
Daniel Gultsch 572b9c2dc6 pulled translations from transifex 2021-09-20 11:29:35 +02:00
Daniel Gultsch f9f994c540 Intent.EXTRA_ALLOW_MULTIPLE is now supported by minSdk 2021-09-20 10:08:11 +02:00
Daniel Gultsch ba9596b37d catch rare exception around execute pending fragment transactions 2021-09-20 10:07:38 +02:00
Daniel Gultsch b01bca74fd fix some linter warnings 2021-09-20 09:54:42 +02:00
Daniel Gultsch bfc8668803 bump appcompat version 2021-09-20 09:27:40 +02:00
Alexei Sorokin 951d84f404 make sure messages_index is always cleaned up fully. fixes #4170 2021-09-20 06:26:35 +00:00
Ashique Bava 231d97ea81
Migrate Fragments to AndroidX 2021-09-20 06:22:55 +00:00
Daniel Gultsch 73000962fe bump transcoder version
fixes #4167
2021-09-15 21:54:03 +02:00
Daniel Gultsch 3075833ab3 swap out transcoder library
the transcoder library we used hasn’t been updated in years

this commit switches to a maintained fork https://natario1.github.io/Transcoder/
2021-09-15 11:38:06 +02:00
Geno eba5dd8654 metadata: add changelog 2.10.0 2021-09-13 00:04:35 +02:00
Geno abe3718353 Merge tag '2.10.0' into develop 2021-09-13 00:02:16 +02:00
Daniel Gultsch 3f315751a1 version bump to 2.10.0 2021-09-11 10:28:34 +02:00
Daniel Gultsch 68d8e2b9cf delete targe file after unsuccessful image compression 2021-09-11 09:55:44 +02:00
Daniel Gultsch c195e8b3d2 run file observer on its own thread. fixes #4164 2021-09-10 19:07:57 +02:00
Daniel Gultsch 25f137441b catch security exception when viewing file from media preview 2021-09-10 18:46:37 +02:00
Daniel Gultsch d436c5f856 catch exception when trying to read display name. fixes #4163 2021-09-10 18:46:10 +02:00
Daniel Gultsch 8d9c51d755 pulled translations from transifex 2021-09-10 10:25:31 +02:00
Daniel Gultsch 2957bccb33 Revert "Fixing trailing characters treated as part of URI error (#3938)."
This reverts commit 8d45cc5827.
2021-09-08 21:30:21 +02:00
Daniel Gultsch 3135550b83 pulled translations from transifex 2021-09-08 20:53:11 +02:00
Millesimus 4d36231fa5 >.< should be quoteable (bugfix). 2021-09-08 16:29:01 +00:00
Millesimus dfeeaff74c >.< should not be rendered as quote (bugfix). 2021-09-08 16:29:01 +00:00
Daniel Gultsch 5a9777f7f1 version bump to 2.10.0-beta.2 + changelog 2021-09-08 11:13:22 +02:00
Daniel Gultsch 63f5f8c89d modify TODOs in JingleRtpConnection upon better understanding of the WebRTC stack 2021-09-08 10:47:34 +02:00
Millesimus ca08c27eef Parse IPv6 URIs (#3841). 2021-09-07 15:14:09 +00:00
Millesimus 4040d5f647 Treat dollar signs as URI chars (fixing #3859). 2021-09-07 15:14:09 +00:00
Millesimus 8d45cc5827 Fixing trailing characters treated as part of URI error (#3938). 2021-09-07 15:14:09 +00:00
Daniel Gultsch 96f0a09a5d pulled translations from transifex 2021-09-07 16:56:24 +02:00
Maximilian Weiler 38a77dbba6 Fix ImportBackupActivity not covered by screenshot prevention feature 2021-09-07 14:50:50 +00:00
Daniel Gultsch 8b817b3bd8 add database migration for new fts scheme 2021-09-07 16:47:40 +02:00
Alexei Sorokin 754773be55 match messages from the full-text index by rowid
"uuid" is a primary key in "messages" but not in "messages_index",
the implication of that is very slow matching by UUID.

What can be done instead is matching messages_index.rowid to messages.rowid,
that is, an always-present clustered index.

This not only improves performance of full-text search but also of just
updating messages in any shape or form.
2021-09-07 16:19:49 +02:00
Alexei Sorokin 4f362aafac make the fulltext index for search more space-efficient
It now uses the data from the messages table instead of having a copy of each
message.

The message UUIDs are no longer part of the index.
2021-09-07 16:19:40 +02:00
Daniel Gultsch ea0dc558cb use androidx ExifInterface to parse rotation. fixes #4154 2021-09-05 16:29:24 +02:00
Daniel Gultsch 80d8b6dd88
Upload APKs after CI 2021-08-27 07:47:55 +00:00
Daniel Gultsch 2819545a43 click on action bar title should open chat details screen 2021-08-25 20:04:47 +02:00
Daniel Gultsch 9526456d75 pulled translations from transifex 2021-08-25 18:57:20 +02:00
Daniel Gultsch f975b5ddac executePendingTransactions before trying to access secondary_fragment
If we don’t executePendingTransactions we might still access the overview fragment
while a replacement operation is in the works. This will lead to two
conversationfragments opening.
2021-08-25 18:54:03 +02:00
Daniel Gultsch 581eb511b9 version bump to 2.10.0-beta 2021-08-24 18:48:50 +02:00
Daniel Gultsch af42e34654 Revert "Always show Quote as last action"
This reverts commit e528b9f5df.

I was originally convinced by the argumentation (quote always in same place)
but testing this out for a while really seems to break 'last correct' for me.

I use that way more frequently that quote
2021-08-24 16:53:23 +02:00
Daniel Gultsch 0495470ca8 pulled translations from transifex 2021-08-24 15:32:10 +02:00
Daniel Gultsch 6c88a4b4fa reset affiliation when inviting someone not currently in group. fixes #4146 2021-08-24 14:42:50 +02:00
Daniel Gultsch 88d7ddf124 PIP aspect ratio should match video aspect ratio. fixes #4077 2021-08-24 14:40:12 +02:00
Daniel Gultsch e6d8bee035 stop agp7 complaining about missing proguard rules 2021-08-24 13:33:19 +02:00
Daniel Gultsch 208c9d91db dexOptions is no longer used in agp7 2021-08-24 13:02:31 +02:00
Licaon_Kter bf3c1d573b Avoid description repetition screenshots 2021-08-24 11:00:43 +00:00
Daniel Gultsch 4e90c0dbbb
Update android.yml to download webrtc 2021-08-24 09:21:17 +00:00
Daniel Gultsch caefec2fbf
Create android.yml 2021-08-24 09:19:08 +00:00
Daniel Gultsch 339ee8f6ea bump libwebrtc version to m92 2021-08-24 11:15:50 +02:00
Daniel Gultsch b00b8996d5 bump gradle version and agp 2021-08-24 10:13:03 +02:00
Maximilian Weiler 06fbb06aee Remove unused import 2021-08-24 07:15:30 +00:00
Maximilian Weiler ef8f10cc13 Optionally prevent taking screenshots
- Add setting to prevent screenshots
- Enforce using FLAG_SECURE in onResume for each activity
2021-08-24 07:15:30 +00:00
Millesimus b6fe1898e7 Minor duplication fix. 2021-08-24 07:14:34 +00:00
Millesimus 955a6f3fe1 Bugfix for 6cc06bcb98acc05c7677c642adf8ded90ffc8372. 2021-08-24 07:14:34 +00:00
Millesimus a0529a4e1e On quoting, translate French quotes to XEP-0393 quotes. 2021-08-24 07:14:34 +00:00
Millesimus 2db2ca95ce Move differentiation between XEP-0393 quotes and French quotes to QuoteHelper. 2021-08-24 07:14:34 +00:00
Millesimus a0bca08997 Rewrite QuoteHelper to integrate French quotes logics. Also reallow QuoteChars not followed by whitespace as indicated in XEP-0393. 2021-08-24 07:14:34 +00:00
Millesimus 748443cd4e Fixing message preview. 2021-08-24 07:14:34 +00:00
Millesimus 3921f3a940 QUOTING_MAX_DEPTH=1 for transitory compatibility with older versions. QUOTE_MAX_DEPTH=7 for performance testing and hiding of a rerendering bug occuring when two adjacent messages are merged. 2021-08-24 07:14:34 +00:00
Millesimus c81c8a62b3 Small refactoring for a more intuitive config. 2021-08-24 07:14:34 +00:00
Millesimus e850900b40 Quoting quotes, limited by nesting depth. 2021-08-24 07:14:34 +00:00
Millesimus 74d60d0131 Implement nested quotes through iteration. 2021-08-24 07:14:34 +00:00
Millesimus 65a72827bc New helper to help with quotes. 2021-08-24 07:14:34 +00:00
Millesimus 309082a9b3 Fixing xmpp:uri bug in channel details. #4139 2021-08-12 08:28:43 +00:00
Licaon_Kter e528b9f5df Always show Quote as last action 2021-08-12 08:28:36 +00:00
Daniel Gultsch 1e1dad780b add .opus file extension to mime table 2021-07-28 16:57:57 +02:00
Daniel Gultsch 0e54cde4bf add omemo media sharing to doap file 2021-07-23 08:04:36 +02:00
Alexei Sorokin b99f9d4f1c make search case-insensitive not only for ASCII 2021-05-26 06:16:20 +00:00
Daniel Gultsch 7466d12505 ring during device discovery 2021-05-22 19:37:20 +02:00
Daniel Gultsch 98ffadd87d log exception when file is not a ceb 2021-05-18 10:11:35 +02:00
Daniel Gultsch af33a57bf2 add description for text/plain 2021-05-17 16:00:00 +02:00
Daniel Gultsch 87f99d3570 Transferables interface needs to differentiate between 0 and null file size 2021-05-17 15:51:21 +02:00
Daniel Gultsch b025265f91 execute status code check on HEAD 2021-05-16 16:17:06 +02:00
Daniel Gultsch 0f3181555a FileParams indicate unavailable file size as null
since 0 is a valid file size we should use null to indicate absence
2021-05-16 15:32:57 +02:00
Daniel Gultsch 76fb0180d6 bump gradle plugin version 2021-05-16 15:29:45 +02:00
Daniel Gultsch e02aaed7d2 call SVGs 'vector graphic' instead of 'image' 2021-05-14 08:37:42 +02:00
Daniel Gultsch c9f1bdc551 fixup: update some libraries 2021-05-13 11:05:26 +02:00
Daniel Gultsch 2b9862adea update some libraries 2021-05-13 10:27:05 +02:00
Daniel Gultsch d91cd3e9e8 bump jxmpp-jid version 2021-05-13 09:19:16 +02:00
Daniel Gultsch 56535e07ff show black bars in video call when video orientation doesn’t match screen orientation
fixes #4056
2021-05-10 09:24:20 +02:00
Dheeraj Chintaluri 60c5906fe9 Update libwebrtc version 2021-05-10 07:20:53 +00:00
Daniel Gultsch 67e5f839f1 ignore crypto callbacks when rtp session has already been terminated 2021-05-08 11:50:18 +02:00
Daniel Gultsch 89012b0f8b synchronize startRinging() to not create multiple vibrate futures 2021-05-08 11:49:24 +02:00
Daniel Gultsch 7476dccc0e do not fetch keys before making call 2021-05-08 11:21:33 +02:00
Daniel Gultsch 9182a300c5 report fingerprint missmatch as securiy exception 2021-05-08 10:35:07 +02:00
Daniel Gultsch faa4c87b5f build omemo session when encountering unknown on RTP proceed 2021-05-08 09:25:51 +02:00
Daniel Gultsch 8d391753d7 encrypt rtp map as future 2021-05-08 08:45:31 +02:00
Daniel Gultsch 337aa4a110 consider Config.REQUIRE_RTP_VERIFICATION on decrypt. fail as future 2021-05-07 22:55:20 +02:00
Daniel Gultsch ddf597e0d3 invoke x509 verification upon receiving prekey message in rtp session 2021-05-06 18:40:35 +02:00
Daniel Gultsch 9c16af25fb bump gradle version 2021-05-06 17:57:06 +02:00
Daniel Gultsch e2324209ed make sure omemo sessions are verified if the the respective config flag is set 2021-05-04 19:04:01 +02:00
Daniel Gultsch 9544b994dc invoke omemo trust/fetch activity when triggering phone call in require_verification mode 2021-05-04 17:52:17 +02:00
Daniel Gultsch 3b25fb9038 encrypt to inactive and untrusted devices in jingle
encrypting to untrusted devices means no degradition of security
compared to not encrypting at all. Trust status display (shield) is made
independently at a later stage.
2021-05-04 10:49:45 +02:00
Daniel Gultsch 48156dd27f a/v calls: seperate out SECURITY error from APP_FAILURE
until now problems with verifying the call (omemo or DTLS missing) would
just be another app failure. This commit displays verifications problems as
their own thing.
2021-05-04 10:10:34 +02:00
Daniel Gultsch 905489e237 bump firebase-messaging libray version 2021-05-04 09:47:09 +02:00
153 changed files with 4439 additions and 2113 deletions

38
.github/workflows/android.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Android CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Download WebRTC
run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build Quicksy (Compat)
run: ./gradlew assembleQuicksyFreeCompatDebug
- name: Build Quicksy (System)
run: ./gradlew assembleQuicksyFreeSystemDebug
- name: Build Conversations (Compat)
run: ./gradlew assembleConversationsFreeCompatDebug
- name: Build Conversations (System)
run: ./gradlew assembleConversationsFreeSystemDebug
- uses: actions/upload-artifact@v2
with:
name: Conversations all-flavors (debug)
path: ./build/outputs/apk/**/debug/Conversations-*.apk

View File

@ -1,22 +0,0 @@
language: android
jdk:
- oraclejdk8
android:
components:
- platform-tools
- tools
- build-tools-28.0.3
- extra-google-google_play_services
licenses:
- '.+'
before_script:
- mkdir libs
- wget -O libs/libwebrtc-m90.aar https://gultsch.de/files/libwebrtc-m90.aar
script:
- ./gradlew assembleQuicksyFreeCompatDebug
- ./gradlew assembleQuicksyFreeSystemDebug
- ./gradlew assembleConversationsFreeCompatDebug
- ./gradlew assembleConversationsFreeSystemDebug
before_install:
- yes | sdkmanager "platforms;android-28"

View File

@ -1,5 +1,21 @@
# Changelog
### Version 2.10.2
* Fix crash when rendering some quotes
* Fix crash in welcome screen
### Version 2.10.1
* Fix issue with some videos not being compressed
* Fix rare crash when opening notification
### Version 2.10.0
* Show black bars when remote video does not match aspect ratio of screen
* Improve search performance
* Add setting to prevent screenshots
### Version 2.9.13
* minor A/V improvements

View File

@ -371,7 +371,7 @@ Unfortunately we dont have a recommendation for iPhones right now. There are
**Note:** Starting with version 2.8.0 you will need to compile libwebrtc.
[Instructions](https://webrtc.github.io/webrtc-org/native-code/android/) can be found on the WebRTC
website. Place the resulting libwebrtc.aar in the `libs/` directory. The PlayStore release currently
uses the stable M81 release and renamed the file name to `libwebrtc-m81.aar` put potentially you can
uses the stable M90 release and renamed the file name to `libwebrtc-m90.aar` put potentially you can
reference any file name by modifying `build.gradle`.
Make sure to have ANDROID_HOME point to your Android SDK. Use the Android SDK Manager to install missing dependencies.

View File

@ -1,14 +1,12 @@
import com.android.build.OutputFile
// Top-level build file where you can add configuration options common to all
// sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.android.tools.build:gradle:7.0.3'
}
}
@ -16,8 +14,8 @@ apply plugin: 'com.android.application'
repositories {
google()
jcenter()
mavenCentral()
jcenter()
}
configurations {
@ -35,23 +33,23 @@ configurations {
dependencies {
implementation 'androidx.viewpager:viewpager:1.0.0'
playstoreImplementation('com.google.firebase:firebase-messaging:21.0.1') {
playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2")
conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2")
quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0'
quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0'
quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.1'
quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.1'
implementation 'org.sufficientlysecure:openpgp-api:10.0'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.exifinterface:exifinterface:1.3.2'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.exifinterface:exifinterface:1.3.3'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.emoji:emoji:1.1.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
compatImplementation 'androidx.emoji:emoji-appcompat:1.1.0'
conversationsFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
quicksyFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
@ -64,20 +62,22 @@ dependencies {
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
implementation 'com.makeramen:roundedimageview:2.3.0'
implementation "com.wefika:flowlayout:0.4.1"
implementation 'net.ypresto.androidtranscoder:android-transcoder:0.3.0'
implementation 'org.jxmpp:jxmpp-jid:0.6.4'
implementation 'com.otaliastudios:transcoder:0.10.4'
implementation 'org.jxmpp:jxmpp-jid:1.0.2'
implementation 'org.osmdroid:osmdroid-android:6.1.10'
implementation 'org.hsluv:hsluv:0.2'
implementation 'org.conscrypt:conscrypt-android:2.2.1'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'me.drakeet.support:toastcompat:1.1.0'
implementation "com.leinardi.android:speed-dial:2.0.1"
implementation "com.leinardi.android:speed-dial:3.2.0"
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.okhttp3:okhttp:4.9.1"
implementation "com.squareup.okhttp3:okhttp:4.9.2"
implementation 'com.google.guava:guava:30.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
// implementation fileTree(include: ['libwebrtc-m90.aar'], dir: 'libs')
implementation 'com.google.guava:guava:30.1.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36'
// implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs')
implementation 'org.webrtc:google-webrtc:1.0.32006'
}
@ -93,8 +93,8 @@ android {
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 42015
versionName "2.9.13"
versionCode 4202301
versionName "2.10.2"
archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId
@ -105,19 +105,13 @@ android {
configurations {
compile.exclude group: 'org.jetbrains' , module:'annotations'
implementation.exclude group: 'org.jetbrains' , module:'annotations'
}
dataBinding {
enabled true
}
dexOptions {
// Skip pre-dexing when running on Travis CI or when disabled via -Dpre-dex=false.
preDexLibraries = preDexEnabled && !travisBuild
jumboMode true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@ -268,14 +262,4 @@ android {
exclude 'META-INF/BCKEY.DSA'
exclude 'META-INF/BCKEY.SF'
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (baseAbiVersionCode != null) {
output.versionCodeOverride = (100 * variant.versionCode) + baseAbiVersionCode
}
}
}
}

View File

@ -453,12 +453,19 @@
<xmpp:version>0.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<release>
<Version>
<revision>2.5.8</revision>
<created>2019-09-12</created>
<file-release rdf:resource="https://github.com/iNPUTmice/Conversations/archive/2.5.8.tar.gz"/>
<revision>2.9.13</revision>
<created>2021-05-03</created>
<file-release rdf:resource="https://github.com/iNPUTmice/Conversations/archive/2.9.13.tar.gz"/>
</Version>
</release>
</Project>

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip

View File

@ -0,0 +1,3 @@
• Show black bars when remote video does not match aspect ratio of screen
• Improve search performance
• Add setting to prevent screenshots

View File

@ -0,0 +1,2 @@
• Fix issue with some videos not being compressed
• Fix rare crash when opening notification

View File

@ -0,0 +1,2 @@
• Fix crash when rendering some quotes
• Fix crash in welcome screen

View File

@ -0,0 +1 @@
• Fix usage directTLS of manuelle enter an address

9
proguard-rules.pro vendored
View File

@ -26,6 +26,15 @@
-dontwarn java.lang.**
-dontwarn javax.lang.**
-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
-keepclassmembers class eu.siacs.conversations.http.services.** {
!transient <fields>;
}

View File

@ -6,7 +6,6 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
@ -30,6 +29,7 @@ import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
import eu.siacs.conversations.services.ImportBackupService;
import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;
public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
@ -55,6 +55,12 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
this.binding.list.setAdapter(this.backupFileAdapter);
this.backupFileAdapter.setOnItemClickedListener(this);
}
@Override
protected void onResume(){
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
@ -125,7 +131,8 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
try {
final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
showEnterPasswordDialog(backupFile, finishOnCancel);
} catch (IOException | IllegalArgumentException e) {
} catch (final IOException | IllegalArgumentException e) {
Log.d(Config.LOGTAG, "unable to open backup file " + uri, e);
Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
}
}
@ -181,6 +188,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (resultCode == RESULT_OK) {
if (requestCode == 0xbac) {
openBackupFileFromUri(intent.getData(), false);
@ -225,15 +233,17 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_open_backup_file) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
}
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
openBackupFile();
return true;
}
return super.onOptionsItemSelected(item);
}
private void openBackupFile() {
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
}
}

View File

@ -228,6 +228,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0) {
if (allGranted(grantResults)) {
switch (requestCode) {

View File

@ -106,7 +106,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
}
@Override
public void onNewIntent(Intent intent) {
public void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
if (intent != null) {
setIntent(intent);
}
@ -201,6 +202,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
if (grantResults.length > 0) {
if (allGranted(grantResults)) {

View File

@ -0,0 +1,11 @@
package eu.siacs.conversations.utils;
import android.content.Context;
import eu.siacs.conversations.xmpp.Jid;
public class PhoneNumberUtilWrapper {
public static String toFormattedPhoneNumber(Context context, Jid jid) {
throw new AssertionError("This method is not implemented in Conversations");
}
}

View File

@ -3,4 +3,18 @@
<string name="pick_a_server">اختر مزود خدمة XMPP الخاص بك</string>
<string name="use_chat.sum7.eu">استخدِم chat.sum7.eu</string>
<string name="create_new_account">أنشئ حسابًا جديدًا</string>
</resources>
<string name="do_you_have_an_account">هل تملك حساب XMPP؟؟ قد يكون ذلك ممكنا لو كنت تستعمل خدمة XMPP أخرى أو إستعملت تطبيق كونفرسايشنز سابقا. أو يمكنك صنع حساب XMPP جديد الآن.
ملاحظة: بعض خدمات البريد الإلكتروني تقدم حسابات XMPP.</string>
<string name="server_select_text">XMPP هي خدمة مستقلة للتواصل بشبكة الرسائل المباشرة. يمكنك إستعمال هذه الخدمة مع أي خادم XMPP تختاره.
سعيا لراحتك جعلنا خلق حساب في كونفيرسايشنز سهلا مع مقدم خدمة خاص بالإستعمال مع كونفيرسايشنز.</string>
<string name="magic_create_text_on_x">لقد تمت دعوتك لـ %1$s. سيتم دلّك على طريقة صنع حساب.
عندما تختار %1$sكمقدّم خدمة سيصبح من الممكن لك التواصل مع مستعملين من أي خادم آخر عن طريق إعطائهم عنوانك الكامل على XMPP.</string>
<string name="magic_create_text_fixed">تمّت دعوتك إلى %1$s. تم إختيار إسم مستخدم خاص بك. سيتم قيادتك عبر طريقة صنع حساب.
سيمكنك التواصل مع مستخدمين من مزودين آخرين عبر إعطائهم كامل عنوانك XMPP.</string>
<string name="your_server_invitation">سيرفر دعوتك</string>
<string name="improperly_formatted_provisioning">لم يتم التقاط الكود بطريقة جيّدة</string>
<string name="tap_share_button_send_invite">إضغط على زر مشاركة لترسل إلى المتصل بك دعوة إلى %1$s.</string>
<string name="if_contact_is_nearby_use_qr">إذا كان المتصل بك قريبا منك، يمكنه فحص الكود بالأسفل ليقبل دعوتك.</string>
<string name="easy_invite_share_text">إنظم %1$s وتحدّث معي: %2$s</string>
<string name="share_invite_with">شارك إستدعاء مع...</string>
</resources>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">XMPP সার্ভার নির্বাচন করুন</string>
<string name="use_conversations.im">conversations.im-ই ব্যবহার করা যাক</string>
<string name="use_chat.sum7.eu">chat.sum7.eu ব্যবহার করা যাক</string>
<string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করা যাক</string>
<string name="do_you_have_an_account">আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।‌\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</string>
<string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im¹ -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
<string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই chat.sum7.eu -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
<string name="magic_create_text_on_x">আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
<string name="magic_create_text_fixed">আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
<string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string>
@ -13,4 +13,4 @@
<string name="if_contact_is_nearby_use_qr">পরিচিত ব্যক্তি যদি নিকটেই থাকেন, তাহলে তারা এই কোডটাও স্ক্যান করে নিতে পারেন</string>
<string name="easy_invite_share_text">%1$sতে এসো, আর আমার সাথে কথা বলো: %2$s</string>
<string name="share_invite_with">একটি আমন্ত্রণপত্র দেওয়া যাক...</string>
</resources>
</resources>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Vælg din XMPP-udbyder</string>
<string name="use_conversations.im">Brug conversations.im</string>
<string name="use_chat.sum7.eu">Brug chat.sum7.eu</string>
<string name="create_new_account">Opret ny konto</string>
<string name="do_you_have_an_account">Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti.</string>
<string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im¹; en udbyder, der er specielt velegnet til brug med Conversations.</string>
<string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på chat.sum7.eu; en udbyder, der er specielt velegnet til brug med Conversations.</string>
<string name="magic_create_text_on_x">Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
<string name="magic_create_text_fixed">Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
<string name="your_server_invitation">Din server invitation</string>
@ -13,4 +13,4 @@
<string name="if_contact_is_nearby_use_qr">Hvis din kontakt er i nærheden, kan de også skanne koden nedenfor for at acceptere din invitation.</string>
<string name="easy_invite_share_text">Deltag med %1$s og chat med mig: %2$s</string>
<string name="share_invite_with">Del invitation med...</string>
</resources>
</resources>

View File

@ -10,7 +10,7 @@
<string name="your_server_invitation">Η πρόσκλησή σας στον διακομιστή</string>
<string name="improperly_formatted_provisioning">Λάθος μορφοποίηση κώδικα παροχής</string>
<string name="tap_share_button_send_invite">Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s.</string>
<string name="if_contact_is_nearby_use_qr">Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σκανάρει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας.</string>
<string name="if_contact_is_nearby_use_qr">Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σαρώσει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας.</string>
<string name="easy_invite_share_text">Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s</string>
<string name="share_invite_with">Διαμοιρασμός πρόσκλησης με...</string>
</resources>

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Scegli il tuo provider XMPP</string>
<string name="pick_a_server">Scegli il tuo fornitore XMPP</string>
<string name="use_chat.sum7.eu">Usa chat.sum7.eu</string>
<string name="create_new_account">Crea un nuovo account</string>
<string name="do_you_have_an_account">Possiedi già un account XMPP? Questo succede se stai già usando un diverso client XMPP o hai già usato prima Conversations. In caso negativo puoi creare un account XMPP adesso.
<string name="create_new_account">Crea un nuovo profilo</string>
<string name="do_you_have_an_account">Possiedi già un profilo XMPP? Questo succede se stai già usando un diverso client XMPP o hai già usato prima Conversations. In caso negativo puoi creare un profilo XMPP adesso.
Suggerimento: alcuni provider di email forniscono anche un account XMPP.</string>
<string name="server_select_text">XMPP è una rete di instant messaging indipendente dal provider. Puoi usare questo client con qualsiasi server XMPP.
In ogni caso per facilitare puoi creare facilmente un account su chat.sum7.eu, un provider pensato apposta per essere usato con Conversations.</string>
<string name="magic_create_text_on_x">Sei stato invitato su %1$s. Ti guideremo nel procedimento per creare un account.\nQuando scegli %1$s come fornitore sarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
<string name="magic_create_text_fixed">Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
<string name="server_select_text">XMPP è una rete di messaggistica istantanea indipendente dal fornitore. Puoi usare questo client con qualsiasi server XMPP.
In ogni caso per facilitare puoi creare facilmente un account su chat.sum7.eu, un fornitore pensato apposta per essere usato con Conversations.</string>
<string name="magic_create_text_on_x">Hai ricevuto un invito per %1$s. Ti guideremo nel procedimento per creare un profilo.\nQuando scegli %1$s come fornitore sarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
<string name="magic_create_text_fixed">Hai ricevuto un invito per %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un profilo.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
<string name="your_server_invitation">Il tuo invito al server</string>
<string name="improperly_formatted_provisioning">Codice di approvvigionamento formattato male</string>
<string name="tap_share_button_send_invite">Tocca il pulsante condividi per inviare al contatto un invito per %1$s.</string>

View File

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">XMPPプロバイダーを選択してください</string>
<string name="use_chat.sum7.eu">chat.sum7.euを利用する</string>
<string name="create_new_account">アカウントを作成</string>
<string name="do_you_have_an_account">XMPPアカウントをお持ちですか既にほかのXMPPクライアントを利用しているか、Conversationsを利用したことがある場合はこちら。初めての方は、今すぐ新しいXMPPアカウントを作成できます。\nヒント: eメールのプロバイダーがXMPPアカウントも提供している場合があります。</string>
<string name="server_select_text">XMPPは、プロバイダーに依存しないインスタントメッセージのプロトコルです。XMPPサーバーならどこでも、このクライアントを使用することができます。\nよろしければ、Conversationsに最適化されたプロバイダーchat.sum7.euで簡単にアカウントを作成することもできます。</string>
<string name="magic_create_text_on_x">%1$sへ招待されました。アカウント作成手順をご案内します。 \n%1$sをプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。</string>
<string name="magic_create_text_fixed">%1$sへ招待されました。ユーザーネームは既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。</string>
<string name="pick_a_server">XMPP プロバイダーを選択してください</string>
<string name="use_chat.sum7.eu">chat.sum7.eu を利用する</string>
<string name="create_new_account">新規アカウントを作成</string>
<string name="do_you_have_an_account">XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。</string>
<string name="server_select_text">XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。\nよろしければ、 Conversations に最適化されたプロバイダー chat.sum7.eu で簡単にアカウントを作成することもできます。</string>
<string name="magic_create_text_on_x">%1$s へ招待されました。アカウント作成手順をご案内します。 \n%1$s をプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。</string>
<string name="magic_create_text_fixed">%1$s へ招待されました。ユーザー名は既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。</string>
<string name="your_server_invitation">サーバーの招待</string>
<string name="improperly_formatted_provisioning">仮コードの書式が不正です</string>
<string name="tap_share_button_send_invite">共有ボタンを叩いて、連絡先の %1$s に招待を送信する。</string>
<string name="if_contact_is_nearby_use_qr">あなたの連絡先が近くにいる場合は、下のコードをスキャンして、あなたの招待を受け取ることもできます。</string>
<string name="easy_invite_share_text">%1$s に参加して私とお話しましょう: %2$s</string>
<string name="share_invite_with">…で招待を共有</string>
</resources>
</resources>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Vyberte si svojho XMPP poskytovateľa</string>
<string name="use_chat.sum7.eu">Použiť chat.sum7.eu</string>
<string name="create_new_account">Vytvoriť nové konto</string>
<string name="do_you_have_an_account">Máte už svoje XMPP konto? Môže to tak byť v prípade, že už používate iného klienta XMPP alebo ste predtým používali Conversations. Ak nie, môžete si vytvoriť nové XMPP konto práve teraz.\nHint: Niektorí poskytovatelia emailu zároveň poskytujú aj XMPP kontá.</string>
<string name="server_select_text">XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na chat.sum7.eu; poskytovateľ špeciálne vhodný na používanie s Conversations.</string>
<string name="magic_create_text_on_x">Boli ste pozvaný do %1$s. Prevedieme vás procesom vytvorenia konta..\nPo výbere %1$s ako poskytovateľa, budete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu.</string>
<string name="magic_create_text_fixed">Boli ste pozvaný do %1$s . Užívateľské meno vám už bolo vopred vybrané. Prevedieme vás procesom vytvorenia konta..\nBudete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu.</string>
<string name="tap_share_button_send_invite">Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu.</string>
<string name="if_contact_is_nearby_use_qr">Ak je váš kontakt blízko, na prijatie vašej pozvánky si môže nasnímať kód nižšie.</string>
<string name="easy_invite_share_text">Pripojte sa k %1$sa rozprávajte sa so mnou: %2$s</string>
<string name="share_invite_with">Zdieľať pozvánku s...</string>
</resources>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Välj din XMPP leverantör</string>
<string name="use_chat.sum7.eu">Använd chat.sum7.eu</string>
<string name="create_new_account">Skapa nytt konto</string>
</resources>
<string name="your_server_invitation">Din server inbjudan</string>
<string name="share_invite_with">Dela inbjudan med...</string>
</resources>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Chọn nhà cung cấp XMPP của bạn</string>
<string name="use_conversations.im">Sử dụng conversations.im</string>
<string name="use_chat.sum7.eu">Sử dụng chat.sum7.eu</string>
<string name="create_new_account">Tạo tài khoản mới</string>
<string name="do_you_have_an_account">Bạn đã có tài khoản XMPP chưa? Điều này có thể đúng nếu bạn đang dùng một ứng dụng khách cho XMPP khác hoặc đã sử dụng Conversations trước đó. Nếu không, bạn có thể tạo tài khoản XMPP mới ngay bây giờ.\nGợi ý: Một số nhà cung cấp email cũng cung cấp tài khoản XMPP.</string>
<string name="server_select_text">XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên conversations.im¹ được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations.</string>
<string name="server_select_text">XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên chat.sum7.eu được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations.</string>
<string name="magic_create_text_on_x">Bạn đã được mời vào %1$s. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nKhi chọn %1$s là nhà cung cấp, bạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn.</string>
<string name="magic_create_text_fixed">Bạn đã được mời vào %1$s. Một tên người dùng đã được chọn sẵn cho bạn. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nBạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn.</string>
<string name="your_server_invitation">Lời mời vào máy chủ của bạn</string>
@ -13,4 +13,4 @@
<string name="if_contact_is_nearby_use_qr">Nếu liên hệ của bạn ở gần đây, họ cũng có thể quét mã ở dưới để chấp nhận lời mời của bạn.</string>
<string name="easy_invite_share_text">Hãy tham gia vào %1$s và trò chuyện với tôi: %2$s</string>
<string name="share_invite_with">Chia sẻ lời mời với...</string>
</resources>
</resources>

View File

@ -39,8 +39,6 @@
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
@ -54,7 +52,8 @@
<application
android:allowBackup="false"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_content"
android:appCategory="social"
android:hardwareAccelerated="true"
android:icon="@mipmap/new_launcher"
@ -144,6 +143,14 @@
<data android:scheme="imto" />
<data android:host="jabber" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="imto" />
<data android:host="xmpp" />
</intent-filter>
</activity>
<activity
android:name=".ui.StartConversationActivity"

View File

@ -118,6 +118,7 @@ public final class Config {
public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
public static final boolean REQUIRE_RTP_VERIFICATION = false; //require a/v calls to be verified with OMEMO
public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments
@ -199,4 +200,9 @@ public final class Config {
public final static float LOCATION_FIX_SPACE_DELTA = 10; // m
public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
}
// How deep nested quotes should be displayed. '2' means one quote nested in another.
public static final int QUOTE_MAX_DEPTH = 7;
// How deep nested quotes should be created on quoting a message.
public static final int QUOTING_MAX_DEPTH = 1;
}

View File

@ -17,6 +17,21 @@ import eu.siacs.conversations.xmpp.Jid;
public class JabberIdContact extends AbstractPhoneContact {
private static final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.PHOTO_URI,
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Im.DATA
};
private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and lower(" + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + ")=?))";
private static final String[] SELECTION_ARGS = {
ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE,
String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER),
String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM),
"xmpp"
};
private final Jid jid;
private JabberIdContact(Cursor cursor) throws IllegalArgumentException {
@ -36,38 +51,26 @@ public class JabberIdContact extends AbstractPhoneContact {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
return Collections.emptyMap();
}
final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.PHOTO_URI,
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Im.DATA};
final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
+ ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
+ "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
+ "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
+ "\")";
final Cursor cursor;
try {
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, null);
} catch (Exception e) {
try (final Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, SELECTION_ARGS, null)) {
if (cursor == null) {
return Collections.emptyMap();
}
final HashMap<Jid, JabberIdContact> contacts = new HashMap<>();
while (cursor.moveToNext()) {
try {
final JabberIdContact contact = new JabberIdContact(cursor);
final JabberIdContact preexisting = contacts.put(contact.getJid(), contact);
if (preexisting == null || preexisting.rating() < contact.rating()) {
contacts.put(contact.getJid(), contact);
}
} catch (final IllegalArgumentException e) {
Log.d(Config.LOGTAG, "unable to create jabber id contact");
}
}
return contacts;
} catch (final Exception e) {
Log.d(Config.LOGTAG, "unable to query", e);
return Collections.emptyMap();
}
final HashMap<Jid, JabberIdContact> contacts = new HashMap<>();
while (cursor != null && cursor.moveToNext()) {
try {
final JabberIdContact contact = new JabberIdContact(cursor);
final JabberIdContact preexisting = contacts.put(contact.getJid(), contact);
if (preexisting == null || preexisting.rating() < contact.rating()) {
contacts.put(contact.getJid(), contact);
}
} catch (IllegalArgumentException e) {
Log.d(Config.LOGTAG,"unable to create jabber id contact");
}
}
if (cursor != null) {
cursor.close();
}
return contacts;
}
}

View File

@ -8,7 +8,12 @@ import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.whispersystems.libsignal.IdentityKey;
@ -733,58 +738,62 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
axolotlStore.setFingerprintStatus(fingerprint, status);
}
private void verifySessionWithPEP(final XmppAxolotlSession session) {
private ListenableFuture<XmppAxolotlSession> verifySessionWithPEP(final XmppAxolotlSession session) {
Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep");
final SignalProtocolAddress address = session.getRemoteAddress();
final IdentityKey identityKey = session.getIdentityKey();
final Jid jid;
try {
IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.of(address.getName()), address.getDeviceId());
mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
Pair<X509Certificate[], byte[]> verification = mXmppConnectionService.getIqParser().verification(packet);
if (verification != null) {
try {
Signature verifier = Signature.getInstance("sha256WithRSA");
verifier.initVerify(verification.first[0]);
verifier.update(identityKey.serialize());
if (verifier.verify(verification.second)) {
try {
mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
String fingerprint = session.getFingerprint();
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint);
setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
try {
final String cn = information.getString("subject_cn");
final Jid jid = Jid.of(address.getName());
Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn);
account.getRoster().getContact(jid).setCommonName(cn);
} catch (final IllegalArgumentException ignored) {
//ignored
}
finishBuildingSessionsFromPEP(address);
return;
} catch (Exception e) {
Log.d(Config.LOGTAG, "could not verify certificate");
}
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
}
} else {
Log.d(Config.LOGTAG, "no verification found");
}
fetchStatusMap.put(address, FetchStatus.SUCCESS);
finishBuildingSessionsFromPEP(address);
}
});
} catch (IllegalArgumentException e) {
jid = Jid.of(address.getName());
} catch (final IllegalArgumentException e) {
fetchStatusMap.put(address, FetchStatus.SUCCESS);
finishBuildingSessionsFromPEP(address);
return Futures.immediateFuture(session);
}
final SettableFuture<XmppAxolotlSession> future = SettableFuture.create();
final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId());
mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> {
Pair<X509Certificate[], byte[]> verification = mXmppConnectionService.getIqParser().verification(response);
if (verification != null) {
try {
Signature verifier = Signature.getInstance("sha256WithRSA");
verifier.initVerify(verification.first[0]);
verifier.update(identityKey.serialize());
if (verifier.verify(verification.second)) {
try {
mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
String fingerprint = session.getFingerprint();
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint);
setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
try {
final String cn = information.getString("subject_cn");
final Jid jid1 = Jid.of(address.getName());
Log.d(Config.LOGTAG, "setting common name for " + jid1 + " to " + cn);
account.getRoster().getContact(jid1).setCommonName(cn);
} catch (final IllegalArgumentException ignored) {
//ignored
}
finishBuildingSessionsFromPEP(address);
future.set(session);
return;
} catch (Exception e) {
Log.d(Config.LOGTAG, "could not verify certificate");
}
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
}
} else {
Log.d(Config.LOGTAG, "no verification found");
}
fetchStatusMap.put(address, FetchStatus.SUCCESS);
finishBuildingSessionsFromPEP(address);
future.set(session);
});
return future;
}
private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) {
@ -900,22 +909,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
}
private void buildSessionFromPEP(final SignalProtocolAddress address) {
buildSessionFromPEP(address, null);
private ListenableFuture<XmppAxolotlSession> buildSessionFromPEP(final SignalProtocolAddress address) {
return buildSessionFromPEP(address, null);
}
private void buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) {
private ListenableFuture<XmppAxolotlSession> buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) {
final SettableFuture<XmppAxolotlSession> sessionSettableFuture = SettableFuture.create();
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString());
if (address.equals(getOwnAxolotlAddress())) {
throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
}
final Jid jid = Jid.of(address.getName());
final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid());
IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId());
mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> {
if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
fetchStatusMap.put(address, FetchStatus.TIMEOUT);
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout"));
} else if (packet.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
final IqParser parser = mXmppConnectionService.getIqParser();
@ -928,6 +938,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (callback != null) {
callback.onSessionBuildFailed();
}
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Invalid"));
return;
}
Random random = new Random();
@ -939,6 +950,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (callback != null) {
callback.onSessionBuildFailed();
}
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. No suitable PreKey found"));
return;
}
@ -953,7 +965,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey());
sessions.put(address, session);
if (Config.X509_VERIFICATION) {
verifySessionWithPEP(session); //TODO; maybe inject callback in here too
sessionSettableFuture.setFuture(verifySessionWithPEP(session)); //TODO; maybe inject callback in here too
} else {
FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize()));
FetchStatus fetchStatus;
@ -969,6 +981,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (callback != null) {
callback.onSessionBuildSuccessful();
}
sessionSettableFuture.set(session);
}
} catch (UntrustedIdentityException | InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
@ -981,6 +994,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (callback != null) {
callback.onSessionBuildFailed();
}
sessionSettableFuture.setException(new CryptoFailedException(e));
}
} else {
fetchStatusMap.put(address, FetchStatus.ERROR);
@ -994,8 +1008,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (callback != null) {
callback.onSessionBuildFailed();
}
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Error"));
}
});
return sessionSettableFuture;
}
private void removeFromDeviceAnnouncement(Integer id) {
@ -1217,7 +1233,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
final String content = child.getContent();
axolotlMessage.encrypt(content);
axolotlMessage.addDevice(session);
axolotlMessage.addDevice(session, true);
fingerprint.addChild(axolotlMessage.toElement());
transportInfo.addChild(fingerprint);
} else {
@ -1228,36 +1244,63 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public OmemoVerifiedPayload<OmemoVerifiedRtpContentMap> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) throws CryptoFailedException {
final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
final XmppAxolotlSession session = sessions.get(address);
if (session == null) {
throw new CryptoFailedException(String.format("No session found for %d", deviceId));
public ListenableFuture<OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) {
return Futures.transformAsync(
getSession(jid, deviceId),
session -> encrypt(rtpContentMap, session),
MoreExecutors.directExecutor()
);
}
private ListenableFuture<OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) {
if (Config.REQUIRE_RTP_VERIFICATION) {
requireVerification(session);
}
final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
final OmemoVerification omemoVerification = new OmemoVerification();
omemoVerification.setDeviceId(deviceId);
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
omemoVerification.setSessionFingerprint(session.getFingerprint());
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : rtpContentMap.contents.entrySet()) {
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
try {
encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
} catch (final CryptoFailedException e) {
return Futures.immediateFailedFuture(e);
}
descriptionTransportBuilder.put(
content.getKey(),
new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo)
);
}
return new OmemoVerifiedPayload<>(
omemoVerification,
new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build())
);
return Futures.immediateFuture(
new OmemoVerifiedPayload<>(
omemoVerification,
new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build())
));
}
public OmemoVerifiedPayload<RtpContentMap> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) throws CryptoFailedException {
private ListenableFuture<XmppAxolotlSession> getSession(final Jid jid, final int deviceId) {
final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
final XmppAxolotlSession session = sessions.get(address);
if (session == null) {
return buildSessionFromPEP(address);
}
return Futures.immediateFuture(session);
}
public ListenableFuture<OmemoVerifiedPayload<RtpContentMap>> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) {
final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
final OmemoVerification omemoVerification = new OmemoVerification();
final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>();
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from);
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport;
try {
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
} catch (CryptoFailedException e) {
return Futures.immediateFailedFuture(e);
}
omemoVerification.setOrEnsureEqual(decryptedTransport);
descriptionTransportBuilder.put(
content.getKey(),
@ -1265,13 +1308,26 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
);
}
processPostponed();
return new OmemoVerifiedPayload<>(
omemoVerification,
new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build())
final ImmutableList<ListenableFuture<XmppAxolotlSession>> sessionFutures = pepVerificationFutures.build();
return Futures.transform(
Futures.allAsList(sessionFutures),
sessions -> {
if (Config.REQUIRE_RTP_VERIFICATION) {
for (XmppAxolotlSession session : sessions) {
requireVerification(session);
}
}
return new OmemoVerifiedPayload<>(
omemoVerification,
new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build())
);
},
MoreExecutors.directExecutor()
);
}
private OmemoVerifiedPayload<IceUdpTransportInfo> decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from) throws CryptoFailedException {
private OmemoVerifiedPayload<IceUdpTransportInfo> decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures) throws CryptoFailedException {
final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes());
final OmemoVerification omemoVerification = new OmemoVerification();
@ -1288,6 +1344,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (preKeyId != null) {
postponedSessions.add(session);
}
if (session.isFresh()) {
pepVerificationFutures.add(putFreshSession(session));
} else if (Config.REQUIRE_RTP_VERIFICATION) {
pepVerificationFutures.add(Futures.immediateFuture(session));
}
fingerprint.setContent(plaintext.getPlaintext());
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
omemoVerification.setSessionFingerprint(plaintext.getFingerprint());
@ -1299,6 +1360,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return new OmemoVerifiedPayload<>(omemoVerification, transportInfo);
}
private static void requireVerification(final XmppAxolotlSession session) {
if (session.getTrust().isVerified()) {
return;
}
throw new NotVerifiedException(String.format(
"session with %s was not verified",
session.getFingerprint()
));
}
public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
executor.execute(new Runnable() {
@Override
@ -1496,15 +1567,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return keyTransportMessage;
}
private void putFreshSession(XmppAxolotlSession session) {
private ListenableFuture<XmppAxolotlSession> putFreshSession(XmppAxolotlSession session) {
sessions.put(session);
if (Config.X509_VERIFICATION) {
if (session.getIdentityKey() != null) {
verifySessionWithPEP(session);
return verifySessionWithPEP(session);
} else {
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification");
}
}
return Futures.immediateFuture(session);
}
public enum FetchStatus {
@ -1690,4 +1762,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return payload;
}
}
public static class NotVerifiedException extends SecurityException {
public NotVerifiedException(String message) {
super(message);
}
}
}

View File

@ -5,6 +5,8 @@ import android.database.Cursor;
import android.os.SystemClock;
import android.util.Log;
import com.google.common.base.Strings;
import org.json.JSONException;
import org.json.JSONObject;
@ -247,7 +249,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public String getHostname() {
return this.hostname == null ? "" : this.hostname;
return Strings.nullToEmpty(this.hostname);
}
public void setHostname(String hostname) {

View File

@ -28,6 +28,7 @@ import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
@ -258,9 +259,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public Message findMessageWithFileAndUuid(final String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
final Transferable transferable = message.getTransferable();
final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
if (message.getUuid().equals(uuid)
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& (message.isFileOrImage() || message.treatAsDownloadable())) {
&& (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
return message;
}
}
}
return null;
}
public Message findMessageWithUuid(final String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
if (message.getUuid().equals(uuid)) {
return message;
}
}

View File

@ -8,6 +8,7 @@ import android.util.Log;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Longs;
import org.json.JSONException;
@ -849,10 +850,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
fileParams.height = parseInt(parts[3]);
case 2:
fileParams.url = URL.tryParse(parts[0]);
fileParams.size = parseLong(parts[1]);
fileParams.size = Longs.tryParse(parts[1]);
break;
case 3:
fileParams.size = parseLong(parts[0]);
fileParams.size = Longs.tryParse(parts[0]);
fileParams.width = parseInt(parts[1]);
fileParams.height = parseInt(parts[2]);
break;
@ -861,14 +862,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return fileParams;
}
private static long parseLong(String value) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return 0;
}
}
private static int parseInt(String value) {
try {
return Integer.parseInt(value);
@ -905,10 +898,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public static class FileParams {
public String url;
public long size = 0;
public Long size = null;
public int width = 0;
public int height = 0;
public int runtime = 0;
public long getSize() {
return size == null ? 0 : size;
}
}
public void setFingerprint(String fingerprint) {
@ -987,13 +984,28 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
if (conversation.getMode() == Conversation.MODE_MULTI) {
final Jid nextCounterpart = conversation.getNextCounterpart();
if (nextCounterpart != null) {
message.setCounterpart(nextCounterpart);
message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
return true;
}
return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
}
return false;
}
public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
final Conversation conversation;
if (message.conversation instanceof Conversation) {
conversation = (Conversation) message.conversation;
} else {
return false;
}
return configurePrivateMessage(conversation, message, counterpart, false);
}
private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
if (counterpart == null) {
return false;
}
message.setCounterpart(counterpart);
message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
return true;
}
}

View File

@ -22,7 +22,7 @@ public interface Transferable {
int getStatus();
long getFileSize();
Long getFileSize();
int getProgress();

View File

@ -18,8 +18,8 @@ public class TransferablePlaceholder implements Transferable {
}
@Override
public long getFileSize() {
return 0;
public Long getFileSize() {
return null;
}
@Override

View File

@ -83,7 +83,7 @@ public class HttpDownloadConnection implements Transferable {
final Message.FileParams fileParams = message.getFileParams();
if (message.hasFileOnRemoteHost()) {
mUrl = AesGcmURL.of(fileParams.url);
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
} else if (message.isOOb() && fileParams.url != null && fileParams.size != null) {
mUrl = AesGcmURL.of(fileParams.url);
} else {
mUrl = AesGcmURL.of(message.getBody().split("\n")[0]);
@ -106,8 +106,9 @@ public class HttpDownloadConnection implements Transferable {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
//TODO add auth tag size to knownFileSize
final long knownFileSize = message.getFileParams().size;
if (knownFileSize > 0 && interactive) {
final Long knownFileSize = message.getFileParams().size;
Log.d(Config.LOGTAG,"knownFileSize: "+knownFileSize+", body="+message.getBody());
if (knownFileSize != null && interactive) {
this.file.setExpectedSize(knownFileSize);
download(true);
} else {
@ -234,11 +235,11 @@ public class HttpDownloadConnection implements Transferable {
}
@Override
public long getFileSize() {
public Long getFileSize() {
if (this.file != null) {
return this.file.getExpectedSize();
} else {
return 0;
return null;
}
}
@ -318,6 +319,7 @@ public class HttpDownloadConnection implements Transferable {
mostRecentCall = client.newCall(request);
try {
final Response response = mostRecentCall.execute();
throwOnInvalidCode(response);
final String contentLength = response.header("Content-Length");
final String contentType = response.header("Content-Type");
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
@ -332,7 +334,11 @@ public class HttpDownloadConnection implements Transferable {
if (Strings.isNullOrEmpty(contentLength)) {
throw new IOException("no content-length found in HEAD response");
}
return Long.parseLong(contentLength, 10);
final long size = Long.parseLong(contentLength, 10);
if (size < 0) {
throw new IOException("Server reported negative file size");
}
return size;
} catch (IOException e) {
Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage());
throw e;
@ -395,45 +401,41 @@ public class HttpDownloadConnection implements Transferable {
final Request request = requestBuilder.build();
mostRecentCall = client.newCall(request);
final Response response = mostRecentCall.execute();
final int code = response.code();
if (code >= 200 && code <= 299) {
final String contentRange = response.header("Content-Range");
final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
final InputStream inputStream = response.body().byteStream();
final OutputStream outputStream;
long transmitted = 0;
if (tryResume && serverResumed) {
Log.d(Config.LOGTAG, "server resumed");
transmitted = file.getSize();
updateProgress(Math.round(((double) transmitted / expected) * 100));
outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
} else {
final String contentLength = response.header("Content-Length");
final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
if (expected != size) {
Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
}
file.getParentFile().mkdirs();
if (!file.exists() && !file.createNewFile()) {
throw new FileWriterException();
}
outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
}
int count;
final byte[] buffer = new byte[4096];
while ((count = inputStream.read(buffer)) != -1) {
transmitted += count;
try {
outputStream.write(buffer, 0, count);
} catch (IOException e) {
throw new FileWriterException();
}
updateProgress(Math.round(((double) transmitted / expected) * 100));
}
outputStream.flush();
throwOnInvalidCode(response);
final String contentRange = response.header("Content-Range");
final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
final InputStream inputStream = response.body().byteStream();
final OutputStream outputStream;
long transmitted = 0;
if (tryResume && serverResumed) {
Log.d(Config.LOGTAG, "server resumed");
transmitted = file.getSize();
updateProgress(Math.round(((double) transmitted / expected) * 100));
outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
} else {
throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
final String contentLength = response.header("Content-Length");
final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
if (expected != size) {
Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
}
file.getParentFile().mkdirs();
if (!file.exists() && !file.createNewFile()) {
throw new FileWriterException();
}
outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
}
int count;
final byte[] buffer = new byte[4096];
while ((count = inputStream.read(buffer)) != -1) {
transmitted += count;
try {
outputStream.write(buffer, 0, count);
} catch (IOException e) {
throw new FileWriterException();
}
updateProgress(Math.round(((double) transmitted / expected) * 100));
}
outputStream.flush();
}
private void updateImageBounds() {
@ -451,4 +453,11 @@ public class HttpDownloadConnection implements Transferable {
}
}
private static void throwOnInvalidCode(final Response response) throws IOException {
final int code = response.code();
if (code < 200 || code >= 300) {
throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
}
}
}

View File

@ -69,8 +69,8 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
}
@Override
public long getFileSize() {
return file == null ? 0 : file.getExpectedSize();
public Long getFileSize() {
return file == null ? null : file.getExpectedSize();
}
@Override

View File

@ -11,6 +11,8 @@ import android.os.SystemClock;
import android.util.Base64;
import android.util.Log;
import com.google.common.base.Stopwatch;
import org.json.JSONException;
import org.json.JSONObject;
import org.whispersystems.libsignal.IdentityKey;
@ -62,7 +64,9 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
public class DatabaseBackend extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 49;
private static final int DATABASE_VERSION = 50;
private static boolean requiresMessageIndexRebuild = false;
private static DatabaseBackend instance = null;
private static final String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -165,16 +169,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ "UNIQUE(" + Resolver.Result.DOMAIN + ") ON CONFLICT REPLACE"
+ ");";
private static final String CREATE_MESSAGE_TIME_INDEX = "create INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")";
private static final String CREATE_MESSAGE_CONVERSATION_INDEX = "create INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")";
private static final String CREATE_MESSAGE_DELETED_INDEX = "create index message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")";
private static final String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "create INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")";
private static final String CREATE_MESSAGE_TYPE_INDEX = "create INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")";
private static final String CREATE_MESSAGE_TIME_INDEX = "CREATE INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")";
private static final String CREATE_MESSAGE_CONVERSATION_INDEX = "CREATE INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")";
private static final String CREATE_MESSAGE_DELETED_INDEX = "CREATE INDEX message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")";
private static final String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "CREATE INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")";
private static final String CREATE_MESSAGE_TYPE_INDEX = "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")";
private static final String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING FTS4(uuid TEXT PRIMARY KEY, body TEXT)";
private static final String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index (uuid,body) VALUES (new.uuid,new.body); END;";
private static final String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE of uuid,body ON " + Message.TABLENAME + " BEGIN update messages_index set body=new.body,uuid=new.uuid WHERE uuid=old.uuid; END;";
private static final String COPY_PREEXISTING_ENTRIES = "INSERT into messages_index(uuid,body) select uuid,body FROM " + Message.TABLENAME + ";";
private static final String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid,body,notindexed=\"uuid\",content=\"" + Message.TABLENAME + "\",tokenize='unicode61')";
private static final String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index(rowid,uuid,body) VALUES(NEW.rowid,NEW.uuid,NEW.body); END;";
private static final String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE OF uuid,body ON " + Message.TABLENAME + " BEGIN UPDATE messages_index SET body=NEW.body,uuid=NEW.uuid WHERE rowid=OLD.rowid; END;";
private static final String CREATE_MESSAGE_DELETE_TRIGGER = "CREATE TRIGGER after_message_delete AFTER DELETE ON " + Message.TABLENAME + " BEGIN DELETE FROM messages_index WHERE rowid=OLD.rowid; END;";
private static final String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(messages_index) VALUES('rebuild');";
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@ -187,6 +192,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return values;
}
public static boolean requiresMessageIndexRebuild() {
return requiresMessageIndexRebuild;
}
public void rebuildMessagesIndex() {
final SQLiteDatabase db = getWritableDatabase();
final Stopwatch stopwatch = Stopwatch.createStarted();
db.execSQL(COPY_PREEXISTING_ENTRIES);
Log.d(Config.LOGTAG,"rebuilt message index in "+ stopwatch.stop().toString());
}
public static synchronized DatabaseBackend getInstance(Context context) {
if (instance == null) {
instance = new DatabaseBackend(context);
@ -263,6 +279,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
}
@Override
@ -515,16 +532,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0");
}
if (oldVersion < 41 && newVersion >= 41) {
db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
db.execSQL(COPY_PREEXISTING_ENTRIES);
if (oldVersion < 39 && newVersion >= 39) {
db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
}
if (oldVersion < 42 && newVersion >= 42) {
db.execSQL("DROP TRIGGER IF EXISTS after_message_delete");
}
if (QuickConversationsService.isQuicksy() && oldVersion < 43 && newVersion >= 43) {
List<Account> accounts = getAccounts(db);
for (Account account : accounts) {
@ -548,10 +559,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (oldVersion < 46 && newVersion >= 46) {
final long start = SystemClock.elapsedRealtime();
db.rawQuery("PRAGMA secure_delete = FALSE", null).close();
db.execSQL("update "+Message.TABLENAME+" set "+Message.EDITED+"=NULL");
db.execSQL("update " + Message.TABLENAME + " set " + Message.EDITED + "=NULL");
db.rawQuery("PRAGMA secure_delete=ON", null).close();
final long diff = SystemClock.elapsedRealtime() - start;
Log.d(Config.LOGTAG,"deleted old edit information in "+diff+"ms");
Log.d(Config.LOGTAG, "deleted old edit information in " + diff + "ms");
}
if (oldVersion < 47 && newVersion >= 47) {
@ -568,6 +579,26 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" + Contact.TABLENAME + "' AND COLUMN_NAME = '" + Contact.RTP_CAPABILITY + "') THEN"
+ "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT");
}
if (oldVersion < 50 && newVersion >= 50) {
db.beginTransaction();
db.execSQL("DROP TRIGGER IF EXISTS after_message_insert;");
db.execSQL("DROP TRIGGER IF EXISTS after_message_update;");
db.execSQL("DROP TRIGGER IF EXISTS after_message_delete;");
db.execSQL("DROP TABLE IF EXISTS messages_index;");
// a hack that should not be necessary, but
// there was at least one occurence when SQLite failed at this
db.execSQL("DROP TABLE IF EXISTS messages_index_docsize;");
db.execSQL("DROP TABLE IF EXISTS messages_index_segdir;");
db.execSQL("DROP TABLE IF EXISTS messages_index_segments;");
db.execSQL("DROP TABLE IF EXISTS messages_index_stat;");
db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
db.setTransactionSuccessful();
db.endTransaction();
requiresMessageIndexRebuild = true;
}
}
private void canonicalizeJids(SQLiteDatabase db) {
@ -782,7 +813,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
try {
list.add(0, Message.fromCursor(cursor, conversation));
} catch (Exception e) {
Log.e(Config.LOGTAG,"unable to restore message");
Log.e(Config.LOGTAG, "unable to restore message");
}
}
cursor.close();
@ -793,12 +824,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
final SQLiteDatabase db = this.getReadableDatabase();
final StringBuilder SQL = new StringBuilder();
final String[] selectionArgs;
SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + '.' + Conversation.CONTACTJID + ',' + Conversation.TABLENAME + '.' + Conversation.ACCOUNT + ',' + Conversation.TABLENAME + '.' + Conversation.MODE + " FROM " + Message.TABLENAME + " join " + Conversation.TABLENAME + " on " + Message.TABLENAME + '.' + Message.CONVERSATION + '=' + Conversation.TABLENAME + '.' + Conversation.UUID + " join messages_index ON messages_index.uuid=messages.uuid where " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + ',' + Message.ENCRYPTION_PGP + ',' + Message.ENCRYPTION_DECRYPTION_FAILED + ',' + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + ',' + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?");
SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + "." + Conversation.CONTACTJID + "," + Conversation.TABLENAME + "." + Conversation.ACCOUNT + "," + Conversation.TABLENAME + "." + Conversation.MODE + " FROM " + Message.TABLENAME + " JOIN " + Conversation.TABLENAME + " ON " + Message.TABLENAME + "." + Message.CONVERSATION + "=" + Conversation.TABLENAME + "." + Conversation.UUID + " JOIN messages_index ON messages_index.rowid=messages.rowid WHERE " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + "," + Message.ENCRYPTION_PGP + "," + Message.ENCRYPTION_DECRYPTION_FAILED + "," + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + "," + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?");
if (uuid == null) {
selectionArgs = new String[]{FtsUtils.toMatchString(term)};
} else {
selectionArgs = new String[]{FtsUtils.toMatchString(term), uuid};
SQL.append(" AND "+Conversation.TABLENAME+'.'+Conversation.UUID+"=?");
SQL.append(" AND " + Conversation.TABLENAME + '.' + Conversation.UUID + "=?");
}
SQL.append(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS);
Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term));
@ -862,7 +893,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public List<FilePathInfo> getFilePathInfo() {
final SQLiteDatabase db = this.getReadableDatabase();
final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2,5) and "+Message.RELATIVE_FILE_PATH+" is not null", null, null, null, null);
final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2,5) and " + Message.RELATIVE_FILE_PATH + " is not null", null, null, null, null);
final List<FilePathInfo> list = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
list.add(new FilePathInfo(cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0));
@ -875,7 +906,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public List<FilePath> getRelativeFilePaths(String account, Jid jid, int limit) {
SQLiteDatabase db = this.getReadableDatabase();
final String SQL = "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and "+Message.RELATIVE_FILE_PATH+" is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc";
final String SQL = "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and " + Message.RELATIVE_FILE_PATH + " is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc";
final String[] args = {account, jid.toString(), jid.toString() + "/%"};
Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + limit : ""), args);
List<FilePath> filesPaths = new ArrayList<>();
@ -900,7 +931,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public boolean deleted;
private FilePathInfo(String uuid, String path, boolean deleted) {
super(uuid,path);
super(uuid, path);
this.deleted = deleted;
}
@ -1044,8 +1075,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
long start = SystemClock.elapsedRealtime();
final SQLiteDatabase db = this.getWritableDatabase();
db.beginTransaction();
String[] args = {conversation.getUuid()};
db.delete("messages_index", "uuid in (select uuid from messages where conversationUuid=?)", args);
final String[] args = {conversation.getUuid()};
int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
db.setTransactionSuccessful();
db.endTransaction();
@ -1056,7 +1086,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
final String[] args = {String.valueOf(timestamp)};
SQLiteDatabase db = this.getReadableDatabase();
db.beginTransaction();
db.delete("messages_index", "uuid in (select uuid from messages where timeSent<?)", args);
db.delete(Message.TABLENAME, "timeSent<?", args);
db.setTransactionSuccessful();
db.endTransaction();

View File

@ -31,6 +31,9 @@ import android.util.LruCache;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.core.content.FileProvider;
import androidx.exifinterface.media.ExifInterface;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
@ -64,7 +67,6 @@ import eu.siacs.conversations.ui.RecordingActivity;
import eu.siacs.conversations.ui.util.Attachment;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.FileWriterException;
import eu.siacs.conversations.utils.MimeUtils;
@ -627,20 +629,20 @@ public class FileBackend {
private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath());
file.getParentFile().mkdirs();
OutputStream os = null;
InputStream is = null;
try {
file.createNewFile();
os = new FileOutputStream(file);
is = mXmppConnectionService.getContentResolver().openInputStream(uri);
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
try {
os.write(buffer, 0, length);
} catch (IOException e) {
throw new FileWriterException();
}
} catch (IOException e) {
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
}
try (final OutputStream os = new FileOutputStream(file);
final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri)) {
if (is == null) {
throw new FileCopyException(R.string.error_file_not_found);
}
try {
ByteStreams.copy(is, os);
} catch (IOException e) {
throw new FileWriterException();
}
try {
os.flush();
@ -648,16 +650,17 @@ public class FileBackend {
throw new FileWriterException();
}
} catch (final FileNotFoundException e) {
cleanup(file);
throw new FileCopyException(R.string.error_file_not_found);
} catch (final FileWriterException e) {
cleanup(file);
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
} catch (final SecurityException e) {
cleanup(file);
throw new FileCopyException(R.string.error_security_exception);
} catch (final IOException e) {
cleanup(file);
throw new FileCopyException(R.string.error_io_exception);
} finally {
close(os);
close(is);
}
}
@ -708,7 +711,7 @@ public class FileBackend {
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException {
final File parent = file.getParentFile();
if (parent.mkdirs()) {
if (parent != null && parent.mkdirs()) {
Log.d(Config.LOGTAG, "created parent directory");
}
InputStream is = null;
@ -753,13 +756,15 @@ public class FileBackend {
}
scaledBitmap.recycle();
} catch (final FileNotFoundException e) {
cleanup(file);
throw new FileCopyException(R.string.error_file_not_found);
} catch (IOException e) {
e.printStackTrace();
} catch (final IOException e) {
cleanup(file);
throw new FileCopyException(R.string.error_io_exception);
} catch (SecurityException e) {
cleanup(file);
throw new FileCopyException(R.string.error_security_exception_during_image_copy);
} catch (OutOfMemoryError e) {
} catch (final OutOfMemoryError e) {
++sampleSize;
if (sampleSize <= 3) {
copyImageToPrivateStorage(file, image, sampleSize);
@ -772,6 +777,14 @@ public class FileBackend {
}
}
private static void cleanup(final File file) {
try {
file.delete();
} catch (Exception e) {
}
}
public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, ImageCompressionException {
Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath());
copyImageToPrivateStorage(file, image, 0);
@ -808,19 +821,34 @@ public class FileBackend {
}
}
private int getRotation(File file) {
return getRotation(Uri.parse("file://" + file.getAbsolutePath()));
private int getRotation(final File file) {
try (final InputStream inputStream = new FileInputStream(file)) {
return getRotation(inputStream);
} catch (Exception e) {
return 0;
}
}
private int getRotation(Uri image) {
InputStream is = null;
try {
is = mXmppConnectionService.getContentResolver().openInputStream(image);
return ExifHelper.getOrientation(is);
} catch (FileNotFoundException e) {
private int getRotation(final Uri image) {
try (final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(image)) {
return is == null ? 0 : getRotation(is);
} catch (final Exception e) {
return 0;
} finally {
close(is);
}
}
private static int getRotation(final InputStream inputStream) throws IOException {
final ExifInterface exif = new ExifInterface(inputStream);
final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default:
return 0;
}
}
@ -1468,7 +1496,8 @@ public class FileBackend {
this.resId = resId;
}
public @StringRes int getResId() {
public @StringRes
int getResId() {
return resId;
}
}

View File

@ -5,6 +5,8 @@ import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
import androidx.core.content.ContextCompat;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.io.CipherOutputStream;
@ -118,7 +120,8 @@ public class AbstractConnectionManager {
}
public long getAutoAcceptFileSize() {
return this.mXmppConnectionService.getLongPreference("auto_accept_file_size", R.integer.auto_accept_filesize);
final long autoAcceptFileSize = this.mXmppConnectionService.getLongPreference("auto_accept_file_size", R.integer.auto_accept_filesize);
return autoAcceptFileSize <= 0 ? -1 : autoAcceptFileSize;
}
public boolean hasStoragePermission() {
@ -134,12 +137,8 @@ public class AbstractConnectionManager {
}
}
public PowerManager.WakeLock createWakeLock(final Thread thread) {
return createWakeLock("conversations:" + thread.getName());
}
public PowerManager.WakeLock createWakeLock(final String name) {
final PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class);
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
}

View File

@ -3,16 +3,19 @@ package eu.siacs.conversations.services;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.util.Log;
import net.ypresto.androidtranscoder.MediaTranscoder;
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
import androidx.annotation.NonNull;
import com.otaliastudios.transcoder.Transcoder;
import com.otaliastudios.transcoder.TranscoderListener;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@ -23,161 +26,164 @@ import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.Android360pFormatStrategy;
import eu.siacs.conversations.utils.Android720pFormatStrategy;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.TranscoderStrategies;
public class AttachFileToConversationRunnable implements Runnable, MediaTranscoder.Listener {
public class AttachFileToConversationRunnable implements Runnable, TranscoderListener {
private final XmppConnectionService mXmppConnectionService;
private final Message message;
private final Uri uri;
private final String type;
private final UiCallback<Message> callback;
private final boolean isVideoMessage;
private final long originalFileSize;
private int currentProgress = -1;
private final XmppConnectionService mXmppConnectionService;
private final Message message;
private final Uri uri;
private final String type;
private final UiCallback<Message> callback;
private final boolean isVideoMessage;
private final long originalFileSize;
private int currentProgress = -1;
AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback<Message> callback) {
this.uri = uri;
this.type = type;
this.mXmppConnectionService = xmppConnectionService;
this.message = message;
this.callback = callback;
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService,uri);
this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression());
}
AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback<Message> callback) {
this.uri = uri;
this.type = type;
this.mXmppConnectionService = xmppConnectionService;
this.message = message;
this.callback = callback;
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri);
this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression());
}
boolean isVideoMessage() {
return this.isVideoMessage;
}
boolean isVideoMessage() {
return this.isVideoMessage;
}
private void processAsFile() {
final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri);
if (path != null && !FileBackend.isPathBlacklisted(path)) {
message.setRelativeFilePath(path);
mXmppConnectionService.getFileBackend().updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
mXmppConnectionService.getPgpEngine().encrypt(message, callback);
} else {
mXmppConnectionService.sendMessage(message);
callback.success(message);
}
} else {
try {
mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
mXmppConnectionService.getFileBackend().updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
if (pgpEngine != null) {
pgpEngine.encrypt(message, callback);
} else if (callback != null) {
callback.error(R.string.unable_to_connect_to_keychain, null);
}
} else {
mXmppConnectionService.sendMessage(message);
callback.success(message);
}
} catch (FileBackend.FileCopyException e) {
callback.error(e.getResId(), message);
}
}
}
private void processAsFile() {
final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri);
if (path != null && !FileBackend.isPathBlacklisted(path)) {
message.setRelativeFilePath(path);
mXmppConnectionService.getFileBackend().updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
mXmppConnectionService.getPgpEngine().encrypt(message, callback);
} else {
mXmppConnectionService.sendMessage(message);
callback.success(message);
}
} else {
try {
mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
mXmppConnectionService.getFileBackend().updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
if (pgpEngine != null) {
pgpEngine.encrypt(message, callback);
} else if (callback != null) {
callback.error(R.string.unable_to_connect_to_keychain, null);
}
} else {
mXmppConnectionService.sendMessage(message);
callback.success(message);
}
} catch (FileBackend.FileCopyException e) {
callback.error(e.getResId(), message);
}
}
}
private void processAsVideo() throws FileNotFoundException {
Log.d(Config.LOGTAG,"processing file as video");
mXmppConnectionService.startForcingForegroundNotification();
message.setRelativeFilePath(message.getUuid() + ".mp4");
final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
final MediaFormatStrategy formatStrategy = "720".equals(getVideoCompression()) ? new Android720pFormatStrategy() : new Android360pFormatStrategy();
file.getParentFile().mkdirs();
final ParcelFileDescriptor parcelFileDescriptor = mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r");
if (parcelFileDescriptor == null) {
throw new FileNotFoundException("Parcel File Descriptor was null");
}
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Future<Void> future = MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), formatStrategy, this);
try {
future.get();
} catch (InterruptedException e) {
throw new AssertionError(e);
} catch (ExecutionException e) {
if (e.getCause() instanceof Error) {
mXmppConnectionService.stopForcingForegroundNotification();
processAsFile();
} else {
Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
}
}
}
private void processAsVideo() throws FileNotFoundException {
Log.d(Config.LOGTAG, "processing file as video");
mXmppConnectionService.startForcingForegroundNotification();
message.setRelativeFilePath(message.getUuid() + ".mp4");
final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
Log.d(Config.LOGTAG, "created parent directory for video file");
}
@Override
public void onTranscodeProgress(double progress) {
final int p = (int) Math.round(progress * 100);
if (p > currentProgress) {
currentProgress = p;
mXmppConnectionService.getNotificationService().updateFileAddingNotification(p,message);
}
}
final boolean highQuality = "720".equals(getVideoCompression());
@Override
public void onTranscodeCompleted() {
mXmppConnectionService.stopForcingForegroundNotification();
final File file = mXmppConnectionService.getFileBackend().getFile(message);
long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
Log.d(Config.LOGTAG,"originalFileSize="+originalFileSize+" convertedFileSize="+convertedFileSize);
if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
if (file.delete()) {
Log.d(Config.LOGTAG,"original file size was smaller. deleting and processing as file");
processAsFile();
return;
} else {
Log.d(Config.LOGTAG,"unable to delete converted file");
}
}
mXmppConnectionService.getFileBackend().updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
mXmppConnectionService.getPgpEngine().encrypt(message, callback);
} else {
mXmppConnectionService.sendMessage(message);
callback.success(message);
}
}
final Future<Void> future = Transcoder.into(file.getAbsolutePath()).
addDataSource(mXmppConnectionService, uri)
.setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P)
.setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ)
.setListener(this)
.transcode();
try {
future.get();
} catch (InterruptedException e) {
throw new AssertionError(e);
} catch (ExecutionException e) {
if (e.getCause() instanceof Error) {
mXmppConnectionService.stopForcingForegroundNotification();
processAsFile();
} else {
Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
}
}
}
@Override
public void onTranscodeCanceled() {
mXmppConnectionService.stopForcingForegroundNotification();
processAsFile();
}
@Override
public void onTranscodeProgress(double progress) {
final int p = (int) Math.round(progress * 100);
if (p > currentProgress) {
currentProgress = p;
mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message);
}
}
@Override
public void onTranscodeFailed(Exception e) {
mXmppConnectionService.stopForcingForegroundNotification();
Log.d(Config.LOGTAG,"video transcoding failed",e);
processAsFile();
}
@Override
public void onTranscodeCompleted(int successCode) {
mXmppConnectionService.stopForcingForegroundNotification();
final File file = mXmppConnectionService.getFileBackend().getFile(message);
long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize);
if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
if (file.delete()) {
Log.d(Config.LOGTAG, "original file size was smaller. deleting and processing as file");
processAsFile();
return;
} else {
Log.d(Config.LOGTAG, "unable to delete converted file");
}
}
mXmppConnectionService.getFileBackend().updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
mXmppConnectionService.getPgpEngine().encrypt(message, callback);
} else {
mXmppConnectionService.sendMessage(message);
callback.success(message);
}
}
@Override
public void run() {
if (this.isVideoMessage()) {
try {
processAsVideo();
} catch (FileNotFoundException e) {
processAsFile();
}
} else {
processAsFile();
}
}
@Override
public void onTranscodeCanceled() {
mXmppConnectionService.stopForcingForegroundNotification();
processAsFile();
}
private String getVideoCompression() {
return getVideoCompression(mXmppConnectionService);
}
@Override
public void onTranscodeFailed(@NonNull @NotNull Throwable exception) {
mXmppConnectionService.stopForcingForegroundNotification();
Log.d(Config.LOGTAG, "video transcoding failed", exception);
processAsFile();
}
public static String getVideoCompression(final Context context) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
}
@Override
public void run() {
if (this.isVideoMessage()) {
try {
processAsVideo();
} catch (FileNotFoundException e) {
processAsFile();
}
} else {
processAsFile();
}
}
private String getVideoCompression() {
return getVideoCompression(mXmppConnectionService);
}
public static String getVideoCompression(final Context context) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
}
}

View File

@ -35,6 +35,7 @@ import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.IconCompat;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import java.io.File;
import java.io.IOException;
@ -397,7 +398,7 @@ public class NotificationService {
notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
}
public void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) {
public synchronized void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) {
showIncomingCallNotification(id, media);
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
final int currentInterruptionFilter;
@ -407,7 +408,7 @@ public class NotificationService {
currentInterruptionFilter = 1; //INTERRUPTION_FILTER_ALL
}
if (currentInterruptionFilter != 1) {
Log.d(Config.LOGTAG,"do not ring or vibrate because interruption filter has been set to "+currentInterruptionFilter);
Log.d(Config.LOGTAG, "do not ring or vibrate because interruption filter has been set to " + currentInterruptionFilter);
return;
}
final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
@ -424,13 +425,13 @@ public class NotificationService {
final Resources resources = mXmppConnectionService.getResources();
final String ringtonePreference = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone));
if (Strings.isNullOrEmpty(ringtonePreference)) {
Log.d(Config.LOGTAG,"ringtone has been set to none");
Log.d(Config.LOGTAG, "ringtone has been set to none");
return;
}
final Uri uri = Uri.parse(ringtonePreference);
this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
if (this.currentlyPlayingRingtone == null) {
Log.d(Config.LOGTAG,"unable to find ringtone for uri "+uri);
Log.d(Config.LOGTAG, "unable to find ringtone for uri " + uri);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@ -487,14 +488,23 @@ public class NotificationService {
notify(INCOMING_CALL_NOTIFICATION_ID, notification);
}
public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) {
final AbstractJingleConnection.Id id = ongoingCall.id;
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
if (media.contains(Media.VIDEO)) {
if (ongoingCall.media.contains(Media.VIDEO)) {
builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
if (ongoingCall.reconnecting) {
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call));
} else {
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
}
} else {
builder.setSmallIcon(R.drawable.ic_call_white_24dp);
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
if (ongoingCall.reconnecting) {
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call));
} else {
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
}
}
builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
@ -790,17 +800,18 @@ public class NotificationService {
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
.setShowsUserInterface(false)
.build();
String replyLabel = mXmppConnectionService.getString(R.string.reply);
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
final String replyLabel = mXmppConnectionService.getString(R.string.reply);
final String lastMessageUuid = Iterables.getLast(messages).getUuid();
final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
R.drawable.ic_send_text_offline,
replyLabel,
createReplyIntent(conversation, false))
createReplyIntent(conversation, lastMessageUuid, false))
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
.setShowsUserInterface(false)
.addRemoteInput(remoteInput).build();
NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
replyLabel,
createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build();
mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
int addedActionsCount = 1;
mBuilder.addAction(markReadAction);
@ -1066,13 +1077,14 @@ public class NotificationService {
return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
}
private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
private PendingIntent createReplyIntent(final Conversation conversation, final String lastMessageUuid, final boolean dismissAfterReply) {
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
intent.putExtra("uuid", conversation.getUuid());
intent.putExtra("dismiss_notification", dismissAfterReply);
intent.putExtra("last_message_uuid", lastMessageUuid);
final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
return PendingIntent.getService(mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent createReadPendingIntent(Conversation conversation) {

View File

@ -75,6 +75,8 @@ import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@ -184,8 +186,9 @@ public class XmppConnectionService extends Service {
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");
private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression");
private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader");
private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor");
@ -471,7 +474,6 @@ public class XmppConnectionService extends Service {
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
private WakeLock wakeLock;
private PowerManager pm;
private LruCache<String, Bitmap> mBitmapCache;
private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
@ -564,14 +566,14 @@ public class XmppConnectionService extends Service {
Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback);
if (runnable.isVideoMessage()) {
mVideoCompressionExecutor.execute(runnable);
VIDEO_COMPRESSION_EXECUTOR.execute(runnable);
} else {
mFileAddingExecutor.execute(runnable);
FILE_ATTACHMENT_EXECUTOR.execute(runnable);
}
}
public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
final String compressPictures = getCompressPicturesPreference();
if ("never".equals(compressPictures)
@ -593,7 +595,7 @@ public class XmppConnectionService extends Service {
message.setType(Message.TYPE_IMAGE);
}
Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
mFileAddingExecutor.execute(() -> {
FILE_ATTACHMENT_EXECUTOR.execute(() -> {
try {
getFileBackend().copyImageToPrivateStorage(message, uri);
} catch (FileBackend.ImageCompressionException e) {
@ -724,6 +726,7 @@ public class XmppConnectionService extends Service {
}
final CharSequence body = remoteInput.getCharSequence("text_reply");
final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
if (body == null || body.length() <= 0) {
break;
}
@ -732,7 +735,7 @@ public class XmppConnectionService extends Service {
restoredFromDatabaseLatch.await();
final Conversation c = findConversationByUuid(uuid);
if (c != null) {
directReply(c, body.toString(), dismissNotification);
directReply(c, body.toString(), lastMessageUuid, dismissNotification);
}
} catch (InterruptedException e) {
Log.d(Config.LOGTAG, "unable to process direct reply");
@ -930,8 +933,12 @@ public class XmppConnectionService extends Service {
}
}
private void directReply(Conversation conversation, String body, final boolean dismissAfterReply) {
Message message = new Message(conversation, body, conversation.getNextEncryption());
private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
final Message message = new Message(conversation, body, conversation.getNextEncryption());
if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
}
message.markUnread();
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
getPgpEngine().encrypt(message, new UiCallback<Message>() {
@ -1147,11 +1154,11 @@ public class XmppConnectionService extends Service {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
startContactObserver();
}
mFileAddingExecutor.execute(fileBackend::deleteHistoricAvatarPath);
FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
if (Compatibility.hasStoragePermission(this)) {
Log.d(Config.LOGTAG, "starting file observer");
mFileAddingExecutor.execute(this.fileObserver::startWatching);
mFileAddingExecutor.execute(this::checkForDeletedFiles);
FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching);
FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
}
if (Config.supportOpenPgp()) {
this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
@ -1172,7 +1179,7 @@ public class XmppConnectionService extends Service {
this.pgpServiceConnection.bindToService();
}
this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class);
this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
toggleForegroundService();
@ -1267,8 +1274,8 @@ public class XmppConnectionService extends Service {
public void restartFileObserver() {
Log.d(Config.LOGTAG, "restarting file observer");
mFileAddingExecutor.execute(this.fileObserver::restartWatching);
mFileAddingExecutor.execute(this::checkForDeletedFiles);
FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching);
FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
}
public void toggleScreenEventReceiver() {
@ -1291,8 +1298,8 @@ public class XmppConnectionService extends Service {
toggleForegroundService(false);
}
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
ongoingCall.set(new OngoingCall(id, media));
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
ongoingCall.set(new OngoingCall(id, media, reconnecting));
toggleForegroundService(false);
}
@ -1308,7 +1315,7 @@ public class XmppConnectionService extends Service {
final Notification notification;
final int id;
if (ongoing != null) {
notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media);
notification = this.mNotificationService.getOngoingCallNotification(ongoing);
id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
startForeground(id, notification);
mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
@ -1885,7 +1892,10 @@ public class XmppConnectionService extends Service {
long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
Runnable runnable = () -> {
long deletionDate = getAutomaticMessageDeletionDate();
if (DatabaseBackend.requiresMessageIndexRebuild()) {
DatabaseBackend.getInstance(this).rebuildMessagesIndex();
}
final long deletionDate = getAutomaticMessageDeletionDate();
mLastExpiryRun.set(SystemClock.elapsedRealtime());
if (deletionDate > 0) {
Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
@ -1925,7 +1935,7 @@ public class XmppConnectionService extends Service {
private void restoreMessages(Conversation conversation) {
conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
conversation.findUnreadMessages(message -> mNotificationService.pushFromBacklog(message));
conversation.findUnreadMessages(mNotificationService::pushFromBacklog);
}
public void loadPhoneContacts() {
@ -3337,35 +3347,26 @@ public class XmppConnectionService extends Service {
public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
final Jid jid = user.asBareJid();
IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
conference.getMucOptions().changeAffiliation(jid, affiliation);
getAvatarService().clear(conference);
final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
sendIqPacket(conference.getAccount(), request, (account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
conference.getMucOptions().changeAffiliation(jid, affiliation);
getAvatarService().clear(conference);
if (callback != null) {
callback.onAffiliationChangedSuccessful(jid);
} else {
callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
Log.d(Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation);
}
} else if (callback != null) {
callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
} else {
Log.d(Config.LOGTAG, "unable to change affiliation");
}
});
}
public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
List<Jid> jids = new ArrayList<>();
for (MucOptions.User user : conference.getMucOptions().getUsers()) {
if (user.getAffiliation() == before && user.getRealJid() != null) {
jids.add(user.getRealJid());
}
}
IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
}
public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) {
IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
Log.d(Config.LOGTAG, request.toString());
sendIqPacket(conference.getAccount(), request, (account, packet) -> {
if (packet.getType() != IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick);
@ -3928,9 +3929,13 @@ public class XmppConnectionService extends Service {
new Thread(() -> reconnectAccount(account, false, true)).start();
}
public void invite(Conversation conversation, Jid contact) {
public void invite(final Conversation conversation, final Jid contact) {
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid());
MessagePacket packet = mMessageGenerator.invite(conversation, contact);
final MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
}
final MessagePacket packet = mMessageGenerator.invite(conversation, contact);
sendMessagePacket(conversation.getAccount(), packet);
}
@ -4864,12 +4869,14 @@ public class XmppConnectionService extends Service {
}
public static class OngoingCall {
private final AbstractJingleConnection.Id id;
private final Set<Media> media;
public final AbstractJingleConnection.Id id;
public final Set<Media> media;
public final boolean reconnecting;
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
this.id = id;
this.media = media;
this.reconnecting = reconnecting;
}
@Override
@ -4877,12 +4884,12 @@ public class XmppConnectionService extends Service {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OngoingCall that = (OngoingCall) o;
return Objects.equal(id, that.id);
return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media);
}
@Override
public int hashCode() {
return Objects.hashCode(id);
return Objects.hashCode(id, media, reconnecting);
}
}
}

View File

@ -5,12 +5,19 @@ import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;
import static eu.siacs.conversations.ui.XmppActivity.configureActionBar;
public class AboutActivity extends AppCompatActivity {
@Override
protected void onResume(){
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

View File

@ -1,5 +1,6 @@
package eu.siacs.conversations.ui;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@ -7,6 +8,7 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -86,6 +88,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
};
public static void open(final Activity activity, final Conversation conversation) {
Intent intent = new Intent(activity, ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
activity.startActivity(intent);
}
private final OnClickListener mNotifyStatusClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
@ -481,6 +490,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead);
this.binding.mucSubject.setAutoLinkMask(0);
this.binding.mucSubject.setVisibility(View.VISIBLE);
this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
} else {
this.binding.mucSubject.setVisibility(View.GONE);
}

View File

@ -45,6 +45,7 @@ import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.services.AbstractQuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.ui.adapter.MediaAdapter;
@ -58,6 +59,7 @@ import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Namespace;
@ -131,15 +133,31 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
}
private void showAddToPhoneBookDialog() {
final Jid jid = contact.getJid();
final boolean quicksyContact = AbstractQuickConversationsService.isQuicksy()
&& Config.QUICKSY_DOMAIN.equals(jid.getDomain())
&& jid.getLocal() != null;
final String value;
if (quicksyContact) {
value = PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, jid);
} else {
value = jid.toEscapedString();
}
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.action_add_phone_book));
builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toEscapedString()));
builder.setMessage(getString(R.string.add_phone_book_text, value));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.add), (dialog, which) -> {
final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
intent.setType(Contacts.CONTENT_ITEM_TYPE);
intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toEscapedString());
intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
if (quicksyContact) {
intent.putExtra(Intents.Insert.PHONE, value);
} else {
intent.putExtra(Intents.Insert.IM_HANDLE, value);
intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
//TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP'
// however we dont have such a field and thus have to use the legacy PROTOCOL_JABBER
}
intent.putExtra("finishActivityOnSaveCompleted", true);
try {
startActivityForResult(intent, 0);
@ -233,6 +251,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {

View File

@ -6,6 +6,8 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import eu.siacs.conversations.ui.util.SettingsUtils;
public class ConversationActivity extends AppCompatActivity {
@Override
@ -14,4 +16,10 @@ public class ConversationActivity extends AppCompatActivity {
startActivity(new Intent(this, ConversationsActivity.class));
finish();
}
@Override
protected void onResume(){
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
}
}

View File

@ -135,6 +135,8 @@ import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
import org.jetbrains.annotations.NotNull;
public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {
@ -186,10 +188,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
@Override
public void onClick(View v) {
Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
startActivity(intent);
ConferenceDetailsActivity.open(getActivity(), conversation);
}
};
private final OnClickListener leaveMuc = new OnClickListener() {
@ -689,14 +688,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
toggleInputMethod();
}
private void attachImageToConversation(Conversation conversation, Uri uri) {
private void attachImageToConversation(Conversation conversation, Uri uri, String type) {
if (conversation == null) {
return;
}
final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
prepareFileToast.show();
activity.delegateUriPermissionsToService(uri);
activity.xmppConnectionService.attachImageToConversation(conversation, uri,
activity.xmppConnectionService.attachImageToConversation(conversation, uri, type,
new UiCallback<Message>() {
@Override
@ -734,7 +733,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (body.length() == 0 || conversation == null) {
return;
}
if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(REQUEST_TRUST_KEYS_TEXT)) {
if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_TEXT)) {
return;
}
final Message message;
@ -757,6 +756,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
}
private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(requestCode);
}
protected boolean trustKeysIfNeeded(int requestCode) {
AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
final List<Jid> targets = axolotlService.getCryptoTargets(conversation);
@ -824,6 +827,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
case REQUEST_TRUST_KEYS_ATTACHMENTS:
commitAttachments();
break;
case REQUEST_START_AUDIO_CALL:
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
break;
case REQUEST_START_VIDEO_CALL:
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
break;
case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
final List<Attachment> imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE);
mediaPreviewAdapter.addMediaPreviews(imageUris);
@ -847,9 +856,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
toggleInputMethod();
break;
case ATTACHMENT_CHOICE_LOCATION:
double latitude = data.getDoubleExtra("latitude", 0);
double longitude = data.getDoubleExtra("longitude", 0);
Uri geo = Uri.parse("geo:" + latitude + "," + longitude);
final double latitude = data.getDoubleExtra("latitude", 0);
final double longitude = data.getDoubleExtra("longitude", 0);
final int accuracy = data.getIntExtra("accuracy", 0);
final Uri geo;
if (accuracy > 0) {
geo = Uri.parse(String.format("geo:%s,%s;u=%s", latitude, longitude, accuracy));
} else {
geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude));
}
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
toggleInputMethod();
break;
@ -870,7 +885,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return;
}
if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(REQUEST_TRUST_KEYS_ATTACHMENTS)) {
if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_ATTACHMENTS)) {
return;
}
final PresenceSelector.OnPresenceSelected callback = () -> {
@ -880,7 +895,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
attachLocationToConversation(conversation, attachment.getUri());
} else if (attachment.getType() == Attachment.Type.IMAGE) {
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
attachImageToConversation(conversation, attachment.getUri());
attachImageToConversation(conversation, attachment.getUri(), attachment.getMime());
} else {
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
attachFileToConversation(conversation, attachment.getUri(), attachment.getMime());
@ -1262,10 +1277,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
activity.switchToContactDetails(conversation.getContact());
break;
case R.id.action_muc_details:
Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
startActivity(intent);
ConferenceDetailsActivity.open(getActivity(), conversation);
break;
case R.id.action_invite:
startActivityForResult(ChooseContactActivity.create(activity, conversation), REQUEST_INVITE_TO_CONVERSATION);
@ -1365,7 +1377,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show();
return;
}
final Contact contact = conversation.getContact();
if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action);
@ -1577,6 +1588,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
if (writeGranted(grantResults, permissions)) {
if (activity != null && activity.xmppConnectionService != null) {
activity.xmppConnectionService.getBitmapCache().evictAll();
activity.xmppConnectionService.restartFileObserver();
}
refresh();
@ -1615,9 +1627,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
@SuppressLint("InflateParams")
protected void clearHistoryDialog(final Conversation conversation) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setTitle(getString(R.string.clear_conversation_history));
final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox);
builder.setView(dialogView);
builder.setNegativeButton(getString(R.string.cancel), null);
@ -1635,7 +1647,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
protected void muteConversationDialog(final Conversation conversation) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setTitle(R.string.disable_notifications);
final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
final CharSequence[] labels = new CharSequence[durations.length];
@ -1651,13 +1663,13 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (durations[which] == -1) {
till = Long.MAX_VALUE;
} else {
till = System.currentTimeMillis() + (durations[which] * 1000);
till = System.currentTimeMillis() + (durations[which] * 1000L);
}
conversation.setMutedTill(till);
activity.xmppConnectionService.updateConversation(conversation);
activity.onConversationsListItemUpdated();
refresh();
getActivity().invalidateOptionsMenu();
requireActivity().invalidateOptionsMenu();
});
builder.create().show();
}
@ -1689,7 +1701,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
this.activity.xmppConnectionService.updateConversation(conversation);
this.activity.onConversationsListItemUpdated();
refresh();
getActivity().invalidateOptionsMenu();
requireActivity().invalidateOptionsMenu();
}
@ -1699,9 +1711,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
switch (attachmentChoice) {
case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
intent.setAction(Intent.ACTION_GET_CONTENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
intent.setType("image/*");
chooser = true;
break;
@ -1719,9 +1729,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
case ATTACHMENT_CHOICE_CHOOSE_FILE:
chooser = true;
intent.setType("*/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setAction(Intent.ACTION_GET_CONTENT);
break;
@ -1804,7 +1812,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
private void showErrorMessage(final Message message) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setTitle(R.string.error_message);
final String errorMessage = message.getErrorMessage();
final String[] errorMessageParts = errorMessage == null ? new String[0] : errorMessage.split("\\u001f");
@ -1825,7 +1833,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private void deleteFile(final Message message) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setNegativeButton(R.string.cancel, null);
builder.setTitle(R.string.delete_file_dialog);
builder.setMessage(R.string.delete_file_dialog_msg);
@ -1854,7 +1862,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (!message.hasFileOnRemoteHost()
&& xmppConnection != null
&& conversation.getMode() == Conversational.MODE_SINGLE
&& !xmppConnection.getFeatures().httpUpload(message.getFileParams().size)) {
&& !xmppConnection.getFeatures().httpUpload(message.getFileParams().getSize())) {
activity.selectPresence(conversation, () -> {
message.setCounterpart(conversation.getNextCounterpart());
activity.xmppConnectionService.resendFailedMessages(message);
@ -1959,7 +1967,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(@NotNull Bundle outState) {
super.onSaveInstanceState(outState);
if (conversation != null) {
outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid());
@ -2183,13 +2191,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE);
final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false);
final String type = extras.getString(ConversationsActivity.EXTRA_TYPE);
final List<Uri> uris = extractUris(extras);
if (uris != null && uris.size() > 0) {
if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) {
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION));
} else {
final List<Uri> cleanedUris = cleanUris(new ArrayList<>(uris));
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris));
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type));
}
toggleInputMethod();
return;
@ -3048,4 +3057,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
activity.switchToAccount(message.getConversation().getAccount(), fingerprint);
}
private Activity requireActivity() {
final Activity activity = getActivity();
if (activity == null) {
throw new IllegalStateException("Activity not attached");
}
return activity;
}
}

View File

@ -30,6 +30,8 @@
package eu.siacs.conversations.ui;
import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
@ -65,13 +67,16 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.databinding.ActivityConversationsBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
import eu.siacs.conversations.ui.interfaces.OnConversationRead;
import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
import eu.siacs.conversations.ui.util.ActionBarUtil;
import eu.siacs.conversations.ui.util.ActivityResult;
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
@ -83,8 +88,6 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged {
public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
@ -96,6 +99,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
public static final String POST_ACTION_RECORD_VOICE = "record_voice";
public static final String EXTRA_TYPE = "type";
private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
ACTION_VIEW_CONVERSATION,
@ -278,6 +282,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@ -425,16 +430,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
private void openConversation(Conversation conversation, Bundle extras) {
ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment);
final FragmentManager fragmentManager = getFragmentManager();
executePendingTransactions(fragmentManager);
ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
final boolean mainNeedsRefresh;
if (conversationFragment == null) {
mainNeedsRefresh = false;
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
if (mainFragment instanceof ConversationFragment) {
conversationFragment = (ConversationFragment) mainFragment;
} else {
conversationFragment = new ConversationFragment();
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
fragmentTransaction.addToBackStack(null);
try {
@ -456,6 +463,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
}
private static void executePendingTransactions(final FragmentManager fragmentManager) {
try {
fragmentManager.executePendingTransactions();
} catch (final Exception e) {
Log.e(Config.LOGTAG,"unable to execute pending fragment transactions");
}
}
public boolean onXmppUriClicked(Uri uri) {
XmppUri xmppUri = new XmppUri(uri);
if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
@ -524,6 +539,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
@Override
protected void onStart() {
super.onStart();
final int theme = findTheme();
if (this.mTheme != theme) {
this.mSkipBackgroundBinding = true;
@ -532,7 +548,6 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
this.mSkipBackgroundBinding = false;
}
mRedirectInProcess.set(false);
super.onStart();
}
@Override
@ -562,17 +577,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
private void initializeFragments() {
FragmentTransaction transaction = getFragmentManager().beginTransaction();
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
final FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
if (mainFragment != null) {
if (binding.secondaryFragment != null) {
if (mainFragment instanceof ConversationFragment) {
getFragmentManager().popBackStack();
transaction.remove(mainFragment);
transaction.commit();
getFragmentManager().executePendingTransactions();
transaction = getFragmentManager().beginTransaction();
fragmentManager.executePendingTransactions();
transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.secondary_fragment, mainFragment);
transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
transaction.commit();
@ -583,7 +599,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
transaction.remove(secondaryFragment);
transaction.commit();
getFragmentManager().executePendingTransactions();
transaction = getFragmentManager().beginTransaction();
transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.main_fragment, secondaryFragment);
transaction.addToBackStack(null);
transaction.commit();
@ -601,18 +617,38 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
private void invalidateActionBarTitle() {
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
if (mainFragment instanceof ConversationFragment) {
final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
if (conversation != null) {
actionBar.setTitle(EmojiWrapper.transform(conversation.getName()));
actionBar.setDisplayHomeAsUpEnabled(true);
return;
}
if (actionBar == null) {
return;
}
final FragmentManager fragmentManager = getFragmentManager();
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
if (mainFragment instanceof ConversationFragment) {
final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
if (conversation != null) {
actionBar.setTitle(EmojiWrapper.transform(conversation.getName()));
actionBar.setDisplayHomeAsUpEnabled(true);
ActionBarUtil.setActionBarOnClickListener(
binding.toolbar,
(v) -> openConversationDetails(conversation)
);
return;
}
}
actionBar.setTitle(R.string.app_name);
actionBar.setDisplayHomeAsUpEnabled(false);
ActionBarUtil.resetActionBarOnClickListeners(binding.toolbar);
}
private void openConversationDetails(final Conversation conversation) {
if (conversation.getMode() == Conversational.MODE_MULTI) {
ConferenceDetailsActivity.open(this, conversation);
} else {
final Contact contact = conversation.getContact();
if (contact.isSelf()) {
switchToAccount(conversation.getAccount());
} else {
switchToContactDetails(contact);
}
actionBar.setTitle(R.string.app_name);
actionBar.setDisplayHomeAsUpEnabled(false);
}
}
@ -621,17 +657,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
if (performRedirectIfNecessary(conversation, false)) {
return;
}
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
final FragmentManager fragmentManager = getFragmentManager();
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
if (mainFragment instanceof ConversationFragment) {
try {
getFragmentManager().popBackStack();
} catch (IllegalStateException e) {
fragmentManager.popBackStack();
} catch (final IllegalStateException e) {
Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e);
//this usually means activity is no longer active; meaning on the next open we will run through this again
}
return;
}
Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
if (secondaryFragment instanceof ConversationFragment) {
if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);

View File

@ -39,6 +39,7 @@ import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.ui.util.LocationHelper;
import eu.siacs.conversations.ui.widget.Marker;
import eu.siacs.conversations.ui.widget.MyLocation;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;
public abstract class LocationActivity extends ActionBarActivity implements LocationListener {
@ -68,6 +69,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
}
}
protected void updateLocationMarkers() {
clearMarkers();
}
@ -222,6 +224,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
@Override
protected void onResume() {
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
Configuration.getInstance().load(this, getPreferences());
map.onResume();
this.setMyLoc(null);

View File

@ -39,6 +39,7 @@ import java.util.logging.Logger;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.MTMDecision;
import eu.siacs.conversations.services.MemorizingTrustManager;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;
public class MemorizingActivity extends AppCompatActivity implements OnClickListener, OnCancelListener {
@ -61,6 +62,8 @@ public class MemorizingActivity extends AppCompatActivity implements OnClickList
@Override
public void onResume() {
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
Intent i = getIntent();
decisionId = i.getIntExtra(MemorizingTrustManager.DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
int titleId = i.getIntExtra(MemorizingTrustManager.DECISION_TITLE_ID, R.string.mtm_accept_cert);

View File

@ -28,6 +28,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityRecordingBinding;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
@ -66,6 +67,12 @@ public class RecordingActivity extends Activity implements View.OnClickListener
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
protected void onResume(){
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
}
@Override
protected void onStart() {
super.onStart();

View File

@ -1,5 +1,8 @@
package eu.siacs.conversations.ui;
import static java.util.Arrays.asList;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;
@ -35,6 +38,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import org.webrtc.RendererCommon;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoTrack;
@ -54,6 +58,7 @@ import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.ui.util.Rationals;
import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xml.Namespace;
@ -64,10 +69,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
import static java.util.Arrays.asList;
public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate {
public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
public static final String EXTRA_WITH = "with";
public static final String EXTRA_SESSION_ID = "session_id";
@ -81,6 +83,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
private static final List<RtpEndUserState> END_CARD = Arrays.asList(
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.SECURITY_ERROR,
RtpEndUserState.DECLINED_OR_BUSY,
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
@ -88,11 +91,22 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
);
private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON = Arrays.asList(
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.CONNECTIVITY_ERROR
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.SECURITY_ERROR
);
private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
RtpEndUserState.CONNECTING,
RtpEndUserState.CONNECTED
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING
);
private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED = Arrays.asList(
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING
);
private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList(
RtpEndUserState.ACCEPTING_CALL,
RtpEndUserState.CONNECTING,
RtpEndUserState.RECONNECTING
);
private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
private static final int REQUEST_ACCEPT_CALL = 0x1111;
@ -443,12 +457,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
public void onStart() {
super.onStart();
mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
this.binding.remoteVideo.setOnAspectRatioChanged(this);
}
@Override
public void onStop() {
mHandler.removeCallbacks(mTickExecutor);
binding.remoteVideo.release();
binding.remoteVideo.setOnAspectRatioChanged(null);
binding.localVideo.release();
final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get();
@ -496,7 +512,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
private boolean isConnected() {
final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED;
return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
}
private boolean switchToPictureInPicture() {
@ -512,9 +528,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
@RequiresApi(api = Build.VERSION_CODES.O)
private void startPictureInPicture() {
try {
final Rational rational = this.binding.remoteVideo.getAspectRatio();
final Rational clippedRational = Rationals.clip(rational);
Log.d(Config.LOGTAG, "suggested rational " + rational + ". clipped to " + clippedRational);
enterPictureInPictureMode(
new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(10, 16))
.setAspectRatio(clippedRational)
.build()
);
} catch (final IllegalStateException e) {
@ -523,6 +542,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
}
}
@Override
public void onAspectRatioChanged(final Rational rational) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) {
final Rational clippedRational = Rationals.clip(rational);
Log.d(Config.LOGTAG, "suggested rational after aspect ratio change " + rational + ". clipped to " + clippedRational);
setPictureInPictureParams(new PictureInPictureParams.Builder()
.setAspectRatio(clippedRational)
.build());
}
}
private boolean deviceSupportsPictureInPicture() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
@ -615,8 +645,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
surfaceViewRenderer.setVisibility(View.VISIBLE);
try {
surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
} catch (IllegalStateException e) {
Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
} catch (final IllegalStateException e) {
//Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
}
surfaceViewRenderer.setEnableHardwareScaler(true);
}
@ -641,6 +671,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
case CONNECTED:
setTitle(R.string.rtp_state_connected);
break;
case RECONNECTING:
setTitle(R.string.rtp_state_reconnecting);
break;
case ACCEPTING_CALL:
setTitle(R.string.rtp_state_accepting_call);
break;
@ -668,6 +701,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
case APPLICATION_ERROR:
setTitle(R.string.rtp_state_application_failure);
break;
case SECURITY_ERROR:
setTitle(R.string.rtp_state_security_error);
break;
case ENDED:
throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
default:
@ -743,7 +779,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.RETRACTED
RtpEndUserState.RETRACTED,
RtpEndUserState.SECURITY_ERROR
).contains(state)) {
this.binding.rejectCall.setContentDescription(getString(R.string.exit));
this.binding.rejectCall.setOnClickListener(this::exit);
@ -779,7 +816,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
@SuppressLint("RestrictedApi")
private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) {
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
if (media.contains(Media.VIDEO)) {
final JingleRtpConnection rtpConnection = requireRtpConnection();
@ -907,14 +944,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
this.binding.duration.setVisibility(View.GONE);
return;
}
final long rtpConnectionStarted = connection.getRtpConnectionStarted();
final long rtpConnectionEnded = connection.getRtpConnectionEnded();
if (rtpConnectionStarted != 0) {
final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded;
this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false));
this.binding.duration.setVisibility(View.VISIBLE);
} else {
if (connection.zeroDuration()) {
this.binding.duration.setVisibility(View.GONE);
} else {
this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
this.binding.duration.setVisibility(View.VISIBLE);
}
}
@ -922,13 +956,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
binding.localVideo.setVisibility(View.GONE);
binding.localVideo.release();
binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE);
binding.remoteVideo.release();
binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
if (isPictureInPicture()) {
binding.appBarLayout.setVisibility(View.GONE);
binding.pipPlaceholder.setVisibility(View.VISIBLE);
if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) {
if (Arrays.asList(
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.SECURITY_ERROR)
.contains(state)) {
binding.pipWarning.setVisibility(View.VISIBLE);
binding.pipWaiting.setVisibility(View.GONE);
} else {
@ -942,9 +980,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
return;
}
if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
binding.localVideo.setVisibility(View.GONE);
binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE);
binding.appBarLayout.setVisibility(View.GONE);
binding.pipPlaceholder.setVisibility(View.VISIBLE);
binding.pipWarning.setVisibility(View.GONE);
@ -966,12 +1004,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
if (remoteVideoTrack.isPresent()) {
ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
addSink(remoteVideoTrack.get(), binding.remoteVideo);
binding.remoteVideo.setScalingType(
RendererCommon.ScalingType.SCALE_ASPECT_FILL,
RendererCommon.ScalingType.SCALE_ASPECT_FIT
);
if (state == RtpEndUserState.CONNECTED) {
binding.appBarLayout.setVisibility(View.GONE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
} else {
binding.appBarLayout.setVisibility(View.VISIBLE);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE);
}
if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
@ -980,7 +1024,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
}
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE);
binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
}
}

View File

@ -60,6 +60,7 @@ import java.util.Map;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.service.CameraManager;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.ui.widget.ScannerView;
/**
@ -181,6 +182,7 @@ public final class ScanActivity extends Activity implements SurfaceTextureListen
@Override
protected void onResume() {
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
maybeOpenCamera();
}

View File

@ -40,6 +40,7 @@ import eu.siacs.conversations.services.MemorizingTrustManager;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xmpp.Jid;
@ -57,8 +58,10 @@ public class SettingsActivity extends XmppActivity implements
public static final String THEME = "theme";
public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
public static final String OMEMO_SETTING = "omemo";
public static final String PREVENT_SCREENSHOTS = "prevent_screenshots";
public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
private SettingsFragment mSettingsFragment;
@Override
@ -393,8 +396,15 @@ public class SettingsActivity extends XmppActivity implements
if (this.mTheme != theme) {
recreate();
}
} else if(name.equals(PREVENT_SCREENSHOTS)){
SettingsUtils.applyScreenshotPreventionSetting(this);
}
}
@Override
public void onResume(){
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
}
@Override

View File

@ -13,10 +13,13 @@ import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.snackbar.Snackbar;
import com.google.common.math.DoubleMath;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.util.GeoPoint;
import java.math.RoundingMode;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityShareLocationBinding;
@ -28,213 +31,213 @@ import eu.siacs.conversations.utils.ThemeHelper;
public class ShareLocationActivity extends LocationActivity implements LocationListener {
private Snackbar snackBar;
private ActivityShareLocationBinding binding;
private boolean marker_fixed_to_loc = false;
private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
private Boolean noAskAgain = false;
private Snackbar snackBar;
private ActivityShareLocationBinding binding;
private boolean marker_fixed_to_loc = false;
private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
private Boolean noAskAgain = false;
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
}
outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
}
}
if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
}
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.binding = DataBindingUtil.setContentView(this,R.layout.activity_share_location);
setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar());
setupMapView(binding.map, LocationProvider.getGeoPoint(this));
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_location);
setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar());
setupMapView(binding.map, LocationProvider.getGeoPoint(this));
this.binding.cancelButton.setOnClickListener(view -> {
setResult(RESULT_CANCELED);
finish();
});
this.binding.cancelButton.setOnClickListener(view -> {
setResult(RESULT_CANCELED);
finish();
});
this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
this.snackBar.setAction(R.string.enable, view -> {
if (isLocationEnabledAndAllowed()) {
updateUi();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
} else if (!isLocationEnabled()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
});
ThemeHelper.fix(this.snackBar);
this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
this.snackBar.setAction(R.string.enable, view -> {
if (isLocationEnabledAndAllowed()) {
updateUi();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
} else if (!isLocationEnabled()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
});
ThemeHelper.fix(this.snackBar);
this.binding.shareButton.setOnClickListener(view -> {
final Intent result = new Intent();
this.binding.shareButton.setOnClickListener(this::shareLocation);
if (marker_fixed_to_loc && myLoc != null) {
result.putExtra("latitude", myLoc.getLatitude());
result.putExtra("longitude", myLoc.getLongitude());
result.putExtra("altitude", myLoc.getAltitude());
result.putExtra("accuracy", (int) myLoc.getAccuracy());
} else {
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
result.putExtra("latitude", markerPoint.getLatitude());
result.putExtra("longitude", markerPoint.getLongitude());
}
this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
setResult(RESULT_OK, result);
finish();
});
this.binding.fab.setOnClickListener(view -> {
if (!marker_fixed_to_loc) {
if (!isLocationEnabled()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(REQUEST_CODE_FAB_PRESSED);
}
}
toggleFixedLocation();
});
}
this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
private void shareLocation(final View view) {
final Intent result = new Intent();
if (marker_fixed_to_loc && myLoc != null) {
result.putExtra("latitude", myLoc.getLatitude());
result.putExtra("longitude", myLoc.getLongitude());
result.putExtra("altitude", myLoc.getAltitude());
result.putExtra("accuracy", DoubleMath.roundToInt(myLoc.getAccuracy(), RoundingMode.HALF_UP));
} else {
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
result.putExtra("latitude", markerPoint.getLatitude());
result.putExtra("longitude", markerPoint.getLongitude());
}
setResult(RESULT_OK, result);
finish();
}
this.binding.fab.setOnClickListener(view -> {
if (!marker_fixed_to_loc) {
if (!isLocationEnabled()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(REQUEST_CODE_FAB_PRESSED);
}
}
toggleFixedLocation();
});
}
@Override
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
@Override
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0 &&
grantResults[0] != PackageManager.PERMISSION_GRANTED &&
Build.VERSION.SDK_INT >= 23 &&
permissions.length > 0 &&
(
Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) ||
Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) ||
Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
) &&
!shouldShowRequestPermissionRationale(permissions[0])) {
noAskAgain = true;
}
if (grantResults.length > 0 &&
grantResults[0] != PackageManager.PERMISSION_GRANTED &&
Build.VERSION.SDK_INT >= 23 &&
permissions.length > 0 &&
(
Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) ||
Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) ||
Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
) &&
!shouldShowRequestPermissionRationale(permissions[0])) {
noAskAgain = true;
}
if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
updateUi();
}
if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
updateUi();
}
@Override
protected void gotoLoc(final boolean setZoomLevel) {
if (this.myLoc != null && mapController != null) {
if (setZoomLevel) {
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
}
mapController.animateTo(new GeoPoint(this.myLoc));
}
}
@Override
protected void gotoLoc(final boolean setZoomLevel) {
if (this.myLoc != null && mapController != null) {
if (setZoomLevel) {
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
}
mapController.animateTo(new GeoPoint(this.myLoc));
}
}
@Override
protected void setMyLoc(final Location location) {
this.myLoc = location;
}
@Override
protected void setMyLoc(final Location location) {
this.myLoc = location;
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void updateLocationMarkers() {
super.updateLocationMarkers();
if (this.myLoc != null) {
this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
if (this.marker_fixed_to_loc) {
this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
} else {
this.binding.map.getOverlays().add(new Marker(marker_icon));
}
} else {
this.binding.map.getOverlays().add(new Marker(marker_icon));
}
}
@Override
protected void updateLocationMarkers() {
super.updateLocationMarkers();
if (this.myLoc != null) {
this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
if (this.marker_fixed_to_loc) {
this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
} else {
this.binding.map.getOverlays().add(new Marker(marker_icon));
}
} else {
this.binding.map.getOverlays().add(new Marker(marker_icon));
}
}
@Override
public void onLocationChanged(final Location location) {
if (this.myLoc == null) {
this.marker_fixed_to_loc = true;
}
updateUi();
if (LocationHelper.isBetterLocation(location, this.myLoc)) {
final Location oldLoc = this.myLoc;
this.myLoc = location;
@Override
public void onLocationChanged(final Location location) {
if (this.myLoc == null) {
this.marker_fixed_to_loc = true;
}
updateUi();
if (LocationHelper.isBetterLocation(location, this.myLoc)) {
final Location oldLoc = this.myLoc;
this.myLoc = location;
// Don't jump back to the users location if they're not moving (more or less).
if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) {
gotoLoc();
}
// Don't jump back to the users location if they're not moving (more or less).
if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) {
gotoLoc();
}
updateLocationMarkers();
}
}
updateLocationMarkers();
}
}
@Override
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
@Override
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
}
}
@Override
public void onProviderEnabled(final String provider) {
@Override
public void onProviderEnabled(final String provider) {
}
}
@Override
public void onProviderDisabled(final String provider) {
@Override
public void onProviderDisabled(final String provider) {
}
}
private boolean isLocationEnabledAndAllowed() {
return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
}
private boolean isLocationEnabledAndAllowed() {
return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
}
private void toggleFixedLocation() {
this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
if (this.marker_fixed_to_loc) {
gotoLoc(false);
}
updateLocationMarkers();
updateUi();
}
private void toggleFixedLocation() {
this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
if (this.marker_fixed_to_loc) {
gotoLoc(false);
}
updateLocationMarkers();
updateUi();
}
@Override
protected void updateUi() {
if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
this.snackBar.dismiss();
} else {
this.snackBar.show();
}
@Override
protected void updateUi() {
if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
this.snackBar.dismiss();
} else {
this.snackBar.show();
}
if (isLocationEnabledAndAllowed()) {
this.binding.fab.setVisibility(View.VISIBLE);
runOnUiThread(() -> {
this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
R.drawable.ic_gps_not_fixed_white_24dp);
this.binding.fab.setContentDescription(getResources().getString(
marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
));
this.binding.fab.invalidate();
});
} else {
this.binding.fab.setVisibility(View.GONE);
}
}
if (isLocationEnabledAndAllowed()) {
this.binding.fab.setVisibility(View.VISIBLE);
runOnUiThread(() -> {
this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
R.drawable.ic_gps_not_fixed_white_24dp);
this.binding.fab.setContentDescription(getResources().getString(
marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
));
this.binding.fab.invalidate();
});
} else {
this.binding.fab.setVisibility(View.GONE);
}
}
}

View File

@ -33,7 +33,8 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
refreshUi();
}
private class Share {
private static class Share {
public String type;
ArrayList<Uri> uris = new ArrayList<>();
public String account;
public String contact;
@ -65,6 +66,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (requestCode == REQUEST_STORAGE_PERMISSION) {
@ -139,6 +141,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
} else if (type != null && uri != null) {
this.share.uris.clear();
this.share.uris.add(uri);
this.share.type = type;
} else {
this.share.text = text;
this.share.asQuote = asQuote;
@ -193,6 +196,9 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
intent.setAction(Intent.ACTION_SEND_MULTIPLE);
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (share.type != null) {
intent.putExtra(ConversationsActivity.EXTRA_TYPE, share.type);
}
} else if (share.text != null) {
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
intent.putExtra(Intent.EXTRA_TEXT, share.text);

View File

@ -37,11 +37,14 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -51,6 +54,8 @@ import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.textfield.TextInputLayout;
import com.leinardi.android.speeddial.SpeedDialActionItem;
import com.leinardi.android.speeddial.SpeedDialView;
import java.util.ArrayList;
import java.util.Collections;
@ -266,8 +271,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar());
binding.speedDial.inflate(R.menu.start_conversation_fab_submenu);
inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu);
binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
@ -338,6 +342,21 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
});
}
private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) {
speedDialView.clearActionItems();
final PopupMenu popupMenu = new PopupMenu(this, new View(this));
popupMenu.inflate(menuRes);
final Menu menu = popupMenu.getMenu();
for (int i = 0; i < menu.size(); i++) {
final MenuItem menuItem = menu.getItem(i);
final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
.setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null)
.setFabImageTintColor(ContextCompat.getColor(this, R.color.white))
.create();
speedDialView.addActionItem(actionItem);
}
}
public static boolean isValidJid(String input) {
try {
Jid jid = Jid.ofEscaped(input);

View File

@ -7,24 +7,39 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import com.google.common.base.Strings;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityUriHandlerBinding;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.ProvisioningUtils;
import eu.siacs.conversations.utils.SignupUtils;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
public class UriHandlerActivity extends AppCompatActivity {
@ -34,7 +49,9 @@ public class UriHandlerActivity extends AppCompatActivity {
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790;
private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n");
private boolean handled = false;
private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>");
private ActivityUriHandlerBinding binding;
private Call call;
public static void scan(final Activity activity) {
scan(activity, false);
@ -77,9 +94,7 @@ public class UriHandlerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.handled = savedInstanceState != null && savedInstanceState.getBoolean("handled", false);
getLayoutInflater().inflate(R.layout.toolbar, findViewById(android.R.id.content));
setSupportActionBar(findViewById(R.id.toolbar));
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler);
}
@Override
@ -88,23 +103,17 @@ public class UriHandlerActivity extends AppCompatActivity {
handleIntent(getIntent());
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putBoolean("handled", this.handled);
super.onSaveInstanceState(savedInstanceState);
}
@Override
public void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleUri(Uri uri) {
handleUri(uri, false);
private boolean handleUri(final Uri uri) {
return handleUri(uri, false);
}
private void handleUri(Uri uri, final boolean scanned) {
private boolean handleUri(final Uri uri, final boolean scanned) {
final Intent intent;
final XmppUri xmppUri = new XmppUri(uri);
final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
@ -114,19 +123,22 @@ public class UriHandlerActivity extends AppCompatActivity {
final Jid jid = xmppUri.getJid();
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
Toast.makeText(this, R.string.account_already_exists, Toast.LENGTH_LONG).show();
return;
showError(R.string.account_already_exists);
return false;
}
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
startActivity(intent);
return;
return true;
}
if (accounts.size() == 0 && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
startActivity(intent);
return;
return true;
}
} else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
showError(R.string.account_registrations_are_not_supported);
return false;
}
if (accounts.size() == 0) {
@ -134,26 +146,19 @@ public class UriHandlerActivity extends AppCompatActivity {
intent = SignupUtils.getSignUpIntent(this);
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
startActivity(intent);
return true;
} else {
Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
showError(R.string.invalid_jid);
return false;
}
return;
}
if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) {
final Jid jid = xmppUri.getJid();
final String body = xmppUri.getBody();
if (jid != null) {
Class clazz;
try {
clazz = Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity");
} catch (ClassNotFoundException e) {
clazz = null;
}
final Class<?> clazz = findShareViaAccountClass();
if (clazz != null) {
intent = new Intent(this, clazz);
intent.putExtra("contact", jid.toEscapedString());
@ -164,7 +169,6 @@ public class UriHandlerActivity extends AppCompatActivity {
intent.setData(uri);
intent.putExtra("account", accounts.get(0).toEscapedString());
}
} else {
intent = new Intent(this, ShareWithActivity.class);
intent.setAction(Intent.ACTION_SEND);
@ -184,36 +188,94 @@ public class UriHandlerActivity extends AppCompatActivity {
intent.putExtra("scanned", scanned);
intent.setData(uri);
} else {
Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
return;
showError(R.string.invalid_jid);
return false;
}
startActivity(intent);
return true;
}
private void handleIntent(Intent data) {
if (handled) {
private void checkForLinkHeader(final HttpUrl url) {
Log.d(Config.LOGTAG, "checking for link header on " + url);
this.call = HttpConnectionManager.OK_HTTP_CLIENT.newCall(new Request.Builder()
.url(url)
.head()
.build());
this.call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d(Config.LOGTAG, "unable to check HTTP url", e);
showError(R.string.no_xmpp_adddress_found);
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) {
if (response.isSuccessful()) {
final String linkHeader = response.header("Link");
if (linkHeader != null && processLinkHeader(linkHeader)) {
return;
}
}
showError(R.string.no_xmpp_adddress_found);
}
});
}
private boolean processLinkHeader(final String header) {
final Matcher matcher = LINK_HEADER_PATTERN.matcher(header);
if (matcher.find()) {
final String group = matcher.group();
final String link = group.substring(1, group.length() - 1);
if (handleUri(Uri.parse(link))) {
finish();
return true;
}
}
return false;
}
private void showError(@StringRes int error) {
this.binding.progress.setVisibility(View.INVISIBLE);
this.binding.error.setText(error);
this.binding.error.setVisibility(View.VISIBLE);
}
private static Class<?> findShareViaAccountClass() {
try {
return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity");
} catch (final ClassNotFoundException e) {
return null;
}
}
private void handleIntent(final Intent data) {
final String action = data == null ? null : data.getAction();
if (action == null) {
return;
}
if (data == null || data.getAction() == null) {
finish();
return;
}
handled = true;
switch (data.getAction()) {
switch (action) {
case Intent.ACTION_MAIN:
binding.progress.setVisibility(call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
break;
case Intent.ACTION_VIEW:
case Intent.ACTION_SENDTO:
handleUri(data.getData());
if (handleUri(data.getData())) {
finish();
}
break;
case ACTION_SCAN_QR_CODE:
Intent intent = new Intent(this, ScanActivity.class);
startActivityForResult(intent, REQUEST_SCAN_QR_CODE);
return;
Log.d(Config.LOGTAG, "scan. allow=" + allowProvisioning());
setIntent(createMainIntent());
startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN_QR_CODE);
break;
}
}
finish();
private Intent createMainIntent() {
final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.putExtra(EXTRA_ALLOW_PROVISIONING, allowProvisioning());
return intent;
}
private boolean allowProvisioning() {
@ -225,6 +287,7 @@ public class UriHandlerActivity extends AppCompatActivity {
public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
super.onActivityResult(requestCode, requestCode, intent);
if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) {
final boolean allowProvisioning = allowProvisioning();
final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
if (Strings.isNullOrEmpty(result)) {
finish();
@ -233,22 +296,38 @@ public class UriHandlerActivity extends AppCompatActivity {
if (result.startsWith("BEGIN:VCARD\n")) {
final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result);
if (matcher.find()) {
handleUri(Uri.parse(matcher.group(2)), true);
if (handleUri(Uri.parse(matcher.group(2)), true)) {
finish();
}
} else {
showError(R.string.no_xmpp_adddress_found);
}
finish();
return;
} else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning()) {
} else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) {
ProvisioningUtils.provision(this, result);
finish();
return;
}
handleUri(Uri.parse(result), true);
final Uri uri = Uri.parse(result.trim());
if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
final HttpUrl httpUrl = HttpUrl.parse(uri.toString());
if (httpUrl != null) {
checkForLinkHeader(httpUrl);
} else {
finish();
}
} else if (handleUri(uri, true)) {
finish();
} else {
setIntent(new Intent(Intent.ACTION_VIEW, uri));
}
} else {
finish();
}
finish();
}
private static boolean looksLikeJsonObject(final String input) {
final String trimmed = Strings.emptyToNull(input).trim();
final String trimmed = Strings.nullToEmpty(input).trim();
return trimmed.charAt(0) == '{' && trimmed.charAt(trimmed.length() - 1) == '}';
}
}

View File

@ -80,6 +80,7 @@ import eu.siacs.conversations.ui.util.PresenceSelector;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
@ -819,8 +820,9 @@ public abstract class XmppActivity extends ActionBarActivity {
}
@Override
public void onResume() {
protected void onResume(){
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
}
protected int findTheme() {

View File

@ -92,6 +92,8 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
attr = R.attr.media_preview_document;
} else if (mime.equals("application/gpx+xml")) {
attr = R.attr.media_preview_tour;
} else if (mime.startsWith("image/")) {
attr = R.attr.media_preview_image;
} else {
attr = R.attr.media_preview_unknown;
}

View File

@ -75,8 +75,10 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
context.startActivity(view);
} catch (ActivityNotFoundException e) {
} catch (final ActivityNotFoundException e) {
Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
} catch (final SecurityException e) {
Toast.makeText(context, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show();
}
}

View File

@ -59,6 +59,7 @@ import eu.siacs.conversations.ui.text.DividerSpan;
import eu.siacs.conversations.ui.text.QuoteSpan;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.ui.util.ViewUtil;
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
import eu.siacs.conversations.utils.CryptoHelper;
@ -182,7 +183,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) {
FileParams params = message.getFileParams();
filesize = params.size > 0 ? UIHelper.filesizeToString(params.size) : null;
filesize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) {
error = true;
}
@ -357,48 +358,51 @@ public class MessageAdapter extends ArrayAdapter<Message> {
*/
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
boolean startsWithQuote = false;
char previous = '\n';
int lineStart = -1;
int lineTextStart = -1;
int quoteStart = -1;
for (int i = 0; i <= body.length(); i++) {
char current = body.length() > i ? body.charAt(i) : '\n';
if (lineStart == -1) {
if (previous == '\n') {
if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i))
|| current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) {
// Line start with quote
lineStart = i;
if (quoteStart == -1) quoteStart = i;
if (i == 0) startsWithQuote = true;
} else if (quoteStart >= 0) {
// Line start without quote, apply spans there
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
quoteStart = -1;
int quoteDepth = 1;
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
char previous = '\n';
int lineStart = -1;
int lineTextStart = -1;
int quoteStart = -1;
for (int i = 0; i <= body.length(); i++) {
char current = body.length() > i ? body.charAt(i) : '\n';
if (lineStart == -1) {
if (previous == '\n') {
if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
// Line start with quote
lineStart = i;
if (quoteStart == -1) quoteStart = i;
if (i == 0) startsWithQuote = true;
} else if (quoteStart >= 0) {
// Line start without quote, apply spans there
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
quoteStart = -1;
}
}
} else {
// Remove extra spaces between > and first character in the line
// > character will be removed too
if (current != ' ' && lineTextStart == -1) {
lineTextStart = i;
}
if (current == '\n') {
body.delete(lineStart, lineTextStart);
i -= lineTextStart - lineStart;
if (i == lineStart) {
// Avoid empty lines because span over empty line can be hidden
body.insert(i++, " ");
}
lineStart = -1;
lineTextStart = -1;
}
}
} else {
// Remove extra spaces between > and first character in the line
// > character will be removed too
if (current != ' ' && lineTextStart == -1) {
lineTextStart = i;
}
if (current == '\n') {
body.delete(lineStart, lineTextStart);
i -= lineTextStart - lineStart;
if (i == lineStart) {
// Avoid empty lines because span over empty line can be hidden
body.insert(i++, " ");
}
lineStart = -1;
lineTextStart = -1;
}
previous = current;
}
previous = current;
}
if (quoteStart >= 0) {
// Apply spans to finishing open quote
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
if (quoteStart >= 0) {
// Apply spans to finishing open quote
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
}
quoteDepth++;
}
return startsWithQuote;
}
@ -798,12 +802,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (message.treatAsDownloadable()) {
try {
final URI uri = new URI(message.getBody());
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize_on_host,
UIHelper.getFileDescriptionString(activity, message),
uri.getHost()),
darkBackground);
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize_on_host,
UIHelper.getFileDescriptionString(activity, message),
uri.getHost()),
darkBackground);
} catch (Exception e) {
displayDownloadableMessage(viewHolder,
message,

View File

@ -0,0 +1,88 @@
package eu.siacs.conversations.ui.util;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
public class ActionBarUtil {
public static void resetActionBarOnClickListeners(@NonNull View view) {
final View title = findActionBarTitle(view);
final View subtitle = findActionBarSubTitle(view);
if (title != null) {
title.setOnClickListener(null);
}
if (subtitle != null) {
subtitle.setOnClickListener(null);
}
}
public static void setActionBarOnClickListener(@NonNull View view,
@NonNull final View.OnClickListener onClickListener) {
final View title = findActionBarTitle(view);
final View subtitle = findActionBarSubTitle(view);
if (title != null) {
title.setOnClickListener(onClickListener);
}
if (subtitle != null) {
subtitle.setOnClickListener(onClickListener);
}
}
private static @Nullable View findActionBarTitle(@NonNull View root) {
return findActionBarItem(root, "action_bar_title", "mTitleTextView");
}
private static @Nullable
View findActionBarSubTitle(@NonNull View root) {
return findActionBarItem(root, "action_bar_subtitle", "mSubtitleTextView");
}
private static @Nullable View findActionBarItem(@NonNull View root,
@NonNull String resourceName,
@NonNull String toolbarFieldName) {
View result = findViewSupportOrAndroid(root, resourceName);
if (result == null) {
View actionBar = findViewSupportOrAndroid(root, "action_bar");
if (actionBar != null) {
result = reflectiveRead(actionBar, toolbarFieldName);
}
}
if (result == null && root.getClass().getName().endsWith("widget.Toolbar")) {
result = reflectiveRead(root, toolbarFieldName);
}
return result;
}
@SuppressWarnings("ConstantConditions")
private static @Nullable View findViewSupportOrAndroid(@NonNull View root,
@NonNull String resourceName) {
Context context = root.getContext();
View result = null;
if (result == null) {
int supportID = context.getResources().getIdentifier(resourceName, "id", context.getPackageName());
result = root.findViewById(supportID);
}
if (result == null) {
int androidID = context.getResources().getIdentifier(resourceName, "id", "android");
result = root.findViewById(androidID);
}
return result;
}
@SuppressWarnings("unchecked")
private static <T> T reflectiveRead(@NonNull Object object, @NonNull String fieldName) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (T) field.get(object);
} catch (final Exception ex) {
return null;
}
}
}

View File

@ -136,10 +136,10 @@ public class Attachment implements Parcelable {
return Collections.singletonList(new Attachment(uri, type, mime));
}
public static List<Attachment> of(final Context context, List<Uri> uris) {
List<Attachment> attachments = new ArrayList<>();
for (Uri uri : uris) {
final String mime = MimeUtils.guessMimeTypeFromUri(context, uri);
public static List<Attachment> of(final Context context, List<Uri> uris, final String type) {
final List<Attachment> attachments = new ArrayList<>();
for (final Uri uri : uris) {
final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type);
attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
}
return attachments;

View File

@ -0,0 +1,102 @@
package eu.siacs.conversations.ui.util;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.UIHelper;
public class QuoteHelper {
public static final char QUOTE_CHAR = '>';
public static final char QUOTE_END_CHAR = '<'; // used for one check, not for actual quoting
public static final char QUOTE_ALT_CHAR = '»';
public static final char QUOTE_ALT_END_CHAR = '«';
public static boolean isPositionQuoteCharacter(CharSequence body, int pos) {
// second part of logical check actually goes against the logic indicated in the method name, since it also checks for context
// but it's very useful
return body.charAt(pos) == QUOTE_CHAR || isPositionAltQuoteStart(body, pos);
}
public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos) {
return body.charAt(pos) == QUOTE_END_CHAR;
}
public static boolean isPositionAltQuoteCharacter(CharSequence body, int pos) {
return body.charAt(pos) == QUOTE_ALT_CHAR;
}
public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos) {
return body.charAt(pos) == QUOTE_ALT_END_CHAR;
}
public static boolean isPositionAltQuoteStart(CharSequence body, int pos) {
return isPositionAltQuoteCharacter(body, pos) && !isPositionFollowedByAltQuoteEnd(body, pos);
}
public static boolean isPositionFollowedByQuoteChar(CharSequence body, int pos) {
return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos + 1);
}
// 'Prequote' means anything we require or can accept in front of a QuoteChar
public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) {
return UIHelper.isPositionPrecededByLineStart(body, pos);
}
public static boolean isPositionQuoteStart(CharSequence body, int pos) {
return (isPositionQuoteCharacter(body, pos)
&& isPositionPrecededByPreQuote(body, pos)
&& (UIHelper.isPositionFollowedByQuoteableCharacter(body, pos)
|| isPositionFollowedByQuoteChar(body, pos)));
}
public static boolean bodyContainsQuoteStart(CharSequence body) {
for (int i = 0; i < body.length(); i++) {
if (isPositionQuoteStart(body, i)) {
return true;
}
}
return false;
}
public static boolean isPositionFollowedByAltQuoteEnd(CharSequence body, int pos) {
if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) {
return false;
}
boolean previousWasWhitespace = false;
for (int i = pos + 1; i < body.length(); i++) {
char c = body.charAt(i);
if (c == '\n' || isPositionAltQuoteCharacter(body, i)) {
return false;
} else if (isPositionAltQuoteEndCharacter(body, i) && !previousWasWhitespace) {
return true;
} else {
previousWasWhitespace = Character.isWhitespace(c);
}
}
return false;
}
public static boolean isNestedTooDeeply(CharSequence line) {
if (isPositionQuoteStart(line, 0)) {
int nestingDepth = 1;
for (int i = 1; i < line.length(); i++) {
if (isPositionQuoteStart(line, i)) {
nestingDepth++;
}
if (nestingDepth > (Config.QUOTING_MAX_DEPTH - 1)) {
return true;
}
}
}
return false;
}
public static String replaceAltQuoteCharsInText(String text) {
for (int i = 0; i < text.length(); i++) {
if (isPositionAltQuoteStart(text, i)) {
text = text.substring(0, i) + QUOTE_CHAR + text.substring(i + 1);
}
}
return text;
}
}

View File

@ -0,0 +1,26 @@
package eu.siacs.conversations.ui.util;
import android.util.Rational;
public final class Rationals {
//between 2.39:1 and 1:2.39 (inclusive).
private static final Rational MIN = new Rational(100,239);
private static final Rational MAX = new Rational(239,100);
private Rationals() {
}
public static Rational clip(final Rational input) {
if (input.compareTo(MIN) < 0) {
return MIN;
}
if (input.compareTo(MAX) > 0) {
return MAX;
}
return input;
}
}

View File

@ -0,0 +1,20 @@
package eu.siacs.conversations.ui.util;
import android.app.Activity;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.Window;
import android.view.WindowManager;
public class SettingsUtils {
public static void applyScreenshotPreventionSetting(Activity activity){
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
boolean preventScreenshots = preferences.getBoolean("prevent_screenshots", false);
Window activityWindow = activity.getWindow();
if(preventScreenshots){
activityWindow.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
activityWindow.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
}

View File

@ -24,6 +24,7 @@ import java.util.concurrent.Executors;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.util.QuoteHelper;
public class EditMessage extends EmojiWrapperEditText {
@ -142,7 +143,8 @@ public class EditMessage extends EmojiWrapperEditText {
}
public void insertAsQuote(String text) {
text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
text = QuoteHelper.replaceAltQuoteCharsInText(text);
text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2").replaceAll("(^|\n)([^" + QuoteHelper.QUOTE_CHAR + "])", "$1> $2").replaceAll("\n$", "");
Editable editable = getEditableText();
int position = getSelectionEnd();
if (position == -1) position = editable.length();

View File

@ -0,0 +1,48 @@
package eu.siacs.conversations.ui.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Rational;
import eu.siacs.conversations.Config;
public class SurfaceViewRenderer extends org.webrtc.SurfaceViewRenderer {
private Rational aspectRatio = new Rational(1,1);
private OnAspectRatioChanged onAspectRatioChanged;
public SurfaceViewRenderer(Context context) {
super(context);
}
public SurfaceViewRenderer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) {
super.onFrameResolutionChanged(videoWidth, videoHeight, rotation);
final int rotatedWidth = rotation != 0 && rotation != 180 ? videoHeight : videoWidth;
final int rotatedHeight = rotation != 0 && rotation != 180 ? videoWidth : videoHeight;
final Rational currentRational = this.aspectRatio;
this.aspectRatio = new Rational(rotatedWidth, rotatedHeight);
Log.d(Config.LOGTAG,"onFrameResolutionChanged("+rotatedWidth+","+rotatedHeight+","+aspectRatio+")");
if (currentRational.equals(this.aspectRatio) || onAspectRatioChanged == null) {
return;
}
onAspectRatioChanged.onAspectRatioChanged(this.aspectRatio);
}
public void setOnAspectRatioChanged(final OnAspectRatioChanged onAspectRatioChanged) {
this.onAspectRatioChanged = onAspectRatioChanged;
}
public Rational getAspectRatio() {
return this.aspectRatio;
}
public interface OnAspectRatioChanged {
void onAspectRatioChanged(final Rational rational);
}
}

View File

@ -1,76 +0,0 @@
package eu.siacs.conversations.utils;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.util.Log;
import androidx.annotation.RequiresApi;
import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants;
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
import net.ypresto.androidtranscoder.format.OutputFormatUnavailableException;
import eu.siacs.conversations.Config;
public class Android360pFormatStrategy implements MediaFormatStrategy {
private static final int LONGER_LENGTH = 640;
private static final int SHORTER_LENGTH = 360;
private static final int DEFAULT_VIDEO_BITRATE = 1000 * 1000;
private static final int DEFAULT_AUDIO_BITRATE = 128 * 1000;
private final int mVideoBitrate;
private final int mAudioBitrate;
private final int mAudioChannels;
public Android360pFormatStrategy() {
mVideoBitrate = DEFAULT_VIDEO_BITRATE;
mAudioBitrate = DEFAULT_AUDIO_BITRATE;
mAudioChannels = 2;
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
@Override
public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) {
int width = inputFormat.getInteger(MediaFormat.KEY_WIDTH);
int height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT);
int longer, shorter, outWidth, outHeight;
if (width >= height) {
longer = width;
shorter = height;
outWidth = LONGER_LENGTH;
outHeight = SHORTER_LENGTH;
} else {
shorter = width;
longer = height;
outWidth = SHORTER_LENGTH;
outHeight = LONGER_LENGTH;
}
if (longer * 9 != shorter * 16) {
throw new OutputFormatUnavailableException("This video is not 16:9, and is not able to transcode. (" + width + "x" + height + ")");
}
if (shorter <= SHORTER_LENGTH) {
Log.d(Config.LOGTAG, "This video is less or equal to 360p, pass-through. (" + width + "x" + height + ")");
return null;
}
MediaFormat format = MediaFormat.createVideoFormat("video/avc", outWidth, outHeight);
format.setInteger(MediaFormat.KEY_BIT_RATE, mVideoBitrate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
format.setInteger(MediaFormat.KEY_PROFILE ,MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel13);
}
return format;
}
@Override
public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) {
final MediaFormat format = MediaFormat.createAudioFormat(MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC, inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), mAudioChannels);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitrate);
return format;
}
}

View File

@ -1,76 +0,0 @@
package eu.siacs.conversations.utils;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.util.Log;
import androidx.annotation.RequiresApi;
import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants;
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
import net.ypresto.androidtranscoder.format.OutputFormatUnavailableException;
import eu.siacs.conversations.Config;
public class Android720pFormatStrategy implements MediaFormatStrategy {
private static final int LONGER_LENGTH = 1280;
private static final int SHORTER_LENGTH = 720;
private static final int DEFAULT_VIDEO_BITRATE = 2000 * 1000;
private static final int DEFAULT_AUDIO_BITRATE = 192 * 1000;
private final int mVideoBitrate;
private final int mAudioBitrate;
private final int mAudioChannels;
public Android720pFormatStrategy() {
mVideoBitrate = DEFAULT_VIDEO_BITRATE;
mAudioBitrate = DEFAULT_AUDIO_BITRATE;
mAudioChannels = 2;
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
@Override
public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) {
int width = inputFormat.getInteger(MediaFormat.KEY_WIDTH);
int height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT);
int longer, shorter, outWidth, outHeight;
if (width >= height) {
longer = width;
shorter = height;
outWidth = LONGER_LENGTH;
outHeight = SHORTER_LENGTH;
} else {
shorter = width;
longer = height;
outWidth = SHORTER_LENGTH;
outHeight = LONGER_LENGTH;
}
if (longer * 9 != shorter * 16) {
throw new OutputFormatUnavailableException("This video is not 16:9, and is not able to transcode. (" + width + "x" + height + ")");
}
if (shorter <= SHORTER_LENGTH) {
Log.d(Config.LOGTAG, "This video is less or equal to 720p, pass-through. (" + width + "x" + height + ")");
return null;
}
MediaFormat format = MediaFormat.createVideoFormat("video/avc", outWidth, outHeight);
format.setInteger(MediaFormat.KEY_BIT_RATE, mVideoBitrate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
format.setInteger(MediaFormat.KEY_PROFILE ,MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel13);
}
return format;
}
@Override
public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) {
final MediaFormat format = MediaFormat.createAudioFormat(MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC, inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), mAudioChannels);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitrate);
return format;
}
}

View File

@ -1,161 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.siacs.conversations.utils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
public class ExifHelper {
private static final String TAG = "CameraExif";
public static int getOrientation(InputStream is) {
if (is == null) {
return 0;
}
byte[] buf = new byte[8];
int length = 0;
// ISO/IEC 10918-1:1993(E)
while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) {
int marker = buf[1] & 0xFF;
// Check if the marker is a padding.
if (marker == 0xFF) {
continue;
}
// Check if the marker is SOI or TEM.
if (marker == 0xD8 || marker == 0x01) {
continue;
}
// Check if the marker is EOI or SOS.
if (marker == 0xD9 || marker == 0xDA) {
return 0;
}
// Get the length and check if it is reasonable.
if (!read(is, buf, 2)) {
return 0;
}
length = pack(buf, 0, 2, false);
if (length < 2) {
Log.e(TAG, "Invalid length");
return 0;
}
length -= 2;
// Break if the marker is EXIF in APP1.
if (marker == 0xE1 && length >= 6) {
if (!read(is, buf, 6)) return 0;
length -= 6;
if (pack(buf, 0, 4, false) == 0x45786966 &&
pack(buf, 4, 2, false) == 0) {
break;
}
}
// Skip other markers.
try {
is.skip(length);
} catch (IOException ex) {
return 0;
}
length = 0;
}
// JEITA CP-3451 Exif Version 2.2
if (length > 8) {
int offset = 0;
byte[] jpeg = new byte[length];
if (!read(is, jpeg, length)) {
return 0;
}
// Identify the byte order.
int tag = pack(jpeg, offset, 4, false);
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
Log.e(TAG, "Invalid byte order");
return 0;
}
boolean littleEndian = (tag == 0x49492A00);
// Get the offset and check if it is reasonable.
int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
if (count < 10 || count > length) {
Log.e(TAG, "Invalid offset");
return 0;
}
offset += count;
length -= count;
// Get the count and go through all the elements.
count = pack(jpeg, offset - 2, 2, littleEndian);
while (count-- > 0 && length >= 12) {
// Get the tag and check if it is orientation.
tag = pack(jpeg, offset, 2, littleEndian);
if (tag == 0x0112) {
// We do not really care about type and count, do we?
int orientation = pack(jpeg, offset + 8, 2, littleEndian);
switch (orientation) {
case 1:
return 0;
case 3:
return 180;
case 6:
return 90;
case 8:
return 270;
}
Log.i(TAG, "Unsupported orientation");
return 0;
}
offset += 12;
length -= 12;
}
}
Log.i(TAG, "Orientation not found");
return 0;
}
private static int pack(byte[] bytes, int offset, int length,
boolean littleEndian) {
int step = 1;
if (littleEndian) {
offset += length - 1;
step = -1;
}
int value = 0;
while (length-- > 0) {
value = (value << 8) | (bytes[offset] & 0xFF);
offset += step;
}
return value;
}
private static boolean read(InputStream is, byte[] buf, int length) {
try {
return is.read(buf, 0, length) == length;
} catch (IOException ex) {
return false;
}
}
}

View File

@ -4,6 +4,8 @@ import android.content.Context;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.core.content.ContextCompat;
import org.osmdroid.util.GeoPoint;
import java.io.BufferedReader;
@ -16,11 +18,14 @@ import eu.siacs.conversations.R;
public class LocationProvider {
public static final GeoPoint FALLBACK = new GeoPoint(0.0,0.0);
public static final GeoPoint FALLBACK = new GeoPoint(0.0, 0.0);
public static String getUserCountry(Context context) {
public static String getUserCountry(final Context context) {
try {
final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class);
if (tm == null) {
return getUserCountryFallback();
}
final String simCountry = tm.getSimCountryIso();
if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
return simCountry.toUpperCase(Locale.US);
@ -30,40 +35,41 @@ public class LocationProvider {
return networkCountry.toUpperCase(Locale.US);
}
}
} catch (Exception e) {
// fallthrough
return getUserCountryFallback();
} catch (final Exception e) {
return getUserCountryFallback();
}
Locale locale = Locale.getDefault();
}
private static String getUserCountryFallback() {
final Locale locale = Locale.getDefault();
return locale.getCountry();
}
public static GeoPoint getGeoPoint(Context context) {
public static GeoPoint getGeoPoint(final Context context) {
return getGeoPoint(context, getUserCountry(context));
}
public static synchronized GeoPoint getGeoPoint(Context context, String country) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)));
public static synchronized GeoPoint getGeoPoint(final Context context, final String country) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)))) {
String line;
while((line = reader.readLine()) != null) {
String[] parts = line.split("\\s+",4);
while ((line = reader.readLine()) != null) {
final String[] parts = line.split("\\s+", 4);
if (parts.length == 4) {
if (country.equalsIgnoreCase(parts[0])) {
try {
return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2]));
} catch (NumberFormatException e) {
} catch (final NumberFormatException e) {
return FALLBACK;
}
}
} else {
Log.d(Config.LOGTAG,"unable to parse line="+line);
}
}
} catch (IOException e) {
Log.d(Config.LOGTAG,e.getMessage());
} catch (final IOException e) {
Log.d(Config.LOGTAG, "unable to parse country->geo map", e);
}
return FALLBACK;
}
}
}

View File

@ -39,6 +39,7 @@ import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.AesGcmURL;
import eu.siacs.conversations.http.URL;
import eu.siacs.conversations.ui.util.QuoteHelper;
public class MessageUtils {
@ -69,8 +70,7 @@ public class MessageUtils {
continue;
}
final char c = line.charAt(0);
if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0)
|| (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) {
if (QuoteHelper.isNestedTooDeeply(line)) {
continue;
}
if (builder.length() != 0) {
@ -115,6 +115,6 @@ public class MessageUtils {
}
public static boolean unInitiatedButKnownSize(Message message) {
return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null;
return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size != null && message.getFileParams().url != null;
}
}

View File

@ -21,6 +21,8 @@ import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import com.google.common.base.Strings;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -247,6 +249,7 @@ public final class MimeUtils {
add("audio/mpeg", "m4a");
add("audio/mpegurl", "m3u");
add("audio/ogg", "oga");
add("audio/opus", "opus");
add("audio/prs.sid", "sid");
add("audio/x-aiff", "aif");
add("audio/x-aiff", "aiff");
@ -273,6 +276,8 @@ public final class MimeUtils {
add("image/ico", "ico");
add("image/ief", "ief");
add("image/heic", "heic");
add("image/heif", "heif");
add("image/avif", "avif");
// add ".jpg" first so it will be the default for guessExtensionFromMimeType
add("image/jpeg", "jpg");
add("image/jpeg", "jpeg");
@ -567,6 +572,8 @@ public final class MimeUtils {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} catch (Exception e) {
return null;
}
return null;
}
@ -584,22 +591,33 @@ public final class MimeUtils {
}
public static String extractRelevantExtension(final String path, final boolean ignoreCryptoExtension) {
if (path == null || path.isEmpty()) {
if (Strings.isNullOrEmpty(path)) {
return null;
}
String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
int dotPosition = filename.lastIndexOf(".");
final String filenameQueryAnchor = path.substring(path.lastIndexOf('/') + 1);
final String filenameQuery = cutBefore(filenameQueryAnchor, '#');
final String filename = cutBefore(filenameQuery, '?');
final int dotPosition = filename.lastIndexOf('.');
if (dotPosition != -1) {
String extension = filename.substring(dotPosition + 1);
// we want the real file extension, not the crypto one
if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
return extractRelevantExtension(filename.substring(0, dotPosition));
} else {
return extension;
}
if (dotPosition == -1) {
return null;
}
final String extension = filename.substring(dotPosition + 1);
// we want the real file extension, not the crypto one
if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
return extractRelevantExtension(filename.substring(0, dotPosition));
} else {
return extension;
}
}
private static String cutBefore(final String input, final char c) {
final int position = input.indexOf(c);
if (position > 0) {
return input.substring(0, position);
} else {
return input;
}
return null;
}
}

View File

@ -254,6 +254,39 @@ public class Patterns {
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ "|[1-9][0-9]|[0-9]))");
/**
* IPv6 address matcher for
* IPv6 addresses
* zero compressed IPv6 addresses (section 2.2 of rfc5952)
* link-local IPv6 addresses with zone index (section 11 of rfc4007)
* IPv4-Embedded IPv6 Address (section 2 of rfc6052)
* IPv4-mapped IPv6 addresses (section 2.1 of rfc2765)
* IPv4-translated addresses (section 2.1 of rfc2765)
*
* Taken from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses/17871737#17871737
*/
public static final Pattern IP6_ADDRESS
= Pattern.compile(
"\\[" +
"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" +
"([0-9a-fA-F]{1,4}:){1,7}:|" +
"([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" +
"([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" +
"([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" +
"([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" +
"([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" +
"[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" +
":((:[0-9a-fA-F]{1,4}){1,7}|:)|" +
"fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" +
"::(ffff(:0{1,4}){0,1}:){0,1}" +
"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" +
"(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" +
"([0-9a-fA-F]{1,4}:){1,4}:" +
"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" +
"(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))" +
"\\]"
);
/**
* Valid UCS characters defined in RFC 3987. Excludes space characters.
*/
@ -296,7 +329,7 @@ public class Patterns {
private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")";
private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD;
public static final Pattern DOMAIN_NAME
= Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")");
= Pattern.compile("(" + HOST_NAME + "|" + IP6_ADDRESS + "|" + IP_ADDRESS +")");
private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/";
/* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
@ -306,7 +339,7 @@ public class Patterns {
private static final String PORT_NUMBER = "\\:\\d{1,5}";
private static final String PATH_AND_QUERY = "\\/(?:(?:[" + LABEL_CHAR
+ "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus optional query params
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*";
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_\\$])|(?:\\%[a-fA-F0-9]{2}))*";
/**
* Regular expression pattern to match most part of RFC 3987
* Internationalized URLs, aka IRIs.
@ -335,12 +368,12 @@ public class Patterns {
* {@link #IP_ADDRESS}
*/
private static final Pattern STRICT_DOMAIN_NAME
= Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + ")");
= Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")");
/**
* Regular expression that matches domain names without a TLD
*/
private static final String RELAXED_DOMAIN_NAME =
"(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + ")";
"(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")";
/**
* Regular expression to match strings that do not start with a supported protocol. The TLDs
* are expected to be one of the known TLDs.
@ -477,4 +510,4 @@ public class Patterns {
* Do not create this static utility class.
*/
private Patterns() {}
}
}

View File

@ -408,6 +408,7 @@ public class Resolver {
Result result = new Result();
result.timeRequested = System.currentTimeMillis();
result.port = port;
result.directTls = useDirectTls(port);
result.hostname = hostname;
result.ip = ip;
return result;

View File

@ -1,5 +1,7 @@
package eu.siacs.conversations.utils;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -12,76 +14,108 @@ import eu.siacs.conversations.Config;
public class SocksSocketFactory {
private static final byte[] LOCALHOST = new byte[]{127,0,0,1};
private static final byte[] LOCALHOST = new byte[]{127, 0, 0, 1};
public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException {
final InputStream proxyIs = socket.getInputStream();
final OutputStream proxyOs = socket.getOutputStream();
proxyOs.write(new byte[]{0x05, 0x01, 0x00});
proxyOs.flush();
final byte[] handshake = new byte[2];
proxyIs.read(handshake);
if (handshake[0] != 0x05 || handshake[1] != 0x00) {
throw new SocksConnectionException("Socks 5 handshake failed");
}
final byte[] dest = destination.getBytes();
final ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
request.put((byte) dest.length);
request.put(dest);
request.putShort((short) port);
proxyOs.write(request.array());
proxyOs.flush();
final byte[] response = new byte[7 + dest.length];
proxyIs.read(response);
if (response[1] != 0x00) {
if (response[1] == 0x04) {
throw new HostNotFoundException("Host unreachable");
}
if (response[1] == 0x05) {
throw new HostNotFoundException("Connection refused");
}
throw new SocksConnectionException("Unable to connect to destination "+(int) (response[1]));
}
}
public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException {
//TODO use different Socks Addr Type if destination is IP or IPv6
final InputStream proxyIs = socket.getInputStream();
final OutputStream proxyOs = socket.getOutputStream();
proxyOs.write(new byte[]{0x05, 0x01, 0x00});
proxyOs.flush();
final byte[] handshake = new byte[2];
ByteStreams.readFully(proxyIs, handshake);
if (handshake[0] != 0x05 || handshake[1] != 0x00) {
throw new SocksConnectionException("Socks 5 handshake failed");
}
final byte[] dest = destination.getBytes();
final ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
request.put((byte) dest.length);
request.put(dest);
request.putShort((short) port);
proxyOs.write(request.array());
proxyOs.flush();
final byte[] response = new byte[4];
ByteStreams.readFully(proxyIs, response);
final byte ver = response[0];
if (ver != 0x05) {
throw new IOException(String.format("Unknown Socks version %02X ", ver));
}
final byte status = response[1];
final byte bndAddrType = response[3];
final byte[] bndDestination = readDestination(bndAddrType, proxyIs);
final byte[] bndPort = new byte[2];
if (bndAddrType == 0x03) {
final String receivedDestination = new String(bndDestination);
if (!receivedDestination.equalsIgnoreCase(destination)) {
throw new IOException(String.format("Destination mismatch. Received %s Expected %s", receivedDestination, destination));
}
}
ByteStreams.readFully(proxyIs, bndPort);
if (status != 0x00) {
if (status == 0x04) {
throw new HostNotFoundException("Host unreachable");
}
if (status == 0x05) {
throw new HostNotFoundException("Connection refused");
}
throw new IOException(String.format("Unknown status code %02X ", status));
}
}
public static boolean contains(byte needle, byte[] haystack) {
for(byte hay : haystack) {
if (hay == needle) {
return true;
}
}
return false;
}
private static byte[] readDestination(final byte type, final InputStream inputStream) throws IOException {
final byte[] bndDestination;
if (type == 0x01) {
bndDestination = new byte[4];
} else if (type == 0x03) {
final int length = inputStream.read();
bndDestination = new byte[length];
} else if (type == 0x04) {
bndDestination = new byte[16];
} else {
throw new IOException(String.format("Unknown Socks address type %02X ", type));
}
ByteStreams.readFully(inputStream, bndDestination);
return bndDestination;
}
private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
Socket socket = new Socket();
try {
socket.connect(address, Config.CONNECT_TIMEOUT * 1000);
} catch (IOException e) {
throw new SocksProxyNotFoundException();
}
createSocksConnection(socket, destination, port);
return socket;
}
public static boolean contains(byte needle, byte[] haystack) {
for (byte hay : haystack) {
if (hay == needle) {
return true;
}
}
return false;
}
public static Socket createSocketOverTor(String destination, int port) throws IOException {
return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port);
}
private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
Socket socket = new Socket();
try {
socket.connect(address, Config.CONNECT_TIMEOUT * 1000);
} catch (IOException e) {
throw new SocksProxyNotFoundException();
}
createSocksConnection(socket, destination, port);
return socket;
}
private static class SocksConnectionException extends IOException {
SocksConnectionException(String message) {
super(message);
}
}
public static Socket createSocketOverTor(String destination, int port) throws IOException {
return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port);
}
public static class SocksProxyNotFoundException extends IOException {
private static class SocksConnectionException extends IOException {
SocksConnectionException(String message) {
super(message);
}
}
}
public static class SocksProxyNotFoundException extends IOException {
public static class HostNotFoundException extends SocksConnectionException {
HostNotFoundException(String message) {
super(message);
}
}
}
public static class HostNotFoundException extends SocksConnectionException {
HostNotFoundException(String message) {
super(message);
}
}
}

View File

@ -71,10 +71,14 @@ public class TimeFrameUtils {
public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) {
final long passed = (since < 0) ? 0 : (to - since);
final int hours = (int) (passed / 3600000);
final int minutes = (int) (passed / 60000) % 60;
final int seconds = (int) (passed / 1000) % 60;
final int milliseconds = (int) (passed / 100) % 10;
return formatElapsedTime(passed, withMilliseconds);
}
public static String formatElapsedTime(final long elapsed, final boolean withMilliseconds) {
final int hours = (int) (elapsed / 3600000);
final int minutes = (int) (elapsed / 60000) % 60;
final int seconds = (int) (elapsed / 1000) % 60;
final int milliseconds = (int) (elapsed / 100) % 10;
if (hours > 0) {
return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds);
} else if (withMilliseconds) {

View File

@ -0,0 +1,41 @@
package eu.siacs.conversations.utils;
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy;
public final class TranscoderStrategies {
public static final DefaultVideoStrategy VIDEO_720P = DefaultVideoStrategy.atMost(720)
.bitRate(2L * 1000 * 1000)
.frameRate(30)
.keyFrameInterval(3F)
.build();
public static final DefaultVideoStrategy VIDEO_360P = DefaultVideoStrategy.atMost(360)
.bitRate(1000 * 1000)
.frameRate(30)
.keyFrameInterval(3F)
.build();
//TODO do we want to add 240p (@500kbs) and 1080p (@4mbs?) ?
// see suggested bit rates on https://www.videoproc.com/media-converter/bitrate-setting-for-h264.htm
public static final DefaultAudioStrategy AUDIO_HQ = DefaultAudioStrategy.builder()
.bitRate(192 * 1000)
.channels(2)
.sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT)
.build();
public static final DefaultAudioStrategy AUDIO_MQ = DefaultAudioStrategy.builder()
.bitRate(128 * 1000)
.channels(2)
.sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT)
.build();
//TODO if we add 144p we definitely want to add a lower audio bit rate as well
private TranscoderStrategies() {
throw new IllegalStateException("Do not instantiate me");
}
}

View File

@ -32,6 +32,7 @@ import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.ExportBackupService;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.xmpp.Jid;
public class UIHelper {
@ -328,7 +329,7 @@ public class UIHelper {
continue;
}
char first = l.charAt(0);
if ((first != '>' || !isPositionFollowedByQuoteableCharacter(l, 0)) && first != '\u00bb') {
if ((!QuoteHelper.isPositionQuoteStart(l, 0))) {
CharSequence line = CharSequenceUtils.trim(l);
if (line.length() == 0) {
continue;
@ -372,6 +373,23 @@ public class UIHelper {
return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input;
}
public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos){
// true if not a single linebreak before current position
for (int i = pos - 1; i >= 0; i--){
if (body.charAt(i) != ' '){
return false;
}
}
return true;
}
public static boolean isPositionPrecededByLineStart(CharSequence body, int pos){
if (isPositionPrecededByBodyStart(body, pos)){
return true;
}
return body.charAt(pos - 1) == '\n';
}
public static boolean isPositionFollowedByQuoteableCharacter(CharSequence body, int pos) {
return !isPositionFollowedByNumber(body, pos)
&& !isPositionFollowedByEmoticon(body, pos)
@ -404,6 +422,7 @@ public class UIHelper {
final char first = body.charAt(pos + 1);
return first == ';'
|| first == ':'
|| first == '.' // do not quote >.< (but >>.<)
|| closingBeforeWhitespace(body, pos + 1);
}
}
@ -413,31 +432,13 @@ public class UIHelper {
final char c = body.charAt(i);
if (Character.isWhitespace(c)) {
return false;
} else if (c == '<' || c == '>') {
} else if (QuoteHelper.isPositionQuoteCharacter(body, pos) || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) {
return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1));
}
}
return false;
}
public static boolean isPositionFollowedByQuote(CharSequence body, int pos) {
if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) {
return false;
}
boolean previousWasWhitespace = false;
for (int i = pos + 1; i < body.length(); i++) {
char c = body.charAt(i);
if (c == '\n' || c == '»') {
return false;
} else if (c == '«' && !previousWasWhitespace) {
return true;
} else {
previousWasWhitespace = Character.isWhitespace(c);
}
}
return false;
}
public static String getDisplayName(MucOptions.User user) {
Contact contact = user.getContact();
if (contact != null) {
@ -475,9 +476,6 @@ public class UIHelper {
}
public static String getFileDescriptionString(final Context context, final Message message) {
if (message.getType() == Message.TYPE_IMAGE) {
return context.getString(R.string.image);
}
final String mime = message.getMimeType();
if (mime == null) {
return context.getString(R.string.file);
@ -487,7 +485,9 @@ public class UIHelper {
return context.getString(R.string.video);
} else if (mime.equals("image/gif")) {
return context.getString(R.string.gif);
} else if (mime.startsWith("image/")) {
} else if (mime.equals("image/svg+xml")) {
return context.getString(R.string.vector_graphic);
} else if (mime.startsWith("image/") || message.getType() == Message.TYPE_IMAGE) {
return context.getString(R.string.image);
} else if (mime.contains("pdf")) {
return context.getString(R.string.pdf_document);
@ -503,6 +503,8 @@ public class UIHelper {
return context.getString(R.string.ebook);
} else if (mime.equals("application/gpx+xml")) {
return context.getString(R.string.gpx_track);
} else if (mime.equals("text/plain")) {
return context.getString(R.string.plain_text_document);
} else {
return mime;
}

View File

@ -4,12 +4,15 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import com.google.common.base.CharMatcher;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -32,6 +35,8 @@ public class XmppUri {
private Map<String, String> parameters = Collections.emptyMap();
private boolean safeSource = true;
public static final String INVITE_DOMAIN = "conversations.im";
public XmppUri(final String uri) {
try {
parse(Uri.parse(uri));
@ -133,10 +138,10 @@ public class XmppUri {
return;
}
this.uri = uri;
String scheme = uri.getScheme();
String host = uri.getHost();
final String scheme = uri.getScheme();
final String host = uri.getHost();
List<String> segments = uri.getPathSegments();
if ("https".equalsIgnoreCase(scheme) && "conversations.im".equalsIgnoreCase(host)) {
if ("https".equalsIgnoreCase(scheme) && INVITE_DOMAIN.equalsIgnoreCase(host)) {
if (segments.size() >= 2 && segments.get(1).contains("@")) {
// sample : https://conversations.im/i/foo@bar.com
try {
@ -167,7 +172,7 @@ public class XmppUri {
}
}
this.fingerprints = parseFingerprints(parameters);
} else if ("imto".equalsIgnoreCase(scheme)) {
} else if ("imto".equalsIgnoreCase(scheme) && Arrays.asList("xmpp", "jabber").contains(uri.getHost())) {
// sample: imto://xmpp/foo@bar.com
try {
jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1].trim();
@ -189,7 +194,10 @@ public class XmppUri {
}
public boolean isAction(final String action) {
return parameters.containsKey(action);
return Collections2.transform(
parameters.keySet(),
s -> CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('A', 'Z')).retainFrom(s)
).contains(action);
}
public Jid getJid() {

View File

@ -1,5 +1,7 @@
package eu.siacs.conversations.xml;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
@ -165,8 +167,9 @@ public class Element {
return this.attributes;
}
@NotNull
public String toString() {
StringBuilder elementOutput = new StringBuilder();
final StringBuilder elementOutput = new StringBuilder();
if ((content == null) && (children.size() == 0)) {
Tag emptyTag = Tag.empty(name);
emptyTag.setAtttributes(this.attributes);

View File

@ -28,6 +28,7 @@ public final class Namespace {
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0";
public static final String JINGLE = "urn:xmpp:jingle:1";
public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1";
public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0";
public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0";
public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0";

View File

@ -54,7 +54,6 @@ import javax.net.ssl.X509TrustManager;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.DomainHostnameVerifier;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.sasl.Anonymous;

View File

@ -136,6 +136,7 @@ public abstract class AbstractJingleConnection {
TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call)
TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button)
TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
TERMINATED_APPLICATION_FAILURE
TERMINATED_APPLICATION_FAILURE,
TERMINATED_SECURITY_ERROR
}
}

View File

@ -206,7 +206,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final Element error = response.addChild("error");
error.setAttribute("type", conditionType);
error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1");
error.addChild(jingleCondition, Namespace.JINGLE_ERRORS);
account.getXmppConnection().sendIqPacket(response, null);
}

View File

@ -1239,11 +1239,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
}
@Override
public long getFileSize() {
public Long getFileSize() {
if (this.file != null) {
return this.file.getExpectedSize();
} else {
return 0;
return null;
}
}

View File

@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle;
import android.os.PowerManager;
import android.util.Log;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -58,22 +60,12 @@ public class JingleSocks5Transport extends JingleTransport {
} else {
destBuilder.append(this.connection.getTransportId());
}
if (candidate.getType() == JingleCandidate.TYPE_PROXY) {
if (candidate.isOurs()) {
destBuilder.append(this.account.getJid());
destBuilder.append(this.connection.getId().with);
} else {
destBuilder.append(this.connection.getId().with);
destBuilder.append(this.account.getJid());
}
if (candidate.isOurs()) {
destBuilder.append(this.account.getJid());
destBuilder.append(this.connection.getId().with);
} else {
if (connection.isInitiator()) {
destBuilder.append(this.account.getJid());
destBuilder.append(this.connection.getId().with);
} else {
destBuilder.append(this.connection.getId().with);
destBuilder.append(this.account.getJid());
}
destBuilder.append(this.connection.getId().with);
destBuilder.append(this.account.getJid());
}
messageDigest.reset();
this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes()));
@ -114,26 +106,26 @@ public class JingleSocks5Transport extends JingleTransport {
final byte[] authBegin = new byte[2];
final InputStream inputStream = socket.getInputStream();
final OutputStream outputStream = socket.getOutputStream();
inputStream.read(authBegin);
ByteStreams.readFully(inputStream, authBegin);
if (authBegin[0] != 0x5) {
socket.close();
}
final short methodCount = authBegin[1];
final byte[] methods = new byte[methodCount];
inputStream.read(methods);
ByteStreams.readFully(inputStream, methods);
if (SocksSocketFactory.contains((byte) 0x00, methods)) {
outputStream.write(new byte[]{0x05, 0x00});
} else {
outputStream.write(new byte[]{0x05, (byte) 0xff});
}
byte[] connectCommand = new byte[4];
inputStream.read(connectCommand);
final byte[] connectCommand = new byte[4];
ByteStreams.readFully(inputStream, connectCommand);
if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) {
int destinationCount = inputStream.read();
final byte[] destination = new byte[destinationCount];
inputStream.read(destination);
ByteStreams.readFully(inputStream, destination);
final byte[] port = new byte[2];
inputStream.read(port);
ByteStreams.readFully(inputStream, port);
final String receivedDestination = new String(destination);
final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
final byte[] responseHeader;
@ -187,7 +179,7 @@ public class JingleSocks5Transport extends JingleTransport {
socket.setSoTimeout(0);
isEstablished = true;
callback.established();
} catch (IOException e) {
} catch (final IOException e) {
callback.failed();
}
}).start();

View File

@ -55,7 +55,7 @@ public class OmemoVerification {
throw new IllegalStateException("No session fingerprint has been previously provided");
}
if (!sessionFingerprint.equals(this.sessionFingerprint)) {
throw new IllegalStateException("Session Fingerprints did not match");
throw new SecurityException("Session Fingerprints did not match");
}
if (this.deviceId == null) {
throw new IllegalStateException("No Device Id has been previously provided");

View File

@ -1,13 +1,12 @@
package eu.siacs.conversations.xmpp.jingle;
import android.util.Log;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@ -17,9 +16,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
@ -97,6 +96,10 @@ public class RtpContentMap {
}
void requireDTLSFingerprint() {
requireDTLSFingerprint(false);
}
void requireDTLSFingerprint(final boolean requireActPass) {
if (this.contents.size() == 0) {
throw new IllegalStateException("No contents available");
}
@ -106,9 +109,13 @@ public class RtpContentMap {
if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) {
throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey()));
}
if (Strings.isNullOrEmpty(fingerprint.getSetup())) {
final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
if (setup == null) {
throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey()));
}
if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
}
}
}
@ -137,7 +144,56 @@ public class RtpContentMap {
final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
newTransportInfo.addChild(candidate);
return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
}
RtpContentMap transportInfo() {
return new RtpContentMap(
null,
Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))
);
}
public IceUdpTransportInfo.Credentials getCredentials() {
final Set<IceUdpTransportInfo.Credentials> allCredentials = ImmutableSet.copyOf(Collections2.transform(
contents.values(),
dt -> dt.transport.getCredentials()
));
final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null);
if (allCredentials.size() == 1 && credentials != null) {
return credentials;
}
throw new IllegalStateException("Content map does not have distinct credentials");
}
public IceUdpTransportInfo.Setup getDtlsSetup() {
final Set<IceUdpTransportInfo.Setup> setups = ImmutableSet.copyOf(Collections2.transform(
contents.values(),
dt -> dt.transport.getFingerprint().getSetup()
));
final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
if (setups.size() == 1 && setup != null) {
return setup;
}
throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
}
public boolean emptyCandidates() {
int count = 0;
for (DescriptionTransport descriptionTransport : contents.values()) {
count += descriptionTransport.transport.getCandidates().size();
}
return count == 0;
}
public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
final RtpDescription rtpDescription = content.getValue().description;
IceUdpTransportInfo transportInfo = content.getValue().transport;
final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup);
contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo));
}
return new RtpContentMap(this.group, contentMapBuilder.build());
}
public static class DescriptionTransport {
@ -159,7 +215,6 @@ public class RtpContentMap {
} else if (description instanceof RtpDescription) {
rtpDescription = (RtpDescription) description;
} else {
Log.d(Config.LOGTAG, "description was " + description);
throw new UnsupportedApplicationException("Content does not contain rtp description");
}
if (transportInfo instanceof IceUdpTransportInfo) {

View File

@ -4,6 +4,7 @@ public enum RtpEndUserState {
INCOMING_CALL, //received a 'propose' message
CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet
CONNECTED, //session-accepted and webrtc peer connection is connected
RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
RINGING, //'propose' has been sent out and it has been 184 acked
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
@ -13,5 +14,6 @@ public enum RtpEndUserState {
CONNECTIVITY_ERROR, //network error; retry button
CONNECTIVITY_LOST_ERROR, //network error but for call duration > 0
RETRACTED, //user pressed home or power button during 'ringing' - shows retry button
APPLICATION_ERROR //something rather bad happened; libwebrtc failed or we got in IQ-error
APPLICATION_ERROR, //something rather bad happened; libwebrtc failed or we got in IQ-error
SECURITY_ERROR //problem with DTLS (missing) or verification
}

View File

@ -156,7 +156,10 @@ public class SessionDescription {
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
if (fingerprint != null) {
mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent());
mediaAttributes.put("setup", fingerprint.getSetup());
final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
if (setup != null) {
mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
}
}
final ImmutableList.Builder<Integer> formatBuilder = new ImmutableList.Builder<>();
for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {

View File

@ -37,7 +37,7 @@ class ToneManager {
private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
if (isInitiator) {
if (asList(RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) {
if (asList(RtpEndUserState.FINDING_DEVICE, RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) {
return ToneState.RINGING;
}
if (state == RtpEndUserState.DECLINED_OR_BUSY) {
@ -51,7 +51,7 @@ class ToneManager {
return ToneState.ENDING_CALL;
}
}
if (state == RtpEndUserState.CONNECTED) {
if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) {
if (media.contains(Media.VIDEO)) {
return ToneState.NULL;
} else {

View File

@ -17,7 +17,6 @@ import com.google.common.util.concurrent.SettableFuture;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerationAndroid;
import org.webrtc.CameraEnumerator;
@ -45,8 +44,13 @@ import org.webrtc.voiceengine.WebRtcAudioEffects;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -59,6 +63,8 @@ public class WebRTCWrapper {
private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
//we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296
private static final Set<String> HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder<String>()
.add("Pixel")
@ -79,6 +85,8 @@ public class WebRTCWrapper {
private static final int CAPTURING_MAX_FRAME_RATE = 30;
private final EventCallback eventCallback;
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() {
@Override
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
@ -98,13 +106,13 @@ public class WebRTCWrapper {
}
@Override
public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
eventCallback.onConnectionChange(newState);
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.d(EXTENDED_LOGGING_TAG, "onIceConnectionChange(" + iceConnectionState + ")");
}
@Override
@ -125,7 +133,11 @@ public class WebRTCWrapper {
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
eventCallback.onIceCandidate(iceCandidate);
if (readyToReceivedIceCandidates.get()) {
eventCallback.onIceCandidate(iceCandidate);
} else {
iceCandidates.add(iceCandidate);
}
}
@Override
@ -150,7 +162,11 @@ public class WebRTCWrapper {
@Override
public void onRenegotiationNeeded() {
Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()");
final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState();
if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) {
eventCallback.onRenegotiationNeeded();
}
}
@Override
@ -251,10 +267,7 @@ public class WebRTCWrapper {
.createPeerConnectionFactory();
final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers);
final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver);
if (peerConnection == null) {
throw new InitializationException("Unable to create PeerConnection");
@ -288,6 +301,31 @@ public class WebRTCWrapper {
this.peerConnection = peerConnection;
}
private static PeerConnection.RTCConfiguration buildConfiguration(final List<PeerConnection.IceServer> iceServers) {
final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
rtcConfig.enableImplicitRollback = true;
return rtcConfig;
}
void reconfigurePeerConnection(final List<PeerConnection.IceServer> iceServers) {
requirePeerConnection().setConfiguration(buildConfiguration(iceServers));
}
void restartIce() {
executorService.execute(() -> requirePeerConnection().restartIce());
}
public void setIsReadyToReceiveIceCandidates(final boolean ready) {
readyToReceivedIceCandidates.set(ready);
while (ready && iceCandidates.peek() != null) {
eventCallback.onIceCandidate(iceCandidates.poll());
}
}
synchronized void close() {
final PeerConnection peerConnection = this.peerConnection;
final CapturerChoice capturerChoice = this.capturerChoice;
@ -402,70 +440,36 @@ public class WebRTCWrapper {
videoTrack.setEnabled(enabled);
}
ListenableFuture<SessionDescription> createOffer() {
synchronized ListenableFuture<SessionDescription> setLocalDescription() {
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<SessionDescription> future = SettableFuture.create();
peerConnection.createOffer(new CreateSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
future.set(sessionDescription);
}
@Override
public void onCreateFailure(String s) {
future.setException(new IllegalStateException("Unable to create offer: " + s));
}
}, new MediaConstraints());
return future;
}, MoreExecutors.directExecutor());
}
ListenableFuture<SessionDescription> createAnswer() {
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<SessionDescription> future = SettableFuture.create();
peerConnection.createAnswer(new CreateSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
future.set(sessionDescription);
}
@Override
public void onCreateFailure(String s) {
future.setException(new IllegalStateException("Unable to create answer: " + s));
}
}, new MediaConstraints());
return future;
}, MoreExecutors.directExecutor());
}
ListenableFuture<Void> setLocalDescription(final SessionDescription sessionDescription) {
Log.d(EXTENDED_LOGGING_TAG, "setting local description:");
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
Log.d(EXTENDED_LOGGING_TAG, line);
}
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<Void> future = SettableFuture.create();
peerConnection.setLocalDescription(new SetSdpObserver() {
@Override
public void onSetSuccess() {
future.set(null);
final SessionDescription description = peerConnection.getLocalDescription();
Log.d(EXTENDED_LOGGING_TAG, "set local description:");
logDescription(description);
future.set(description);
}
@Override
public void onSetFailure(final String s) {
future.setException(new IllegalArgumentException("unable to set local session description: " + s));
public void onSetFailure(final String message) {
future.setException(new FailureToSetDescriptionException(message));
}
}, sessionDescription);
});
return future;
}, MoreExecutors.directExecutor());
}
ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) {
Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
private static void logDescription(final SessionDescription sessionDescription) {
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
Log.d(EXTENDED_LOGGING_TAG, line);
}
}
synchronized ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) {
Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
logDescription(sessionDescription);
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<Void> future = SettableFuture.create();
peerConnection.setRemoteDescription(new SetSdpObserver() {
@ -475,9 +479,8 @@ public class WebRTCWrapper {
}
@Override
public void onSetFailure(String s) {
future.setException(new IllegalArgumentException("unable to set remote session description: " + s));
public void onSetFailure(final String message) {
future.setException(new FailureToSetDescriptionException(message));
}
}, sessionDescription);
return future;
@ -488,26 +491,26 @@ public class WebRTCWrapper {
private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
final PeerConnection peerConnection = this.peerConnection;
if (peerConnection == null) {
return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first"));
return Futures.immediateFailedFuture(new PeerConnectionNotInitialized());
} else {
return Futures.immediateFuture(peerConnection);
}
}
private PeerConnection requirePeerConnection() {
final PeerConnection peerConnection = this.peerConnection;
if (peerConnection == null) {
throw new PeerConnectionNotInitialized();
}
return peerConnection;
}
void addIceCandidate(IceCandidate iceCandidate) {
requirePeerConnection().addIceCandidate(iceCandidate);
}
private CameraEnumerator getCameraEnumerator() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new Camera2Enumerator(requireContext());
} else {
return new Camera1Enumerator();
}
}
private Optional<CapturerChoice> getVideoCapturer() {
final CameraEnumerator enumerator = getCameraEnumerator();
final CameraEnumerator enumerator = new Camera2Enumerator(requireContext());
final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames());
for (final String deviceName : deviceNames) {
if (isFrontFacing(enumerator, deviceName)) {
@ -526,10 +529,15 @@ public class WebRTCWrapper {
}
}
public PeerConnection.PeerConnectionState getState() {
PeerConnection.PeerConnectionState getState() {
return requirePeerConnection().connectionState();
}
public PeerConnection.SignalingState getSignalingState() {
return requirePeerConnection().signalingState();
}
EglBase.Context getEglBaseContext() {
return this.eglBase.getEglBaseContext();
}
@ -542,14 +550,6 @@ public class WebRTCWrapper {
return Optional.fromNullable(this.remoteVideoTrack);
}
private PeerConnection requirePeerConnection() {
final PeerConnection peerConnection = this.peerConnection;
if (peerConnection == null) {
throw new PeerConnectionNotInitialized();
}
return peerConnection;
}
private Context requireContext() {
final Context context = this.context;
if (context == null) {
@ -562,12 +562,18 @@ public class WebRTCWrapper {
return appRTCAudioManager;
}
void execute(final Runnable command) {
executorService.execute(command);
}
public interface EventCallback {
void onIceCandidate(IceCandidate iceCandidate);
void onConnectionChange(PeerConnection.PeerConnectionState newState);
void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
void onRenegotiationNeeded();
}
private static abstract class SetSdpObserver implements SdpObserver {
@ -618,6 +624,12 @@ public class WebRTCWrapper {
}
private static class FailureToSetDescriptionException extends IllegalArgumentException {
public FailureToSetDescriptionException(String message) {
super(message);
}
}
private static class CapturerChoice {
private final CameraVideoCapturer cameraVideoCapturer;
private final CameraEnumerationAndroid.CaptureFormat captureFormat;

View File

@ -1,6 +1,8 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
@ -8,6 +10,8 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
@ -58,6 +62,12 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return fingerprint == null ? null : Fingerprint.upgrade(fingerprint);
}
public Credentials getCredentials() {
final String ufrag = this.getAttribute("ufrag");
final String password = this.getAttribute("pwd");
return new Credentials(ufrag, password);
}
public List<Candidate> getCandidates() {
final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>();
for (final Element child : getChildren()) {
@ -74,6 +84,53 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return transportInfo;
}
public IceUdpTransportInfo modifyCredentials(final Credentials credentials, final Setup setup) {
final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
transportInfo.setAttribute("ufrag", credentials.ufrag);
transportInfo.setAttribute("pwd", credentials.password);
for (final Element child : getChildren()) {
if (child.getName().equals("fingerprint") && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) {
final Fingerprint fingerprint = new Fingerprint();
fingerprint.setAttributes(new Hashtable<>(child.getAttributes()));
fingerprint.setContent(child.getContent());
fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT));
transportInfo.addChild(fingerprint);
}
}
return transportInfo;
}
public static class Credentials {
public final String ufrag;
public final String password;
public Credentials(String ufrag, String password) {
this.ufrag = ufrag;
this.password = password;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Credentials that = (Credentials) o;
return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password);
}
@Override
public int hashCode() {
return Objects.hashCode(ufrag, password);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("ufrag", ufrag)
.add("password", password)
.toString();
}
}
public static class Candidate extends Element {
private Candidate() {
@ -89,7 +146,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
}
// https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1
public static Candidate fromSdpAttribute(final String attribute) {
public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) {
final String[] pair = attribute.split(":", 2);
if (pair.length == 2 && "candidate".equals(pair[0])) {
final String[] segments = pair[1].split(" ");
@ -105,6 +162,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
for (int i = 6; i < segments.length - 1; i = i + 2) {
additional.put(segments[i], segments[i + 1]);
}
final String ufrag = additional.get("ufrag");
if (ufrag != null && !ufrag.equals(currentUfrag)) {
return null;
}
final Candidate candidate = new Candidate();
candidate.setAttribute("component", component);
candidate.setAttribute("foundation", foundation);
@ -285,8 +346,31 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return this.getAttribute("hash");
}
public String getSetup() {
return this.getAttribute("setup");
public Setup getSetup() {
final String setup = this.getAttribute("setup");
return setup == null ? null : Setup.of(setup);
}
}
public enum Setup {
ACTPASS, PASSIVE, ACTIVE;
public static Setup of(String setup) {
try {
return valueOf(setup.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
return null;
}
}
public Setup flip() {
if (this == PASSIVE) {
return ACTIVE;
}
if (this == ACTIVE) {
return PASSIVE;
}
throw new IllegalStateException(this.name()+" can not be flipped");
}
}
}

View File

@ -3,7 +3,10 @@ package eu.siacs.conversations.xmpp.jingle.stanzas;
import androidx.annotation.NonNull;
import com.google.common.base.CaseFormat;
import com.google.common.base.Throwables;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
public enum Reason {
@ -51,4 +54,15 @@ public enum Reason {
return FAILED_APPLICATION;
}
}
public static Reason ofThrowable(final Throwable throwable) {
final Throwable root = Throwables.getRootCause(throwable);
if (root instanceof RuntimeException) {
return of((RuntimeException) root);
}
if (root instanceof CryptoFailedException) {
return SECURITY_ERROR;
}
return FAILED_APPLICATION;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

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