Compare commits
293 Commits
Author | SHA1 | Date |
---|---|---|
genofire | aa8b9f338f | |
genofire | 658c1c58d5 | |
genofire | 39792f0815 | |
Daniel Gultsch | db834a1f07 | |
Daniel Gultsch | f8a94161db | |
Daniel Gultsch | 5d526a77e3 | |
Daniel Gultsch | a508a81553 | |
Daniel Gultsch | 61fb38cd84 | |
Daniel Gultsch | 1bf2d5dd8f | |
Daniel Gultsch | 0a18c8613f | |
Daniel Gultsch | abb671616c | |
Daniel Gultsch | 297a843b9c | |
Daniel Gultsch | 0698fa0d8c | |
Daniel Gultsch | 70b5d8d81a | |
Daniel Gultsch | 0a3947b8e3 | |
Daniel Gultsch | 3f402b132b | |
Daniel Gultsch | 5b80c62a63 | |
Daniel Gultsch | 717c83753f | |
Daniel Gultsch | b6dee6da6a | |
Daniel Gultsch | 9c3f55bef2 | |
Daniel Gultsch | 9843b72f6f | |
Daniel Gultsch | 61851e5f84 | |
Daniel Gultsch | 4ec0996dff | |
genofire | fa1363cea0 | |
Daniel Gultsch | fda45a7c86 | |
Daniel Gultsch | b5786787f0 | |
Daniel Gultsch | d4cbf2e11e | |
Daniel Gultsch | 7d7e158fd7 | |
Daniel Gultsch | a8ff88398d | |
Daniel Gultsch | bae9fc45c2 | |
Daniel Gultsch | ba4a47204b | |
Daniel Gultsch | 226eb739bd | |
Daniel Gultsch | 869a135bab | |
Daniel Gultsch | 7ddd60d314 | |
Daniel Gultsch | 2ca00265db | |
Daniel Gultsch | e0c4964cc2 | |
Daniel Gultsch | 3706981645 | |
genofire | a124b3df9a | |
genofire | 5a7bae592d | |
Daniel Gultsch | 6d2e406ee5 | |
Daniel Gultsch | cc489ef7bf | |
Daniel Gultsch | 495537d087 | |
Daniel Gultsch | 20e4d108d4 | |
Daniel Gultsch | d0af5a002e | |
Daniel Gultsch | 86de21f6a8 | |
Daniel Gultsch | e664a27cd0 | |
Daniel Gultsch | 4a6df90f0c | |
Daniel Gultsch | fdaab1c27e | |
Daniel Gultsch | f8c59a7b75 | |
Daniel Gultsch | f182fe6697 | |
Daniel Gultsch | daf1bbfca5 | |
Daniel Gultsch | b8eec6ae5b | |
Daniel Gultsch | 3ede2d00bd | |
Daniel Gultsch | d2a387e82f | |
Daniel Gultsch | da14f83a42 | |
Daniel Gultsch | 586fff5485 | |
Daniel Gultsch | ea9b73c1fe | |
Daniel Gultsch | e791e19265 | |
Daniel Gultsch | 3de8147b41 | |
Daniel Gultsch | b9ceb67104 | |
Daniel Gultsch | 90a0d36362 | |
Daniel Gultsch | bd4d939a29 | |
Daniel Gultsch | 64a6edd3fb | |
Daniel Gultsch | 75c20a7a2b | |
Daniel Gultsch | d5994a8d65 | |
Daniel Gultsch | 572b9c2dc6 | |
Daniel Gultsch | f9f994c540 | |
Daniel Gultsch | ba9596b37d | |
Daniel Gultsch | b01bca74fd | |
Daniel Gultsch | bfc8668803 | |
Alexei Sorokin | 951d84f404 | |
Ashique Bava | 231d97ea81 | |
Daniel Gultsch | 73000962fe | |
Daniel Gultsch | 3075833ab3 | |
Geno | eba5dd8654 | |
Geno | abe3718353 | |
Daniel Gultsch | 3f315751a1 | |
Daniel Gultsch | 68d8e2b9cf | |
Daniel Gultsch | c195e8b3d2 | |
Daniel Gultsch | 25f137441b | |
Daniel Gultsch | d436c5f856 | |
Daniel Gultsch | 8d9c51d755 | |
Daniel Gultsch | 2957bccb33 | |
Daniel Gultsch | 3135550b83 | |
Millesimus | 4d36231fa5 | |
Millesimus | dfeeaff74c | |
Daniel Gultsch | 5a9777f7f1 | |
Daniel Gultsch | 63f5f8c89d | |
Millesimus | ca08c27eef | |
Millesimus | 4040d5f647 | |
Millesimus | 8d45cc5827 | |
Daniel Gultsch | 96f0a09a5d | |
Maximilian Weiler | 38a77dbba6 | |
Daniel Gultsch | 8b817b3bd8 | |
Alexei Sorokin | 754773be55 | |
Alexei Sorokin | 4f362aafac | |
Daniel Gultsch | ea0dc558cb | |
Daniel Gultsch | 80d8b6dd88 | |
Daniel Gultsch | 2819545a43 | |
Daniel Gultsch | 9526456d75 | |
Daniel Gultsch | f975b5ddac | |
Daniel Gultsch | 581eb511b9 | |
Daniel Gultsch | af42e34654 | |
Daniel Gultsch | 0495470ca8 | |
Daniel Gultsch | 6c88a4b4fa | |
Daniel Gultsch | 88d7ddf124 | |
Daniel Gultsch | e6d8bee035 | |
Daniel Gultsch | 208c9d91db | |
Licaon_Kter | bf3c1d573b | |
Daniel Gultsch | 4e90c0dbbb | |
Daniel Gultsch | caefec2fbf | |
Daniel Gultsch | 339ee8f6ea | |
Daniel Gultsch | b00b8996d5 | |
Maximilian Weiler | 06fbb06aee | |
Maximilian Weiler | ef8f10cc13 | |
Millesimus | b6fe1898e7 | |
Millesimus | 955a6f3fe1 | |
Millesimus | a0529a4e1e | |
Millesimus | 2db2ca95ce | |
Millesimus | a0bca08997 | |
Millesimus | 748443cd4e | |
Millesimus | 3921f3a940 | |
Millesimus | c81c8a62b3 | |
Millesimus | e850900b40 | |
Millesimus | 74d60d0131 | |
Millesimus | 65a72827bc | |
Millesimus | 309082a9b3 | |
Licaon_Kter | e528b9f5df | |
Daniel Gultsch | 1e1dad780b | |
Daniel Gultsch | 0e54cde4bf | |
Alexei Sorokin | b99f9d4f1c | |
Daniel Gultsch | 7466d12505 | |
Daniel Gultsch | 98ffadd87d | |
Daniel Gultsch | af33a57bf2 | |
Daniel Gultsch | 87f99d3570 | |
Daniel Gultsch | b025265f91 | |
Daniel Gultsch | 0f3181555a | |
Daniel Gultsch | 76fb0180d6 | |
Daniel Gultsch | e02aaed7d2 | |
Daniel Gultsch | c9f1bdc551 | |
Daniel Gultsch | 2b9862adea | |
Daniel Gultsch | d91cd3e9e8 | |
Geno | e21d49efc8 | |
Daniel Gultsch | 56535e07ff | |
Dheeraj Chintaluri | 60c5906fe9 | |
Daniel Gultsch | 67e5f839f1 | |
Daniel Gultsch | 89012b0f8b | |
Daniel Gultsch | 7476dccc0e | |
Daniel Gultsch | 9182a300c5 | |
Daniel Gultsch | faa4c87b5f | |
Daniel Gultsch | 8d391753d7 | |
Daniel Gultsch | 337aa4a110 | |
Daniel Gultsch | ddf597e0d3 | |
Daniel Gultsch | 9c16af25fb | |
Daniel Gultsch | e2324209ed | |
Daniel Gultsch | 9544b994dc | |
Daniel Gultsch | 3b25fb9038 | |
Daniel Gultsch | 48156dd27f | |
Daniel Gultsch | 905489e237 | |
Daniel Gultsch | a5ad2b7fc6 | |
Daniel Gultsch | 5d3ad6e36b | |
Daniel Gultsch | 6d91551f59 | |
Daniel Gultsch | 0717f9ba18 | |
Daniel Gultsch | ac7855a332 | |
Daniel Gultsch | a40b82b85b | |
Daniel Gultsch | c5e90199c3 | |
Daniel Gultsch | 53908dd56e | |
Daniel Gultsch | 9d9514a091 | |
Daniel Gultsch | bc58fb0fbd | |
Daniel Gultsch | ec061bedc1 | |
Geno | a7f4abbab5 | |
Daniel Gultsch | f9b292fd6a | |
Daniel Gultsch | 8aed588405 | |
Daniel Gultsch | ea2acc2963 | |
Daniel Gultsch | 2760f07307 | |
Daniel Gultsch | ec22a39538 | |
Daniel Gultsch | 331fd30699 | |
Daniel Gultsch | c469b2dc22 | |
Daniel Gultsch | 202bde46ed | |
Daniel Gultsch | 37ce311764 | |
Daniel Gultsch | 9fc04c4b1e | |
Daniel Gultsch | 5f020af2cc | |
Daniel Gultsch | 55b2f2656d | |
Daniel Gultsch | fb681dfd60 | |
Daniel Gultsch | 5e59f20685 | |
Daniel Gultsch | 0fc191d004 | |
Daniel Gultsch | 30c9e7399e | |
Geno | 35e6c476dd | |
Daniel Gultsch | f632c7bbc9 | |
Daniel Gultsch | 08f27ddcf8 | |
Daniel Gultsch | 1822a71c2a | |
Daniel Gultsch | 77f448692c | |
Daniel Gultsch | 9cc95d4cc2 | |
Daniel Gultsch | ff756647a9 | |
Daniel Gultsch | 9a7fc3d9b8 | |
Daniel Gultsch | d37140ebf0 | |
Daniel Gultsch | d288f5bff2 | |
Daniel Gultsch | aad34783ad | |
Daniel Gultsch | 914ea9c398 | |
Daniel Gultsch | 02b16063c6 | |
Daniel Gultsch | 45c5f9aa90 | |
Daniel Gultsch | 4ac64f3a3b | |
Daniel Gultsch | 8b90c1c498 | |
Daniel Gultsch | 70fc08314f | |
Daniel Gultsch | ce7f59a76c | |
Daniel Gultsch | 1cd95aefa6 | |
Daniel Gultsch | 739d20428a | |
Daniel Gultsch | 6ee2807027 | |
Daniel Gultsch | a6244d986a | |
Daniel Gultsch | 8ac97b0027 | |
Daniel Gultsch | 7a115cb967 | |
Daniel Gultsch | d1195d21ae | |
Daniel Gultsch | 72828c6c4e | |
Daniel Gultsch | 38ef69a926 | |
Daniel Gultsch | aaac8296b3 | |
Daniel Gultsch | e217551a82 | |
Daniel Gultsch | b09a1432a3 | |
Geno | f161a9c4bf | |
Geno | f8e69f9fc2 | |
Daniel Gultsch | 6f1b71970d | |
Daniel Gultsch | 3baacf8862 | |
Daniel Gultsch | 9c2da0a1b8 | |
Daniel Gultsch | 2681ad82e1 | |
Daniel Gultsch | 8764d11cce | |
Daniel Gultsch | 3c60de54cb | |
Daniel Gultsch | d30a08266a | |
Daniel Gultsch | 859f3b2a1d | |
Daniel Gultsch | bf25b24967 | |
Daniel Gultsch | 7c53dcc4f4 | |
Daniel Gultsch | bf9d1a5759 | |
Daniel Gultsch | ddb54bb222 | |
Daniel Gultsch | 813b07e18d | |
Daniel Gultsch | 0fa06d65b5 | |
Daniel Gultsch | e947a3f808 | |
Daniel Gultsch | b34f6e0720 | |
Daniel Gultsch | b8c61b795e | |
Daniel Gultsch | cf68c544aa | |
Christopher Vollick | ef24d2050b | |
Daniel Gultsch | 4a175f915d | |
Daniel Gultsch | 5848013a1e | |
Daniel Gultsch | c5f801c1fe | |
Daniel Gultsch | d52c46d582 | |
Daniel Gultsch | e81fb1b24e | |
Daniel Gultsch | 3ee70b1d48 | |
Daniel Gultsch | e4b2bb4a42 | |
Daniel Gultsch | 8a6430ae29 | |
Geno | a213b00091 | |
Geno | 64e88d946f | |
Daniel Gultsch | 47a904b4fc | |
Daniel Gultsch | 33e73a2b47 | |
Daniel Gultsch | d889c02a0a | |
Daniel Gultsch | 073b6a998a | |
Daniel Gultsch | 9450d49b0b | |
Daniel Gultsch | 5e0c158cde | |
Daniel Gultsch | 16b1c561d4 | |
Daniel Gultsch | 24f2f52512 | |
Daniel Gultsch | e98ec40b7f | |
Daniel Gultsch | f92ea5c70b | |
Daniel Gultsch | 81505c6202 | |
Daniel Gultsch | 638f30b902 | |
Daniel Gultsch | 0812bae1ab | |
Daniel Gultsch | 6bfe16f044 | |
Daniel Gultsch | 53da64b7e2 | |
Daniel Gultsch | ebb38d7d75 | |
Daniel Gultsch | 484f633180 | |
Daniel Gultsch | 78c89664c4 | |
Daniel Gultsch | 90270069da | |
Daniel Gultsch | 49992f300b | |
Daniel Gultsch | 72e268e6b1 | |
Daniel Gultsch | 78901e3339 | |
Daniel Gultsch | 149224a073 | |
Daniel Gultsch | db447f845e | |
Daniel Gultsch | 6cab0ad496 | |
Daniel Gultsch | 9f869d3895 | |
Daniel Gultsch | b808a03702 | |
Daniel Gultsch | 7330d8a7f0 | |
Daniel Gultsch | 6e3dc0eef6 | |
Daniel Gultsch | 3847ab8465 | |
Daniel Gultsch | deee31e517 | |
mimi89999 | ab17f935c3 | |
mimi89999 | d51b4380d7 | |
Daniel Gultsch | b6d62c13ef | |
Daniel Gultsch | b76b60df5c | |
Daniel Gultsch | f82ae0a9b8 | |
Daniel Gultsch | 358c70828f | |
maxim432 | 156c4da2b3 | |
Daniel Gultsch | ca496fd39f | |
Daniel Gultsch | 53a038d90e | |
Daniel Gultsch | d77d89b356 | |
Daniel Gultsch | 2155a50875 | |
Daniel Gultsch | 4a9dfb9567 | |
Daniel Gultsch | 10382e83bf | |
Daniel Gultsch | c11ac40df4 |
|
@ -1,14 +0,0 @@
|
|||
#!/bin/sh
|
||||
find -type d -name "siacs" -print0 | xargs --null -I{} bash -c 'x="{}"; mv "$x" "${x/siacs/sum7}" '
|
||||
find -type f -name "*.java" -exec sed -i "/eu.siacs.conversations.axolotl/! s/eu.siacs./eu.sum7./" "{}" \;
|
||||
find -type f -name "*.xml" -exec sed -i "/eu.siacs.conversations.axolotl/! s/eu.siacs./eu.sum7./" "{}" \;
|
||||
# workaround for bug in fdroid nightly
|
||||
sed -i "s/-debug.apk/-unsigned.apk/" /usr/lib/python3/dist-packages/fdroidserver/nightly.py
|
||||
# generate new version
|
||||
git fetch --tags
|
||||
export versionCode="$DRONE_BUILD_NUMBER"
|
||||
export versionName="$(git describe --tag --abbrev=0)-${DRONE_BUILD_NUMBER}"
|
||||
echo "set VersionCode '${versionCode}' and VersonName '${versionName}'"
|
||||
sed -i "s/^\(\s*versionCode\s*\).*$/\1$versionCode/" build.gradle
|
||||
sed -i "0,/versionName/s/^\(\s*versionName\).*/\1 \"$versionName\"/" build.gradle
|
||||
cat -n build.gradle
|
31
.drone.yml
31
.drone.yml
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- image: registry.gitlab.com/fdroid/ci-images-client:latest
|
||||
name: build
|
||||
when:
|
||||
branch:
|
||||
exclude:
|
||||
- droneci
|
||||
commands:
|
||||
- ./gradlew assembleConversationsFreeCompatDebug
|
||||
|
||||
- image: registry.gitlab.com/fdroid/ci-images-client:latest
|
||||
name: publish
|
||||
when:
|
||||
branch:
|
||||
include:
|
||||
- droneci
|
||||
environment:
|
||||
DEBUG_KEYSTORE:
|
||||
from_secret: DEBUG_KEYSTORE
|
||||
commands:
|
||||
- .ci/prepare-publish.sh
|
||||
# build free version
|
||||
- ./gradlew assembleConversationsFreeCompatRelease
|
||||
- ls build/outputs/apk/conversationsFreeCompat/*
|
||||
# publish on nightly fdroid repo
|
||||
- fdroid nightly -v
|
|
@ -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
|
||||
|
||||
|
22
.travis.yml
22
.travis.yml
|
@ -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-m87.aar https://gultsch.de/files/libwebrtc-m87.aar
|
||||
script:
|
||||
- ./gradlew assembleQuicksyFreeCompatDebug
|
||||
- ./gradlew assembleQuicksyFreeSystemDebug
|
||||
- ./gradlew assembleConversationsFreeCompatDebug
|
||||
- ./gradlew assembleConversationsFreeSystemDebug
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-28"
|
|
@ -2,7 +2,7 @@
|
|||
host = https://www.transifex.com
|
||||
lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw
|
||||
|
||||
[conversations.strings]
|
||||
[conversations.main-strings]
|
||||
file_filter = src/main/res/values-<lang>/strings.xml
|
||||
source_file = src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
|
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -1,5 +1,54 @@
|
|||
# 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
|
||||
|
||||
### Version 2.9.12
|
||||
|
||||
* Always verify domain name. No user overwrite
|
||||
* Support roster pre authentication
|
||||
|
||||
### Version 2.9.11
|
||||
|
||||
* Fixed 'No Connectivity' issues on Android 7.1
|
||||
|
||||
### Version 2.9.10
|
||||
* fix HTTP up/download for users that don’t trust system CAs
|
||||
|
||||
### Version 2.9.9
|
||||
|
||||
* Various bug fixes around Tor support
|
||||
* Improve call compatibility with Dino
|
||||
|
||||
### Version 2.9.8
|
||||
|
||||
* Verify A/V calls with preexisting OMEMO sessions
|
||||
* Improve compatibility with non libwebrtc WebRTC implementations
|
||||
|
||||
### Version 2.9.7
|
||||
|
||||
* Ability to select incoming call ringtone
|
||||
* Fix OpenPGP key id discovery for OpenKeychain 5.6+
|
||||
* Properly verify punycode TLS certificates
|
||||
* Improve stability of RTP session establishment (calling)
|
||||
|
||||
### Version 2.9.6
|
||||
|
||||
* Show call button for offline contacts if they previously announced support
|
||||
|
@ -12,6 +61,7 @@
|
|||
|
||||
### Version 2.9.4
|
||||
* minor stability improvements for A/V calls
|
||||
* Conversations releases from here on forward require Android 5
|
||||
|
||||
### Version 2.9.3
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
* End-to-end encryption with [OMEMO](http://conversations.im/omemo/) or [OpenPGP](http://openpgp.org/about/)
|
||||
* Send and receive images as well as other kind of files
|
||||
* Encrypted audio and video calls (DTLS-SRTP)
|
||||
* [Encrypted audio and video calls (DTLS-SRTP)](https://help.conversations.im)
|
||||
* Share your location
|
||||
* Send voice messages
|
||||
* Indication when your contact has read your message
|
||||
|
@ -139,7 +139,7 @@ Note: This is kind of a weird quirk in OpenFire. Most other servers would just t
|
|||
|
||||
Maybe you attempted to use the Jabber ID `test@b.tld` because `a.tld` doesn’t point to the correct host. In that case you might have to enable the extended connection settings in the expert settings of Conversations and set a host name.
|
||||
|
||||
### I get 'Stream opening error'. What does that mean?
|
||||
#### I get 'Stream opening error'. What does that mean?
|
||||
|
||||
In most cases this error is caused by ejabberd advertising support for TLSv1.3 but not properly supporting it. This can happen if the OpenSSL version on the server already supports TLSv1.3 but the fast\_tls wrapper library used by ejabberd not (properly) support it. Upgrading fast\_tls and ejabberd or - theoretically - downgrading OpenSSL should fix the issue. A work around is to explicitly disable TLSv1.3 support in the ejabberd configuration. More information can be found on [this issue on the ejabberd issue tracker](https://github.com/processone/ejabberd/issues/2614).
|
||||
|
||||
|
@ -371,7 +371,7 @@ Unfortunately we don‘t 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.
|
||||
|
|
74
build.gradle
74
build.gradle
|
@ -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.2'
|
||||
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,24 +33,23 @@ configurations {
|
|||
dependencies {
|
||||
implementation 'androidx.viewpager:viewpager:1.0.0'
|
||||
|
||||
//should remain that low because later versions introduce dependency to androidx (not sure exactly from what version)
|
||||
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.2.1'
|
||||
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'
|
||||
|
@ -65,21 +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 'org.osmdroid:osmdroid-android:6.1.5'
|
||||
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"
|
||||
//retrofit needs to stick with 2.6.x (https://github.com/square/retrofit/blob/master/CHANGELOG.md)
|
||||
implementation "com.squareup.retrofit2:retrofit:2.6.4"
|
||||
implementation "com.squareup.retrofit2:converter-gson:2.6.4"
|
||||
//okhttp needs to stick with 3.12.x
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
|
||||
implementation 'com.google.guava:guava:27.1-android'
|
||||
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.16'
|
||||
//implementation fileTree(include: ['libwebrtc-m87.aar'], dir: 'libs')
|
||||
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.2"
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
|
@ -95,26 +93,25 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 408
|
||||
versionName "2.9.6.1"
|
||||
versionCode 4202301
|
||||
versionName "2.10.2"
|
||||
archivesBaseName += "-$versionName"
|
||||
applicationId "eu.sum7.conversations"
|
||||
resValue "string", "applicationId", applicationId
|
||||
resValue "string", "app_name", "Conv6ations"
|
||||
buildConfigField "String", "LOGTAG", "\"conver6ations\""
|
||||
def appName = "Conv6ations"
|
||||
resValue "string", "app_name", appName
|
||||
buildConfigField "String", "APP_NAME", "\"$appName\"";
|
||||
}
|
||||
|
||||
|
||||
configurations {
|
||||
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
|
||||
|
@ -127,9 +124,11 @@ android {
|
|||
quicksy {
|
||||
dimension "mode"
|
||||
applicationId = "im.quicksy.client"
|
||||
resValue "string", "app_name", "Quicksy"
|
||||
resValue "string", "applicationId", applicationId
|
||||
buildConfigField "String", "LOGTAG", "\"quicksy\""
|
||||
|
||||
def appName = "Quicksy"
|
||||
resValue "string", "app_name", appName
|
||||
buildConfigField "String", "APP_NAME", "\"$appName\"";
|
||||
}
|
||||
|
||||
conversations {
|
||||
|
@ -263,5 +262,4 @@ android {
|
|||
exclude 'META-INF/BCKEY.DSA'
|
||||
exclude 'META-INF/BCKEY.SF'
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
Conversations is a messenger for the next decade. Based on already established
|
||||
internet standards that have been around for over ten years Conversations isn’t
|
||||
trying to replace current commercial messengers. It will simply outlive them.
|
||||
Commercial, closed source products are coming and going. 15 years ago we had ICQ
|
||||
which was replaced by Skype. MySpace was replaced by Facebook. WhatsApp and
|
||||
Hangouts will disappear soon. Internet standards however stick around. People
|
||||
are still using IRC and e-mail even though these protocols have been around for
|
||||
decades. Utilizing proven standards doesn’t mean one can not evolve. GMail has
|
||||
revolutionized the way we look at e-mail. Firefox and Chrome have changed the
|
||||
way we use the Web. Conversations will change the way we look at instant
|
||||
messaging. Being less obtrusive than a telephone call instant messaging has
|
||||
always played an important role in modern society. Conversations will show that
|
||||
instant messaging can be fast, reliable and private. Conversations will not
|
||||
force its security and privacy aspects upon the user. For those willing to use
|
||||
encryption Conversations will make it as uncomplicated as possible. However
|
||||
Conversations is aware that end-to-end encryption by the very principle isn’t
|
||||
trivial. Instead of trying the impossible and making encryption easier than
|
||||
comparing a fingerprint Conversations will try to educate the willing user and
|
||||
explain the necessary steps and the reasons behind them. Those unwilling to
|
||||
learn about encryption will still be protected by the design principals of
|
||||
Conversations. Conversations will simply not share or generate certain
|
||||
information for example by encouraging the use of federated servers.
|
||||
Conversations will always utilize the best available standards for encryption
|
||||
and media encoding instead of reinventing the wheel. However it isn’t afraid to
|
||||
break with behavior patterns that have been proven ineffective.
|
32
docs/XEPs.md
32
docs/XEPs.md
|
@ -1,32 +0,0 @@
|
|||
* XEP-0027: Current Jabber OpenPGP Usage
|
||||
* XEP-0030: Service Discovery
|
||||
* XEP-0045: Multi-User Chat
|
||||
* XEP-0048: Bookmarks
|
||||
* XEP-0084: User Avatar
|
||||
* XEP-0085: Chat State Notifications
|
||||
* XEP-0092: Software Version
|
||||
* XEP-0115: Entity Capabilities
|
||||
* XEP-0163: Personal Eventing Protocol (avatars and nicks)
|
||||
* XEP-0166: Jingle (only used for file transfer)
|
||||
* XEP-0172: User Nickname
|
||||
* XEP-0184: Message Delivery Receipts (reply only)
|
||||
* XEP-0191: Blocking command
|
||||
* XEP-0198: Stream Management
|
||||
* XEP-0199: XMPP Ping
|
||||
* XEP-0234: Jingle File Transfer
|
||||
* XEP-0237: Roster Versioning
|
||||
* XEP-0245: The /me Command
|
||||
* XEP-0249: Direct MUC Invitations (receiving only)
|
||||
* XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
|
||||
* XEP-0261: Jingle In-Band Bytestreams Transport Method
|
||||
* XEP-0280: Message Carbons
|
||||
* XEP-0308: Last Message Correction
|
||||
* XEP-0313: Message Archive Management
|
||||
* XEP-0319: Last User Interaction in Presence
|
||||
* XEP-0333: Chat Markers
|
||||
* XEP-0352: Client State Indication
|
||||
* XEP-0357: Push Notifications
|
||||
* XEP-0363: HTTP File Upload
|
||||
* XEP-0368: SRV records for XMPP over TLS
|
||||
* XEP-0377: Spam Reporting
|
||||
* XEP-0384: OMEMO Encryption
|
|
@ -1,97 +0,0 @@
|
|||
Observations on implementing XMPP
|
||||
=================================
|
||||
After spending the last two and a half month basically writing my own XMPP
|
||||
library from scratch I decided to share some of the observations I made in the
|
||||
process. In part this article can be seen as a response to a blog post made by
|
||||
Dr. Ing. Georg Lukas. The blog post introduces a couple of XEP (XMPP Extensions)
|
||||
which make the life on mobile devices a lot easier but states that they are
|
||||
currently very few implementations of those XEPs. So I went ahead and
|
||||
implemented all of them in my Android XMPP client.
|
||||
|
||||
### General observations
|
||||
The first thing I noticed is that XMPP is actually okish designed. If you were
|
||||
to design a new chat protocol today you probably wouldn’t choose XML again
|
||||
however the protocol basically consists of only three different packages which
|
||||
are quickly hidden under some sort of abstraction layer within your library.
|
||||
Getting from zero to sending messages to other users actually was very simple
|
||||
and straight forward. But then came the XEPs.
|
||||
|
||||
### Multi-User Chat
|
||||
The first one was XEP-0045 Multi-User Chat. This is the one XEP of the XEPs I’m
|
||||
going to mention in my article which is actually wildly adopted. Most clients
|
||||
and servers I know of support MUC. However the level of completeness varies.
|
||||
MUC actually introduces access and permission roles which are far more complex
|
||||
than what some of us are used to from IRC but a lot of clients just don’t
|
||||
implement them. I’m not implementing them myself (at least for now) because I
|
||||
somewhat doubt that someone would actually use them (however this might be some
|
||||
sort of chicken or egg problem). I did find some strange bugs though which might
|
||||
be interesting for other library developers. In theory a MUC server
|
||||
implementation can allow a single user (same jid) to join a conference room
|
||||
multiple times with the same nick from different clients. This means if someone
|
||||
wants to participate in a conference from two different devices (mobile and
|
||||
desktop for example) one wouldn’t have to name oneself `userDesktop` and
|
||||
`userMobile` but just `user`. Both ejabberd and prosody support this but with
|
||||
strange side effects. Prosody for example doesn’t allow a user to change its
|
||||
name once two clients are “merged” by having the same nick.
|
||||
|
||||
### Carbons and Stream Management
|
||||
Two of the other XEPs Lukas mentions — Carbons (XEP-0280) and Stream Management
|
||||
(XEP-0198) — were actually fairly easy to implement. The only challenges were to
|
||||
find a server to support them (I ended up running my own Prosody server) and a
|
||||
desktop client to test them with. For carbons there is a patched Mcabber version
|
||||
and Gajim. After implementing stream management I had very good results on my
|
||||
mobile device. I had sessions running for up to 24 hours with a walking outside,
|
||||
loosing mobile coverage for a few minutes and so on. The only limitation was
|
||||
that I had to keep on developing and reinstalling my app.
|
||||
|
||||
### Off the record
|
||||
And then came OTR... This is were I spend the most time debugging stuff and
|
||||
trying to get things right and compatible with other clients. This is the part
|
||||
were I want to help other developers not to make the same mistakes and maybe
|
||||
come to some sort of consent among XMPP developers to ultimately increase the
|
||||
interoperability. OTR has some down sides which make it difficult or at times
|
||||
even dangerous to implement within XMPP. First of all it is a synchronous
|
||||
protocol which is tunneled through a different protocol (XMPP). Synchronous
|
||||
means — among other things — auto replies. (An OTR session begins with “hi I’m
|
||||
speaking otr give me your key” “ok cool here is my key”) And auto replies — we
|
||||
know that since the first time an out of office auto responder went postal — are
|
||||
dangerous. Things really start to get messy when you use one of the best
|
||||
features of XMPP — multiple clients. The way XMPP works is that clients are
|
||||
encouraged to send their messages to the raw jid and let the server decide what
|
||||
full jid the messages are routed to. If in doubt even all of them. So what
|
||||
happens when Alice sends a start-otr-message to Bobs raw jid? Bob receives the
|
||||
message on his notebook as well as his cell phone. Both of them answer. Alice
|
||||
gets two different replies. Shit explodes. Even if Alice sends the message to
|
||||
bob/notebook chances are that Bob has carbon messages enabled and still receives
|
||||
the messages on both devices. Now assuming that Bobs client is clever enough not
|
||||
to auto reply to carbonated messages Bob/cellphone will still end up with a lot
|
||||
of garbage messages. (Essentially the entire conversation between Alice and
|
||||
Bob/notebook but unreadable of course) Therefor it should be good practice to
|
||||
tag OTR messages as both private and no-copy (private is part of the carbons
|
||||
XEP, no-copy is a general hint). I found that prosody for some reasons doesn’t
|
||||
honor the private tag on outgoing messages. While this is easily fixed I presume
|
||||
that having both the private and the no-copy tag will make it more compatible
|
||||
with servers or clients I don’t know about yet.
|
||||
|
||||
#### Rules to follow when implementing OTR
|
||||
To summarize my observations on implementing OTR in XMPP let me make the
|
||||
following three statements.
|
||||
|
||||
1. While it is good practice for unencrypted messages to be send to the raw jid
|
||||
and have the receiving server or user decide how they should be routed OTR
|
||||
messages must be send to a specific resource. To make this work the user should
|
||||
be given the option to select the presence (which can be assisted with some
|
||||
educated guessing by the client based on previous messages). Furthermore a
|
||||
client should encourage a user to choose meaningful presences instead of the
|
||||
clients name or even random ones. Something like `/mobile`, `/notebook`,
|
||||
`/desktop` is a greater assist to any one who wants to start an otr session then
|
||||
`/Gajim`, `/mcabber` or `/pidgin`.
|
||||
|
||||
2. Messages should be tagged private and no-copy to avoid unnecessary traffic or
|
||||
otr error loops with faulty clients. This tagging should be done even if your
|
||||
own client doesn’t support carbons.
|
||||
|
||||
3. When dealing with “legacy clients” — meaning clients which don’t follow my
|
||||
advise — a client should be extra careful not to create message loops. This
|
||||
means to not respond with otr errors if a client is not 100% sure it is the only
|
||||
client which received the message
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
• Ability to select incoming call ringtone
|
||||
• Fix OpenPGP key id discovery for OpenKeychain 5.6+
|
||||
• Properly verify punycode TLS certificates
|
||||
• Improve stability of RTP session establishment (calling)
|
|
@ -0,0 +1,2 @@
|
|||
• Verify A/V calls with preexisting OMEMO sessions
|
||||
• Improve compatibility with non libwebrtc WebRTC implementations
|
|
@ -0,0 +1 @@
|
|||
• Various bug fixes around Tor support
|
|
@ -0,0 +1,3 @@
|
|||
• Improve call compatibility with Dino
|
||||
• fix HTTP up/download for users that don’t trust system CAs
|
||||
• Fixed 'No Connectivity' issues on Android 7.1
|
|
@ -0,0 +1,3 @@
|
|||
• Always verify domain name. No user overwrite
|
||||
• Support roster pre authentication
|
||||
• minor A/V improvements
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
• Fix issue with some videos not being compressed
|
||||
• Fix rare crash when opening notification
|
|
@ -0,0 +1,2 @@
|
|||
• Fix crash when rendering some quotes
|
||||
• Fix crash in welcome screen
|
|
@ -0,0 +1 @@
|
|||
• Fix usage directTLS of manuelle enter an address
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +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 প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</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>
|
||||
<string name="improperly_formatted_provisioning">Provisioning code-এ গরমিল আছে</string>
|
||||
<string name="tap_share_button_send_invite">Share বোতামটা টিপে %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>
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">Vælg din XMPP-udbyder</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å 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>
|
||||
<string name="improperly_formatted_provisioning">Forkert formateret klargøringskode</string>
|
||||
<string name="tap_share_button_send_invite">Tryk på deleknappen for at sende din kontakt en invitation til %1$s.</string>
|
||||
<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>
|
|
@ -9,4 +9,8 @@
|
|||
<string name="magic_create_text_fixed">Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΘα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας.</string>
|
||||
<string name="your_server_invitation">Η πρόσκλησή σας στον διακομιστή</string>
|
||||
<string name="improperly_formatted_provisioning">Λάθος μορφοποίηση κώδικα παροχής</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
|
|
@ -9,4 +9,8 @@
|
|||
<string name="magic_create_text_fixed">Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète.</string>
|
||||
<string name="your_server_invitation">Votre invitation au serveur</string>
|
||||
<string name="improperly_formatted_provisioning">Code de provisionnement mal formaté</string>
|
||||
</resources>
|
||||
<string name="tap_share_button_send_invite">Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Si vos contacts sont à votre côté, ils peuvent aussi scanner le code ci dessous pour accepter votre invitation</string>
|
||||
<string name="easy_invite_share_text">Rejoignez %1$set discutez avec moi : %2$s</string>
|
||||
<string name="share_invite_with">Partager une invitation avec ...</string>
|
||||
</resources>
|
||||
|
|
|
@ -9,4 +9,8 @@
|
|||
<string name="magic_create_text_fixed">Meghívást kapott a(z) %1$s kiszolgálóra. Már kiválasztottak Önnek egy felhasználónevet. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nKépes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét.</string>
|
||||
<string name="your_server_invitation">Az Ön kiszolgálómeghívása</string>
|
||||
<string name="improperly_formatted_provisioning">Helytelenül formázott kiépítési kód</string>
|
||||
</resources>
|
||||
<string name="tap_share_button_send_invite">Koppintson a megosztás gombra, hogy meghívót küldjön a partnerének erre: %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Ha a partnere a közelben van, akkor a meghívás elfogadásához leolvashatja a lenti kódot.</string>
|
||||
<string name="easy_invite_share_text">Csatlakozzon ehhez: %1$s, és csevegjen velem: %2$s</string>
|
||||
<string name="share_invite_with">Meghívás megosztása…</string>
|
||||
</resources>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,12 +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>
|
||||
</resources>
|
||||
<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>
|
||||
|
|
|
@ -9,4 +9,8 @@
|
|||
<string name="magic_create_text_fixed">Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. Этот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес.</string>
|
||||
<string name="your_server_invitation">Ваше приглашение</string>
|
||||
<string name="improperly_formatted_provisioning">Неправильный формат кода</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?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_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 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>
|
||||
<string name="improperly_formatted_provisioning">Mã cung cấp không được định dạng đúng</string>
|
||||
<string name="tap_share_button_send_invite">Nhấn nút chia sẻ để gửi lời mời vào %1$s đến liên hệ của bạn.</string>
|
||||
<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>
|
|
@ -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"
|
||||
|
|
|
@ -3,9 +3,12 @@ package eu.siacs.conversations;
|
|||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||
|
||||
|
@ -33,7 +36,7 @@ public final class Config {
|
|||
return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
|
||||
}
|
||||
|
||||
public static final String LOGTAG = BuildConfig.LOGTAG;
|
||||
public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US);
|
||||
|
||||
public static final Jid BUG_REPORTS = Jid.of("bugs@chat.sum7.eu");
|
||||
public static final Uri HELP = Uri.parse("https://sum7.eu/chat");
|
||||
|
@ -97,10 +100,11 @@ public final class Config {
|
|||
|
||||
//remove *other* omemo devices from *your* device list announcement after not seeing any activity from them for 42 days. They will automatically add themselves after coming back online.
|
||||
public static final long OMEMO_AUTO_EXPIRY = 42 * MILLISECONDS_IN_DAY;
|
||||
|
||||
|
||||
public static final boolean REMOVE_BROKEN_DEVICES = false;
|
||||
public static final boolean OMEMO_PADDING = false;
|
||||
public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
|
||||
public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true;
|
||||
|
||||
public static final boolean USE_BOOKMARKS2 = false;
|
||||
|
||||
|
@ -114,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
|
||||
|
||||
|
@ -173,7 +178,14 @@ public final class Config {
|
|||
|
||||
//if the contacts domain matches one of the following domains OMEMO won’t be turned on automatically
|
||||
//can be used for well known, widely used gateways
|
||||
public static final List<String> CONTACT_DOMAINS = Collections.singletonList("cheogram.com");
|
||||
private static final List<String> CONTACT_DOMAINS = Arrays.asList(
|
||||
"cheogram.com",
|
||||
"*.covid.monal.im"
|
||||
);
|
||||
|
||||
public static boolean matchesContactDomain(final String domain) {
|
||||
return XmppDomainVerifier.matchDomain(domain, CONTACT_DOMAINS);
|
||||
}
|
||||
}
|
||||
|
||||
private Config() {
|
||||
|
@ -188,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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package eu.siacs.conversations.crypto;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
public interface DomainHostnameVerifier extends HostnameVerifier {
|
||||
|
||||
boolean verify(String domain, String hostname, SSLSession sslSession);
|
||||
boolean verify(String domain, String hostname, SSLSession sslSession) throws SSLPeerUnverifiedException;
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import java.io.FileOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -209,7 +208,7 @@ public class PgpDecryptionService {
|
|||
message.setRelativeFilePath(path);
|
||||
}
|
||||
}
|
||||
URL url = message.getFileParams().url;
|
||||
final String url = message.getFileParams().url;
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
|
|
|
@ -6,6 +6,10 @@ import android.util.Log;
|
|||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
|
@ -18,6 +22,7 @@ import java.io.FileOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
|
@ -29,268 +34,258 @@ import eu.siacs.conversations.entities.Message;
|
|||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.UiCallback;
|
||||
import eu.siacs.conversations.utils.AsciiArmor;
|
||||
|
||||
public class PgpEngine {
|
||||
private final OpenPgpApi api;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
private final OpenPgpApi api;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
|
||||
public PgpEngine(OpenPgpApi api, XmppConnectionService service) {
|
||||
this.api = api;
|
||||
this.mXmppConnectionService = service;
|
||||
}
|
||||
public PgpEngine(OpenPgpApi api, XmppConnectionService service) {
|
||||
this.api = api;
|
||||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
private static void logError(Account account, OpenPgpError error) {
|
||||
if (error != null) {
|
||||
error.describeContents();
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": OpenKeychain error '" + error.getMessage() + "' code=" + error.getErrorId()+" class="+error.getClass().getName());
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": OpenKeychain error with no message");
|
||||
}
|
||||
}
|
||||
private static void logError(Account account, OpenPgpError error) {
|
||||
if (error != null) {
|
||||
error.describeContents();
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": OpenKeychain error '" + error.getMessage() + "' code=" + error.getErrorId() + " class=" + error.getClass().getName());
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": OpenKeychain error with no message");
|
||||
}
|
||||
}
|
||||
|
||||
public void encrypt(final Message message, final UiCallback<Message> callback) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_ENCRYPT);
|
||||
final Conversation conversation = (Conversation) message.getConversation();
|
||||
if (conversation.getMode() == Conversation.MODE_SINGLE) {
|
||||
long[] keys = {
|
||||
conversation.getContact().getPgpKeyId(),
|
||||
conversation.getAccount().getPgpId()
|
||||
};
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys);
|
||||
} else {
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, conversation.getMucOptions().getPgpKeyIds());
|
||||
}
|
||||
public void encrypt(final Message message, final UiCallback<Message> callback) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_ENCRYPT);
|
||||
final Conversation conversation = (Conversation) message.getConversation();
|
||||
if (conversation.getMode() == Conversation.MODE_SINGLE) {
|
||||
long[] keys = {
|
||||
conversation.getContact().getPgpKeyId(),
|
||||
conversation.getAccount().getPgpId()
|
||||
};
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys);
|
||||
} else {
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, conversation.getMucOptions().getPgpKeyIds());
|
||||
}
|
||||
|
||||
if (!message.needsUploading()) {
|
||||
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
String body;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
body = message.getFileParams().url.toString();
|
||||
} else {
|
||||
body = message.getBody();
|
||||
}
|
||||
InputStream is = new ByteArrayInputStream(body.getBytes());
|
||||
final OutputStream os = new ByteArrayOutputStream();
|
||||
api.executeApiAsync(params, is, os, result -> {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
try {
|
||||
os.flush();
|
||||
StringBuilder encryptedMessageBody = new StringBuilder();
|
||||
String[] lines = os.toString().split("\n");
|
||||
for (int i = 2; i < lines.length - 1; ++i) {
|
||||
if (!lines[i].contains("Version")) {
|
||||
encryptedMessageBody.append(lines[i].trim());
|
||||
}
|
||||
}
|
||||
message.setEncryptedBody(encryptedMessageBody.toString());
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
mXmppConnectionService.sendMessage(message);
|
||||
callback.success(message);
|
||||
} catch (IOException e) {
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
}
|
||||
if (!message.needsUploading()) {
|
||||
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
String body;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
body = message.getFileParams().url;
|
||||
} else {
|
||||
body = message.getBody();
|
||||
}
|
||||
InputStream is = new ByteArrayInputStream(body.getBytes());
|
||||
final OutputStream os = new ByteArrayOutputStream();
|
||||
api.executeApiAsync(params, is, os, result -> {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
try {
|
||||
os.flush();
|
||||
final ArrayList<String> encryptedMessageBody = new ArrayList<>();
|
||||
final String[] lines = os.toString().split("\n");
|
||||
for (int i = 2; i < lines.length - 1; ++i) {
|
||||
if (!lines[i].contains("Version")) {
|
||||
encryptedMessageBody.add(lines[i].trim());
|
||||
}
|
||||
}
|
||||
message.setEncryptedBody(Joiner.on('\n').join(encryptedMessageBody));
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
mXmppConnectionService.sendMessage(message);
|
||||
callback.success(message);
|
||||
} catch (IOException e) {
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
}
|
||||
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
String errorMessage = error != null ? error.getMessage() : null;
|
||||
@StringRes final int res;
|
||||
if (errorMessage != null && errorMessage.startsWith("Bad key for encryption")) {
|
||||
res = R.string.bad_key_for_encryption;
|
||||
} else {
|
||||
res = R.string.openpgp_error;
|
||||
}
|
||||
logError(conversation.getAccount(), error);
|
||||
callback.error(res, message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
DownloadableFile inputFile = this.mXmppConnectionService
|
||||
.getFileBackend().getFile(message, true);
|
||||
DownloadableFile outputFile = this.mXmppConnectionService
|
||||
.getFileBackend().getFile(message, false);
|
||||
outputFile.getParentFile().mkdirs();
|
||||
outputFile.createNewFile();
|
||||
final InputStream is = new FileInputStream(inputFile);
|
||||
final OutputStream os = new FileOutputStream(outputFile);
|
||||
api.executeApiAsync(params, is, os, result -> {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
try {
|
||||
os.flush();
|
||||
} catch (IOException ignored) {
|
||||
//ignored
|
||||
}
|
||||
FileBackend.close(os);
|
||||
mXmppConnectionService.sendMessage(message);
|
||||
callback.success(message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} catch (final IOException e) {
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
String errorMessage = error != null ? error.getMessage() : null;
|
||||
@StringRes final int res;
|
||||
if (errorMessage != null && errorMessage.startsWith("Bad key for encryption")) {
|
||||
res = R.string.bad_key_for_encryption;
|
||||
} else {
|
||||
res = R.string.openpgp_error;
|
||||
}
|
||||
logError(conversation.getAccount(), error);
|
||||
callback.error(res, message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
DownloadableFile inputFile = this.mXmppConnectionService
|
||||
.getFileBackend().getFile(message, true);
|
||||
DownloadableFile outputFile = this.mXmppConnectionService
|
||||
.getFileBackend().getFile(message, false);
|
||||
outputFile.getParentFile().mkdirs();
|
||||
outputFile.createNewFile();
|
||||
final InputStream is = new FileInputStream(inputFile);
|
||||
final OutputStream os = new FileOutputStream(outputFile);
|
||||
api.executeApiAsync(params, is, os, result -> {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
try {
|
||||
os.flush();
|
||||
} catch (IOException ignored) {
|
||||
//ignored
|
||||
}
|
||||
FileBackend.close(os);
|
||||
mXmppConnectionService.sendMessage(message);
|
||||
callback.success(message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} catch (final IOException e) {
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long fetchKeyId(Account account, String status, String signature) {
|
||||
if ((signature == null) || (api == null)) {
|
||||
return 0;
|
||||
}
|
||||
if (status == null) {
|
||||
status = "";
|
||||
}
|
||||
final StringBuilder pgpSig = new StringBuilder();
|
||||
pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----");
|
||||
pgpSig.append('\n');
|
||||
pgpSig.append('\n');
|
||||
pgpSig.append(status);
|
||||
pgpSig.append('\n');
|
||||
pgpSig.append("-----BEGIN PGP SIGNATURE-----");
|
||||
pgpSig.append('\n');
|
||||
pgpSig.append('\n');
|
||||
pgpSig.append(signature.replace("\n", "").trim());
|
||||
pgpSig.append('\n');
|
||||
pgpSig.append("-----END PGP SIGNATURE-----");
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
|
||||
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
Intent result = api.executeApi(params, is, os);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
|
||||
OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
OpenPgpSignatureResult sigResult = result
|
||||
.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
|
||||
if (sigResult != null) {
|
||||
return sigResult.getKeyId();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
return 0;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
public long fetchKeyId(final Account account, final String status, final String signature) {
|
||||
if (signature == null || api == null) {
|
||||
return 0;
|
||||
}
|
||||
final Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
|
||||
try {
|
||||
params.putExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE, AsciiArmor.decode(signature));
|
||||
} catch (final Exception e) {
|
||||
Log.d(Config.LOGTAG, "unable to parse signature", e);
|
||||
return 0;
|
||||
}
|
||||
final InputStream is = new ByteArrayInputStream(Strings.nullToEmpty(status).getBytes());
|
||||
final ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
final Intent result = api.executeApi(params, is, os);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
|
||||
OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
final OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
|
||||
//TODO unsure that sigResult.getResult() is either 1, 2 or 3
|
||||
if (sigResult != null) {
|
||||
return sigResult.getKeyId();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
return 0;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void chooseKey(final Account account, final UiCallback<Account> callback) {
|
||||
Intent p = new Intent();
|
||||
p.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID);
|
||||
api.executeApiAsync(p, null, null, result -> {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
callback.success(account);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, account);
|
||||
}
|
||||
});
|
||||
}
|
||||
public void chooseKey(final Account account, final UiCallback<Account> callback) {
|
||||
Intent p = new Intent();
|
||||
p.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID);
|
||||
api.executeApiAsync(p, null, null, result -> {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
callback.success(account);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, account);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void generateSignature(Intent intent, final Account account, String status, final UiCallback<String> callback) {
|
||||
if (account.getPgpId() == 0) {
|
||||
return;
|
||||
}
|
||||
Intent params = intent == null ? new Intent() : intent;
|
||||
params.setAction(OpenPgpApi.ACTION_CLEARTEXT_SIGN);
|
||||
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId());
|
||||
InputStream is = new ByteArrayInputStream(status.getBytes());
|
||||
final OutputStream os = new ByteArrayOutputStream();
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": signing status message \"" + status + "\"");
|
||||
api.executeApiAsync(params, is, os, result -> {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
StringBuilder signatureBuilder = new StringBuilder();
|
||||
try {
|
||||
os.flush();
|
||||
String[] lines = os.toString().split("\n");
|
||||
boolean sig = false;
|
||||
for (String line : lines) {
|
||||
if (sig) {
|
||||
if (line.contains("END PGP SIGNATURE")) {
|
||||
sig = false;
|
||||
} else {
|
||||
if (!line.contains("Version")) {
|
||||
signatureBuilder.append(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (line.contains("BEGIN PGP SIGNATURE")) {
|
||||
sig = true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
callback.error(R.string.openpgp_error, null);
|
||||
return;
|
||||
}
|
||||
callback.success(signatureBuilder.toString());
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
if (error != null && "signing subkey not found!".equals(error.getMessage())) {
|
||||
callback.error(0, null);
|
||||
} else {
|
||||
logError(account, error);
|
||||
callback.error(R.string.unable_to_connect_to_keychain, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
public void generateSignature(Intent intent, final Account account, String status, final UiCallback<String> callback) {
|
||||
if (account.getPgpId() == 0) {
|
||||
return;
|
||||
}
|
||||
Intent params = intent == null ? new Intent() : intent;
|
||||
params.setAction(OpenPgpApi.ACTION_CLEARTEXT_SIGN);
|
||||
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId());
|
||||
InputStream is = new ByteArrayInputStream(status.getBytes());
|
||||
final OutputStream os = new ByteArrayOutputStream();
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": signing status message \"" + status + "\"");
|
||||
api.executeApiAsync(params, is, os, result -> {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
final ArrayList<String> signature = new ArrayList<>();
|
||||
try {
|
||||
os.flush();
|
||||
boolean sig = false;
|
||||
for (final String line : Splitter.on('\n').split(os.toString())) {
|
||||
if (sig) {
|
||||
if (line.contains("END PGP SIGNATURE")) {
|
||||
sig = false;
|
||||
} else {
|
||||
if (!line.contains("Version")) {
|
||||
signature.add(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (line.contains("BEGIN PGP SIGNATURE")) {
|
||||
sig = true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
callback.error(R.string.openpgp_error, null);
|
||||
return;
|
||||
}
|
||||
callback.success(Joiner.on('\n').join(signature));
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
if (error != null && "signing subkey not found!".equals(error.getMessage())) {
|
||||
callback.error(0, null);
|
||||
} else {
|
||||
logError(account, error);
|
||||
callback.error(R.string.unable_to_connect_to_keychain, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void hasKey(final Contact contact, final UiCallback<Contact> callback) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_GET_KEY);
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
|
||||
api.executeApiAsync(params, null, null, new IOpenPgpCallback() {
|
||||
public void hasKey(final Contact contact, final UiCallback<Contact> callback) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_GET_KEY);
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
|
||||
api.executeApiAsync(params, null, null, new IOpenPgpCallback() {
|
||||
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
callback.success(contact);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(contact.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, contact);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
callback.success(contact);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(contact.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, contact);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public PendingIntent getIntentForKey(long pgpKeyId) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_GET_KEY);
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId);
|
||||
Intent result = api.executeApi(params, null, null);
|
||||
return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
}
|
||||
public PendingIntent getIntentForKey(long pgpKeyId) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_GET_KEY);
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId);
|
||||
Intent result = api.executeApi(params, null, null);
|
||||
return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package eu.siacs.conversations.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Primitive;
|
||||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.DERTaggedObject;
|
||||
|
@ -16,17 +17,20 @@ import org.bouncycastle.asn1.x500.style.IETFUtils;
|
|||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.IDN;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
public class XmppDomainVerifier implements DomainHostnameVerifier {
|
||||
public class XmppDomainVerifier {
|
||||
|
||||
private static final String LOGTAG = "XmppDomainVerifier";
|
||||
|
||||
|
@ -71,8 +75,8 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean matchDomain(String needle, List<String> haystack) {
|
||||
for (String entry : haystack) {
|
||||
public static boolean matchDomain(final String needle, final List<String> haystack) {
|
||||
for (final String entry : haystack) {
|
||||
if (entry.startsWith("*.")) {
|
||||
int offset = 0;
|
||||
while (offset < needle.length()) {
|
||||
|
@ -80,16 +84,13 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
|
|||
if (i < 0) {
|
||||
break;
|
||||
}
|
||||
Log.d(LOGTAG, "comparing " + needle.substring(i) + " and " + entry.substring(1));
|
||||
if (needle.substring(i).equalsIgnoreCase(entry.substring(1))) {
|
||||
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
|
||||
return true;
|
||||
}
|
||||
offset = i + 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.equalsIgnoreCase(needle)) {
|
||||
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -97,66 +98,93 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String domain, String hostname, SSLSession sslSession) {
|
||||
public boolean verify(final String unicodeDomain, final String unicodeHostname, SSLSession sslSession) throws SSLPeerUnverifiedException {
|
||||
final String domain = IDN.toASCII(unicodeDomain);
|
||||
final String hostname = unicodeHostname == null ? null : IDN.toASCII(unicodeHostname);
|
||||
final Certificate[] chain = sslSession.getPeerCertificates();
|
||||
if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
|
||||
return false;
|
||||
}
|
||||
final X509Certificate certificate = (X509Certificate) chain[0];
|
||||
final List<String> commonNames = getCommonNames(certificate);
|
||||
if (isSelfSigned(certificate)) {
|
||||
if (commonNames.size() == 1 && matchDomain(domain, commonNames)) {
|
||||
Log.d(LOGTAG, "accepted CN in self signed cert as work around for " + domain);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Certificate[] chain = sslSession.getPeerCertificates();
|
||||
if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
|
||||
return false;
|
||||
}
|
||||
X509Certificate certificate = (X509Certificate) chain[0];
|
||||
final List<String> commonNames = getCommonNames(certificate);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && isSelfSigned(certificate)) {
|
||||
if (commonNames.size() == 1 && matchDomain(domain, commonNames)) {
|
||||
Log.d(LOGTAG, "accepted CN in self signed cert as work around for " + domain);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
|
||||
List<String> xmppAddrs = new ArrayList<>();
|
||||
List<String> srvNames = new ArrayList<>();
|
||||
List<String> domains = new ArrayList<>();
|
||||
if (alternativeNames != null) {
|
||||
for (List<?> san : alternativeNames) {
|
||||
final Integer type = (Integer) san.get(0);
|
||||
if (type == 0) {
|
||||
final Pair<String, String> otherName = parseOtherName((byte[]) san.get(1));
|
||||
if (otherName != null && otherName.first != null && otherName.second != null) {
|
||||
switch (otherName.first) {
|
||||
case SRV_NAME:
|
||||
srvNames.add(otherName.second.toLowerCase(Locale.US));
|
||||
break;
|
||||
case XMPP_ADDR:
|
||||
xmppAddrs.add(otherName.second.toLowerCase(Locale.US));
|
||||
break;
|
||||
default:
|
||||
Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
|
||||
}
|
||||
}
|
||||
} else if (type == 2) {
|
||||
final Object value = san.get(1);
|
||||
if (value instanceof String) {
|
||||
domains.add(((String) value).toLowerCase(Locale.US));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) {
|
||||
domains.addAll(commonNames);
|
||||
}
|
||||
Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + srvNames + " xmppAddrs: " + xmppAddrs + " domains:" + domains);
|
||||
final ValidDomains validDomains = parseValidDomains(certificate);
|
||||
Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + validDomains.srvNames + " xmppAddrs: " + validDomains.xmppAddrs + " domains:" + validDomains.domains);
|
||||
if (hostname != null) {
|
||||
Log.d(LOGTAG, "also trying to verify hostname " + hostname);
|
||||
}
|
||||
return xmppAddrs.contains(domain)
|
||||
|| srvNames.contains("_xmpp-client." + domain)
|
||||
|| matchDomain(domain, domains)
|
||||
|| (hostname != null && matchDomain(hostname, domains));
|
||||
} catch (Exception e) {
|
||||
return validDomains.xmppAddrs.contains(domain)
|
||||
|| validDomains.srvNames.contains("_xmpp-client." + domain)
|
||||
|| matchDomain(domain, validDomains.domains)
|
||||
|| (hostname != null && matchDomain(hostname, validDomains.domains));
|
||||
} catch (final Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static ValidDomains parseValidDomains(final X509Certificate certificate) throws CertificateParsingException {
|
||||
final List<String> commonNames = getCommonNames(certificate);
|
||||
final Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
|
||||
final List<String> xmppAddrs = new ArrayList<>();
|
||||
final List<String> srvNames = new ArrayList<>();
|
||||
final List<String> domains = new ArrayList<>();
|
||||
if (alternativeNames != null) {
|
||||
for (List<?> san : alternativeNames) {
|
||||
final Integer type = (Integer) san.get(0);
|
||||
if (type == 0) {
|
||||
final Pair<String, String> otherName = parseOtherName((byte[]) san.get(1));
|
||||
if (otherName != null && otherName.first != null && otherName.second != null) {
|
||||
switch (otherName.first) {
|
||||
case SRV_NAME:
|
||||
srvNames.add(otherName.second.toLowerCase(Locale.US));
|
||||
break;
|
||||
case XMPP_ADDR:
|
||||
xmppAddrs.add(otherName.second.toLowerCase(Locale.US));
|
||||
break;
|
||||
default:
|
||||
Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
|
||||
}
|
||||
}
|
||||
} else if (type == 2) {
|
||||
final Object value = san.get(1);
|
||||
if (value instanceof String) {
|
||||
domains.add(((String) value).toLowerCase(Locale.US));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) {
|
||||
domains.addAll(commonNames);
|
||||
}
|
||||
return new ValidDomains(xmppAddrs, srvNames, domains);
|
||||
}
|
||||
|
||||
public static final class ValidDomains {
|
||||
final List<String> xmppAddrs;
|
||||
final List<String> srvNames;
|
||||
final List<String> domains;
|
||||
|
||||
private ValidDomains(List<String> xmppAddrs, List<String> srvNames, List<String> domains) {
|
||||
this.xmppAddrs = xmppAddrs;
|
||||
this.srvNames = srvNames;
|
||||
this.domains = domains;
|
||||
}
|
||||
|
||||
public List<String> all() {
|
||||
ImmutableList.Builder<String> all = new ImmutableList.Builder<>();
|
||||
all.addAll(xmppAddrs);
|
||||
all.addAll(srvNames);
|
||||
all.addAll(domains);
|
||||
return all.build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSelfSigned(X509Certificate certificate) {
|
||||
try {
|
||||
certificate.verify(certificate.getPublicKey());
|
||||
|
@ -165,9 +193,4 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String domain, SSLSession sslSession) {
|
||||
return verify(domain, null, sslSession);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,13 @@ 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;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
|
@ -49,9 +56,15 @@ import eu.siacs.conversations.services.XmppConnectionService;
|
|||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
|
||||
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
|
||||
import eu.siacs.conversations.xmpp.jingle.OmemoVerification;
|
||||
import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
|
||||
import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.pep.PublishOptions;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||
|
@ -725,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) {
|
||||
|
@ -892,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();
|
||||
|
@ -920,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();
|
||||
|
@ -931,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;
|
||||
}
|
||||
|
||||
|
@ -945,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;
|
||||
|
@ -961,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 + ": "
|
||||
|
@ -973,6 +994,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
if (callback != null) {
|
||||
callback.onSessionBuildFailed();
|
||||
}
|
||||
sessionSettableFuture.setException(new CryptoFailedException(e));
|
||||
}
|
||||
} else {
|
||||
fetchStatusMap.put(address, FetchStatus.ERROR);
|
||||
|
@ -986,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) {
|
||||
|
@ -1161,7 +1185,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||
final String content;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
content = message.getFileParams().url.toString();
|
||||
content = message.getFileParams().url;
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
|
@ -1198,6 +1222,154 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
});
|
||||
}
|
||||
|
||||
private OmemoVerifiedIceUdpTransportInfo encrypt(final IceUdpTransportInfo element, final XmppAxolotlSession session) throws CryptoFailedException {
|
||||
final OmemoVerifiedIceUdpTransportInfo transportInfo = new OmemoVerifiedIceUdpTransportInfo();
|
||||
transportInfo.setAttributes(element.getAttributes());
|
||||
for (final Element child : element.getChildren()) {
|
||||
if ("fingerprint".equals(child.getName()) && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) {
|
||||
final Element fingerprint = new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION);
|
||||
fingerprint.setAttribute("setup", child.getAttribute("setup"));
|
||||
fingerprint.setAttribute("hash", child.getAttribute("hash"));
|
||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||
final String content = child.getContent();
|
||||
axolotlMessage.encrypt(content);
|
||||
axolotlMessage.addDevice(session, true);
|
||||
fingerprint.addChild(axolotlMessage.toElement());
|
||||
transportInfo.addChild(fingerprint);
|
||||
} else {
|
||||
transportInfo.addChild(child);
|
||||
}
|
||||
}
|
||||
return transportInfo;
|
||||
}
|
||||
|
||||
|
||||
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(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;
|
||||
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 Futures.immediateFuture(
|
||||
new OmemoVerifiedPayload<>(
|
||||
omemoVerification,
|
||||
new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build())
|
||||
));
|
||||
}
|
||||
|
||||
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;
|
||||
try {
|
||||
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
|
||||
} catch (CryptoFailedException e) {
|
||||
return Futures.immediateFailedFuture(e);
|
||||
}
|
||||
omemoVerification.setOrEnsureEqual(decryptedTransport);
|
||||
descriptionTransportBuilder.put(
|
||||
content.getKey(),
|
||||
new RtpContentMap.DescriptionTransport(descriptionTransport.description, decryptedTransport.payload)
|
||||
);
|
||||
}
|
||||
processPostponed();
|
||||
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, ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures) throws CryptoFailedException {
|
||||
final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
|
||||
transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes());
|
||||
final OmemoVerification omemoVerification = new OmemoVerification();
|
||||
for (final Element child : verifiedIceUdpTransportInfo.getChildren()) {
|
||||
if ("fingerprint".equals(child.getName()) && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) {
|
||||
final Element fingerprint = new Element("fingerprint", Namespace.JINGLE_APPS_DTLS);
|
||||
fingerprint.setAttribute("setup", child.getAttribute("setup"));
|
||||
fingerprint.setAttribute("hash", child.getAttribute("hash"));
|
||||
final Element encrypted = child.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
|
||||
final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, from.asBareJid());
|
||||
final XmppAxolotlSession session = getReceivingSession(xmppAxolotlMessage);
|
||||
final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = xmppAxolotlMessage.decrypt(session, getOwnDeviceId());
|
||||
final Integer preKeyId = session.getPreKeyIdAndReset();
|
||||
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());
|
||||
transportInfo.addChild(fingerprint);
|
||||
} else {
|
||||
transportInfo.addChild(child);
|
||||
}
|
||||
}
|
||||
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
|
||||
|
@ -1267,7 +1439,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
} catch (final BrokenSessionException e) {
|
||||
throw e;
|
||||
} catch (final OutdatedSenderException e) {
|
||||
Log.e(Config.LOGTAG,account.getJid().asBareJid()+": "+e.getMessage());
|
||||
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
|
||||
throw e;
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e);
|
||||
|
@ -1318,7 +1490,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": nothing to flush. Not republishing key");
|
||||
}
|
||||
if (trustedOrPreviouslyResponded(session)) {
|
||||
if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) {
|
||||
completeSession(session);
|
||||
}
|
||||
}
|
||||
|
@ -1333,7 +1505,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
final Iterator<XmppAxolotlSession> iterator = postponedSessions.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
final XmppAxolotlSession session = iterator.next();
|
||||
if (trustedOrPreviouslyResponded(session)) {
|
||||
if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) {
|
||||
completeSession(session);
|
||||
}
|
||||
iterator.remove();
|
||||
|
@ -1395,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 {
|
||||
|
@ -1565,4 +1738,36 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class OmemoVerifiedPayload<T> {
|
||||
private final int deviceId;
|
||||
private final String fingerprint;
|
||||
private final T payload;
|
||||
|
||||
private OmemoVerifiedPayload(OmemoVerification omemoVerification, T payload) {
|
||||
this.deviceId = omemoVerification.getDeviceId();
|
||||
this.fingerprint = omemoVerification.getFingerprint();
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
public T getPayload() {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NotVerifiedException extends SecurityException {
|
||||
|
||||
public NotVerifiedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ public class XmppAxolotlMessage {
|
|||
switch (keyElement.getName()) {
|
||||
case KEYTAG:
|
||||
try {
|
||||
Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
|
||||
int recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
|
||||
byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
|
||||
boolean isPreKey = keyElement.getAttributeAsBoolean("prekey");
|
||||
this.keys.add(new XmppAxolotlSession.AxolotlKey(recipientId, key, isPreKey));
|
||||
|
@ -145,7 +145,7 @@ public class XmppAxolotlMessage {
|
|||
return ciphertext != null;
|
||||
}
|
||||
|
||||
void encrypt(String plaintext) throws CryptoFailedException {
|
||||
void encrypt(final String plaintext) throws CryptoFailedException {
|
||||
try {
|
||||
SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||
|
|
|
@ -4,7 +4,8 @@ import android.content.ContentValues;
|
|||
import android.database.Cursor;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -64,7 +65,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
public static final int OPTION_FIXED_USERNAME = 9;
|
||||
private static final String KEY_PGP_SIGNATURE = "pgp_signature";
|
||||
private static final String KEY_PGP_ID = "pgp_id";
|
||||
public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
|
||||
protected final JSONObject keys;
|
||||
private final Roster roster = new Roster(this);
|
||||
private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
|
||||
|
@ -148,7 +148,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
|
||||
public boolean httpUploadAvailable(long filesize) {
|
||||
return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer());
|
||||
return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
|
||||
}
|
||||
|
||||
public boolean httpUploadAvailable() {
|
||||
|
@ -249,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) {
|
||||
|
@ -637,6 +637,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
REGISTRATION_INVALID_TOKEN(true,false),
|
||||
REGISTRATION_PASSWORD_TOO_WEAK(true, false),
|
||||
TLS_ERROR,
|
||||
TLS_ERROR_DOMAIN,
|
||||
INCOMPATIBLE_SERVER,
|
||||
TOR_NOT_AVAILABLE,
|
||||
DOWNGRADE_ATTACK,
|
||||
|
@ -703,6 +704,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
return R.string.account_status_regis_invalid_token;
|
||||
case TLS_ERROR:
|
||||
return R.string.account_status_tls_error;
|
||||
case TLS_ERROR_DOMAIN:
|
||||
return R.string.account_status_tls_error_domain;
|
||||
case INCOMPATIBLE_SERVER:
|
||||
return R.string.account_status_incompatible_server;
|
||||
case TOR_NOT_AVAILABLE:
|
||||
|
|
|
@ -588,8 +588,7 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
|
||||
public RtpCapability.Capability getRtpCapability() {
|
||||
|
||||
return this.rtpCapability;
|
||||
return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
|
||||
}
|
||||
|
||||
public static final class Options {
|
||||
|
|
|
@ -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;
|
||||
|
@ -143,7 +144,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
final String contact = conversation.getJid().getDomain().toEscapedString();
|
||||
final String account = conversation.getAccount().getServer();
|
||||
if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
|
||||
if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
|
||||
return false;
|
||||
}
|
||||
return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -788,7 +802,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
|
||||
String otherBody;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
otherBody = message.getFileParams().url.toString();
|
||||
otherBody = message.getFileParams().url;
|
||||
} else {
|
||||
otherBody = message.body;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -22,7 +22,7 @@ public interface Transferable {
|
|||
|
||||
int getStatus();
|
||||
|
||||
long getFileSize();
|
||||
Long getFileSize();
|
||||
|
||||
int getProgress();
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@ public class TransferablePlaceholder implements Transferable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize() {
|
||||
return 0;
|
||||
public Long getFileSize() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -12,6 +12,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import eu.siacs.conversations.BuildConfig;
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
|
@ -65,7 +66,6 @@ public abstract class AbstractGenerator {
|
|||
Namespace.JINGLE_MESSAGE
|
||||
};
|
||||
protected XmppConnectionService mXmppConnectionService;
|
||||
private String mVersion = null;
|
||||
|
||||
AbstractGenerator(XmppConnectionService service) {
|
||||
this.mXmppConnectionService = service;
|
||||
|
@ -77,18 +77,11 @@ public abstract class AbstractGenerator {
|
|||
}
|
||||
|
||||
String getIdentityVersion() {
|
||||
if (mVersion == null) {
|
||||
this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService);
|
||||
}
|
||||
return this.mVersion;
|
||||
return BuildConfig.VERSION_NAME;
|
||||
}
|
||||
|
||||
String getIdentityName() {
|
||||
return mXmppConnectionService.getString(R.string.app_name);
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return mXmppConnectionService.getString(R.string.app_name) + '/' + getIdentityVersion();
|
||||
return BuildConfig.APP_NAME;
|
||||
}
|
||||
|
||||
String getIdentityType() {
|
||||
|
|
|
@ -408,20 +408,6 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket requestP1S3Slot(Jid host, String md5) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
packet.setTo(host);
|
||||
packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5", md5);
|
||||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket requestP1S3Url(Jid host, String fileId) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
|
||||
packet.setTo(host);
|
||||
packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("fileid", fileId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
private static String convertFilename(String name) {
|
||||
int pos = name.indexOf('.');
|
||||
if (pos != -1) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.siacs.conversations.generator;
|
||||
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
@ -14,7 +13,6 @@ import eu.siacs.conversations.entities.Account;
|
|||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Conversational;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
@ -103,18 +101,9 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
MessagePacket packet = preparePacket(message);
|
||||
String content;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
Message.FileParams fileParams = message.getFileParams();
|
||||
final URL url = fileParams.url;
|
||||
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
|
||||
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
|
||||
final String file = url.getFile();
|
||||
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
|
||||
x.setAttribute("fileid", url.getHost());
|
||||
return packet;
|
||||
} else {
|
||||
content = url.toString();
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
|
||||
}
|
||||
final Message.FileParams fileParams = message.getFileParams();
|
||||
content = fileParams.url;
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
|
@ -126,16 +115,9 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
MessagePacket packet = preparePacket(message);
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
Message.FileParams fileParams = message.getFileParams();
|
||||
final URL url = fileParams.url;
|
||||
if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
|
||||
Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
|
||||
final String file = url.getFile();
|
||||
x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
|
||||
x.setAttribute("fileid", url.getHost());
|
||||
} else {
|
||||
packet.setBody(url.toString());
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString());
|
||||
}
|
||||
final String url = fileParams.url;
|
||||
packet.setBody(url);
|
||||
packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
|
||||
} else {
|
||||
if (Config.supportUnencrypted()) {
|
||||
packet.setBody(PGP_FALLBACK_MESSAGE);
|
||||
|
@ -225,7 +207,7 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) {
|
||||
public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) {
|
||||
final MessagePacket receivedPacket = new MessagePacket();
|
||||
receivedPacket.setType(type);
|
||||
receivedPacket.setTo(from);
|
||||
|
|
|
@ -25,12 +25,19 @@ public class PresenceGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
public PresencePacket requestPresenceUpdatesFrom(Contact contact) {
|
||||
public PresencePacket requestPresenceUpdatesFrom(final Contact contact) {
|
||||
return requestPresenceUpdatesFrom(contact, null);
|
||||
}
|
||||
|
||||
public PresencePacket requestPresenceUpdatesFrom(final Contact contact, final String preAuth) {
|
||||
PresencePacket packet = subscription("subscribe", contact);
|
||||
String displayName = contact.getAccount().getDisplayName();
|
||||
if (!TextUtils.isEmpty(displayName)) {
|
||||
packet.addChild("nick", Namespace.NICK).setContent(displayName);
|
||||
}
|
||||
if (preAuth != null) {
|
||||
packet.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public final class AesGcmURL {
|
||||
|
||||
/**
|
||||
* This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors
|
||||
*/
|
||||
public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}");
|
||||
|
||||
public static final String PROTOCOL_NAME = "aesgcm";
|
||||
|
||||
private AesGcmURL() {
|
||||
|
||||
}
|
||||
|
||||
public static String toAesGcmUrl(HttpUrl url) {
|
||||
if (url.isHttps()) {
|
||||
return PROTOCOL_NAME + url.toString().substring(5);
|
||||
} else {
|
||||
return url.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpUrl of(final String url) {
|
||||
final int end = url.indexOf("://");
|
||||
if (end < 0) {
|
||||
throw new IllegalArgumentException("Scheme not found");
|
||||
}
|
||||
final String protocol = url.substring(0, end);
|
||||
if (PROTOCOL_NAME.equals(protocol)) {
|
||||
return HttpUrl.get("https" + url.substring(PROTOCOL_NAME.length()));
|
||||
} else {
|
||||
return HttpUrl.get(url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class AesGcmURLStreamHandler extends URLStreamHandler {
|
||||
|
||||
/**
|
||||
* This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors
|
||||
*/
|
||||
public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}");
|
||||
|
||||
public static final String PROTOCOL_NAME = "aesgcm";
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new URL("https"+url.toString().substring(url.getProtocol().length())).openConnection();
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import java.net.URLStreamHandler;
|
||||
import java.net.URLStreamHandlerFactory;
|
||||
|
||||
public class CustomURLStreamHandlerFactory implements URLStreamHandlerFactory {
|
||||
|
||||
@Override
|
||||
public URLStreamHandler createURLStreamHandler(String protocol) {
|
||||
if (AesGcmURLStreamHandler.PROTOCOL_NAME.equals(protocol)) {
|
||||
return new AesGcmURLStreamHandler();
|
||||
} else if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(protocol)) {
|
||||
return new P1S3UrlStreamHandler();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +1,81 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import eu.siacs.conversations.BuildConfig;
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.TLSSocketFactory;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public class HttpConnectionManager extends AbstractConnectionManager {
|
||||
|
||||
private final List<HttpDownloadConnection> downloadConnections = new ArrayList<>();
|
||||
private final List<HttpUploadConnection> uploadConnections = new ArrayList<>();
|
||||
|
||||
public static final Executor EXECUTOR = Executors.newFixedThreadPool(4);
|
||||
|
||||
public static final OkHttpClient OK_HTTP_CLIENT;
|
||||
|
||||
static {
|
||||
OK_HTTP_CLIENT = new OkHttpClient.Builder()
|
||||
.addInterceptor(chain -> {
|
||||
final Request original = chain.request();
|
||||
final Request modified = original.newBuilder()
|
||||
.header("User-Agent", getUserAgent())
|
||||
.build();
|
||||
return chain.proceed(modified);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
public static String getUserAgent() {
|
||||
return String.format("%s/%s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME);
|
||||
}
|
||||
|
||||
public HttpConnectionManager(XmppConnectionService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
public static Proxy getProxy() throws IOException {
|
||||
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050));
|
||||
public static Proxy getProxy() {
|
||||
final InetAddress localhost;
|
||||
try {
|
||||
localhost = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
|
||||
} catch (final UnknownHostException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(localhost, 9050));
|
||||
} else {
|
||||
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(localhost, 8118));
|
||||
}
|
||||
}
|
||||
|
||||
public void createNewDownloadConnection(Message message) {
|
||||
|
@ -45,7 +84,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
|
||||
public void createNewDownloadConnection(final Message message, boolean interactive) {
|
||||
synchronized (this.downloadConnections) {
|
||||
for(HttpDownloadConnection connection : this.downloadConnections) {
|
||||
for (HttpDownloadConnection connection : this.downloadConnections) {
|
||||
if (connection.getMessage() == message) {
|
||||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": download already in progress");
|
||||
return;
|
||||
|
@ -71,15 +110,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean checkConnection(Message message) {
|
||||
final Account account = message.getConversation().getAccount();
|
||||
final URL url = message.getFileParams().url;
|
||||
if (url.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) && account.getStatus() != Account.State.ONLINE) {
|
||||
return false;
|
||||
}
|
||||
return mXmppConnectionService.hasInternetConnection();
|
||||
}
|
||||
|
||||
void finishConnection(HttpDownloadConnection connection) {
|
||||
synchronized (this.downloadConnections) {
|
||||
this.downloadConnections.remove(connection);
|
||||
|
@ -92,9 +122,25 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) {
|
||||
OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) {
|
||||
return buildHttpClient(url, account, 30, interactive);
|
||||
}
|
||||
|
||||
OkHttpClient buildHttpClient(final HttpUrl url, final Account account, int readTimeout, boolean interactive) {
|
||||
final String slotHostname = url.host();
|
||||
final boolean onionSlot = slotHostname.endsWith(".onion");
|
||||
final OkHttpClient.Builder builder = OK_HTTP_CLIENT.newBuilder();
|
||||
builder.writeTimeout(30, TimeUnit.SECONDS);
|
||||
builder.readTimeout(readTimeout, TimeUnit.SECONDS);
|
||||
setupTrustManager(builder, interactive);
|
||||
if (mXmppConnectionService.useTorToConnect() || account.isOnion() || onionSlot) {
|
||||
builder.proxy(HttpConnectionManager.getProxy()).build();
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) {
|
||||
final X509TrustManager trustManager;
|
||||
final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
|
||||
if (interactive) {
|
||||
trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
|
||||
} else {
|
||||
|
@ -102,9 +148,27 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
}
|
||||
try {
|
||||
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
||||
connection.setSSLSocketFactory(sf);
|
||||
connection.setHostnameVerifier(hostnameVerifier);
|
||||
builder.sslSocketFactory(sf, trustManager);
|
||||
builder.hostnameVerifier(new StrictHostnameVerifier());
|
||||
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static InputStream open(final String url, final boolean tor) throws IOException {
|
||||
return open(HttpUrl.get(url), tor);
|
||||
}
|
||||
|
||||
public static InputStream open(final HttpUrl httpUrl, final boolean tor) throws IOException {
|
||||
final OkHttpClient.Builder builder = OK_HTTP_CLIENT.newBuilder();
|
||||
if (tor) {
|
||||
builder.proxy(HttpConnectionManager.getProxy()).build();
|
||||
}
|
||||
final OkHttpClient client = builder.build();
|
||||
final Request request = new Request.Builder().get().url(httpUrl).build();
|
||||
final ResponseBody body = client.newCall(request).execute().body();
|
||||
if (body == null) {
|
||||
throw new IOException("No response body found");
|
||||
}
|
||||
return body.byteStream();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
|
@ -33,28 +27,30 @@ import eu.siacs.conversations.services.XmppConnectionService;
|
|||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.FileWriterException;
|
||||
import eu.siacs.conversations.utils.MimeUtils;
|
||||
import eu.siacs.conversations.utils.WakeLockHelper;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
|
||||
|
||||
public class HttpDownloadConnection implements Transferable {
|
||||
|
||||
private final Message message;
|
||||
private final boolean mUseTor;
|
||||
private final HttpConnectionManager mHttpConnectionManager;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
private URL mUrl;
|
||||
private HttpUrl mUrl;
|
||||
private DownloadableFile file;
|
||||
private int mStatus = Transferable.STATUS_UNKNOWN;
|
||||
private boolean acceptedAutomatically = false;
|
||||
private int mProgress = 0;
|
||||
private boolean canceled = false;
|
||||
private Method method = Method.HTTP_UPLOAD;
|
||||
private Call mostRecentCall;
|
||||
|
||||
HttpDownloadConnection(Message message, HttpConnectionManager manager) {
|
||||
this.message = message;
|
||||
this.mHttpConnectionManager = manager;
|
||||
this.mXmppConnectionService = manager.getXmppConnectionService();
|
||||
this.mUseTor = mXmppConnectionService.useTorToConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,13 +82,13 @@ public class HttpDownloadConnection implements Transferable {
|
|||
try {
|
||||
final Message.FileParams fileParams = message.getFileParams();
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
mUrl = CryptoHelper.toHttpsUrl(fileParams.url);
|
||||
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
|
||||
mUrl = fileParams.url;
|
||||
mUrl = AesGcmURL.of(fileParams.url);
|
||||
} else if (message.isOOb() && fileParams.url != null && fileParams.size != null) {
|
||||
mUrl = AesGcmURL.of(fileParams.url);
|
||||
} else {
|
||||
mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0]));
|
||||
mUrl = AesGcmURL.of(message.getBody().split("\n")[0]);
|
||||
}
|
||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
|
||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
|
||||
if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
|
||||
this.message.setEncryption(Message.ENCRYPTION_PGP);
|
||||
} else if (message.getEncryption() != Message.ENCRYPTION_OTR
|
||||
|
@ -109,22 +105,23 @@ public class HttpDownloadConnection implements Transferable {
|
|||
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
|
||||
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
||||
}
|
||||
method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD;
|
||||
long knownFileSize = message.getFileParams().size;
|
||||
if (knownFileSize > 0 && interactive && method != Method.P1_S3) {
|
||||
//TODO add auth tag size to knownFileSize
|
||||
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 {
|
||||
checkFileSize(interactive);
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
} catch (final IllegalArgumentException e) {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFile() {
|
||||
final String reference = mUrl.getRef();
|
||||
if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
|
||||
final String reference = mUrl.fragment();
|
||||
if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
|
||||
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
|
||||
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
|
||||
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
|
||||
|
@ -133,17 +130,20 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
}
|
||||
|
||||
private void download(boolean interactive) {
|
||||
new Thread(new FileDownloader(interactive)).start();
|
||||
private void download(final boolean interactive) {
|
||||
EXECUTOR.execute(new FileDownloader(interactive));
|
||||
}
|
||||
|
||||
private void checkFileSize(boolean interactive) {
|
||||
new Thread(new FileSizeChecker(interactive)).start();
|
||||
private void checkFileSize(final boolean interactive) {
|
||||
EXECUTOR.execute(new FileSizeChecker(interactive));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
this.canceled = true;
|
||||
final Call call = this.mostRecentCall;
|
||||
if (call != null && !call.isCanceled()) {
|
||||
call.cancel();
|
||||
}
|
||||
mHttpConnectionManager.finishConnection(this);
|
||||
message.setTransferable(null);
|
||||
if (message.isFileOrImage()) {
|
||||
|
@ -207,14 +207,19 @@ public class HttpDownloadConnection implements Transferable {
|
|||
mHttpConnectionManager.updateConversationUi(true);
|
||||
}
|
||||
|
||||
private void showToastForException(Exception e) {
|
||||
private void showToastForException(final Exception e) {
|
||||
final Call call = mostRecentCall;
|
||||
final boolean cancelled = call != null && call.isCanceled();
|
||||
if (e == null || cancelled) {
|
||||
return;
|
||||
}
|
||||
if (e instanceof java.net.UnknownHostException) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
|
||||
} else if (e instanceof java.net.ConnectException) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
|
||||
} else if (e instanceof FileWriterException) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
|
||||
} else if (!(e instanceof CancellationException)) {
|
||||
} else {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
|
||||
}
|
||||
}
|
||||
|
@ -230,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,41 +263,13 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
|
||||
retrieveUrl();
|
||||
} else {
|
||||
check();
|
||||
}
|
||||
check();
|
||||
}
|
||||
|
||||
private void retrieveUrl() {
|
||||
changeStatus(STATUS_CHECKING);
|
||||
final Account account = message.getConversation().getAccount();
|
||||
IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(account.getDomain(), mUrl.getHost());
|
||||
mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
String download = packet.query().getAttribute("download");
|
||||
if (download != null) {
|
||||
try {
|
||||
mUrl = new URL(download);
|
||||
check();
|
||||
return;
|
||||
} catch (MalformedURLException e) {
|
||||
//fallthrough
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(Config.LOGTAG, "unable to retrieve actual download url");
|
||||
retrieveFailed(null);
|
||||
});
|
||||
}
|
||||
|
||||
private void retrieveFailed(@Nullable Exception e) {
|
||||
private void retrieveFailed(@Nullable final Exception e) {
|
||||
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
|
||||
if (interactive) {
|
||||
if (e != null) {
|
||||
showToastForException(e);
|
||||
}
|
||||
showToastForException(e);
|
||||
} else {
|
||||
HttpDownloadConnection.this.acceptedAutomatically = false;
|
||||
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
|
||||
|
@ -304,7 +281,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
long size;
|
||||
try {
|
||||
size = retrieveFileSize();
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
|
||||
retrieveFailed(e);
|
||||
return;
|
||||
|
@ -328,46 +305,24 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
|
||||
private long retrieveFileSize() throws IOException {
|
||||
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
|
||||
changeStatus(STATUS_CHECKING);
|
||||
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||
mUrl,
|
||||
message.getConversation().getAccount(),
|
||||
interactive
|
||||
);
|
||||
final Request request = new Request.Builder()
|
||||
.url(URL.stripFragment(mUrl))
|
||||
.head()
|
||||
.build();
|
||||
mostRecentCall = client.newCall(request);
|
||||
try {
|
||||
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
|
||||
changeStatus(STATUS_CHECKING);
|
||||
HttpURLConnection connection;
|
||||
final String hostname = mUrl.getHost();
|
||||
final boolean onion = hostname != null && hostname.endsWith(".onion");
|
||||
if (mUseTor || message.getConversation().getAccount().isOnion() || onion) {
|
||||
connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
|
||||
} else {
|
||||
connection = (HttpURLConnection) mUrl.openConnection();
|
||||
}
|
||||
if (method == Method.P1_S3) {
|
||||
connection.setRequestMethod("GET");
|
||||
connection.addRequestProperty("Range", "bytes=0-0");
|
||||
} else {
|
||||
connection.setRequestMethod("HEAD");
|
||||
}
|
||||
connection.setUseCaches(false);
|
||||
Log.d(Config.LOGTAG, "url: " + connection.getURL().toString());
|
||||
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
|
||||
if (connection instanceof HttpsURLConnection) {
|
||||
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
|
||||
}
|
||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.connect();
|
||||
String contentLength;
|
||||
if (method == Method.P1_S3) {
|
||||
String contentRange = connection.getHeaderField("Content-Range");
|
||||
String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/");
|
||||
if (contentRangeParts.length != 2) {
|
||||
contentLength = null;
|
||||
} else {
|
||||
contentLength = contentRangeParts[1];
|
||||
}
|
||||
} else {
|
||||
contentLength = connection.getHeaderField("Content-Length");
|
||||
}
|
||||
final String contentType = connection.getContentType();
|
||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
|
||||
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());
|
||||
if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
|
||||
final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
|
||||
if (fileExtension != null) {
|
||||
|
@ -376,11 +331,14 @@ public class HttpDownloadConnection implements Transferable {
|
|||
setupFile();
|
||||
}
|
||||
}
|
||||
connection.disconnect();
|
||||
if (contentLength == null) {
|
||||
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,8 +353,6 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
private final boolean interactive;
|
||||
|
||||
private OutputStream os;
|
||||
|
||||
public FileDownloader(boolean interactive) {
|
||||
this.interactive = interactive;
|
||||
}
|
||||
|
@ -409,9 +365,10 @@ public class HttpDownloadConnection implements Transferable {
|
|||
decryptIfNeeded();
|
||||
updateImageBounds();
|
||||
finish();
|
||||
} catch (SSLHandshakeException e) {
|
||||
} catch (final SSLHandshakeException e) {
|
||||
changeStatus(STATUS_OFFER);
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": unable to download file", e);
|
||||
if (interactive) {
|
||||
showToastForException(e);
|
||||
} else {
|
||||
|
@ -423,108 +380,84 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
|
||||
private void download() throws Exception {
|
||||
InputStream is = null;
|
||||
HttpURLConnection connection = null;
|
||||
PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_" + message.getUuid());
|
||||
try {
|
||||
wakeLock.acquire();
|
||||
if (mUseTor || message.getConversation().getAccount().isOnion()) {
|
||||
connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
|
||||
} else {
|
||||
connection = (HttpURLConnection) mUrl.openConnection();
|
||||
}
|
||||
if (connection instanceof HttpsURLConnection) {
|
||||
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
|
||||
}
|
||||
connection.setUseCaches(false);
|
||||
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
|
||||
final long expected = file.getExpectedSize();
|
||||
final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
|
||||
long resumeSize = 0;
|
||||
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||
mUrl,
|
||||
message.getConversation().getAccount(),
|
||||
interactive
|
||||
);
|
||||
|
||||
if (tryResume) {
|
||||
resumeSize = file.getSize();
|
||||
Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
|
||||
connection.setRequestProperty("Range", "bytes=" + resumeSize + "-");
|
||||
final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl));
|
||||
|
||||
final long expected = file.getExpectedSize();
|
||||
final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
|
||||
final long resumeSize;
|
||||
if (tryResume) {
|
||||
resumeSize = file.getSize();
|
||||
Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
|
||||
requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
|
||||
} else {
|
||||
resumeSize = 0;
|
||||
}
|
||||
final Request request = requestBuilder.build();
|
||||
mostRecentCall = client.newCall(request);
|
||||
final Response response = mostRecentCall.execute();
|
||||
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 {
|
||||
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 + ")");
|
||||
}
|
||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.connect();
|
||||
is = new BufferedInputStream(connection.getInputStream());
|
||||
final String contentRange = connection.getHeaderField("Content-Range");
|
||||
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
|
||||
long transmitted = 0;
|
||||
if (tryResume && serverResumed) {
|
||||
Log.d(Config.LOGTAG, "server resumed");
|
||||
transmitted = file.getSize();
|
||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||
os = AbstractConnectionManager.createOutputStream(file, true, false);
|
||||
if (os == null) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
} else {
|
||||
long reportedContentLengthOnGet;
|
||||
try {
|
||||
reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length"));
|
||||
} catch (NumberFormatException | NullPointerException e) {
|
||||
reportedContentLengthOnGet = 0;
|
||||
}
|
||||
if (expected != reportedContentLengthOnGet) {
|
||||
Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
||||
}
|
||||
file.getParentFile().mkdirs();
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
os = AbstractConnectionManager.createOutputStream(file, false, false);
|
||||
}
|
||||
int count;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((count = is.read(buffer)) != -1) {
|
||||
transmitted += count;
|
||||
try {
|
||||
os.write(buffer, 0, count);
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||
if (canceled) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
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 {
|
||||
os.flush();
|
||||
outputStream.write(buffer, 0, count);
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
} catch (CancellationException | IOException e) {
|
||||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": http download failed", e);
|
||||
throw e;
|
||||
} finally {
|
||||
FileBackend.close(os);
|
||||
FileBackend.close(is);
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
WakeLockHelper.release(wakeLock);
|
||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
private void updateImageBounds() {
|
||||
final boolean privateMessage = message.isPrivateMessage();
|
||||
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
|
||||
final URL url;
|
||||
final String ref = mUrl.getRef();
|
||||
if (method == Method.P1_S3) {
|
||||
url = message.getFileParams().url;
|
||||
} else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
|
||||
url = CryptoHelper.toAesGcmUrl(mUrl);
|
||||
final String url;
|
||||
final String ref = mUrl.fragment();
|
||||
if (ref != null && AesGcmURL.IV_KEY.matcher(ref).matches()) {
|
||||
url = AesGcmURL.toAesGcmUrl(mUrl);
|
||||
} else {
|
||||
url = mUrl;
|
||||
url = mUrl.toString();
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,248 +1,211 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.Checksum;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.WakeLockHelper;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class HttpUploadConnection implements Transferable {
|
||||
public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
|
||||
|
||||
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
|
||||
"Authorization",
|
||||
"Cookie",
|
||||
"Expires"
|
||||
);
|
||||
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
|
||||
"Authorization",
|
||||
"Cookie",
|
||||
"Expires"
|
||||
);
|
||||
|
||||
private final HttpConnectionManager mHttpConnectionManager;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
private final SlotRequester mSlotRequester;
|
||||
private final Method method;
|
||||
private final boolean mUseTor;
|
||||
private boolean cancelled = false;
|
||||
private boolean delayed = false;
|
||||
private DownloadableFile file;
|
||||
private final Message message;
|
||||
private String mime;
|
||||
private SlotRequester.Slot slot;
|
||||
private byte[] key = null;
|
||||
private final HttpConnectionManager mHttpConnectionManager;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
private final Method method;
|
||||
private boolean delayed = false;
|
||||
private DownloadableFile file;
|
||||
private final Message message;
|
||||
private SlotRequester.Slot slot;
|
||||
private byte[] key = null;
|
||||
|
||||
private long transmitted = 0;
|
||||
private long transmitted = 0;
|
||||
private Call mostRecentCall;
|
||||
private ListenableFuture<SlotRequester.Slot> slotFuture;
|
||||
|
||||
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
|
||||
this.message = message;
|
||||
this.method = method;
|
||||
this.mHttpConnectionManager = httpConnectionManager;
|
||||
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
|
||||
this.mSlotRequester = new SlotRequester(this.mXmppConnectionService);
|
||||
this.mUseTor = mXmppConnectionService.useTorToConnect();
|
||||
}
|
||||
public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
|
||||
this.message = message;
|
||||
this.method = method;
|
||||
this.mHttpConnectionManager = httpConnectionManager;
|
||||
this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start() {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean start() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStatus() {
|
||||
return STATUS_UPLOADING;
|
||||
}
|
||||
@Override
|
||||
public int getStatus() {
|
||||
return STATUS_UPLOADING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize() {
|
||||
return file == null ? 0 : file.getExpectedSize();
|
||||
}
|
||||
@Override
|
||||
public Long getFileSize() {
|
||||
return file == null ? null : file.getExpectedSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProgress() {
|
||||
if (file == null) {
|
||||
return 0;
|
||||
}
|
||||
return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
|
||||
}
|
||||
@Override
|
||||
public int getProgress() {
|
||||
if (file == null) {
|
||||
return 0;
|
||||
}
|
||||
return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
this.cancelled = true;
|
||||
}
|
||||
@Override
|
||||
public void cancel() {
|
||||
final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
|
||||
if (slotFuture != null && !slotFuture.isDone()) {
|
||||
if (slotFuture.cancel(true)) {
|
||||
Log.d(Config.LOGTAG,"cancelled slot requester");
|
||||
}
|
||||
}
|
||||
final Call call = this.mostRecentCall;
|
||||
if (call != null && !call.isCanceled()) {
|
||||
call.cancel();
|
||||
Log.d(Config.LOGTAG,"cancelled HTTP request");
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(String errorMessage) {
|
||||
finish();
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
|
||||
}
|
||||
private void fail(String errorMessage) {
|
||||
finish();
|
||||
final Call call = this.mostRecentCall;
|
||||
final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
|
||||
final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled());
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
mHttpConnectionManager.finishUploadConnection(this);
|
||||
message.setTransferable(null);
|
||||
}
|
||||
private void finish() {
|
||||
mHttpConnectionManager.finishUploadConnection(this);
|
||||
message.setTransferable(null);
|
||||
}
|
||||
|
||||
public void init(boolean delay) {
|
||||
final Account account = message.getConversation().getAccount();
|
||||
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
|
||||
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
||||
this.mime = "application/pgp-encrypted";
|
||||
} else {
|
||||
this.mime = this.file.getMimeType();
|
||||
}
|
||||
final long originalFileSize = file.getSize();
|
||||
this.delayed = delay;
|
||||
if (Config.ENCRYPT_ON_HTTP_UPLOADED
|
||||
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|
||||
|| message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||
this.key = new byte[44];
|
||||
mXmppConnectionService.getRNG().nextBytes(this.key);
|
||||
this.file.setKeyAndIv(this.key);
|
||||
}
|
||||
public void init(boolean delay) {
|
||||
final Account account = message.getConversation().getAccount();
|
||||
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
|
||||
final String mime;
|
||||
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
||||
mime = "application/pgp-encrypted";
|
||||
} else {
|
||||
mime = this.file.getMimeType();
|
||||
}
|
||||
final long originalFileSize = file.getSize();
|
||||
this.delayed = delay;
|
||||
if (Config.ENCRYPT_ON_HTTP_UPLOADED
|
||||
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|
||||
|| message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||
this.key = new byte[44];
|
||||
mXmppConnectionService.getRNG().nextBytes(this.key);
|
||||
this.file.setKeyAndIv(this.key);
|
||||
}
|
||||
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
|
||||
message.resetFileParams();
|
||||
this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, mime);
|
||||
Futures.addCallback(this.slotFuture, new FutureCallback<SlotRequester.Slot>() {
|
||||
@Override
|
||||
public void onSuccess(@NullableDecl SlotRequester.Slot result) {
|
||||
HttpUploadConnection.this.slot = result;
|
||||
try {
|
||||
HttpUploadConnection.this.upload();
|
||||
} catch (final Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
final String md5;
|
||||
@Override
|
||||
public void onFailure(@NotNull final Throwable throwable) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable);
|
||||
fail(throwable.getMessage());
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
message.setTransferable(this);
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
|
||||
}
|
||||
|
||||
if (method == Method.P1_S3) {
|
||||
try {
|
||||
md5 = Checksum.md5(AbstractConnectionManager.upgrade(file, new FileInputStream(file)));
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to calculate md5()", e);
|
||||
fail(e.getMessage());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
md5 = null;
|
||||
}
|
||||
private void upload() {
|
||||
final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
|
||||
slot.put,
|
||||
message.getConversation().getAccount(),
|
||||
0,
|
||||
true
|
||||
);
|
||||
final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this);
|
||||
final Request request = new Request.Builder()
|
||||
.url(slot.put)
|
||||
.put(requestBody)
|
||||
.headers(slot.headers)
|
||||
.build();
|
||||
Log.d(Config.LOGTAG, "uploading file to " + slot.put);
|
||||
this.mostRecentCall = client.newCall(request);
|
||||
this.mostRecentCall.enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(@NotNull Call call, IOException e) {
|
||||
Log.d(Config.LOGTAG, "http upload failed", e);
|
||||
fail(e.getMessage());
|
||||
}
|
||||
|
||||
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
|
||||
message.resetFileParams();
|
||||
this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() {
|
||||
@Override
|
||||
public void success(SlotRequester.Slot slot) {
|
||||
if (!cancelled) {
|
||||
HttpUploadConnection.this.slot = slot;
|
||||
new Thread(HttpUploadConnection.this::upload).start();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onResponse(@NotNull Call call, @NotNull Response response) {
|
||||
final int code = response.code();
|
||||
if (code == 200 || code == 201) {
|
||||
Log.d(Config.LOGTAG, "finished uploading file");
|
||||
final String get;
|
||||
if (key != null) {
|
||||
get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
|
||||
} else {
|
||||
get = slot.get.toString();
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
finish();
|
||||
if (!message.isPrivateMessage()) {
|
||||
message.setCounterpart(message.getConversation().getJid().asBareJid());
|
||||
}
|
||||
mXmppConnectionService.resendMessage(message, delayed);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "http upload failed because response code was " + code);
|
||||
fail("http upload failed because response code was " + code);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure(String message) {
|
||||
fail(message);
|
||||
}
|
||||
});
|
||||
message.setTransferable(this);
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
|
||||
}
|
||||
public Message getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
private void upload() {
|
||||
OutputStream os = null;
|
||||
InputStream fileInputStream = null;
|
||||
HttpURLConnection connection = null;
|
||||
PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid());
|
||||
try {
|
||||
fileInputStream = new FileInputStream(file);
|
||||
final String slotHostname = slot.getPutUrl().getHost();
|
||||
final boolean onionSlot = slotHostname != null && slotHostname.endsWith(".onion");
|
||||
final int expectedFileSize = (int) file.getExpectedSize();
|
||||
final int readTimeout = (expectedFileSize / 2048) + Config.SOCKET_TIMEOUT; //assuming a minimum transfer speed of 16kbit/s
|
||||
wakeLock.acquire(readTimeout);
|
||||
Log.d(Config.LOGTAG, "uploading to " + slot.getPutUrl().toString()+ " w/ read timeout of "+readTimeout+"s");
|
||||
|
||||
if (mUseTor || message.getConversation().getAccount().isOnion() || onionSlot) {
|
||||
connection = (HttpURLConnection) slot.getPutUrl().openConnection(HttpConnectionManager.getProxy());
|
||||
} else {
|
||||
connection = (HttpURLConnection) slot.getPutUrl().openConnection();
|
||||
}
|
||||
if (connection instanceof HttpsURLConnection) {
|
||||
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true);
|
||||
}
|
||||
connection.setUseCaches(false);
|
||||
connection.setRequestMethod("PUT");
|
||||
connection.setFixedLengthStreamingMode(expectedFileSize);
|
||||
connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getUserAgent());
|
||||
if(slot.getHeaders() != null) {
|
||||
for(HashMap.Entry<String,String> entry : slot.getHeaders().entrySet()) {
|
||||
connection.setRequestProperty(entry.getKey(),entry.getValue());
|
||||
}
|
||||
}
|
||||
connection.setDoOutput(true);
|
||||
connection.setDoInput(true);
|
||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(readTimeout * 1000);
|
||||
connection.connect();
|
||||
final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
|
||||
os = connection.getOutputStream();
|
||||
transmitted = 0;
|
||||
int count;
|
||||
byte[] buffer = new byte[4096];
|
||||
while (((count = innerInputStream.read(buffer)) != -1) && !cancelled) {
|
||||
transmitted += count;
|
||||
os.write(buffer, 0, count);
|
||||
mHttpConnectionManager.updateConversationUi(false);
|
||||
}
|
||||
os.flush();
|
||||
os.close();
|
||||
int code = connection.getResponseCode();
|
||||
InputStream is = connection.getErrorStream();
|
||||
if (is != null) {
|
||||
try (Scanner scanner = new Scanner(is)) {
|
||||
scanner.useDelimiter("\\Z");
|
||||
Log.d(Config.LOGTAG, "body: " + scanner.next());
|
||||
}
|
||||
}
|
||||
if (code == 200 || code == 201) {
|
||||
Log.d(Config.LOGTAG, "finished uploading file");
|
||||
final URL get;
|
||||
if (key != null) {
|
||||
if (method == Method.P1_S3) {
|
||||
get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key));
|
||||
} else {
|
||||
get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key)));
|
||||
}
|
||||
} else {
|
||||
get = slot.getGetUrl();
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
finish();
|
||||
if (!message.isPrivateMessage()) {
|
||||
message.setCounterpart(message.getConversation().getJid().asBareJid());
|
||||
}
|
||||
mXmppConnectionService.resendMessage(message, delayed);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
|
||||
fail("http upload failed because response code was "+code);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
|
||||
fail(e.getMessage());
|
||||
} finally {
|
||||
FileBackend.close(fileInputStream);
|
||||
FileBackend.close(os);
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
WakeLockHelper.release(wakeLock);
|
||||
}
|
||||
}
|
||||
|
||||
public Message getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onProgress(final long progress) {
|
||||
this.transmitted = progress;
|
||||
mHttpConnectionManager.updateConversationUi(false);
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ import eu.siacs.conversations.entities.Account;
|
|||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
|
||||
public enum Method {
|
||||
P1_S3, HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
|
||||
HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
|
||||
|
||||
public static Method determine(Account account) {
|
||||
XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
|
||||
|
@ -44,8 +44,6 @@ public enum Method {
|
|||
return HTTP_UPLOAD_LEGACY;
|
||||
} else if (features.httpUpload(0)) {
|
||||
return HTTP_UPLOAD;
|
||||
} else if (features.p1S3FileTransfer()) {
|
||||
return P1_S3;
|
||||
} else {
|
||||
return HTTP_UPLOAD;
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018, Daniel Gultsch All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation and/or
|
||||
* other materials provided with the distribution.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package eu.siacs.conversations.http;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
|
||||
public class P1S3UrlStreamHandler extends URLStreamHandler {
|
||||
|
||||
public static final String PROTOCOL_NAME = "p1s3";
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) {
|
||||
throw new IllegalStateException("Unable to open connection with stub protocol");
|
||||
}
|
||||
|
||||
public static URL of(String fileId, String filename) throws MalformedURLException {
|
||||
if (fileId == null || filename == null) {
|
||||
throw new MalformedURLException("Paramaters must not be null");
|
||||
}
|
||||
return new URL(PROTOCOL_NAME+"://" + fileId + "/" + filename);
|
||||
}
|
||||
|
||||
public static URL of(Element x) {
|
||||
try {
|
||||
return of(x.getAttribute("fileid"),x.getAttribute("name"));
|
||||
} catch (MalformedURLException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,162 +29,126 @@
|
|||
|
||||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.parser.IqParser;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.IqResponseException;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class SlotRequester {
|
||||
|
||||
private final XmppConnectionService service;
|
||||
private final XmppConnectionService service;
|
||||
|
||||
public SlotRequester(XmppConnectionService service) {
|
||||
this.service = service;
|
||||
}
|
||||
public SlotRequester(XmppConnectionService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public void request(Method method, Account account, DownloadableFile file, String mime, String md5, OnSlotRequested callback) {
|
||||
if (method == Method.HTTP_UPLOAD) {
|
||||
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
|
||||
requestHttpUpload(account, host, file, mime, callback);
|
||||
} else if (method == Method.HTTP_UPLOAD_LEGACY) {
|
||||
Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
|
||||
requestHttpUploadLegacy(account, host, file, mime, callback);
|
||||
} else {
|
||||
requestP1S3(account, account.getDomain(), file.getName(), md5, callback);
|
||||
}
|
||||
}
|
||||
public ListenableFuture<Slot> request(Method method, Account account, DownloadableFile file, String mime) {
|
||||
if (method == Method.HTTP_UPLOAD_LEGACY) {
|
||||
final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
|
||||
return requestHttpUploadLegacy(account, host, file, mime);
|
||||
} else {
|
||||
final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
|
||||
return requestHttpUpload(account, host, file, mime);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) {
|
||||
IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
|
||||
service.sendIqPacket(account, request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
|
||||
if (slotElement != null) {
|
||||
try {
|
||||
final String putUrl = slotElement.findChildContent("put");
|
||||
final String getUrl = slotElement.findChildContent("get");
|
||||
if (getUrl != null && putUrl != null) {
|
||||
Slot slot = new Slot(new URL(putUrl));
|
||||
slot.getUrl = new URL(getUrl);
|
||||
slot.headers = new HashMap<>();
|
||||
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
||||
callback.success(slot);
|
||||
return;
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
//fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet);
|
||||
callback.failure(IqParser.extractErrorMessage(packet));
|
||||
});
|
||||
private ListenableFuture<Slot> requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) {
|
||||
final SettableFuture<Slot> future = SettableFuture.create();
|
||||
final IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
|
||||
service.sendIqPacket(account, request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
|
||||
if (slotElement != null) {
|
||||
try {
|
||||
final String putUrl = slotElement.findChildContent("put");
|
||||
final String getUrl = slotElement.findChildContent("get");
|
||||
if (getUrl != null && putUrl != null) {
|
||||
final Slot slot = new Slot(
|
||||
HttpUrl.get(putUrl),
|
||||
HttpUrl.get(getUrl),
|
||||
Headers.of("Content-Type", mime == null ? "application/octet-stream" : mime)
|
||||
);
|
||||
future.set(slot);
|
||||
return;
|
||||
}
|
||||
} catch (final IllegalArgumentException e) {
|
||||
future.setException(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
future.setException(new IqResponseException(IqParser.extractErrorMessage(packet)));
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
}
|
||||
private ListenableFuture<Slot> requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime) {
|
||||
final SettableFuture<Slot> future = SettableFuture.create();
|
||||
final IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
|
||||
service.sendIqPacket(account, request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
|
||||
if (slotElement != null) {
|
||||
try {
|
||||
final Element put = slotElement.findChild("put");
|
||||
final Element get = slotElement.findChild("get");
|
||||
final String putUrl = put == null ? null : put.getAttribute("url");
|
||||
final String getUrl = get == null ? null : get.getAttribute("url");
|
||||
if (getUrl != null && putUrl != null) {
|
||||
final ImmutableMap.Builder<String, String> headers = new ImmutableMap.Builder<>();
|
||||
for (final Element child : put.getChildren()) {
|
||||
if ("header".equals(child.getName())) {
|
||||
final String name = child.getAttribute("name");
|
||||
final String value = child.getContent();
|
||||
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
|
||||
headers.put(name, value.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
||||
final Slot slot = new Slot(HttpUrl.get(putUrl), HttpUrl.get(getUrl), headers.build());
|
||||
future.set(slot);
|
||||
return;
|
||||
}
|
||||
} catch (final IllegalArgumentException e) {
|
||||
future.setException(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
future.setException(new IqResponseException(IqParser.extractErrorMessage(packet)));
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
private void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) {
|
||||
IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
|
||||
service.sendIqPacket(account, request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
|
||||
if (slotElement != null) {
|
||||
try {
|
||||
final Element put = slotElement.findChild("put");
|
||||
final Element get = slotElement.findChild("get");
|
||||
final String putUrl = put == null ? null : put.getAttribute("url");
|
||||
final String getUrl = get == null ? null : get.getAttribute("url");
|
||||
if (getUrl != null && putUrl != null) {
|
||||
Slot slot = new Slot(new URL(putUrl));
|
||||
slot.getUrl = new URL(getUrl);
|
||||
slot.headers = new HashMap<>();
|
||||
for (Element child : put.getChildren()) {
|
||||
if ("header".equals(child.getName())) {
|
||||
final String name = child.getAttribute("name");
|
||||
final String value = child.getContent();
|
||||
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
|
||||
slot.headers.put(name, value.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
|
||||
callback.success(slot);
|
||||
return;
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
//fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet);
|
||||
callback.failure(IqParser.extractErrorMessage(packet));
|
||||
});
|
||||
public static class Slot {
|
||||
public final HttpUrl put;
|
||||
public final HttpUrl get;
|
||||
public final Headers headers;
|
||||
|
||||
}
|
||||
private Slot(HttpUrl put, HttpUrl get, Headers headers) {
|
||||
this.put = put;
|
||||
this.get = get;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
private void requestP1S3(final Account account, Jid host, String filename, String md5, OnSlotRequested callback) {
|
||||
IqPacket request = service.getIqGenerator().requestP1S3Slot(host, md5);
|
||||
service.sendIqPacket(account, request, (a, packet) -> {
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
String putUrl = packet.query(Namespace.P1_S3_FILE_TRANSFER).getAttribute("upload");
|
||||
String id = packet.query().getAttribute("fileid");
|
||||
try {
|
||||
if (putUrl != null && id != null) {
|
||||
Slot slot = new Slot(new URL(putUrl));
|
||||
slot.getUrl = P1S3UrlStreamHandler.of(id, filename);
|
||||
slot.headers = new HashMap<>();
|
||||
slot.headers.put("Content-MD5", md5);
|
||||
slot.headers.put("Content-Type", " "); //required to force it to empty. otherwise library will set something
|
||||
callback.success(slot);
|
||||
return;
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
//fall through;
|
||||
}
|
||||
}
|
||||
callback.failure("unable to request slot");
|
||||
});
|
||||
Log.d(Config.LOGTAG, "requesting slot with p1. md5=" + md5);
|
||||
}
|
||||
|
||||
|
||||
public interface OnSlotRequested {
|
||||
|
||||
void success(Slot slot);
|
||||
|
||||
void failure(String message);
|
||||
|
||||
}
|
||||
|
||||
public static class Slot {
|
||||
private final URL putUrl;
|
||||
private URL getUrl;
|
||||
private HashMap<String, String> headers;
|
||||
|
||||
private Slot(URL putUrl) {
|
||||
this.putUrl = putUrl;
|
||||
}
|
||||
|
||||
public URL getPutUrl() {
|
||||
return putUrl;
|
||||
}
|
||||
|
||||
public URL getGetUrl() {
|
||||
return getUrl;
|
||||
}
|
||||
|
||||
public HashMap<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
|
||||
this.put = put;
|
||||
this.get = getUrl;
|
||||
this.headers = Headers.of(headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class URL {
|
||||
|
||||
public static final List<String> WELL_KNOWN_SCHEMES = Arrays.asList("http", "https", AesGcmURL.PROTOCOL_NAME);
|
||||
|
||||
public static String tryParse(String url) {
|
||||
final URI uri;
|
||||
try {
|
||||
uri = new URI(url);
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
if (WELL_KNOWN_SCHEMES.contains(uri.getScheme())) {
|
||||
return uri.toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpUrl stripFragment(final HttpUrl url) {
|
||||
return url.newBuilder().fragment(null).build();
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,6 @@ package eu.siacs.conversations.parser;
|
|||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
@ -33,7 +32,6 @@ import eu.siacs.conversations.entities.ReadByMarker;
|
|||
import eu.siacs.conversations.entities.ReceiptRequest;
|
||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
|
||||
import eu.siacs.conversations.services.MessageArchiveService;
|
||||
import eu.siacs.conversations.services.QuickConversationsService;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
|
@ -384,7 +382,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
return;
|
||||
}
|
||||
} else if (query != null) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from ("+original.getFrom()+") or queryId ("+queryId+")");
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")");
|
||||
return;
|
||||
} else if (original.fromServer(account)) {
|
||||
Pair<MessagePacket, Long> f;
|
||||
|
@ -408,8 +406,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
|
||||
final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
|
||||
final Element oob = packet.findChild("x", Namespace.OOB);
|
||||
final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
|
||||
final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
|
||||
final String oobUrl = oob != null ? oob.findChildContent("url") : null;
|
||||
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
|
||||
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
|
||||
|
@ -464,7 +460,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
}
|
||||
}
|
||||
|
||||
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
|
||||
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
|
||||
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
|
||||
final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
|
||||
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
|
||||
|
@ -488,13 +484,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
if (conversation.getMucOptions().isSelf(counterpart)) {
|
||||
status = Message.STATUS_SEND_RECEIVED;
|
||||
isCarbon = true; //not really carbon but received from another resource
|
||||
//TODO this would be the place to change the body after something like mod_pastebin
|
||||
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) {
|
||||
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId, body)) {
|
||||
return;
|
||||
} else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
|
||||
LocalizedContent localizedBody = packet.getBody();
|
||||
if (localizedBody != null) {
|
||||
Message message = conversation.findSentMessageWithBody(localizedBody.content);
|
||||
if (body != null) {
|
||||
Message message = conversation.findSentMessageWithBody(body.content);
|
||||
if (message != null) {
|
||||
mXmppConnectionService.markMessage(message, status);
|
||||
return;
|
||||
|
@ -506,13 +500,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
}
|
||||
}
|
||||
final Message message;
|
||||
if (xP1S3url != null) {
|
||||
message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
|
||||
message.setOob(true);
|
||||
if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
}
|
||||
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
||||
if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
||||
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
||||
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
|
||||
Jid origin;
|
||||
|
|
|
@ -2,6 +2,8 @@ package eu.siacs.conversations.parser;
|
|||
|
||||
import android.util.Log;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -313,9 +315,10 @@ public class PresenceParser extends AbstractParser implements
|
|||
PgpEngine pgp = mXmppConnectionService.getPgpEngine();
|
||||
Element x = packet.findChild("x", "jabber:x:signed");
|
||||
if (pgp != null && x != null) {
|
||||
Element status = packet.findChild("status");
|
||||
String msg = status != null ? status.getContent() : "";
|
||||
if (contact.setPgpKeyId(pgp.fetchKeyId(account, msg, x.getContent()))) {
|
||||
final String status = packet.findChildContent("status");
|
||||
final long keyId = pgp.fetchKeyId(account, status, x.getContent());
|
||||
if (keyId != 0 && contact.setPgpKeyId(keyId)) {
|
||||
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": found OpenPGP key id for "+contact.getJid()+" "+OpenPgpUtils.convertKeyIdToHex(keyId));
|
||||
mXmppConnectionService.syncRoster(account);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
@ -564,7 +575,29 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.PRESENCE_NAME + " TEXT");
|
||||
}
|
||||
if (oldVersion < 49 && newVersion >= 49) {
|
||||
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT");
|
||||
// dirty workaround for failed migration
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -780,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();
|
||||
|
@ -791,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));
|
||||
|
@ -860,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));
|
||||
|
@ -873,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<>();
|
||||
|
@ -898,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;
|
||||
}
|
||||
|
||||
|
@ -1042,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();
|
||||
|
@ -1054,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();
|
||||
|
|
|
@ -29,7 +29,11 @@ import android.util.Log;
|
|||
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;
|
||||
|
@ -43,7 +47,6 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -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;
|
||||
|
@ -223,8 +225,12 @@ public class FileBackend {
|
|||
}
|
||||
|
||||
private static boolean hasAlpha(final Bitmap bitmap) {
|
||||
for (int x = 0; x < bitmap.getWidth(); ++x) {
|
||||
for (int y = 0; y < bitmap.getWidth(); ++y) {
|
||||
final int w = bitmap.getWidth();
|
||||
final int h = bitmap.getHeight();
|
||||
final int yStep = Math.max(1, w / 100);
|
||||
final int xStep = Math.max(1, h / 100);
|
||||
for (int x = 0; x < w; x += xStep) {
|
||||
for (int y = 0; y < h; y += yStep) {
|
||||
if (Color.alpha(bitmap.getPixel(x, y)) < 255) {
|
||||
return true;
|
||||
}
|
||||
|
@ -291,12 +297,7 @@ public class FileBackend {
|
|||
if (dimensions != null) {
|
||||
return dimensions;
|
||||
}
|
||||
final int rotation;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
rotation = extractRotationFromMediaRetriever(metadataRetriever);
|
||||
} else {
|
||||
rotation = 0;
|
||||
}
|
||||
final int rotation = extractRotationFromMediaRetriever(metadataRetriever);
|
||||
boolean rotated = rotation == 90 || rotation == 270;
|
||||
int height;
|
||||
try {
|
||||
|
@ -317,7 +318,6 @@ public class FileBackend {
|
|||
return rotated ? new Dimensions(width, height) : new Dimensions(height, width);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) {
|
||||
String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
||||
try {
|
||||
|
@ -417,9 +417,9 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
public static void updateFileParams(Message message, URL url, long size) {
|
||||
public static void updateFileParams(Message message, String url, long size) {
|
||||
final StringBuilder body = new StringBuilder();
|
||||
body.append(url.toString()).append('|').append(size);
|
||||
body.append(url).append('|').append(size);
|
||||
message.setBody(body.toString());
|
||||
}
|
||||
|
||||
|
@ -596,12 +596,12 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean useImageAsIs(Uri uri) {
|
||||
String path = getOriginalPath(uri);
|
||||
public boolean useImageAsIs(final Uri uri) {
|
||||
final String path = getOriginalPath(uri);
|
||||
if (path == null || isPathBlacklisted(path)) {
|
||||
return false;
|
||||
}
|
||||
File file = new File(path);
|
||||
final File file = new File(path);
|
||||
long size = file.length();
|
||||
if (size == 0 || size >= mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize)) {
|
||||
return false;
|
||||
|
@ -609,12 +609,15 @@ public class FileBackend {
|
|||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
try {
|
||||
BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options);
|
||||
final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(uri);
|
||||
BitmapFactory.decodeStream(inputStream, null, options);
|
||||
close(inputStream);
|
||||
if (options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) {
|
||||
return false;
|
||||
}
|
||||
return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.d(Config.LOGTAG, "unable to get image dimensions", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -626,36 +629,38 @@ 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();
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
} catch (final FileNotFoundException e) {
|
||||
cleanup(file);
|
||||
throw new FileCopyException(R.string.error_file_not_found);
|
||||
} catch (FileWriterException e) {
|
||||
} catch (final FileWriterException e) {
|
||||
cleanup(file);
|
||||
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -704,8 +709,11 @@ public class FileBackend {
|
|||
return pos > 0 ? filename.substring(pos + 1) : null;
|
||||
}
|
||||
|
||||
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, NotAnImageFileException {
|
||||
file.getParentFile().mkdirs();
|
||||
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException {
|
||||
final File parent = file.getParentFile();
|
||||
if (parent != null && parent.mkdirs()) {
|
||||
Log.d(Config.LOGTAG, "created parent directory");
|
||||
}
|
||||
InputStream is = null;
|
||||
OutputStream os = null;
|
||||
try {
|
||||
|
@ -716,18 +724,22 @@ public class FileBackend {
|
|||
if (is == null) {
|
||||
throw new FileCopyException(R.string.error_not_an_image_file);
|
||||
}
|
||||
Bitmap originalBitmap;
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
int inSampleSize = (int) Math.pow(2, sampleSize);
|
||||
final Bitmap originalBitmap;
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
final int inSampleSize = (int) Math.pow(2, sampleSize);
|
||||
Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
|
||||
options.inSampleSize = inSampleSize;
|
||||
originalBitmap = BitmapFactory.decodeStream(is, null, options);
|
||||
is.close();
|
||||
if (originalBitmap == null) {
|
||||
throw new NotAnImageFileException();
|
||||
throw new ImageCompressionException("Source file was not an image");
|
||||
}
|
||||
if (!"image/jpeg".equals(options.outMimeType) && hasAlpha(originalBitmap)) {
|
||||
originalBitmap.recycle();
|
||||
throw new ImageCompressionException("Source file had alpha channel");
|
||||
}
|
||||
Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
|
||||
int rotation = getRotation(image);
|
||||
final int rotation = getRotation(image);
|
||||
scaledBitmap = rotate(scaledBitmap, rotation);
|
||||
boolean targetSizeReached = false;
|
||||
int quality = Config.IMAGE_QUALITY;
|
||||
|
@ -743,14 +755,16 @@ public class FileBackend {
|
|||
quality -= 5;
|
||||
}
|
||||
scaledBitmap.recycle();
|
||||
} catch (FileNotFoundException e) {
|
||||
} 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);
|
||||
|
@ -763,12 +777,20 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, NotAnImageFileException {
|
||||
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);
|
||||
}
|
||||
|
||||
public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException, NotAnImageFileException {
|
||||
public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException, ImageCompressionException {
|
||||
switch (Config.IMAGE_FORMAT) {
|
||||
case JPEG:
|
||||
message.setRelativeFilePath(message.getUuid() + ".jpg");
|
||||
|
@ -784,31 +806,49 @@ public class FileBackend {
|
|||
updateFileParams(message);
|
||||
}
|
||||
|
||||
public boolean unusualBounds(Uri image) {
|
||||
public boolean unusualBounds(final Uri image) {
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
|
||||
final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(image);
|
||||
BitmapFactory.decodeStream(inputStream, null, options);
|
||||
close(inputStream);
|
||||
float ratio = (float) options.outHeight / options.outWidth;
|
||||
return ratio > (21.0f / 9.0f) || ratio < (9.0f / 21.0f);
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
Log.w(Config.LOGTAG, "unable to detect image bounds", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -829,11 +869,11 @@ public class FileBackend {
|
|||
} else if (mime.startsWith("video/")) {
|
||||
thumbnail = getVideoPreview(file, size);
|
||||
} else {
|
||||
Bitmap fullsize = getFullSizeImagePreview(file, size);
|
||||
if (fullsize == null) {
|
||||
final Bitmap fullSize = getFullSizeImagePreview(file, size);
|
||||
if (fullSize == null) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
thumbnail = resize(fullsize, size);
|
||||
thumbnail = resize(fullSize, size);
|
||||
thumbnail = rotate(thumbnail, getRotation(file));
|
||||
if (mime.equals("image/gif")) {
|
||||
Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888, true);
|
||||
|
@ -1173,7 +1213,7 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
if (file.delete()) {
|
||||
Log.d(Config.LOGTAG,"deleted "+file.getAbsolutePath());
|
||||
Log.d(Config.LOGTAG, "deleted " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1281,9 +1321,11 @@ public class FileBackend {
|
|||
}
|
||||
|
||||
private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
|
||||
final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(image);
|
||||
BitmapFactory.decodeStream(inputStream, null, options);
|
||||
close(inputStream);
|
||||
return calcSampleSize(options, size);
|
||||
}
|
||||
|
||||
|
@ -1291,7 +1333,7 @@ public class FileBackend {
|
|||
updateFileParams(message, null);
|
||||
}
|
||||
|
||||
public void updateFileParams(Message message, URL url) {
|
||||
public void updateFileParams(Message message, String url) {
|
||||
DownloadableFile file = getFile(message);
|
||||
final String mime = file.getMimeType();
|
||||
final boolean privateMessage = message.isPrivateMessage();
|
||||
|
@ -1301,7 +1343,7 @@ public class FileBackend {
|
|||
final boolean pdf = "application/pdf".equals(mime);
|
||||
final StringBuilder body = new StringBuilder();
|
||||
if (url != null) {
|
||||
body.append(url.toString());
|
||||
body.append(url);
|
||||
}
|
||||
body.append('|').append(file.getSize());
|
||||
if (image || video || (pdf && Compatibility.runsTwentyOne())) {
|
||||
|
@ -1439,18 +1481,23 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
public static class NotAnImageFileException extends Exception {
|
||||
public static class ImageCompressionException extends Exception {
|
||||
|
||||
ImageCompressionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class FileCopyException extends Exception {
|
||||
private final int resId;
|
||||
|
||||
private FileCopyException(int resId) {
|
||||
private FileCopyException(@StringRes int resId) {
|
||||
this.resId = resId;
|
||||
}
|
||||
|
||||
public int getResId() {
|
||||
public @StringRes
|
||||
int getResId() {
|
||||
return resId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -13,22 +15,25 @@ import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
|||
import org.bouncycastle.crypto.params.AEADParameters;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.utils.Compatibility;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okio.BufferedSink;
|
||||
import okio.Okio;
|
||||
import okio.Source;
|
||||
|
||||
import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
|
||||
|
||||
|
@ -42,7 +47,7 @@ public class AbstractConnectionManager {
|
|||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
public static InputStream upgrade(DownloadableFile file, InputStream is) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, NoSuchProviderException {
|
||||
public static InputStream upgrade(DownloadableFile file, InputStream is) {
|
||||
if (file.getKey() != null && file.getIv() != null) {
|
||||
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
||||
cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
|
||||
|
@ -52,6 +57,43 @@ public class AbstractConnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
//For progress tracking see:
|
||||
//https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
|
||||
|
||||
public static RequestBody requestBody(final DownloadableFile file, final ProgressListener progressListener) {
|
||||
return new RequestBody() {
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return file.getSize() + (file.getKey() != null ? 16 : 0);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return MediaType.parse(file.getMimeType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(final BufferedSink sink) throws IOException {
|
||||
long transmitted = 0;
|
||||
try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) {
|
||||
long read;
|
||||
while ((read = source.read(sink.buffer(), 8196)) != -1) {
|
||||
transmitted += read;
|
||||
sink.flush();
|
||||
progressListener.onProgress(transmitted);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public interface ProgressListener {
|
||||
void onProgress(long progress);
|
||||
}
|
||||
|
||||
public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) {
|
||||
FileOutputStream os;
|
||||
try {
|
||||
|
@ -78,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() {
|
||||
|
@ -94,8 +137,8 @@ public class AbstractConnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public PowerManager.WakeLock createWakeLock(String name) {
|
||||
PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
|
||||
public PowerManager.WakeLock createWakeLock(final String name) {
|
||||
final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class);
|
||||
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
|
||||
}
|
||||
|
||||
|
@ -117,6 +160,7 @@ public class AbstractConnectionManager {
|
|||
}
|
||||
|
||||
public static Extension of(String path) {
|
||||
//TODO accept List<String> pathSegments
|
||||
final int pos = path.lastIndexOf('/');
|
||||
final String filename = path.substring(pos + 1).toLowerCase();
|
||||
final String[] parts = filename.split("\\.");
|
||||
|
|
|
@ -3,19 +3,19 @@ package eu.siacs.conversations.services;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.ypresto.androidtranscoder.MediaTranscoder;
|
||||
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
|
||||
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;
|
||||
|
||||
|
@ -26,162 +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 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,14 +50,9 @@ public class ChannelDiscoveryService {
|
|||
}
|
||||
|
||||
void initializeMuclumbusService() {
|
||||
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||
|
||||
final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
|
||||
if (service.useTorToConnect()) {
|
||||
try {
|
||||
builder.proxy(HttpConnectionManager.getProxy());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to use Tor proxy", e);
|
||||
}
|
||||
builder.proxy(HttpConnectionManager.getProxy());
|
||||
}
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.client(builder.build())
|
||||
|
@ -73,7 +68,7 @@ public class ChannelDiscoveryService {
|
|||
}
|
||||
|
||||
void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) {
|
||||
List<Room> result = cache.getIfPresent(key(method, query));
|
||||
final List<Room> result = cache.getIfPresent(key(method, query));
|
||||
if (result != null) {
|
||||
onChannelSearchResultsFound.onChannelSearchResultsFound(result);
|
||||
return;
|
||||
|
|
|
@ -31,6 +31,7 @@ import android.app.NotificationManager;
|
|||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
|
@ -40,19 +41,20 @@ import android.util.SparseArray;
|
|||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.io.CharStreams;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.MessageDigest;
|
||||
|
@ -60,12 +62,10 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -73,16 +73,14 @@ import java.util.logging.Level;
|
|||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.DomainHostnameVerifier;
|
||||
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
||||
import eu.siacs.conversations.entities.MTMDecision;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.ui.MemorizingActivity;
|
||||
|
||||
|
@ -98,12 +96,12 @@ import eu.siacs.conversations.ui.MemorizingActivity;
|
|||
*/
|
||||
public class MemorizingTrustManager {
|
||||
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
||||
|
||||
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
|
||||
public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
|
||||
public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
|
||||
public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
|
||||
final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
|
||||
final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
|
||||
private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||
|
@ -111,7 +109,6 @@ public class MemorizingTrustManager {
|
|||
private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
|
||||
private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
|
||||
private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
|
||||
private final static int NOTIFICATION_ID = 100509;
|
||||
static String KEYSTORE_DIR = "KeyStore";
|
||||
static String KEYSTORE_FILE = "KeyStore.bks";
|
||||
private static int decisionId = 0;
|
||||
|
@ -165,20 +162,6 @@ public class MemorizingTrustManager {
|
|||
this.defaultTrustManager = getTrustManager(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the path for the KeyStore file.
|
||||
* <p>
|
||||
* The actual filename relative to the app's directory will be
|
||||
* <code>app_<i>dirname</i>/<i>filename</i></code>.
|
||||
*
|
||||
* @param dirname directory to store the KeyStore.
|
||||
* @param filename file name for the KeyStore.
|
||||
*/
|
||||
public static void setKeyStoreFile(String dirname, String filename) {
|
||||
KEYSTORE_DIR = dirname;
|
||||
KEYSTORE_FILE = filename;
|
||||
}
|
||||
|
||||
private static boolean isIp(final String server) {
|
||||
return server != null && (
|
||||
PATTERN_IPV4.matcher(server).matches()
|
||||
|
@ -214,9 +197,7 @@ public class MemorizingTrustManager {
|
|||
MessageDigest md = MessageDigest.getInstance(digest);
|
||||
md.update(cert.getEncoded());
|
||||
return hexString(md.digest());
|
||||
} catch (java.security.cert.CertificateEncodingException e) {
|
||||
return e.getMessage();
|
||||
} catch (java.security.NoSuchAlgorithmException e) {
|
||||
} catch (CertificateEncodingException | NoSuchAlgorithmException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +218,7 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
}
|
||||
|
||||
void init(Context m) {
|
||||
void init(final Context m) {
|
||||
master = m;
|
||||
masterHandler = new Handler(m.getMainLooper());
|
||||
notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
@ -260,36 +241,6 @@ public class MemorizingTrustManager {
|
|||
appKeyStore = loadAppKeyStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds an Activity to the MTM for displaying the query dialog.
|
||||
* <p>
|
||||
* This is useful if your connection is run from a service that is
|
||||
* triggered by user interaction -- in such cases the activity is
|
||||
* visible and the user tends to ignore the service notification.
|
||||
* <p>
|
||||
* You should never have a hidden activity bound to MTM! Use this
|
||||
* function in onResume() and @see unbindDisplayActivity in onPause().
|
||||
*
|
||||
* @param act Activity to be bound
|
||||
*/
|
||||
public void bindDisplayActivity(AppCompatActivity act) {
|
||||
foregroundAct = act;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an Activity from the MTM display stack.
|
||||
* <p>
|
||||
* Always call this function when the Activity added with
|
||||
* {@link #bindDisplayActivity(AppCompatActivity)} is hidden.
|
||||
*
|
||||
* @param act Activity to be unbound
|
||||
*/
|
||||
public void unbindDisplayActivity(AppCompatActivity act) {
|
||||
// do not remove if it was overridden by a different activity
|
||||
if (foregroundAct == act)
|
||||
foregroundAct = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all certificate aliases stored in MTM.
|
||||
*
|
||||
|
@ -304,21 +255,6 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a certificate for a given alias.
|
||||
*
|
||||
* @param alias the certificate's alias as returned by {@link #getCertificates()}.
|
||||
* @return the certificate associated with the alias or <tt>null</tt> if none found.
|
||||
*/
|
||||
public Certificate getCertificate(String alias) {
|
||||
try {
|
||||
return appKeyStore.getCertificate(alias);
|
||||
} catch (KeyStoreException e) {
|
||||
// this should never happen, however...
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given certificate from MTMs key store.
|
||||
*
|
||||
|
@ -337,32 +273,6 @@ public class MemorizingTrustManager {
|
|||
keyStoreUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new hostname verifier supporting user interaction.
|
||||
*
|
||||
* <p>This method creates a new {@link HostnameVerifier} that is bound to
|
||||
* the given instance of {@link MemorizingTrustManager}, and leverages an
|
||||
* existing {@link HostnameVerifier}. The returned verifier performs the
|
||||
* following steps, returning as soon as one of them succeeds:
|
||||
* /p>
|
||||
* <ol>
|
||||
* <li>Success, if the wrapped defaultVerifier accepts the certificate.</li>
|
||||
* <li>Success, if the server certificate is stored in the keystore under the given hostname.</li>
|
||||
* <li>Ask the user and return accordingly.</li>
|
||||
* <li>Failure on exception.</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param defaultVerifier the {@link HostnameVerifier} that should perform the actual check
|
||||
* @return a new hostname verifier using the MTM's key store
|
||||
* @throws IllegalArgumentException if the defaultVerifier parameter is null
|
||||
*/
|
||||
public DomainHostnameVerifier wrapHostnameVerifier(final HostnameVerifier defaultVerifier, final boolean interactive) {
|
||||
if (defaultVerifier == null)
|
||||
throw new IllegalArgumentException("The default verifier may not be null");
|
||||
|
||||
return new MemorizingHostnameVerifier(defaultVerifier, interactive);
|
||||
}
|
||||
|
||||
X509TrustManager getTrustManager(KeyStore ks) {
|
||||
try {
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
|
||||
|
@ -449,16 +359,8 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isExpiredException(Throwable e) {
|
||||
do {
|
||||
if (e instanceof CertificateExpiredException)
|
||||
return true;
|
||||
e = e.getCause();
|
||||
} while (e != null);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
|
||||
private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
|
||||
throws CertificateException {
|
||||
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
|
||||
try {
|
||||
|
@ -467,13 +369,8 @@ public class MemorizingTrustManager {
|
|||
appTrustManager.checkServerTrusted(chain, authType);
|
||||
else
|
||||
appTrustManager.checkClientTrusted(chain, authType);
|
||||
} catch (CertificateException ae) {
|
||||
} catch (final CertificateException ae) {
|
||||
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
|
||||
// if the cert is stored in our appTrustManager, we ignore expiredness
|
||||
if (isExpiredException(ae)) {
|
||||
LOGGER.log(Level.INFO, "checkCertTrusted: accepting expired certificate from keystore");
|
||||
return;
|
||||
}
|
||||
if (isCertKnown(chain[0])) {
|
||||
LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
|
||||
return;
|
||||
|
@ -486,15 +383,18 @@ public class MemorizingTrustManager {
|
|||
defaultTrustManager.checkServerTrusted(chain, authType);
|
||||
else
|
||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
||||
} catch (CertificateException e) {
|
||||
boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false);
|
||||
if (domain != null && isServer && trustSystemCAs && !isIp(domain)) {
|
||||
} catch (final CertificateException e) {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
|
||||
final boolean trustSystemCAs = !preferences.getBoolean("dont_trust_system_cas", false);
|
||||
if (domain != null && isServer && trustSystemCAs && !isIp(domain) && !domain.endsWith(".onion")) {
|
||||
final String hash = getBase64Hash(chain[0], "SHA-256");
|
||||
final List<String> fingerprints = getPoshFingerprints(domain);
|
||||
if (hash != null && fingerprints.size() > 0) {
|
||||
if (fingerprints.contains(hash)) {
|
||||
Log.d("mtm", "trusted cert fingerprint of " + domain + " via posh");
|
||||
return;
|
||||
} else {
|
||||
Log.d("mtm", "fingerprint " + hash + " not found in " + fingerprints);
|
||||
}
|
||||
if (getPoshCacheFile(domain).delete()) {
|
||||
Log.d("mtm", "deleted posh file for " + domain + " after not being able to verify");
|
||||
|
@ -511,7 +411,7 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
|
||||
private List<String> getPoshFingerprints(String domain) {
|
||||
List<String> cached = getPoshFingerprintsFromCache(domain);
|
||||
final List<String> cached = getPoshFingerprintsFromCache(domain);
|
||||
if (cached == null) {
|
||||
return getPoshFingerprintsFromServer(domain);
|
||||
} else {
|
||||
|
@ -525,19 +425,13 @@ public class MemorizingTrustManager {
|
|||
|
||||
private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) {
|
||||
Log.d("mtm", "downloading json for " + domain + " from " + url);
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
|
||||
final boolean useTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", master.getResources().getBoolean(R.bool.use_tor));
|
||||
try {
|
||||
List<String> results = new ArrayList<>();
|
||||
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String inputLine;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while ((inputLine = in.readLine()) != null) {
|
||||
builder.append(inputLine);
|
||||
}
|
||||
JSONObject jsonObject = new JSONObject(builder.toString());
|
||||
in.close();
|
||||
final List<String> results = new ArrayList<>();
|
||||
final InputStream inputStream = HttpConnectionManager.open(url, useTor);
|
||||
final String body = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
|
||||
final JSONObject jsonObject = new JSONObject(body);
|
||||
int expires = jsonObject.getInt("expires");
|
||||
if (expires <= 0) {
|
||||
return new ArrayList<>();
|
||||
|
@ -554,17 +448,15 @@ public class MemorizingTrustManager {
|
|||
if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) {
|
||||
return getPoshFingerprintsFromServer(domain, redirect, expires, false);
|
||||
}
|
||||
JSONArray fingerprints = jsonObject.getJSONArray("fingerprints");
|
||||
final JSONArray fingerprints = jsonObject.getJSONArray("fingerprints");
|
||||
for (int i = 0; i < fingerprints.length(); i++) {
|
||||
JSONObject fingerprint = fingerprints.getJSONObject(i);
|
||||
String sha256 = fingerprint.getString("sha-256");
|
||||
if (sha256 != null) {
|
||||
results.add(sha256);
|
||||
}
|
||||
final JSONObject fingerprint = fingerprints.getJSONObject(i);
|
||||
final String sha256 = fingerprint.getString("sha-256");
|
||||
results.add(sha256);
|
||||
}
|
||||
writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
|
||||
return results;
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
Log.d("mtm", "error fetching posh " + e.getMessage());
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
@ -575,7 +467,7 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
|
||||
private void writeFingerprintsToCache(String domain, List<String> results, long expires) {
|
||||
File file = getPoshCacheFile(domain);
|
||||
final File file = getPoshCacheFile(domain);
|
||||
file.getParentFile().mkdirs();
|
||||
try {
|
||||
file.createNewFile();
|
||||
|
@ -592,20 +484,11 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
|
||||
private List<String> getPoshFingerprintsFromCache(String domain) {
|
||||
File file = getPoshCacheFile(domain);
|
||||
final File file = getPoshCacheFile(domain);
|
||||
try {
|
||||
InputStream is = new FileInputStream(file);
|
||||
BufferedReader buf = new BufferedReader(new InputStreamReader(is));
|
||||
|
||||
String line = buf.readLine();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
while (line != null) {
|
||||
sb.append(line).append("\n");
|
||||
line = buf.readLine();
|
||||
}
|
||||
JSONObject jsonObject = new JSONObject(sb.toString());
|
||||
is.close();
|
||||
final InputStream inputStream = new FileInputStream(file);
|
||||
final String json = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
|
||||
final JSONObject jsonObject = new JSONObject(json);
|
||||
long expires = jsonObject.getLong("expires");
|
||||
long expiresIn = expires - System.currentTimeMillis();
|
||||
if (expiresIn < 0) {
|
||||
|
@ -614,15 +497,13 @@ public class MemorizingTrustManager {
|
|||
} else {
|
||||
Log.d("mtm", "posh fingerprints expire in " + (expiresIn / 1000) + "s");
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
|
||||
final List<String> result = new ArrayList<>();
|
||||
final JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
|
||||
for (int i = 0; i < jsonArray.length(); ++i) {
|
||||
result.add(jsonArray.getString(i));
|
||||
}
|
||||
return result;
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
} catch (final IOException e) {
|
||||
return null;
|
||||
} catch (JSONException e) {
|
||||
file.delete();
|
||||
|
@ -632,7 +513,7 @@ public class MemorizingTrustManager {
|
|||
|
||||
private X509Certificate[] getAcceptedIssuers() {
|
||||
LOGGER.log(Level.FINE, "getAcceptedIssuers()");
|
||||
return defaultTrustManager.getAcceptedIssuers();
|
||||
return defaultTrustManager == null ? new X509Certificate[0] : defaultTrustManager.getAcceptedIssuers();
|
||||
}
|
||||
|
||||
private int createDecisionId(MTMDecision d) {
|
||||
|
@ -645,14 +526,24 @@ public class MemorizingTrustManager {
|
|||
return myId;
|
||||
}
|
||||
|
||||
private void certDetails(StringBuffer si, X509Certificate c) {
|
||||
SimpleDateFormat validityDateFormater = new SimpleDateFormat("yyyy-MM-dd");
|
||||
private void certDetails(final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
|
||||
|
||||
si.append("\n");
|
||||
si.append(c.getSubjectDN().toString());
|
||||
if (showValidFor) {
|
||||
try {
|
||||
si.append("Valid for: ");
|
||||
si.append(Joiner.on(", ").join(XmppDomainVerifier.parseValidDomains(c).all()));
|
||||
} catch (final CertificateParsingException e) {
|
||||
si.append("Unable to parse Certificate");
|
||||
}
|
||||
si.append("\n");
|
||||
} else {
|
||||
si.append(c.getSubjectDN());
|
||||
}
|
||||
si.append("\n");
|
||||
si.append(validityDateFormater.format(c.getNotBefore()));
|
||||
si.append(DATE_FORMAT.format(c.getNotBefore()));
|
||||
si.append(" - ");
|
||||
si.append(validityDateFormater.format(c.getNotAfter()));
|
||||
si.append(DATE_FORMAT.format(c.getNotAfter()));
|
||||
si.append("\nSHA-256: ");
|
||||
si.append(certHash(c, "SHA-256"));
|
||||
si.append("\nSHA-1: ");
|
||||
|
@ -665,7 +556,7 @@ public class MemorizingTrustManager {
|
|||
private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
|
||||
Throwable e = cause;
|
||||
LOGGER.log(Level.FINE, "certChainMessage for " + e);
|
||||
StringBuffer si = new StringBuffer();
|
||||
final StringBuffer si = new StringBuffer();
|
||||
if (e.getCause() != null) {
|
||||
e = e.getCause();
|
||||
// HACK: there is no sane way to check if the error is a "trust anchor
|
||||
|
@ -680,46 +571,13 @@ public class MemorizingTrustManager {
|
|||
si.append(master.getString(R.string.mtm_connect_anyway));
|
||||
si.append("\n\n");
|
||||
si.append(master.getString(R.string.mtm_cert_details));
|
||||
for (X509Certificate c : chain) {
|
||||
certDetails(si, c);
|
||||
si.append('\n');
|
||||
for(int i = 0; i < chain.length; ++i) {
|
||||
certDetails(si, chain[i], i == 0);
|
||||
}
|
||||
return si.toString();
|
||||
}
|
||||
|
||||
private String hostNameMessage(X509Certificate cert, String hostname) {
|
||||
StringBuffer si = new StringBuffer();
|
||||
|
||||
si.append(master.getString(R.string.mtm_hostname_mismatch, hostname));
|
||||
si.append("\n\n");
|
||||
try {
|
||||
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
|
||||
if (sans == null) {
|
||||
si.append(cert.getSubjectDN());
|
||||
si.append("\n");
|
||||
} else for (List<?> altName : sans) {
|
||||
Object name = altName.get(1);
|
||||
if (name instanceof String) {
|
||||
si.append("[");
|
||||
si.append(altName.get(0));
|
||||
si.append("] ");
|
||||
si.append(name);
|
||||
si.append("\n");
|
||||
}
|
||||
}
|
||||
} catch (CertificateParsingException e) {
|
||||
e.printStackTrace();
|
||||
si.append("<Parsing error: ");
|
||||
si.append(e.getLocalizedMessage());
|
||||
si.append(">\n");
|
||||
}
|
||||
si.append("\n");
|
||||
si.append(master.getString(R.string.mtm_connect_anyway));
|
||||
si.append("\n\n");
|
||||
si.append(master.getString(R.string.mtm_cert_details));
|
||||
certDetails(si, cert);
|
||||
return si.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top-most entry of the activity stack.
|
||||
*
|
||||
|
@ -777,17 +635,6 @@ public class MemorizingTrustManager {
|
|||
}
|
||||
}
|
||||
|
||||
boolean interactHostname(X509Certificate cert, String hostname) {
|
||||
switch (interact(hostNameMessage(cert, hostname), R.string.mtm_accept_servername)) {
|
||||
case MTMDecision.DECISION_ALWAYS:
|
||||
storeCert(hostname, cert);
|
||||
case MTMDecision.DECISION_ONCE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public X509TrustManager getNonInteractive(String domain) {
|
||||
return new NonInteractiveMemorizingTrustManager(domain);
|
||||
}
|
||||
|
@ -804,57 +651,6 @@ public class MemorizingTrustManager {
|
|||
return new InteractiveMemorizingTrustManager(null);
|
||||
}
|
||||
|
||||
class MemorizingHostnameVerifier implements DomainHostnameVerifier {
|
||||
private final HostnameVerifier defaultVerifier;
|
||||
private final boolean interactive;
|
||||
|
||||
public MemorizingHostnameVerifier(HostnameVerifier wrapped, boolean interactive) {
|
||||
this.defaultVerifier = wrapped;
|
||||
this.interactive = interactive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String domain, String hostname, SSLSession session) {
|
||||
LOGGER.log(Level.FINE, "hostname verifier for " + domain + ", trying default verifier first");
|
||||
// if the default verifier accepts the hostname, we are done
|
||||
if (defaultVerifier instanceof DomainHostnameVerifier) {
|
||||
if (((DomainHostnameVerifier) defaultVerifier).verify(domain, hostname, session)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (defaultVerifier.verify(domain, session)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// otherwise, we check if the hostname is an alias for this cert in our keystore
|
||||
try {
|
||||
X509Certificate cert = (X509Certificate) session.getPeerCertificates()[0];
|
||||
//Log.d(TAG, "cert: " + cert);
|
||||
if (cert.equals(appKeyStore.getCertificate(domain.toLowerCase(Locale.US)))) {
|
||||
LOGGER.log(Level.FINE, "certificate for " + domain + " is in our keystore. accepting.");
|
||||
return true;
|
||||
} else {
|
||||
LOGGER.log(Level.FINE, "server " + domain + " provided wrong certificate, asking user.");
|
||||
if (interactive) {
|
||||
return interactHostname(cert, domain);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String domain, SSLSession sslSession) {
|
||||
return verify(domain, null, sslSession);
|
||||
}
|
||||
}
|
||||
|
||||
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
|
||||
|
||||
private final String domain;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,10 +12,12 @@ import android.content.res.Resources;
|
|||
import android.graphics.Bitmap;
|
||||
import android.graphics.Typeface;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Vibrator;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.StyleSpan;
|
||||
|
@ -32,6 +34,9 @@ import androidx.core.app.RemoteInput;
|
|||
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;
|
||||
import java.util.ArrayList;
|
||||
|
@ -43,6 +48,10 @@ import java.util.LinkedHashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -70,12 +79,13 @@ import eu.siacs.conversations.xmpp.jingle.Media;
|
|||
|
||||
public class NotificationService {
|
||||
|
||||
private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
public static final Object CATCHUP_LOCK = new Object();
|
||||
|
||||
private static final int LED_COLOR = 0xff00ff00;
|
||||
|
||||
private static final int CALL_DAT = 120;
|
||||
private static final long[] CALL_PATTERN = {0, 3 * CALL_DAT, CALL_DAT, CALL_DAT, 3 * CALL_DAT, CALL_DAT, CALL_DAT};
|
||||
private static final long[] CALL_PATTERN = {0, 500, 300, 600};
|
||||
|
||||
private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
|
||||
private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
|
||||
|
@ -92,6 +102,10 @@ public class NotificationService {
|
|||
private boolean mIsInForeground;
|
||||
private long mLastNotification;
|
||||
|
||||
private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
|
||||
private Ringtone currentlyPlayingRingtone = null;
|
||||
private ScheduledFuture<?> vibrationFuture;
|
||||
|
||||
NotificationService(final XmppConnectionService service) {
|
||||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
@ -129,6 +143,7 @@ public class NotificationService {
|
|||
}
|
||||
|
||||
notificationManager.deleteNotificationChannel("export");
|
||||
notificationManager.deleteNotificationChannel("incoming_calls");
|
||||
|
||||
notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information)));
|
||||
notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages)));
|
||||
|
@ -136,7 +151,7 @@ public class NotificationService {
|
|||
final NotificationChannel foregroundServiceChannel = new NotificationChannel("foreground",
|
||||
c.getString(R.string.foreground_service_channel_name),
|
||||
NotificationManager.IMPORTANCE_MIN);
|
||||
foregroundServiceChannel.setDescription(c.getString(R.string.foreground_service_channel_description));
|
||||
foregroundServiceChannel.setDescription(c.getString(R.string.foreground_service_channel_description, c.getString(R.string.app_name)));
|
||||
foregroundServiceChannel.setShowBadge(false);
|
||||
foregroundServiceChannel.setGroup("status");
|
||||
notificationManager.createNotificationChannel(foregroundServiceChannel);
|
||||
|
@ -162,20 +177,16 @@ public class NotificationService {
|
|||
exportChannel.setGroup("status");
|
||||
notificationManager.createNotificationChannel(exportChannel);
|
||||
|
||||
final NotificationChannel incomingCallsChannel = new NotificationChannel("incoming_calls",
|
||||
final NotificationChannel incomingCallsChannel = new NotificationChannel(INCOMING_CALLS_NOTIFICATION_CHANNEL,
|
||||
c.getString(R.string.incoming_calls_channel_name),
|
||||
NotificationManager.IMPORTANCE_HIGH);
|
||||
incomingCallsChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE), new AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.build());
|
||||
incomingCallsChannel.setSound(null, null);
|
||||
incomingCallsChannel.setShowBadge(false);
|
||||
incomingCallsChannel.setLightColor(LED_COLOR);
|
||||
incomingCallsChannel.enableLights(true);
|
||||
incomingCallsChannel.setGroup("calls");
|
||||
incomingCallsChannel.setBypassDnd(true);
|
||||
incomingCallsChannel.enableVibration(true);
|
||||
incomingCallsChannel.setVibrationPattern(CALL_PATTERN);
|
||||
incomingCallsChannel.enableVibration(false);
|
||||
notificationManager.createNotificationChannel(incomingCallsChannel);
|
||||
|
||||
final NotificationChannel ongoingCallsChannel = new NotificationChannel("ongoing_calls",
|
||||
|
@ -358,8 +369,8 @@ public class NotificationService {
|
|||
|
||||
public void pushFailedDelivery(final Message message) {
|
||||
final Conversation conversation = (Conversation) message.getConversation();
|
||||
final boolean isScreenOn = mXmppConnectionService.isInteractive();
|
||||
if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
|
||||
final boolean isScreenLocked = !mXmppConnectionService.isScreenLocked();
|
||||
if (this.mIsInForeground && isScreenLocked && this.mOpenConversation == message.getConversation()) {
|
||||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing failed delivery notification because conversation is open");
|
||||
return;
|
||||
}
|
||||
|
@ -387,14 +398,56 @@ public class NotificationService {
|
|||
notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
|
||||
}
|
||||
|
||||
public void showIncomingCallNotification(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;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) {
|
||||
currentInterruptionFilter = notificationManager.getCurrentInterruptionFilter();
|
||||
} else {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
|
||||
this.vibrationFuture = SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
|
||||
new VibrationRunnable(),
|
||||
0,
|
||||
3,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
if (currentVibrationFuture != null) {
|
||||
currentVibrationFuture.cancel(true);
|
||||
}
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
|
||||
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");
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
this.currentlyPlayingRingtone.setLooping(true);
|
||||
}
|
||||
this.currentlyPlayingRingtone.play();
|
||||
}
|
||||
|
||||
private void showIncomingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
|
||||
final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class);
|
||||
fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString());
|
||||
fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
|
||||
fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
|
||||
fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "incoming_calls");
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL);
|
||||
if (media.contains(Media.VIDEO)) {
|
||||
builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
|
||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
|
||||
|
@ -435,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);
|
||||
|
@ -468,9 +530,27 @@ public class NotificationService {
|
|||
}
|
||||
|
||||
public void cancelIncomingCallNotification() {
|
||||
stopSoundAndVibration();
|
||||
cancel(INCOMING_CALL_NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
public boolean stopSoundAndVibration() {
|
||||
int stopped = 0;
|
||||
if (this.currentlyPlayingRingtone != null) {
|
||||
if (this.currentlyPlayingRingtone.isPlaying()) {
|
||||
Log.d(Config.LOGTAG, "stop playing ring tone");
|
||||
++stopped;
|
||||
}
|
||||
this.currentlyPlayingRingtone.stop();
|
||||
}
|
||||
if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
|
||||
Log.d(Config.LOGTAG, "stop vibration");
|
||||
this.vibrationFuture.cancel(true);
|
||||
++stopped;
|
||||
}
|
||||
return stopped > 0;
|
||||
}
|
||||
|
||||
public static void cancelIncomingCallNotification(final Context context) {
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
try {
|
||||
|
@ -486,8 +566,8 @@ public class NotificationService {
|
|||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off");
|
||||
return;
|
||||
}
|
||||
final boolean isScreenOn = mXmppConnectionService.isInteractive();
|
||||
if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
|
||||
final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
|
||||
if (this.mIsInForeground && !isScreenLocked && this.mOpenConversation == message.getConversation()) {
|
||||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because conversation is open");
|
||||
return;
|
||||
}
|
||||
|
@ -495,7 +575,7 @@ public class NotificationService {
|
|||
pushToStack(message);
|
||||
final Conversational conversation = message.getConversation();
|
||||
final Account account = conversation.getAccount();
|
||||
final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
|
||||
final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked)
|
||||
&& !account.inGracePeriod()
|
||||
&& !this.inMiniGracePeriod(account);
|
||||
updateNotification(doNotify, Collections.singletonList(conversation.getUuid()));
|
||||
|
@ -636,17 +716,7 @@ public class NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
private void modifyIncomingCall(Builder mBuilder) {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
|
||||
final Resources resources = mXmppConnectionService.getResources();
|
||||
final String ringtone = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone));
|
||||
mBuilder.setVibrate(CALL_PATTERN);
|
||||
final Uri uri = Uri.parse(ringtone);
|
||||
try {
|
||||
mBuilder.setSound(fixRingtoneUri(uri));
|
||||
} catch (SecurityException e) {
|
||||
Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
|
||||
}
|
||||
private void modifyIncomingCall(final Builder mBuilder) {
|
||||
mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||
setNotificationColor(mBuilder);
|
||||
mBuilder.setLights(LED_COLOR, 2000, 3000);
|
||||
|
@ -730,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);
|
||||
|
@ -1006,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) {
|
||||
|
@ -1253,4 +1325,13 @@ public class NotificationService {
|
|||
Log.d(Config.LOGTAG, "unable to cancel notification", e);
|
||||
}
|
||||
}
|
||||
|
||||
private class VibrationRunnable implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final Vibrator vibrator = (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
vibrator.vibrate(CALL_PATTERN, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.Manifest;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.KeyguardManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
|
@ -19,6 +20,8 @@ import android.database.ContentObserver;
|
|||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
|
@ -54,7 +57,6 @@ import org.openintents.openpgp.util.OpenPgpApi;
|
|||
import org.openintents.openpgp.util.OpenPgpServiceConnection;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
|
@ -73,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;
|
||||
|
@ -103,7 +107,6 @@ import eu.siacs.conversations.generator.AbstractGenerator;
|
|||
import eu.siacs.conversations.generator.IqGenerator;
|
||||
import eu.siacs.conversations.generator.MessageGenerator;
|
||||
import eu.siacs.conversations.generator.PresenceGenerator;
|
||||
import eu.siacs.conversations.http.CustomURLStreamHandlerFactory;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.parser.AbstractParser;
|
||||
import eu.siacs.conversations.parser.IqParser;
|
||||
|
@ -135,6 +138,7 @@ import eu.siacs.conversations.utils.TorServiceUtils;
|
|||
import eu.siacs.conversations.utils.WakeLockHelper;
|
||||
import eu.siacs.conversations.utils.XmppUri;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.LocalizedContent;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.OnBindListener;
|
||||
|
@ -152,6 +156,7 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
|||
import eu.siacs.conversations.xmpp.forms.Data;
|
||||
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
|
||||
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
|
||||
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
||||
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
|
||||
import eu.siacs.conversations.xmpp.mam.MamReference;
|
||||
|
@ -180,13 +185,10 @@ public class XmppConnectionService extends Service {
|
|||
|
||||
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
|
||||
|
||||
static {
|
||||
URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
|
||||
}
|
||||
|
||||
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");
|
||||
|
@ -251,10 +253,23 @@ public class XmppConnectionService extends Service {
|
|||
private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
|
||||
|
||||
@Override
|
||||
public boolean onMessageAcknowledged(Account account, String uuid) {
|
||||
public boolean onMessageAcknowledged(final Account account, final Jid to, final String id) {
|
||||
if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
|
||||
final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
|
||||
mJingleConnectionManager.updateProposedSessionDiscovered(
|
||||
account,
|
||||
to,
|
||||
sessionId,
|
||||
JingleConnectionManager.DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
final Jid bare = to.asBareJid();
|
||||
|
||||
for (final Conversation conversation : getConversations()) {
|
||||
if (conversation.getAccount() == account) {
|
||||
Message message = conversation.findUnsentMessageWithUuid(uuid);
|
||||
if (conversation.getAccount() == account && conversation.getJid().asBareJid().equals(bare)) {
|
||||
final Message message = conversation.findUnsentMessageWithUuid(id);
|
||||
if (message != null) {
|
||||
message.setStatus(Message.STATUS_SEND);
|
||||
message.setErrorMessage(null);
|
||||
|
@ -330,7 +345,7 @@ public class XmppConnectionService extends Service {
|
|||
synchronized (account.inProgressConferencePings) {
|
||||
account.inProgressConferencePings.clear();
|
||||
}
|
||||
mJingleConnectionManager.notifyRebound();
|
||||
mJingleConnectionManager.notifyRebound(account);
|
||||
mQuickConversationsService.considerSyncBackground(false);
|
||||
fetchRosterFromServer(account);
|
||||
|
||||
|
@ -356,6 +371,7 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
connectMultiModeConversations(account);
|
||||
syncDirtyContacts(account);
|
||||
|
||||
}
|
||||
};
|
||||
private final AtomicLong mLastExpiryRun = new AtomicLong(0);
|
||||
|
@ -458,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();
|
||||
|
@ -551,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)
|
||||
|
@ -580,10 +595,11 @@ 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.NotAnImageFileException e) {
|
||||
} catch (FileBackend.ImageCompressionException e) {
|
||||
Log.d(Config.LOGTAG, "unable to compress image. fall back to file transfer", e);
|
||||
attachFileToConversation(conversation, uri, mimeType, callback);
|
||||
return;
|
||||
} catch (final FileBackend.FileCopyException e) {
|
||||
|
@ -636,7 +652,7 @@ public class XmppConnectionService extends Service {
|
|||
switch (action) {
|
||||
case QuickConversationsService.SMS_RETRIEVED_ACTION:
|
||||
mQuickConversationsService.handleSmsReceived(intent);
|
||||
break;
|
||||
break;
|
||||
case ConnectivityManager.CONNECTIVITY_ACTION:
|
||||
if (hasInternetConnection()) {
|
||||
if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
|
||||
|
@ -645,6 +661,7 @@ public class XmppConnectionService extends Service {
|
|||
if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
|
||||
resetAllAttemptCounts(true, false);
|
||||
}
|
||||
Resolver.clearCache();
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_SHUTDOWN:
|
||||
|
@ -674,6 +691,7 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
case TorServiceUtils.ACTION_STATUS:
|
||||
final String status = intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
|
||||
//TODO port and host are in 'extras' - but this may not be a reliable source?
|
||||
if ("ON".equals(status)) {
|
||||
handleOrbotStartedEvent();
|
||||
return START_STICKY;
|
||||
|
@ -708,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;
|
||||
}
|
||||
|
@ -716,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");
|
||||
|
@ -758,8 +777,9 @@ public class XmppConnectionService extends Service {
|
|||
break;
|
||||
case Intent.ACTION_SCREEN_ON:
|
||||
deactivateGracePeriod();
|
||||
case Intent.ACTION_USER_PRESENT:
|
||||
case Intent.ACTION_SCREEN_OFF:
|
||||
if (awayWhenScreenOff()) {
|
||||
if (awayWhenScreenLocked()) {
|
||||
refreshAllPresences();
|
||||
}
|
||||
break;
|
||||
|
@ -913,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>() {
|
||||
|
@ -959,7 +983,7 @@ public class XmppConnectionService extends Service {
|
|||
return getBooleanPreference(SettingsActivity.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
|
||||
}
|
||||
|
||||
private boolean awayWhenScreenOff() {
|
||||
private boolean awayWhenScreenLocked() {
|
||||
return getBooleanPreference(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
|
||||
}
|
||||
|
||||
|
@ -970,29 +994,19 @@ public class XmppConnectionService extends Service {
|
|||
private Presence.Status getTargetPresence() {
|
||||
if (dndOnSilentMode() && isPhoneSilenced()) {
|
||||
return Presence.Status.DND;
|
||||
} else if (awayWhenScreenOff() && !isInteractive()) {
|
||||
} else if (awayWhenScreenLocked() && isScreenLocked()) {
|
||||
return Presence.Status.AWAY;
|
||||
} else {
|
||||
return Presence.Status.ONLINE;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@SuppressWarnings("deprecation")
|
||||
public boolean isInteractive() {
|
||||
try {
|
||||
final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
|
||||
final boolean isScreenOn;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
isScreenOn = pm.isScreenOn();
|
||||
} else {
|
||||
isScreenOn = pm.isInteractive();
|
||||
}
|
||||
return isScreenOn;
|
||||
} catch (RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
public boolean isScreenLocked() {
|
||||
final KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||
final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
|
||||
final boolean interactive = powerManager != null && powerManager.isInteractive();
|
||||
return locked || !interactive;
|
||||
}
|
||||
|
||||
private boolean isPhoneSilenced() {
|
||||
|
@ -1070,11 +1084,20 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
|
||||
public boolean hasInternetConnection() {
|
||||
final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
final ConnectivityManager cm = ContextCompat.getSystemService(this, ConnectivityManager.class);
|
||||
if (cm == null) {
|
||||
return true; //if internet connection can not be checked it is probably best to just try
|
||||
}
|
||||
try {
|
||||
final NetworkInfo activeNetwork = cm == null ? null : cm.getActiveNetworkInfo();
|
||||
return activeNetwork != null && (activeNetwork.isConnected() || activeNetwork.getType() == ConnectivityManager.TYPE_ETHERNET);
|
||||
} catch (RuntimeException e) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
final Network activeNetwork = cm.getActiveNetwork();
|
||||
final NetworkCapabilities capabilities = activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork);
|
||||
return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
||||
} else {
|
||||
final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
|
||||
return networkInfo != null && (networkInfo.isConnected() || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET);
|
||||
}
|
||||
} catch (final RuntimeException e) {
|
||||
Log.d(Config.LOGTAG, "unable to check for internet connection", e);
|
||||
return true; //if internet connection can not be checked it is probably best to just try
|
||||
}
|
||||
|
@ -1131,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() {
|
||||
|
@ -1156,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();
|
||||
|
@ -1240,6 +1263,7 @@ public class XmppConnectionService extends Service {
|
|||
public void onDestroy() {
|
||||
try {
|
||||
unregisterReceiver(this.mInternalEventReceiver);
|
||||
unregisterReceiver(this.mInternalScreenEventReceiver);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
//ignored
|
||||
}
|
||||
|
@ -1250,15 +1274,16 @@ 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() {
|
||||
if (awayWhenScreenOff() && !manuallyChangePresence()) {
|
||||
if (awayWhenScreenLocked() && !manuallyChangePresence()) {
|
||||
final IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_SCREEN_ON);
|
||||
filter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||
filter.addAction(Intent.ACTION_USER_PRESENT);
|
||||
registerReceiver(this.mInternalScreenEventReceiver, filter);
|
||||
} else {
|
||||
try {
|
||||
|
@ -1273,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);
|
||||
}
|
||||
|
||||
|
@ -1290,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);
|
||||
|
@ -1785,7 +1810,7 @@ public class XmppConnectionService extends Service {
|
|||
IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString());
|
||||
sendIqPacket(account, request, (a, response) -> {
|
||||
if (response.getType() == IqPacket.TYPE.ERROR) {
|
||||
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getError());
|
||||
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition());
|
||||
}
|
||||
});
|
||||
} else if (connection.getFeatures().bookmarksConversion()) {
|
||||
|
@ -1867,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));
|
||||
|
@ -1907,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() {
|
||||
|
@ -2855,13 +2883,12 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onFetchFailed(final Conversation conversation, Element error) {
|
||||
public void onFetchFailed(final Conversation conversation, final String errorCondition) {
|
||||
if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result");
|
||||
|
||||
return;
|
||||
}
|
||||
if (error != null && "remote-server-not-found".equals(error.getName())) {
|
||||
if ("remote-server-not-found".equals(errorCondition)) {
|
||||
synchronized (account.inProgressConferenceJoins) {
|
||||
account.inProgressConferenceJoins.remove(conversation);
|
||||
}
|
||||
|
@ -3227,7 +3254,7 @@ public class XmppConnectionService extends Service {
|
|||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch");
|
||||
} else {
|
||||
if (callback != null) {
|
||||
callback.onFetchFailed(conversation, packet.getError());
|
||||
callback.onFetchFailed(conversation, packet.getErrorCondition());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3320,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);
|
||||
|
@ -3431,15 +3449,23 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
public void createContact(Contact contact, boolean autoGrant) {
|
||||
public void createContact(final Contact contact, final boolean autoGrant) {
|
||||
createContact(contact, autoGrant, null);
|
||||
}
|
||||
|
||||
public void createContact(final Contact contact, final boolean autoGrant, final String preAuth) {
|
||||
if (autoGrant) {
|
||||
contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
|
||||
contact.setOption(Contact.Options.ASKING);
|
||||
}
|
||||
pushContactToServer(contact);
|
||||
pushContactToServer(contact, preAuth);
|
||||
}
|
||||
|
||||
public void pushContactToServer(final Contact contact) {
|
||||
pushContactToServer(contact, null);
|
||||
}
|
||||
|
||||
private void pushContactToServer(final Contact contact, final String preAuth) {
|
||||
contact.resetOption(Contact.Options.DIRTY_DELETE);
|
||||
contact.setOption(Contact.Options.DIRTY_PUSH);
|
||||
final Account account = contact.getAccount();
|
||||
|
@ -3455,7 +3481,7 @@ public class XmppConnectionService extends Service {
|
|||
sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
|
||||
}
|
||||
if (ask) {
|
||||
sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact));
|
||||
sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
|
||||
}
|
||||
} else {
|
||||
syncRoster(contact.getAccount());
|
||||
|
@ -3522,7 +3548,7 @@ public class XmppConnectionService extends Service {
|
|||
if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
|
||||
callback.onAvatarPublicationSucceeded();
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getError());
|
||||
Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition());
|
||||
callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
|
||||
}
|
||||
});
|
||||
|
@ -3903,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);
|
||||
}
|
||||
|
||||
|
@ -3942,16 +3972,30 @@ public class XmppConnectionService extends Service {
|
|||
return null;
|
||||
}
|
||||
|
||||
public boolean markMessage(Conversation conversation, String uuid, int status, String serverMessageId) {
|
||||
public boolean markMessage(final Conversation conversation, final String uuid, final int status, final String serverMessageId) {
|
||||
return markMessage(conversation, uuid, status, serverMessageId, null);
|
||||
}
|
||||
|
||||
public boolean markMessage(final Conversation conversation, final String uuid, final int status, final String serverMessageId, final LocalizedContent body) {
|
||||
if (uuid == null) {
|
||||
return false;
|
||||
} else {
|
||||
Message message = conversation.findSentMessageWithUuid(uuid);
|
||||
final Message message = conversation.findSentMessageWithUuid(uuid);
|
||||
if (message != null) {
|
||||
if (message.getServerMsgId() == null) {
|
||||
message.setServerMsgId(serverMessageId);
|
||||
}
|
||||
markMessage(message, status);
|
||||
if (message.getEncryption() == Message.ENCRYPTION_NONE
|
||||
&& message.isTypeText()
|
||||
&& isBodyModified(message, body)) {
|
||||
message.setBody(body.content);
|
||||
if (body.count > 1) {
|
||||
message.setBodyLanguage(body.language);
|
||||
}
|
||||
markMessage(message, status, null, true);
|
||||
} else {
|
||||
markMessage(message, status);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -3959,12 +4003,23 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean isBodyModified(final Message message, final LocalizedContent body) {
|
||||
if (body == null || body.content == null) {
|
||||
return false;
|
||||
}
|
||||
return !body.content.equals(message.getBody());
|
||||
}
|
||||
|
||||
public void markMessage(Message message, int status) {
|
||||
markMessage(message, status, null);
|
||||
}
|
||||
|
||||
|
||||
public void markMessage(final Message message, final int status, final String errorMessage) {
|
||||
markMessage(message, status, errorMessage, false);
|
||||
}
|
||||
|
||||
public void markMessage(final Message message, final int status, final String errorMessage, final boolean includeBody) {
|
||||
final int oldStatus = message.getStatus();
|
||||
if (status == Message.STATUS_SEND_FAILED && (oldStatus == Message.STATUS_SEND_RECEIVED || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
|
||||
return;
|
||||
|
@ -3974,7 +4029,7 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
message.setErrorMessage(errorMessage);
|
||||
message.setStatus(status);
|
||||
databaseBackend.updateMessage(message, false);
|
||||
databaseBackend.updateMessage(message, includeBody);
|
||||
updateConversationUi();
|
||||
if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
|
||||
mNotificationService.pushFailedDelivery(message);
|
||||
|
@ -4279,7 +4334,7 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
|
||||
public void sendMessagePacket(Account account, MessagePacket packet) {
|
||||
XmppConnection connection = account.getXmppConnection();
|
||||
final XmppConnection connection = account.getXmppConnection();
|
||||
if (connection != null) {
|
||||
connection.sendMessagePacket(packet);
|
||||
}
|
||||
|
@ -4314,7 +4369,7 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
|
||||
private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
|
||||
Presence.Status status;
|
||||
final Presence.Status status;
|
||||
if (manuallyChangePresence()) {
|
||||
status = account.getPresenceStatus();
|
||||
} else {
|
||||
|
@ -4557,11 +4612,6 @@ public class XmppConnectionService extends Service {
|
|||
syncRoster(account);
|
||||
}
|
||||
} else {
|
||||
if (account.inProgressDiscoFetches.contains(key)) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping duplicate disco request for " + key.second + " to " + jid);
|
||||
return;
|
||||
}
|
||||
account.inProgressDiscoFetches.add(key);
|
||||
final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
|
||||
request.setTo(jid);
|
||||
final String node = presence.getNode();
|
||||
|
@ -4573,7 +4623,7 @@ public class XmppConnectionService extends Service {
|
|||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid);
|
||||
sendIqPacket(account, request, (a, response) -> {
|
||||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||
ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response);
|
||||
final ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response);
|
||||
if (presence.getVer().equals(discoveryResult.getVer())) {
|
||||
databaseBackend.insertDiscoveryResult(discoveryResult);
|
||||
injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult);
|
||||
|
@ -4583,7 +4633,6 @@ public class XmppConnectionService extends Service {
|
|||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to fetch caps from " + jid);
|
||||
}
|
||||
a.inProgressDiscoFetches.remove(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4788,7 +4837,7 @@ public class XmppConnectionService extends Service {
|
|||
public interface OnConferenceConfigurationFetched {
|
||||
void onConferenceConfigurationFetched(Conversation conversation);
|
||||
|
||||
void onFetchFailed(Conversation conversation, Element error);
|
||||
void onFetchFailed(Conversation conversation, String errorCondition);
|
||||
}
|
||||
|
||||
public interface OnConferenceJoined {
|
||||
|
@ -4820,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
|
||||
|
@ -4833,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -5,24 +5,26 @@ import android.content.Intent;
|
|||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import eu.siacs.conversations.BuildConfig;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.utils.PhoneHelper;
|
||||
|
||||
public class AboutPreference extends Preference {
|
||||
public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
final String appName = context.getString(R.string.app_name);
|
||||
setSummary(appName +' '+ PhoneHelper.getVersionName(context));
|
||||
setTitle(context.getString(R.string.title_activity_about_x, appName));
|
||||
setSummaryAndTitle(context);
|
||||
}
|
||||
|
||||
public AboutPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
final String appName = context.getString(R.string.app_name);
|
||||
setSummary(appName +' '+ PhoneHelper.getVersionName(context));
|
||||
setTitle(context.getString(R.string.title_activity_about_x, appName));
|
||||
setSummaryAndTitle(context);
|
||||
}
|
||||
|
||||
private void setSummaryAndTitle(final Context context) {
|
||||
setSummary(String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
|
||||
setTitle(context.getString(R.string.title_activity_about_x, BuildConfig.APP_NAME));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
super.onClick();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 don’t 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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
@ -986,7 +1001,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
menuCall.setVisible(false);
|
||||
menuOngoingCall.setVisible(false);
|
||||
} else {
|
||||
final XmppConnectionService service = activity.xmppConnectionService;
|
||||
final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService;
|
||||
final Optional<OngoingRtpSession> ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact());
|
||||
if (ongoingRtpSession.isPresent()) {
|
||||
menuOngoingCall.setVisible(true);
|
||||
|
@ -994,8 +1009,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
} else {
|
||||
menuOngoingCall.setVisible(false);
|
||||
final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact());
|
||||
final boolean cameraAvailable = activity != null && activity.isCameraFeatureAvailable();
|
||||
menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE);
|
||||
menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO);
|
||||
menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable);
|
||||
}
|
||||
menuContactDetails.setVisible(!this.conversation.withSelf());
|
||||
menuMucDetails.setVisible(false);
|
||||
|
@ -1037,7 +1053,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
messageListAdapter = new MessageAdapter((XmppActivity) getActivity(), this.messageList);
|
||||
messageListAdapter.setOnContactPictureClicked(this);
|
||||
messageListAdapter.setOnContactPictureLongClicked(this);
|
||||
messageListAdapter.setOnQuoteListener(this::quoteText);
|
||||
binding.messagesView.setAdapter(messageListAdapter);
|
||||
|
||||
registerForContextMenu(binding.messagesView);
|
||||
|
@ -1055,7 +1070,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
Log.d(Config.LOGTAG, "ConversationFragment.onDestroyView()");
|
||||
messageListAdapter.setOnContactPictureClicked(null);
|
||||
messageListAdapter.setOnContactPictureLongClicked(null);
|
||||
messageListAdapter.setOnQuoteListener(null);
|
||||
}
|
||||
|
||||
private void quoteText(String text) {
|
||||
|
@ -1263,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);
|
||||
|
@ -1366,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);
|
||||
|
@ -1573,11 +1583,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
} else {
|
||||
res = R.string.no_storage_permission;
|
||||
}
|
||||
Toast.makeText(getActivity(), res, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getActivity(), getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
if (writeGranted(grantResults, permissions)) {
|
||||
if (activity != null && activity.xmppConnectionService != null) {
|
||||
activity.xmppConnectionService.getBitmapCache().evictAll();
|
||||
activity.xmppConnectionService.restartFileObserver();
|
||||
}
|
||||
refresh();
|
||||
|
@ -1607,7 +1618,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
}
|
||||
|
||||
private void createNewConnection(final Message message) {
|
||||
if (!activity.xmppConnectionService.getHttpConnectionManager().checkConnection(message)) {
|
||||
if (!activity.xmppConnectionService.hasInternetConnection()) {
|
||||
Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
@ -1616,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);
|
||||
|
@ -1636,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];
|
||||
|
@ -1652,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();
|
||||
}
|
||||
|
@ -1690,7 +1701,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
this.activity.xmppConnectionService.updateConversation(conversation);
|
||||
this.activity.onConversationsListItemUpdated();
|
||||
refresh();
|
||||
getActivity().invalidateOptionsMenu();
|
||||
requireActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
|
||||
|
@ -1700,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;
|
||||
|
@ -1720,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;
|
||||
|
@ -1805,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");
|
||||
|
@ -1826,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);
|
||||
|
@ -1855,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);
|
||||
|
@ -1960,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());
|
||||
|
@ -2184,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;
|
||||
|
@ -2993,6 +3001,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
final Menu menu = popupMenu.getMenu();
|
||||
menu.findItem(R.id.action_manage_accounts).setVisible(QuickConversationsService.isConversations());
|
||||
popupMenu.setOnMenuItemClickListener(item -> {
|
||||
final XmppActivity activity = this.activity;
|
||||
if (activity == null) {
|
||||
Log.e(Config.LOGTAG,"Unable to perform action. no context provided");
|
||||
return true;
|
||||
}
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_show_qr_code:
|
||||
activity.showQrCode(conversation.getAccount().getShareableUri());
|
||||
|
@ -3044,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -224,7 +228,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
|||
&& getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.battery_optimizations_enabled);
|
||||
builder.setMessage(R.string.battery_optimizations_enabled_dialog);
|
||||
builder.setMessage(getString(R.string.battery_optimizations_enabled_dialog, getString(R.string.app_name)));
|
||||
builder.setPositiveButton(R.string.next, (dialog, which) -> {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
Uri uri = Uri.parse("package:" + getPackageName());
|
||||
|
@ -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);
|
||||
|
|
|
@ -38,7 +38,6 @@ import com.google.common.base.CharMatcher;
|
|||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
@ -78,6 +77,7 @@ import eu.siacs.conversations.xmpp.XmppConnection;
|
|||
import eu.siacs.conversations.xmpp.XmppConnection.Features;
|
||||
import eu.siacs.conversations.xmpp.forms.Data;
|
||||
import eu.siacs.conversations.xmpp.pep.Avatar;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
|
||||
OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
|
||||
|
@ -188,7 +188,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
final boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB;
|
||||
final boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
|
||||
final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl;
|
||||
URL url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null;
|
||||
final HttpUrl url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null;
|
||||
if (url != null && !wasDisabled) {
|
||||
try {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
|
||||
|
@ -531,7 +531,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
}
|
||||
} else {
|
||||
XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
|
||||
URL url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null;
|
||||
HttpUrl url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null;
|
||||
if (url != null) {
|
||||
this.binding.saveButton.setText(R.string.open_website);
|
||||
} else if (inNeedOfSaslAccept()) {
|
||||
|
@ -542,7 +542,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
}
|
||||
} else {
|
||||
XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
|
||||
URL url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null;
|
||||
HttpUrl url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null;
|
||||
if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) {
|
||||
this.binding.saveButton.setText(R.string.open_website);
|
||||
} else {
|
||||
|
@ -736,7 +736,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
public void onNewIntent(final Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
if (intent != null && intent.getData() != null) {
|
||||
final XmppUri uri = new XmppUri(intent.getData());
|
||||
if (xmppConnectionServiceBound) {
|
||||
|
@ -1071,9 +1072,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
} else {
|
||||
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
|
||||
}
|
||||
} else if (features.p1S3FileTransfer()) {
|
||||
this.binding.serverInfoHttpUploadDescription.setText(R.string.p1_s3_filetransfer);
|
||||
this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
|
||||
} else {
|
||||
this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
|
||||
}
|
||||
|
@ -1209,7 +1207,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
this.binding.osOptimization.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE);
|
||||
if (showDataSaverWarning && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled);
|
||||
this.binding.osOptimizationBody.setText(R.string.data_saver_enabled_explained);
|
||||
this.binding.osOptimizationBody.setText(getString(R.string.data_saver_enabled_explained, getString(R.string.app_name)));
|
||||
this.binding.osOptimizationDisable.setText(R.string.allow);
|
||||
this.binding.osOptimizationDisable.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
|
||||
|
@ -1218,13 +1216,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
|
|||
try {
|
||||
startActivityForResult(intent, REQUEST_DATA_SAVER);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_data_saver, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(EditAccountActivity.this, getString(R.string.device_does_not_support_data_saver, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else if (showBatteryWarning && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
this.binding.osOptimizationDisable.setText(R.string.disable);
|
||||
this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled);
|
||||
this.binding.osOptimizationBody.setText(R.string.battery_optimizations_enabled_explained);
|
||||
this.binding.osOptimizationBody.setText(getString(R.string.battery_optimizations_enabled_explained, getString(R.string.app_name)));
|
||||
this.binding.osOptimizationDisable.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
Uri uri = Uri.parse("package:" + getPackageName());
|
||||
|
|
|
@ -31,8 +31,6 @@ import org.osmdroid.views.MapView;
|
|||
import org.osmdroid.views.overlay.CopyrightOverlay;
|
||||
import org.osmdroid.views.overlay.Overlay;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import eu.siacs.conversations.BuildConfig;
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
|
@ -41,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 {
|
||||
|
@ -70,6 +69,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
protected void updateLocationMarkers() {
|
||||
clearMarkers();
|
||||
}
|
||||
|
@ -98,11 +98,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
|
|||
config.load(ctx, getPreferences());
|
||||
config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
|
||||
if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
|
||||
try {
|
||||
config.setHttpProxy(HttpConnectionManager.getProxy());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to configure proxy");
|
||||
}
|
||||
config.setHttpProxy(HttpConnectionManager.getProxy());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -131,6 +131,7 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC
|
|||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
|
||||
CropImage.ActivityResult result = CropImage.getActivityResult(data);
|
||||
if (resultCode == RESULT_OK) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
@ -14,6 +17,7 @@ import android.os.PowerManager;
|
|||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
@ -34,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;
|
||||
|
||||
|
@ -53,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;
|
||||
|
@ -63,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";
|
||||
|
@ -80,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,
|
||||
|
@ -87,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;
|
||||
|
@ -146,6 +161,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
if (xmppConnectionService != null) {
|
||||
if (xmppConnectionService.getNotificationService().stopSoundAndVibration()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
private boolean isHelpButtonVisible() {
|
||||
try {
|
||||
return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());
|
||||
|
@ -376,6 +403,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
if (state != null) {
|
||||
Log.d(Config.LOGTAG, "restored last state from intent extra");
|
||||
updateButtonConfiguration(state);
|
||||
updateVerifiedShield(false);
|
||||
updateStateDisplay(state);
|
||||
updateProfilePicture(state);
|
||||
invalidateOptionsMenu();
|
||||
|
@ -421,7 +449,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
} else {
|
||||
throw new IllegalStateException("Invalid permission result request");
|
||||
}
|
||||
Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -429,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();
|
||||
|
@ -475,15 +505,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
return;
|
||||
}
|
||||
//TODO apparently this method is not getting called on Android 10 when using the task switcher
|
||||
final boolean emptyReference = rtpConnectionReference == null || rtpConnectionReference.get() == null;
|
||||
if (emptyReference && xmppConnectionService != null) {
|
||||
if (emptyReference(rtpConnectionReference) && xmppConnectionService != null) {
|
||||
retractSessionProposal();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -499,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) {
|
||||
|
@ -510,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);
|
||||
|
@ -545,6 +588,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
}
|
||||
this.rtpConnectionReference = reference;
|
||||
final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
|
||||
final boolean verified = requireRtpConnection().isVerified();
|
||||
if (currentState == RtpEndUserState.ENDED) {
|
||||
reference.get().throwStateTransitionException();
|
||||
finish();
|
||||
|
@ -560,6 +604,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
binding.with.setText(getWith().getDisplayName());
|
||||
updateVideoViews(currentState);
|
||||
updateStateDisplay(currentState, media);
|
||||
updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
|
||||
updateButtonConfiguration(currentState, media);
|
||||
updateProfilePicture(currentState);
|
||||
invalidateOptionsMenu();
|
||||
|
@ -578,6 +623,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
updateStateDisplay(state);
|
||||
updateProfilePicture(state);
|
||||
updateCallDuration();
|
||||
updateVerifiedShield(false);
|
||||
invalidateOptionsMenu();
|
||||
binding.with.setText(account.getRoster().getContact(with).getDisplayName());
|
||||
}
|
||||
|
@ -599,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);
|
||||
}
|
||||
|
@ -625,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;
|
||||
|
@ -652,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:
|
||||
|
@ -659,6 +711,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
}
|
||||
}
|
||||
|
||||
private void updateVerifiedShield(final boolean verified) {
|
||||
if (isPictureInPicture()) {
|
||||
this.binding.verified.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void updateProfilePicture(final RtpEndUserState state) {
|
||||
updateProfilePicture(state, null);
|
||||
}
|
||||
|
@ -719,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);
|
||||
|
@ -755,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();
|
||||
|
@ -850,7 +911,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
}
|
||||
|
||||
private void enableVideo(View view) {
|
||||
requireRtpConnection().setVideoEnabled(true);
|
||||
try {
|
||||
requireRtpConnection().setVideoEnabled(true);
|
||||
} catch (final IllegalStateException e) {
|
||||
Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
|
||||
}
|
||||
|
||||
|
@ -878,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -893,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 {
|
||||
|
@ -913,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);
|
||||
|
@ -937,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);
|
||||
|
@ -951,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);
|
||||
}
|
||||
}
|
||||
|
@ -1052,7 +1125,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
updateRtpSessionProposalState(account, with, state);
|
||||
return;
|
||||
}
|
||||
if (this.rtpConnectionReference == null) {
|
||||
if (emptyReference(this.rtpConnectionReference)) {
|
||||
if (END_CARD.contains(state)) {
|
||||
Log.d(Config.LOGTAG, "not reinitializing session");
|
||||
return;
|
||||
|
@ -1062,6 +1135,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
return;
|
||||
}
|
||||
final AbstractJingleConnection.Id id = requireRtpConnection().getId();
|
||||
final boolean verified = requireRtpConnection().isVerified();
|
||||
final Set<Media> media = getMedia();
|
||||
final Contact contact = getWith();
|
||||
if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
|
||||
|
@ -1071,6 +1145,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
}
|
||||
runOnUiThread(() -> {
|
||||
updateStateDisplay(state, media);
|
||||
updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state));
|
||||
updateButtonConfiguration(state, media);
|
||||
updateVideoViews(state);
|
||||
updateProfilePicture(state, contact);
|
||||
|
@ -1120,6 +1195,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
}
|
||||
if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
|
||||
runOnUiThread(() -> {
|
||||
updateVerifiedShield(false);
|
||||
updateStateDisplay(state);
|
||||
updateButtonConfiguration(state);
|
||||
updateProfilePicture(state);
|
||||
|
@ -1147,4 +1223,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
private static boolean emptyReference(final WeakReference<?> weakReference) {
|
||||
return weakReference == null || weakReference.get() == null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -405,12 +415,16 @@ public class SettingsActivity extends XmppActivity implements
|
|||
createBackup();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, getString(R.string.no_storage_permission, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void createBackup() {
|
||||
ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class));
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(R.string.backup_started_message);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void displayToast(final String msg) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
@ -75,7 +77,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, getString(R.string.no_storage_permission, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -237,7 +237,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
|
|||
}
|
||||
this.binding.keyErrorHintMutual.setVisibility(anyWithoutMutualPresenceSubscription ? View.VISIBLE : View.GONE);
|
||||
Contact contact = mAccount.getRoster().getContact(contactJids.get(0));
|
||||
binding.keyErrorGeneral.setText(getString(R.string.error_trustkey_general, contact.getDisplayName()));
|
||||
binding.keyErrorGeneral.setText(getString(R.string.error_trustkey_general, getString(R.string.app_name), contact.getDisplayName()));
|
||||
binding.ownKeysDetails.removeAllViews();
|
||||
if (OmemoSetting.isAlways()) {
|
||||
binding.disableButton.setVisibility(View.GONE);
|
||||
|
|
|
@ -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
|
||||
|
@ -89,21 +104,16 @@ public class UriHandlerActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
savedInstanceState.putBoolean("handled", this.handled);
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
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);
|
||||
|
@ -113,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 (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
|
||||
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) {
|
||||
|
@ -133,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());
|
||||
|
@ -163,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);
|
||||
|
@ -183,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() {
|
||||
|
@ -224,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();
|
||||
|
@ -232,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) == '}';
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ import android.os.IBinder;
|
|||
import android.os.PowerManager;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
@ -51,6 +52,8 @@ import androidx.appcompat.app.AlertDialog.Builder;
|
|||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
|
@ -77,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;
|
||||
|
@ -248,7 +252,7 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(getString(R.string.openkeychain_required));
|
||||
builder.setIconAttribute(android.R.attr.alertDialogIcon);
|
||||
builder.setMessage(getText(R.string.openkeychain_required_long));
|
||||
builder.setMessage(Html.fromHtml(getString(R.string.openkeychain_required_long, getString(R.string.app_name))));
|
||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||
builder.setNeutralButton(getString(R.string.restart),
|
||||
(dialog, which) -> {
|
||||
|
@ -405,11 +409,7 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
metrics = getResources().getDisplayMetrics();
|
||||
ExceptionHelper.init(getApplicationContext());
|
||||
new EmojiService(this).init();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
} else {
|
||||
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
|
||||
}
|
||||
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
this.mTheme = findTheme();
|
||||
setTheme(this.mTheme);
|
||||
}
|
||||
|
@ -572,13 +572,7 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
if (account.getPgpId() == 0) {
|
||||
choosePgpSignId(account);
|
||||
} else {
|
||||
String status = null;
|
||||
if (manuallyChangePresence()) {
|
||||
status = account.getPresenceStatusMessage();
|
||||
}
|
||||
if (status == null) {
|
||||
status = "";
|
||||
}
|
||||
final String status = Strings.nullToEmpty(account.getPresenceStatusMessage());
|
||||
xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback<String>() {
|
||||
|
||||
@Override
|
||||
|
@ -826,8 +820,9 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
protected void onResume(){
|
||||
super.onResume();
|
||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
||||
}
|
||||
|
||||
protected int findTheme() {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue