Compare commits
No commits in common. "develop" and "2.8.10-sum7" have entirely different histories.
develop
...
2.8.10-sum
|
@ -1,38 +0,0 @@
|
|||
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
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ src/quicksyPlaystore/res/values/push.xml
|
|||
# https://github.com/github/gitignore/blob/master/Gradle.gitignore
|
||||
.gradle/
|
||||
build/
|
||||
gradle.properties
|
||||
captures/
|
||||
signing.properties
|
||||
# Ignore Gradle GUI config
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
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-m84.aar http://gultsch.de/files/libwebrtc-m84.aar
|
||||
script:
|
||||
- ./gradlew assembleConversationsFreeSystemRelease
|
||||
- ./gradlew assembleQuicksyFreeCompatRelease
|
||||
|
||||
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.main-strings]
|
||||
[conversations.strings]
|
||||
file_filter = src/main/res/values-<lang>/strings.xml
|
||||
source_file = src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
|
|
100
CHANGELOG.md
100
CHANGELOG.md
|
@ -1,92 +1,5 @@
|
|||
# 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
|
||||
* Back button no longer ends call when call is connected
|
||||
* bug fixes
|
||||
|
||||
### Version 2.9.5
|
||||
|
||||
* Quicksy: Automatically receive verification SMS
|
||||
|
||||
### Version 2.9.4
|
||||
* minor stability improvements for A/V calls
|
||||
* Conversations releases from here on forward require Android 5
|
||||
|
||||
### Version 2.9.3
|
||||
|
||||
* Fixed connectivity issues when different accounts used different SCRAM mechanisms
|
||||
* Add support for SCRAM-SHA-512
|
||||
* Allow P2P (Jingle) file transfer with self contact
|
||||
|
||||
### Version 2.9.2
|
||||
|
||||
* Offer Easy Invite generation on supporting servers
|
||||
* Display GIFs send from Movim
|
||||
* store avatars in cache
|
||||
|
||||
### Version 2.9.1
|
||||
|
||||
* fixed search on Android <= 5
|
||||
* optimize memory consumption
|
||||
|
||||
### Version 2.9.0
|
||||
|
||||
* Search individual conversations
|
||||
* Notify user if message delivery fails
|
||||
* Remember display names (nicks) from Quicksy users across restarts
|
||||
* Add button to start Orbot (Tor) from notification if necessary
|
||||
|
||||
### Version 2.8.10
|
||||
|
||||
* Handle GPX files
|
||||
|
@ -499,9 +412,10 @@
|
|||
* Icons for attach menu
|
||||
|
||||
### Version 1.16.2
|
||||
* change mam catchup strategy. support mam:1
|
||||
* change mam catchup strategie. support mam:1
|
||||
* bug fixes
|
||||
|
||||
|
||||
### Version 1.16.1
|
||||
* UI performance fixes
|
||||
* bug fixes
|
||||
|
@ -551,7 +465,7 @@
|
|||
* bug fixes
|
||||
|
||||
### Version 1.14.6
|
||||
* make error notification dismissible
|
||||
* make error notification dismissable
|
||||
* bug fixes
|
||||
|
||||
|
||||
|
@ -575,7 +489,7 @@
|
|||
* bug fixes
|
||||
|
||||
### Version 1.14.0
|
||||
* Improvements for N
|
||||
* Improvments for N
|
||||
* Quick Reply to Notifications on N
|
||||
* Don't download avatars and files when data saver is on
|
||||
* bug fixes
|
||||
|
@ -753,7 +667,7 @@
|
|||
|
||||
### Version 1.7.0
|
||||
* CAPTCHA support
|
||||
* SASL EXTERNAL (client certificates)
|
||||
* SASL EXTERNAL (client certifiates)
|
||||
* fetching MUC history via MAM
|
||||
* redownload deleted files from HTTP hosts
|
||||
* Expert setting to automatically set presence
|
||||
|
@ -861,7 +775,7 @@
|
|||
* accept more ciphers
|
||||
|
||||
### Version 1.0
|
||||
* MUC controls (Affiliation changes)
|
||||
* MUC controls (Affiliaton changes)
|
||||
* Added download button to notification
|
||||
* Added check box to hide offline contacts
|
||||
* Use Material theme and icons on Android L
|
||||
|
@ -967,7 +881,7 @@
|
|||
* XEP-0333. Mark whether the other party has read your messages
|
||||
* Delayed messages are now tagged properly
|
||||
* Share images from the Gallery
|
||||
* Infinite history scrolling
|
||||
* Infinit history scrolling
|
||||
* Mark the last used presence in presence selection dialog
|
||||
|
||||
### Version 0.3
|
||||
|
|
18
README.md
18
README.md
|
@ -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)](https://help.conversations.im)
|
||||
* Encrypted audio and video calls (DTLS-SRTP)
|
||||
* Share your location
|
||||
* Send voice messages
|
||||
* Indication when your contact has read your message
|
||||
|
@ -139,9 +139,9 @@ 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).
|
||||
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 explicity 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).
|
||||
|
||||
|
||||
#### I’m getting this annoying permanent notification
|
||||
|
@ -149,7 +149,7 @@ Starting with Conversations 2.3.6 Conversations releases distributed over the Go
|
|||
|
||||
However you can disable the notification via settings of the operating system. (Not settings in Conversations.)
|
||||
|
||||
**The battery consumption and the entire behavior of Conversations will remain the same (as good or as bad as it was before). Why is Google doing this to you? We have no idea.**
|
||||
**The battery consumption and the entire behaviour of Conversations will remain the same (as good or as bad as it was before). Why is Google doing this to you? We have no idea.**
|
||||
|
||||
##### Android <= 7.1 or Conversations from F-Droid (all Android versions)
|
||||
The foreground notification is still controlled over the expert settings within Conversations as it always has been. Whether or not you need to enable it depends on how aggressive the non-standard 'power saving' features are that your phone vendor has built into the operating system.
|
||||
|
@ -173,7 +173,7 @@ You can find a detailed description of how your server, the app server and FCM a
|
|||
|
||||
|
||||
#### But why do I need a permanent notification if I use Google Push?
|
||||
FCM (Google Push) allows an app to wake up from *Doze* which is (as the name suggests) a hibernation feature of the Android operating system that cuts the network connection and also reduces the number of times the app is allowed to wake up (to ping the server for example). The app can ask to be excluded from doze. Non push variants of the app (from F-Droid or if the server doesn’t support it) will do this on first start up. So if you get exemption from *Doze*, or if you get regular push events sent to you, Doze should not pose a threat to Conversatons working properly. But even with *Doze* the app is still open in the background (kept in memory); it is just limited in the actions it can do. Conversations needs to stay in memory to hold certain session state (online status of contacts, join status of group chats, …). However with Android 8 Google changed all of this again and now an App that wants to stay in memory needs to have a foreground service which is visible to the user via the annoying notification. But why does Conversations need to hold that state? XMPP is a statefull protocol that has a lot of per-session information; packets need to be counted, presence information needs to be held, some features like Message Carbons get activated once per session, MAM catch-up happens once, service discovery happens only once; the list goes on. When Conversations was created in early 2014 none of this was a problem because apps were just allowed to stay in memory. Basically every XMPP client out there holds that information in memory because it would be a lot more complicated trying to persist it to disk. An entire rewrite of Conversations in the year 2019 would attempt to do that and would probably succeed however it would require exactly that; a complete rewrite which is not feasible right now. That’s by the way also the reason why it is difficult to write an XMPP client on iOS. Or more broadly put this is also the reason why other protocols are designed as or migrated to stateless protocols (often based on HTTP); take for example the migration of IMAP to [JMAP](https://jmap.io/).
|
||||
FCM (Google Push) allows an app to wake up from *Doze* which is (as the name suggests) a hibernation feature of the Android operating system that cuts the network connection and also reduces the number of times the app is allowed to wake up (to ping the server for example). The app can ask to be excluded from doze. Non push variants of the app (from F-Droid or if the server doesn’t support it) will do this on first start up. So if you get exemption from *Doze*, or if you get regular push events sent to you, Doze should not pose a threat to Conversatons working properly. But even with *Doze* the app is still open in the background (kept in memory); it is just limited in the actions it can do. Conversations needs to stay in memory to hold certain session state (online status of contacts, join status of group chats, …). However with Android 8 Google changed all of this again and now an App that wants to stay in memory needs to have a foreground service which is visible to the user via the annoying notification. But why does Conversations need to hold that state? XMPP is a stateful protocol that has a lot of per-session information; packets need to be counted, presence information needs to be held, some features like Message Carbons get activated once per session, MAM catchup happens once, service discovery happens only once; the list goes on. When Conversations was created in early 2014 none of this was a problem because apps were just allowed to stay in memory. Basically every XMPP client out there holds that information in memory because it would be a lot more complicated trying to persist it to disk. An entire rewrite of Conversations in the year 2019 would attempt to do that and would probably succeed however it would require exactly that; a complete rewrite which is not feasible right now. That’s by the way also the reason why it is difficult to write an XMPP client on iOS. Or more broadly put this is also the reason why other protocols are designed as or migrated to stateless protocols (often based on HTTP); take for example the migration of IMAP to [JMAP](https://jmap.io/).
|
||||
|
||||
#### Conversations doesn’t work for me. Where can I get help?
|
||||
|
||||
|
@ -267,11 +267,11 @@ and introduce yourself to `iNPUTmice` so he can approve your join request.
|
|||
#### How do I backup / move Conversations to a new device?
|
||||
On the one hand Conversations supports Message Archive Management to keep a server side history of your messages so when migrating to a new device that device can display your entire history. However that does not work if you enable OMEMO due to its forward secrecy. (Read [The State of Mobile XMPP in 2016](https://gultsch.de/xmpp_2016.html) especially the section on encryption.)
|
||||
|
||||
As of version 2.4.0 an integrated Backup & Restore function will help with this, go to Settings and you’ll find a setting called Create backup. A notification will pop-up during the creation process that will announce you when it's ready. After the files, one for each account, are created, you can move the **Conversations** folder *(if you want your old media files too)* or only the **Conversations/Backup** folder *(for OMEMO keys and history only)* to your new device (or to a storage place) where a freshly installed Conversations can restore each account. Don't forget to enable the accounts after a successfull restore.
|
||||
As of version 2.4.0 an integrated Backup & Restore function will help with this, go to Settings and you’ll find a setting called Create backup. A notification will pop-up during the creation process that will announce you when it's ready. After the files, one for each account, are created, you can move the **Conversations** folder *(if you want your old media files too)* or only the **Conversations/Backup** folder *(for OMEMO keys and history only)* to your new device (or to a storage place) where a freshly installed Conversations can restore each account. Don't forget to enable the accounts after a succesful restore.
|
||||
|
||||
This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore.
|
||||
|
||||
In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine.
|
||||
In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the offical backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine.
|
||||
|
||||
**WARNING**: Be sure to know your accounts passwords or find ways to reset them **before** doing the backup as the files are encrypted using those passwords and the Restore process will ask for them.
|
||||
**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device.
|
||||
|
@ -327,7 +327,7 @@ OMEMO has two requirements: Your server and the server of your contact need to s
|
|||
OMEMO encryption works only in private (members only) conferences that are non-anonymous. Non-anonymous (being able to discover the real JID of other participants) is a technical requirement to discover the key material. Members only is a sort of arbitrary requirement imposed by Conversations. (see 'OMEMO is grayed out')
|
||||
|
||||
The server of all participants need to pass the OMEMO [Compliance Test](https://conversations.im/compliance/).
|
||||
In other words they either need to run ejabberd 18.01+ or Prosody 0.11+.
|
||||
In other words they either need to run Ejabberd 18.01+ or Prosody 0.11+.
|
||||
|
||||
(Alternatively it would also work if all participants had each other in their contact list; But that rarely is the case in larger group chats.)
|
||||
|
||||
|
@ -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 M90 release and renamed the file name to `libwebrtc-m90.aar` put potentially you can
|
||||
uses the stable M81 release and renamed the file name to `libwebrtc-m81.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.
|
||||
|
|
117
build.gradle
117
build.gradle
|
@ -1,12 +1,14 @@
|
|||
import com.android.build.OutputFile
|
||||
|
||||
// Top-level build file where you can add configuration options common to all
|
||||
// sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,8 +16,8 @@ apply plugin: 'com.android.application'
|
|||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
@ -24,35 +26,37 @@ configurations {
|
|||
conversationsFreeCompatImplementation
|
||||
conversationsPlaystoreCompatImplementation
|
||||
conversationsPlaystoreSystemImplementation
|
||||
quicksyPlaystoreCompatImplementation
|
||||
quicksyPlaystoreSystemImplementation
|
||||
quicksyFreeCompatImplementation
|
||||
quicksyImplementation
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.viewpager:viewpager:1.0.0'
|
||||
ext {
|
||||
supportLibVersion = '28.0.0'
|
||||
}
|
||||
|
||||
playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') {
|
||||
dependencies {
|
||||
//should remain that low because later versions introduce dependency to androidx (not sure exactly from what version)
|
||||
playstoreImplementation('com.google.firebase:firebase-messaging:17.3.4') {
|
||||
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.1'
|
||||
quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.1'
|
||||
conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:1.1.2")
|
||||
conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:1.1.2")
|
||||
implementation 'org.sufficientlysecure:openpgp-api:10.0'
|
||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||
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.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'
|
||||
implementation('com.theartofdev.edmodo:android-image-cropper:2.7.+') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
exclude group: 'com.android.support', module: 'exifinterface'
|
||||
}
|
||||
implementation "com.android.support:support-v13:$supportLibVersion"
|
||||
implementation "com.android.support:appcompat-v7:$supportLibVersion"
|
||||
implementation "com.android.support:exifinterface:$supportLibVersion"
|
||||
implementation "com.android.support:cardview-v7:$supportLibVersion"
|
||||
implementation "com.android.support:support-emoji:$supportLibVersion"
|
||||
implementation "com.android.support:design:$supportLibVersion"
|
||||
compatImplementation "com.android.support:support-emoji-appcompat:$supportLibVersion"
|
||||
conversationsFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion"
|
||||
quicksyFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion"
|
||||
implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
|
||||
//zxing stopped supporting Java 7 so we have to stick with 3.3.3
|
||||
//https://github.com/zxing/zxing/issues/1170
|
||||
|
@ -62,23 +66,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 'com.otaliastudios:transcoder:0.10.4'
|
||||
|
||||
implementation 'org.jxmpp:jxmpp-jid:1.0.2'
|
||||
implementation 'org.osmdroid:osmdroid-android:6.1.10'
|
||||
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 'org.hsluv:hsluv:0.2'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.2.1'
|
||||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||
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'
|
||||
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.11.1'
|
||||
//implementation fileTree(include: ['libwebrtc-m83.aar'], dir: 'libs')
|
||||
implementation 'org.webrtc:google-webrtc:1.0.30039'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -91,27 +94,28 @@ android {
|
|||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 4202301
|
||||
versionName "2.10.2"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
versionCode 397
|
||||
versionName "2.8.10"
|
||||
archivesBaseName += "-$versionName"
|
||||
applicationId "eu.sum7.conversations"
|
||||
resValue "string", "applicationId", applicationId
|
||||
def appName = "Conv6ations"
|
||||
resValue "string", "app_name", appName
|
||||
buildConfigField "String", "APP_NAME", "\"$appName\"";
|
||||
resValue "string", "app_name", "Conv6ations"
|
||||
buildConfigField "String", "LOGTAG", "\"conver6ations\""
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
@ -124,11 +128,9 @@ android {
|
|||
quicksy {
|
||||
dimension "mode"
|
||||
applicationId = "im.quicksy.client"
|
||||
resValue "string", "app_name", "Quicksy"
|
||||
resValue "string", "applicationId", applicationId
|
||||
|
||||
def appName = "Quicksy"
|
||||
resValue "string", "app_name", appName
|
||||
buildConfigField "String", "APP_NAME", "\"$appName\"";
|
||||
buildConfigField "String", "LOGTAG", "\"quicksy\""
|
||||
}
|
||||
|
||||
conversations {
|
||||
|
@ -150,21 +152,14 @@ android {
|
|||
}
|
||||
|
||||
sourceSets {
|
||||
quicksyFreeSystem {
|
||||
java {
|
||||
srcDir 'src/quicksyFree/java'
|
||||
}
|
||||
}
|
||||
quicksyFreeCompat {
|
||||
java {
|
||||
srcDir 'src/freeCompat/java'
|
||||
srcDir 'src/quicksyFree/java'
|
||||
}
|
||||
}
|
||||
quicksyPlaystoreCompat {
|
||||
java {
|
||||
srcDir 'src/playstoreCompat/java'
|
||||
srcDir 'src/quicksyPlaystore/java'
|
||||
}
|
||||
res {
|
||||
srcDir 'src/playstoreCompat/res'
|
||||
|
@ -172,9 +167,6 @@ android {
|
|||
}
|
||||
}
|
||||
quicksyPlaystoreSystem {
|
||||
java {
|
||||
srcDir 'src/quicksyPlaystore/java'
|
||||
}
|
||||
res {
|
||||
srcDir 'src/quicksyPlaystore/res'
|
||||
}
|
||||
|
@ -262,4 +254,5 @@ android {
|
|||
exclude 'META-INF/BCKEY.DSA'
|
||||
exclude 'META-INF/BCKEY.SF'
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet href="../style.xsl" type="text/xsl"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<Project xmlns="http://usefulinc.com/ns/doap#"
|
||||
xmlns:foaf="http://xmlns.com/foaf/0.1/"
|
||||
xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#"
|
||||
xmlns:schema="https://schema.org/">
|
||||
<Project xmlns="http://usefulinc.com/ns/doap#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#">
|
||||
<name>Conversations</name>
|
||||
|
||||
<created>2014-01-14</created>
|
||||
|
@ -25,21 +22,13 @@
|
|||
<!-- See https://github.com/ewilderj/doap/issues/49 -->
|
||||
<language>en</language>
|
||||
|
||||
<schema:logo rdf:resource="https://raw.githubusercontent.com/iNPUTmice/Conversations/master/art/ic_launcher.svg"/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png'/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/02.png'/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/03.png'/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/04.png'/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/05.png'/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/06.png'/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/07.png'/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/08.png'/>
|
||||
<schema:screenshot rdf:resource='https://raw.githubusercontent.com/iNPUTmice/Conversations/master/fastlane/metadata/android/en-US/images/phoneScreenshots/09.png'/>
|
||||
<logo rdf:resource="https://raw.githubusercontent.com/iNPUTmice/Conversations/master/doap.rdf"/>
|
||||
|
||||
<programming-language>Java</programming-language>
|
||||
|
||||
<os>Android</os>
|
||||
|
||||
<!-- TODO: Categories are URIs, find a better location for them. -->
|
||||
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-xmpp"/>
|
||||
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-jabber"/>
|
||||
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-client"/>
|
||||
|
@ -91,28 +80,6 @@
|
|||
<xmpp:version>1.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0049.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.2</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html"/>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
<xmpp:version>1.2</xmpp:version>
|
||||
<xmpp:note xml:lang='en'>Avatars only</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html"/>
|
||||
|
@ -141,14 +108,6 @@
|
|||
<xmpp:version>1.5.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html"/>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
<xmpp:version>1.1</xmpp:version>
|
||||
<xmpp:note xml:lang='en'>Read only. Publication via XEP-0398</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html"/>
|
||||
|
@ -162,14 +121,7 @@
|
|||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0166.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.1.2</xmpp:version>
|
||||
<xmpp:note>File transfer + A/V calls</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0167.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.2.1</xmpp:version>
|
||||
<xmpp:note>File transfer only</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
|
@ -180,13 +132,6 @@
|
|||
<xmpp:note>read only</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0176.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
|
||||
|
@ -215,27 +160,6 @@
|
|||
<xmpp:version>2.0.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>2.0.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0215.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>0.7</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0223.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html"/>
|
||||
|
@ -285,25 +209,11 @@
|
|||
<xmpp:version>0.13.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0293.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.0.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0294.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.2.0</xmpp:version>
|
||||
<xmpp:version>1.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
|
@ -321,13 +231,6 @@
|
|||
<xmpp:note>opt-in</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0320.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html"/>
|
||||
|
@ -335,20 +238,6 @@
|
|||
<xmpp:version>0.3</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0338.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0339.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0352.html"/>
|
||||
|
@ -356,13 +245,6 @@
|
|||
<xmpp:version>0.3.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0353.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>0.3.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0357.html"/>
|
||||
|
@ -371,13 +253,6 @@
|
|||
<xmpp:note>Only available in the version distributed over Google Play</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html"/>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html"/>
|
||||
|
@ -453,19 +328,12 @@
|
|||
<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.9.13</revision>
|
||||
<created>2021-05-03</created>
|
||||
<file-release rdf:resource="https://github.com/iNPUTmice/Conversations/archive/2.9.13.tar.gz"/>
|
||||
<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"/>
|
||||
</Version>
|
||||
</release>
|
||||
</Project>
|
|
@ -0,0 +1,25 @@
|
|||
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.
|
|
@ -0,0 +1,32 @@
|
|||
* 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
|
|
@ -0,0 +1,97 @@
|
|||
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
|
|
@ -1,3 +0,0 @@
|
|||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx4096m
|
|
@ -1,6 +1,6 @@
|
|||
#Sat Nov 14 09:59:55 CET 2020
|
||||
#Sun Jul 26 11:32:42 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
• WebRTC update (with security fixes)
|
|
@ -1,4 +0,0 @@
|
|||
• Search individual conversations
|
||||
• Notify user if message delivery fails
|
||||
• Remember display names (nicks) from Quicksy users across restarts
|
||||
• Add button to start Orbot (Tor) from notification if necessary
|
|
@ -1,3 +0,0 @@
|
|||
• Offer Easy Invite generation on supporting servers
|
||||
• Display GIFs send from Movim
|
||||
• store avatars in cache
|
|
@ -1,4 +0,0 @@
|
|||
• Fixed connectivity issues when different accounts used different SCRAM mechanisms
|
||||
• Add support for SCRAM-SHA-512
|
||||
• Allow P2P (Jingle) file transfer with self contact
|
||||
• minor stability improvements for A/V calls
|
|
@ -1,3 +0,0 @@
|
|||
• Show call button for offline contacts if they previously announced support
|
||||
• Back button no longer ends call when call is connected
|
||||
• bug fixes
|
|
@ -1 +0,0 @@
|
|||
• fix crashes (error on internal database migration)
|
|
@ -1,4 +0,0 @@
|
|||
• 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)
|
|
@ -1,2 +0,0 @@
|
|||
• Verify A/V calls with preexisting OMEMO sessions
|
||||
• Improve compatibility with non libwebrtc WebRTC implementations
|
|
@ -1 +0,0 @@
|
|||
• Various bug fixes around Tor support
|
|
@ -1,3 +0,0 @@
|
|||
• 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
|
|
@ -1,3 +0,0 @@
|
|||
• Always verify domain name. No user overwrite
|
||||
• Support roster pre authentication
|
||||
• minor A/V improvements
|
|
@ -1,3 +0,0 @@
|
|||
• Show black bars when remote video does not match aspect ratio of screen
|
||||
• Improve search performance
|
||||
• Add setting to prevent screenshots
|
|
@ -1,2 +0,0 @@
|
|||
• Fix issue with some videos not being compressed
|
||||
• Fix rare crash when opening notification
|
|
@ -1,2 +0,0 @@
|
|||
• Fix crash when rendering some quotes
|
||||
• Fix crash in welcome screen
|
|
@ -1 +0,0 @@
|
|||
• Fix usage directTLS of manuelle enter an address
|
|
@ -26,15 +26,6 @@
|
|||
-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>;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package eu.siacs.conversations.ui.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.text.emoji.widget.EmojiAppCompatEditText;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.emoji.widget.EmojiAppCompatEditText;
|
||||
|
||||
public class EmojiWrapperEditText extends EmojiAppCompatEditText {
|
||||
|
||||
public EmojiWrapperEditText(Context context) {
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
package eu.siacs.conversations.utils;
|
||||
|
||||
import androidx.emoji.text.EmojiCompat;
|
||||
import android.support.text.emoji.EmojiCompat;
|
||||
|
||||
public class EmojiWrapper {
|
||||
|
||||
|
|
|
@ -20,10 +20,6 @@
|
|||
android:name=".ui.MagicCreateActivity"
|
||||
android:label="@string/create_new_account"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.EasyOnboardingInviteActivity"
|
||||
android:label="@string/invite_to_app"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.ImportBackupActivity"
|
||||
android:label="@string/restore_backup"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
|
|
@ -12,11 +12,10 @@ import android.net.Uri;
|
|||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.io.CountingInputStream;
|
||||
|
@ -61,7 +60,7 @@ import eu.siacs.conversations.xmpp.Jid;
|
|||
public class ImportBackupService extends Service {
|
||||
|
||||
private static final int NOTIFICATION_ID = 21;
|
||||
private static final AtomicBoolean running = new AtomicBoolean(false);
|
||||
private static AtomicBoolean running = new AtomicBoolean(false);
|
||||
private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
|
||||
private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
|
||||
private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
package eu.siacs.conversations.services;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
|
||||
public class QuickConversationsService extends AbstractQuickConversationsService {
|
||||
|
||||
QuickConversationsService(XmppConnectionService xmppConnectionService) {
|
||||
|
@ -30,9 +25,4 @@ public class QuickConversationsService extends AbstractQuickConversationsService
|
|||
public void considerSyncBackground(boolean force) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSmsReceived(Intent intent) {
|
||||
Log.d(Config.LOGTAG,"ignoring received SMS");
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Point;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.ActivityEasyInviteBinding;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.services.BarcodeProvider;
|
||||
import eu.siacs.conversations.utils.EasyOnboardingInvite;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOnboardingInvite.OnInviteRequested {
|
||||
|
||||
private ActivityEasyInviteBinding binding;
|
||||
|
||||
private EasyOnboardingInvite easyOnboardingInvite;
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_easy_invite);
|
||||
setSupportActionBar(binding.toolbar);
|
||||
configureActionBar(getSupportActionBar(), true);
|
||||
this.binding.shareButton.setOnClickListener(v -> share());
|
||||
if (bundle != null && bundle.containsKey("invite")) {
|
||||
this.easyOnboardingInvite = bundle.getParcelable("invite");
|
||||
if (this.easyOnboardingInvite != null) {
|
||||
showInvite(this.easyOnboardingInvite);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.showLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.easy_onboarding_invite, menu);
|
||||
final MenuItem share = menu.findItem(R.id.action_share);
|
||||
share.setVisible(easyOnboardingInvite != null);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
if (menuItem.getItemId() == R.id.action_share) {
|
||||
share();
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(menuItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void share() {
|
||||
final String shareText = getString(
|
||||
R.string.easy_invite_share_text,
|
||||
easyOnboardingInvite.getDomain(),
|
||||
easyOnboardingInvite.getShareableLink()
|
||||
);
|
||||
final Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
|
||||
sendIntent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_invite_with)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshUiReal() {
|
||||
invalidateOptionsMenu();
|
||||
if (easyOnboardingInvite != null) {
|
||||
showInvite(easyOnboardingInvite);
|
||||
} else {
|
||||
showLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private void showLoading() {
|
||||
this.binding.inProgress.setVisibility(View.VISIBLE);
|
||||
this.binding.invite.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void showInvite(final EasyOnboardingInvite invite) {
|
||||
this.binding.inProgress.setVisibility(View.GONE);
|
||||
this.binding.invite.setVisibility(View.VISIBLE);
|
||||
this.binding.tapToShare.setText(getString(R.string.tap_share_button_send_invite, invite.getDomain()));
|
||||
final Point size = new Point();
|
||||
getWindowManager().getDefaultDisplay().getSize(size);
|
||||
final int width = Math.min(size.x, size.y);
|
||||
final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableLink(), width);
|
||||
binding.qrCode.setImageBitmap(bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
super.onSaveInstanceState(bundle);
|
||||
if (easyOnboardingInvite != null) {
|
||||
bundle.putParcelable("invite", easyOnboardingInvite);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void onBackendConnected() {
|
||||
if (easyOnboardingInvite != null) {
|
||||
return;
|
||||
}
|
||||
final Intent launchIntent = getIntent();
|
||||
final String accountExtra = launchIntent.getStringExtra(EXTRA_ACCOUNT);
|
||||
final Jid jid = accountExtra == null ? null : Jid.ofEscaped(accountExtra);
|
||||
if (jid == null) {
|
||||
return;
|
||||
}
|
||||
final Account account = xmppConnectionService.findAccountByJid(jid);
|
||||
xmppConnectionService.requestEasyOnboardingInvite(account, this);
|
||||
}
|
||||
|
||||
public static void launch(final Account account, final Activity context) {
|
||||
final Intent intent = new Intent(context, EasyOnboardingInviteActivity.class);
|
||||
intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inviteRequested(EasyOnboardingInvite invite) {
|
||||
this.easyOnboardingInvite = invite;
|
||||
Log.d(Config.LOGTAG, "invite requested");
|
||||
refreshUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inviteRequestFailed(final String message) {
|
||||
runOnUiThread(() -> {
|
||||
if (!Strings.isNullOrEmpty(message)) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
finish();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,21 +5,21 @@ import android.content.Context;
|
|||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -29,7 +29,6 @@ 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 {
|
||||
|
@ -49,19 +48,13 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
|||
setTheme(this.mTheme);
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
|
||||
setSupportActionBar(binding.toolbar);
|
||||
setSupportActionBar((Toolbar) binding.toolbar);
|
||||
setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false));
|
||||
this.backupFileAdapter = new BackupFileAdapter();
|
||||
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) {
|
||||
getMenuInflater().inflate(R.menu.import_backup, menu);
|
||||
|
@ -131,8 +124,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
|||
try {
|
||||
final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
|
||||
showEnterPasswordDialog(backupFile, finishOnCancel);
|
||||
} catch (final IOException | IllegalArgumentException e) {
|
||||
Log.d(Config.LOGTAG, "unable to open backup file " + uri, e);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
@ -188,7 +180,6 @@ 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);
|
||||
|
@ -233,17 +224,15 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
|||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_open_backup_file) {
|
||||
openBackupFile();
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@ package eu.siacs.conversations.ui;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
|
@ -61,7 +61,7 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher {
|
|||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
this.binding = DataBindingUtil.setContentView(this, R.layout.magic_create);
|
||||
setSupportActionBar(this.binding.toolbar);
|
||||
setSupportActionBar((Toolbar) this.binding.toolbar);
|
||||
configureActionBar(getSupportActionBar(), this.domain == null);
|
||||
if (username != null && domain != null) {
|
||||
binding.title.setText(R.string.your_server_invitation);
|
||||
|
|
|
@ -5,6 +5,9 @@ import android.content.Intent;
|
|||
import android.os.Bundle;
|
||||
import android.security.KeyChain;
|
||||
import android.security.KeyChainAliasCallback;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Pair;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
|
@ -15,10 +18,6 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
|
|||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -32,8 +31,8 @@ import eu.siacs.conversations.services.XmppConnectionService;
|
|||
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
|
||||
import eu.siacs.conversations.ui.adapter.AccountAdapter;
|
||||
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
|
||||
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
|
||||
|
@ -227,8 +226,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);
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
if (grantResults.length > 0) {
|
||||
if (allGranted(grantResults)) {
|
||||
switch (requestCode) {
|
||||
|
|
|
@ -2,12 +2,12 @@ package eu.siacs.conversations.ui;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
|
@ -66,7 +66,7 @@ public class PickServerActivity extends XmppActivity {
|
|||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
ActivityPickServerBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_pick_server);
|
||||
setSupportActionBar(binding.toolbar);
|
||||
setSupportActionBar((Toolbar) binding.toolbar);
|
||||
configureActionBar(getSupportActionBar());
|
||||
binding.useCim.setOnClickListener(v -> {
|
||||
final Intent intent = new Intent(this, MagicCreateActivity.class);
|
||||
|
|
|
@ -4,19 +4,20 @@ import android.Manifest;
|
|||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.security.KeyChain;
|
||||
import android.security.KeyChainAliasCallback;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -106,8 +107,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(final Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
public void onNewIntent(Intent intent) {
|
||||
if (intent != null) {
|
||||
setIntent(intent);
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
|
|||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
ActivityWelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome);
|
||||
setSupportActionBar(binding.toolbar);
|
||||
setSupportActionBar((Toolbar) binding.toolbar);
|
||||
configureActionBar(getSupportActionBar(), false);
|
||||
binding.registerNewAccount.setOnClickListener(v -> {
|
||||
final Intent intent = new Intent(this, PickServerActivity.class);
|
||||
|
@ -202,7 +202,6 @@ 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)) {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package eu.siacs.conversations.ui.adapter;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -12,10 +15,6 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
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");
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:background="?attr/color_background_primary"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/in_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/invite"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="@dimen/activity_horizontal_margin"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:layout_marginRight="@dimen/activity_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/activity_vertical_margin"
|
||||
android:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tap_to_share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tap_share_button_send_invite"
|
||||
android:textAppearance="@style/TextAppearance.Conversations.Body1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scan_the_code"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/tap_to_share"
|
||||
android:layout_marginTop="24sp"
|
||||
android:text="@string/if_contact_is_nearby_use_qr"
|
||||
android:textAppearance="@style/TextAppearance.Conversations.Body1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qr_code"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_above="@+id/share_button"
|
||||
android:layout_below="@id/scan_the_code"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="24sp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/share_button"
|
||||
style="@style/Widget.Conversations.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:text="@string/share"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textColor="?attr/colorAccent" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
|
@ -26,20 +26,20 @@
|
|||
|
||||
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/color_background_primary">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/color_background_primary"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
|
@ -22,7 +22,7 @@
|
|||
android:text="@string/restore_warning"
|
||||
android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/account_password_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -42,6 +42,6 @@
|
|||
android:textColor="?attr/edit_text_color"
|
||||
style="@style/Widget.Conversations.EditText"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</layout>
|
|
@ -1,10 +0,0 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:icon="?attr/icon_share"
|
||||
android:title="@string/invite"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -3,18 +3,4 @@
|
|||
<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 أخرى أو إستعملت تطبيق كونفرسايشنز سابقا. أو يمكنك صنع حساب 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>
|
||||
</resources>
|
||||
|
|
|
@ -5,13 +5,4 @@
|
|||
<string name="create_new_account">Създаване не нов профил</string>
|
||||
<string name="do_you_have_an_account">Имате ли вече XMPP профил? Това може да се случи, ако вече използвате друг клиент на XMPP или сте използвали преди това Conversations. Ако не, можете да създадете нов XMPP профил в момента.\nСъвет: Някои доставчици на имейл също предоставят XMPP профили.
|
||||
</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. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на акаунт.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP.</string>
|
||||
<string name="your_server_invitation">Вашата покана за сървъра</string>
|
||||
<string name="improperly_formatted_provisioning">Неправилно форматиран код за достъп</string>
|
||||
<string name="tap_share_button_send_invite">Докоснете бутона за споделяне, за да изпратите на контакта си покана за %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Ако контактът Ви е наблизо, може да сканира кода по-долу, за да приеме поканата Ви.</string>
|
||||
<string name="easy_invite_share_text">Присъедини се в %1$s и си пиши с мен: %2$s</string>
|
||||
<string name="share_invite_with">Споделяне на поканата чрез…</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?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>
|
|
@ -2,16 +2,4 @@
|
|||
<resources>
|
||||
<string name="pick_a_server">Triï el seu proveïdor de XMPP
|
||||
</string>
|
||||
<string name="use_chat.sum7.eu">Fer servir chat.sum7.eu</string>
|
||||
<string name="create_new_account">Crear un compte nou</string>
|
||||
<string name="do_you_have_an_account">Ja tens un compte XMPP? Aquest podria ser el cas si ja estàs usant un client XMPP diferent o has usat Converses abans. Si no, pots crear un nou compte XMPP ara mateix.\nPista: Alguns proveïdors de correu electrònic també proporcionen comptes XMPP.</string>
|
||||
<string name="server_select_text">XMPP és una xarxa de missatgeria instantània independent del proveïdor. Pots usar aquest client amb qualsevol servidor XMPP que triïs. No obstant això, per a la teva conveniència, hem fet fàcil la creació d\'un compte en Conversaciones.im¹; un proveïdor especialment adequat per a l\'ús amb Conversations.</string>
|
||||
<string name="magic_create_text_on_x">Has estat convidat a %1$s. Et guiarem a través del procés de creació d\'un compte.\nEn triar%1$s com a proveïdor podràs comunicar-se amb els usuaris d\'altres proveïdors donant-los la seva adreça XMPP completa.</string>
|
||||
<string name="magic_create_text_fixed">Has estat convidat a %1$s . Ja s\'ha triat un nom d\'usuari per a tu. Et guiarem en el procés de creació d\'un compte. Podràs comunicar-te amb usuaris d\'altres proveïdors donant-los la teva adreça XMPP completa.</string>
|
||||
<string name="your_server_invitation">La teva invitació al servidor</string>
|
||||
<string name="improperly_formatted_provisioning">Codi d\'aprovisionament mal formatat</string>
|
||||
<string name="tap_share_button_send_invite">Toca el botó de compartir per a enviar al teu contacte una invitació a %1$s .</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Si el teu contacte està a prop, també pot escanejar el codi de baix per a acceptar la teva invitació.</string>
|
||||
<string name="easy_invite_share_text">Uneix-te %1$s i xerra amb mi: %2$s</string>
|
||||
<string name="share_invite_with">Comparteix la invitació amb...</string>
|
||||
</resources>
|
||||
</resources>
|
|
@ -1,16 +0,0 @@
|
|||
<?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,8 +9,4 @@
|
|||
<string name="magic_create_text_fixed">Du wurdest zu %1$seingeladen. Ein Benutzername ist bereits für dich ausgewählt worden. Wir führen dich durch den Prozess der Kontoerstellung.\nDu kannst mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst.</string>
|
||||
<string name="your_server_invitation">Deine Einladung für den Server</string>
|
||||
<string name="improperly_formatted_provisioning">Falsch formatierter Provisionierungscode</string>
|
||||
<string name="tap_share_button_send_invite">Tippe auf die \"Teilen\"-Schaltfläche, um deinem Kontakt eine Einladung an %1$s zu senden.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Wenn dein Kontakt in der Nähe ist, kann er auch den untenstehenden Code einscannen, um deine Einladung anzunehmen.</string>
|
||||
<string name="easy_invite_share_text">Komme zu %1$s und chatte mit mir: %2$s</string>
|
||||
<string name="share_invite_with">Einladung teilen mit…</string>
|
||||
</resources>
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
<string name="create_new_account">Δημιουργία νέου λογαριασμού</string>
|
||||
<string name="do_you_have_an_account">Έχετε ήδη λογαριασμό XMPP; Αυτό μπορεί να συμβαίνει αν ήδη χρησιμοποιείτε ένα άλλο πρόγραμμα XMPP ή έχετε χρησιμοποιήσει το Conversations παλιότερα. Αν όχι, μπορείτε να δημιουργήσετε ένα νέο λογαριασμό XMPP τώρα.\nΧρήσιμη πληροφορία: Κάποιοι πάροχοι e-mail παρέχουν επίσης και λογαριασμούς XMPP.</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_on_x">Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΕπιλέγοντας τον %1$s ως πάροχο θα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας.</string>
|
||||
<string name="magic_create_text_fixed">Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΘα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας.</string>
|
||||
<string name="your_server_invitation">Η πρόσκλησή σας στον διακομιστή</string>
|
||||
<string name="improperly_formatted_provisioning">Λάθος μορφοποίηση κώδικα παροχής</string>
|
||||
<string name="tap_share_button_send_invite">Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σαρώσει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας.</string>
|
||||
<string name="easy_invite_share_text">Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s</string>
|
||||
<string name="share_invite_with">Διαμοιρασμός πρόσκλησης με...</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -9,8 +9,4 @@
|
|||
<string name="magic_create_text_fixed">Has sido invitado a %1$s. Un nombre de usuario ya ha sido escogido para ti. Te guiaremos durante el proceso de creación de la cuenta.\nPodrás comunicarte con otros usuarios de otros servidores proporcionándoles tu dirección XMPP completa. </string>
|
||||
<string name="your_server_invitation">Tu invitación al servidor</string>
|
||||
<string name="improperly_formatted_provisioning">Código de abastecimiento formateado incorrectamente</string>
|
||||
<string name="tap_share_button_send_invite">Pulsa el botón de compartir para enviar a tu contacto una invitación a %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Si tu contacto está cerca, también puede escanear el código mostrado debajo para aceptar tu invitación.</string>
|
||||
<string name="easy_invite_share_text">Únete a %1$s y chatea conmigo: %2$s</string>
|
||||
<string name="share_invite_with">Compartir invitación con...</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">لطفا سرویس دهنده پیام خود را انتخاب نمائید. برای مثال artalk.im</string>
|
||||
<string name="use_chat.sum7.eu">از Conversations.im استفاده کنید</string>
|
||||
<string name="use_conversations.im">از Conversations.im استفاده کنید</string>
|
||||
<string name="create_new_account">حساب کاربری جدیدی بسازید</string>
|
||||
</resources>
|
|
@ -9,8 +9,4 @@
|
|||
<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>
|
||||
<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,8 +9,4 @@
|
|||
<string name="magic_create_text_fixed">Convidáronte a %1$s. Escollemos un nome de usuaria por ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias de outros provedores cando lles digas o teu enderezo XMPP completo.</string>
|
||||
<string name="your_server_invitation">O convite do teu servidor</string>
|
||||
<string name="improperly_formatted_provisioning">Código de aprovisionamento con formato non válido</string>
|
||||
<string name="tap_share_button_send_invite">Toca no botón compartir para convidar ó teu contacto a %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Se o contacto está preto de ti, pode escanear o código inferior para aceptar o teu convite.</string>
|
||||
<string name="easy_invite_share_text">Únete a %1$s e conversa conmigo: %2$s</string>
|
||||
<string name="share_invite_with">Enviar convite a...</string>
|
||||
</resources>
|
||||
|
|
|
@ -8,9 +8,4 @@
|
|||
<string name="magic_create_text_on_x">Meghívást kapott a(z) %1$s kiszolgálóra. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nHa a(z) %1$s kiszolgálót választja szolgáltatóként, akkor ké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="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>
|
||||
<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>
|
||||
</resources>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">Pilih XMPP server anda</string>
|
||||
<string name="use_chat.sum7.eu">Gunakan chat.sum7.eu</string>
|
||||
<string name="create_new_account">Buat akun baru</string>
|
||||
<string name="do_you_have_an_account">Anda sudah memiliki akun XMPP? Ini mungkin terjadi jika Anda sudah menggunakan aplikasi XMPP yang berbeda atau pernah menggunakan Conversations sebelumnya. Jika tidak, Anda dapat membuat akun XMPP baru. \ NPetunjuk: Beberapa penyedia layanan email juga menyediakan akun XMPP.</string>
|
||||
<string name="server_select_text">XMPP adalah jaringan penyedia pesan instan independen. Anda dapat menggunakan aplikasi ini dengan server XMPP pilihan Anda. \ NNamun demi kenyamanan Anda, kami permudah untuk membuat akun di Conversations.im¹; provider yang sangat cocok digunakan dengan Conversations.</string>
|
||||
<string name="magic_create_text_on_x">Anda telah diundang ke %1$s. Kami akan memandu Anda melalui proses pembuatan akun. \nSaat memilih %1$s sebagai penyedia, Anda akan dapat berkomunikasi dengan pengguna provider lain dengan memberikan alamat XMPP lengkap Anda kepada mereka.</string>
|
||||
<string name="magic_create_text_fixed">Anda telah diundang ke%1$s. Username telah dipilihkan untuk Anda. Kami akan memandu Anda melalui proses pembuatan akun. \nAnda dapat berkomunikasi dengan pengguna provider lain dengan memberi mereka alamat XMPP lengkap Anda.</string>
|
||||
<string name="your_server_invitation">Undangan server Anda</string>
|
||||
<string name="improperly_formatted_provisioning">Kode provisioning tidak diformat dengan benar</string>
|
||||
<string name="tap_share_button_send_invite">Klik tombol bagikan untuk mengirim undangan ke kontak Anda %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Jika kontak Anda di dekat Anda, mereka juga dapat memindai kode di bawah ini untuk menerima undangan Anda</string>
|
||||
<string name="easy_invite_share_text">Bergabung %1$s dan mengobrol dengan saya: %2$s</string>
|
||||
<string name="share_invite_with">Bagikan undangan dengan...</string>
|
||||
</resources>
|
|
@ -1,18 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">Scegli il tuo fornitore XMPP</string>
|
||||
<string name="pick_a_server">Scegli il tuo provider XMPP</string>
|
||||
<string name="use_chat.sum7.eu">Usa chat.sum7.eu</string>
|
||||
<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.
|
||||
<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.
|
||||
Suggerimento: alcuni provider di email forniscono anche un account XMPP.</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="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="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>
|
||||
<string name="if_contact_is_nearby_use_qr">Se il contatto è vicino, può anche scansionare il codice sottostante per accettare il tuo invito.</string>
|
||||
<string name="easy_invite_share_text">Unisciti a %1$s e chatta con me: %2$s</string>
|
||||
<string name="share_invite_with">Condividi invito con...</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?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="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>
|
|
@ -8,9 +8,4 @@
|
|||
<string name="magic_create_text_on_x">Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP.</string>
|
||||
<string name="magic_create_text_fixed">Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP.</string>
|
||||
<string name="your_server_invitation">Zaproszenie twojego serwera</string>
|
||||
<string name="improperly_formatted_provisioning">Niepoprawnie sformatowany kod zaopatrywania</string>
|
||||
<string name="tap_share_button_send_invite">Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie.</string>
|
||||
<string name="easy_invite_share_text">Dołącz do %1$s aby porozmawiać ze mną: %2$s</string>
|
||||
<string name="share_invite_with">Udostępnij zaproszenie...</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -9,8 +9,4 @@
|
|||
<string name="magic_create_text_fixed">Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string>
|
||||
<string name="your_server_invitation">Seu convite do servidor</string>
|
||||
<string name="improperly_formatted_provisioning">Código de provisionamento formatado de maneira imprópria</string>
|
||||
<string name="tap_share_button_send_invite">Toque no botão compartilhar para enviar, para seu contato, um convite para %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Se seu contato estiver por perto, ele também pode escanear o código abaixo para aceitar seu convite.</string>
|
||||
<string name="easy_invite_share_text">Junte-se a %1$s e converse comigo: %2$s</string>
|
||||
<string name="share_invite_with">Compartilhe o convite com...</string>
|
||||
</resources>
|
||||
|
|
|
@ -9,8 +9,4 @@
|
|||
<string name="magic_create_text_fixed">Ați fost invitați la %1$s. Un nume de utilizator a fost deja ales pentru dumneavoastră. Vă vom ghida prin procesul de creare al unui cont.\nVeți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP.</string>
|
||||
<string name="your_server_invitation">Invitația serverului dumneavoastră</string>
|
||||
<string name="improperly_formatted_provisioning">Cod de acces formatat necorespunzător</string>
|
||||
<string name="tap_share_button_send_invite">Atingeți butonul de partajare pentru a trimite contactului o invitație la %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Dacă e în apropiere, contactul poate scana codul de mai jos pentru a vă accepta invitația.</string>
|
||||
<string name="easy_invite_share_text">Alătură-te %1$s și discută cu mine: %2$s</string>
|
||||
<string name="share_invite_with">Partajează invitația cu…</string>
|
||||
</resources>
|
||||
|
|
|
@ -9,8 +9,4 @@
|
|||
<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>
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<?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,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">Одаберите вашег ИксМПП провајдера</string>
|
||||
<string name="use_chat.sum7.eu">Користи chat.sum7.eu</string>
|
||||
<string name="create_new_account">Направи нови налог</string>
|
||||
<string name="do_you_have_an_account">Да ли већ имате ИксМПП налог? Извесно је да га имате ако користите неки ИксМПП клијент или сте раније користили Конверзацију. Ако немате, сада можете направити нови ИксМПП налог.\nСавет: неки поштански провајдери такође омогућавају и ИксМПП налоге.</string>
|
||||
<string name="server_select_text">ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору.\nДа бисмо вам олакшали, омогућили смо креирање налога на chat.sum7.eu; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију</string>
|
||||
<string name="your_server_invitation">Ваша серверска позивница</string>
|
||||
</resources>
|
|
@ -1,8 +1,5 @@
|
|||
<?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>
|
||||
<string name="your_server_invitation">Din server inbjudan</string>
|
||||
<string name="share_invite_with">Dela inbjudan med...</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">XMPP sağlayıcınızı seçin</string>
|
||||
<string name="use_chat.sum7.eu">chat.sum7.eu kullan</string>
|
||||
<string name="create_new_account">Yeni hesap oluştur</string>
|
||||
<string name="do_you_have_an_account">Zaten bir XMPP hesabınız var mı? Bunun sebebi, zaten başka bir XMPP istemcisi kullanıyor oluşunuz veya Conversations\'ı önceden kullanmış olmanız olabilir. Eğer durum bu değilse şimdi yeni bir XMPP hesabı oluşturabilirsiniz.\nİpucu: Bağzı e-posta sağlayıcıları da XMPP hesapları kullanabilir.</string>
|
||||
<string name="server_select_text">XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz.\nAncak kullanım rahatlığı adına sizin için chat.sum7.eu; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık.</string>
|
||||
<string name="magic_create_text_on_x">%1$s sağlayıcısına davet edildiniz. Sizi hesap oluşturulması konusunda yönlendireceğiz.\n%1$s bir sağlayıcı olark seçildiğinde, başka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz.</string>
|
||||
<string name="magic_create_text_fixed">%1$s sağlayıcısına davet edildiniz. Sizin için zaten bir kullanıcı adı seçildi. Sizi hesap oluşturulması konusunda yönlendireceğiz.\nBaşka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz.</string>
|
||||
<string name="your_server_invitation">Sunucu davetiyeniz</string>
|
||||
<string name="improperly_formatted_provisioning">Yanlış ayarlanmış düzenleme kodu</string>
|
||||
<string name="tap_share_button_send_invite">Kişinize, %1$s grubuna davet etmek için Paylaş düğmesine basın.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">Kişiniz yakınınızda ise, aşağıdaki kodu tarayak daveti kabul edebilirler.</string>
|
||||
<string name="easy_invite_share_text">%1$s grubuna katıl ve benimle sohpet et: %2$s</string>
|
||||
<string name="share_invite_with">Daveti şununla paylaş...</string>
|
||||
</resources>
|
|
@ -9,4 +9,4 @@
|
|||
<string name="magic_create_text_fixed">Вас запросили до %1$s. Для вас створено ім\'я користувача. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nВи зможете спілкуватися з користувачами інших постачальників, для цього повідомите їм свою повну адресу XMPP.</string>
|
||||
<string name="your_server_invitation">Ваше запрошення до сервера</string>
|
||||
<string name="improperly_formatted_provisioning">Неправильно відформатований код забезпечення</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?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>
|
|
@ -9,8 +9,4 @@
|
|||
<string name="magic_create_text_fixed">您已受邀参加%1$s。 已经为您选择了一个用户名。 我们将指导您完成创建帐户的过程。\n您可以通过向其他提供商的用户提供完整的XMPP地址来与他们进行交流。</string>
|
||||
<string name="your_server_invitation">你的服务器邀请</string>
|
||||
<string name="improperly_formatted_provisioning">格式不正确的配置代码</string>
|
||||
<string name="tap_share_button_send_invite">点击分享按钮向您的联系人发送加入 %1$s 的邀请。</string>
|
||||
<string name="if_contact_is_nearby_use_qr">如果你的联系人在附近,他们也可以扫描下面的代码来接受你的邀请。</string>
|
||||
<string name="easy_invite_share_text">加入 %1$s 和我聊天:%2$s</string>
|
||||
<string name="share_invite_with">分享邀请</string>
|
||||
</resources>
|
||||
|
|
|
@ -9,8 +9,4 @@
|
|||
<string name="magic_create_text_fixed">You have been invited to %1$s. A username has already been picked for you. We will guide you through the process of creating an account.\nYou will be able to communicate with users of other providers by giving them your full XMPP address.</string>
|
||||
<string name="your_server_invitation">Your server invitation</string>
|
||||
<string name="improperly_formatted_provisioning">Improperly formatted provisioning code</string>
|
||||
<string name="tap_share_button_send_invite">Tap the share button to send your contact an invitation to %1$s.</string>
|
||||
<string name="if_contact_is_nearby_use_qr">If your contact is nearby, they can also scan the code below to accept your invitation.</string>
|
||||
<string name="easy_invite_share_text">Join %1$s and chat with me: %2$s</string>
|
||||
<string name="share_invite_with">Share invite with…</string>
|
||||
</resources>
|
||||
|
|
|
@ -2,9 +2,14 @@ package eu.siacs.conversations.ui.service;
|
|||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.emoji.text.EmojiCompat;
|
||||
import androidx.emoji.text.FontRequestEmojiCompatConfig;
|
||||
import androidx.emoji.bundled.BundledEmojiCompatConfig;
|
||||
import android.support.text.emoji.EmojiCompat;
|
||||
import android.support.text.emoji.FontRequestEmojiCompatConfig;
|
||||
import android.support.text.emoji.bundled.BundledEmojiCompatConfig;
|
||||
import android.support.v4.provider.FontRequest;
|
||||
import android.util.Log;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
|
||||
public class EmojiService {
|
||||
|
||||
|
|
|
@ -7,9 +7,6 @@
|
|||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_PROFILE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_PHONE_STATE"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
@ -39,6 +36,12 @@
|
|||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_PHONE_STATE"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
@ -52,18 +55,15 @@
|
|||
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:allowBackup="false"
|
||||
android:appCategory="social"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/new_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:networkSecurityConfig="@xml/network_security_configuration"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/ConversationsTheme"
|
||||
tools:replace="android:label"
|
||||
tools:targetApi="q">
|
||||
tools:targetApi="o">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
|
@ -131,7 +131,7 @@
|
|||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="chat.sum7.eu" />
|
||||
<data android:host="conversations.im" />
|
||||
<data android:pathPrefix="/i/" />
|
||||
<data android:pathPrefix="/j/" />
|
||||
</intent-filter>
|
||||
|
@ -143,14 +143,6 @@
|
|||
<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"
|
||||
|
@ -273,7 +265,7 @@
|
|||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
|
|
@ -3,14 +3,11 @@ 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;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public final class Config {
|
||||
private static final int UNENCRYPTED = 1;
|
||||
|
@ -36,7 +33,7 @@ public final class Config {
|
|||
return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
|
||||
}
|
||||
|
||||
public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US);
|
||||
public static final String LOGTAG = BuildConfig.LOGTAG;
|
||||
|
||||
public static final Jid BUG_REPORTS = Jid.of("bugs@chat.sum7.eu");
|
||||
public static final Uri HELP = Uri.parse("https://sum7.eu/chat");
|
||||
|
@ -104,7 +101,6 @@ public final class Config {
|
|||
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;
|
||||
|
||||
|
@ -118,12 +114,11 @@ 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
|
||||
|
||||
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
|
||||
public static final boolean MUC_LEAVE_BEFORE_JOIN = false;
|
||||
public static final boolean MUC_LEAVE_BEFORE_JOIN = true;
|
||||
|
||||
public static final boolean USE_LMC_VERSION_1_1 = true;
|
||||
|
||||
|
@ -178,14 +173,7 @@ 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
|
||||
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);
|
||||
}
|
||||
public static final List<String> CONTACT_DOMAINS = Collections.singletonList("cheogram.com");
|
||||
}
|
||||
|
||||
private Config() {
|
||||
|
@ -200,9 +188,4 @@ 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,21 +17,6 @@ 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 {
|
||||
|
@ -51,26 +36,38 @@ 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();
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
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,11 +1,10 @@
|
|||
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) throws SSLPeerUnverifiedException;
|
||||
boolean verify(String domain, String hostname, SSLSession sslSession);
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ 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;
|
||||
|
@ -208,7 +209,7 @@ public class PgpDecryptionService {
|
|||
message.setRelativeFilePath(path);
|
||||
}
|
||||
}
|
||||
final String url = message.getFileParams().url;
|
||||
URL url = message.getFileParams().url;
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
|
|
|
@ -2,14 +2,9 @@ package eu.siacs.conversations.crypto;
|
|||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.StringRes;
|
||||
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;
|
||||
|
@ -22,7 +17,6 @@ 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;
|
||||
|
@ -34,258 +28,268 @@ 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 OpenPgpApi api;
|
||||
private 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;
|
||||
} 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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(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 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 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:
|
||||
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 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 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,10 +1,9 @@
|
|||
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;
|
||||
|
@ -17,20 +16,17 @@ 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 {
|
||||
public class XmppDomainVerifier implements DomainHostnameVerifier {
|
||||
|
||||
private static final String LOGTAG = "XmppDomainVerifier";
|
||||
|
||||
|
@ -75,8 +71,8 @@ public class XmppDomainVerifier {
|
|||
}
|
||||
}
|
||||
|
||||
public static boolean matchDomain(final String needle, final List<String> haystack) {
|
||||
for (final String entry : haystack) {
|
||||
private static boolean matchDomain(String needle, List<String> haystack) {
|
||||
for (String entry : haystack) {
|
||||
if (entry.startsWith("*.")) {
|
||||
int offset = 0;
|
||||
while (offset < needle.length()) {
|
||||
|
@ -84,13 +80,16 @@ public class XmppDomainVerifier {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
@ -98,90 +97,63 @@ public class XmppDomainVerifier {
|
|||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public boolean verify(String domain, String hostname, SSLSession sslSession) {
|
||||
try {
|
||||
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);
|
||||
Certificate[] chain = sslSession.getPeerCertificates();
|
||||
if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
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));
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
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);
|
||||
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 false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,4 +165,9 @@ public class XmppDomainVerifier {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String domain, SSLSession sslSession) {
|
||||
return verify(domain, null, sslSession);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,26 +2,18 @@ package eu.siacs.conversations.crypto.axolotl;
|
|||
|
||||
import android.os.Bundle;
|
||||
import android.security.KeyChain;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
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.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
import org.whispersystems.libsignal.SessionBuilder;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.UntrustedIdentityException;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.state.PreKeyBundle;
|
||||
|
@ -56,18 +48,12 @@ 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;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||
|
||||
|
@ -99,9 +85,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
private int numPublishTriesOnEmptyPep = 0;
|
||||
private boolean pepBroken = false;
|
||||
private int lastDeviceListNotificationHash = 0;
|
||||
private final Set<XmppAxolotlSession> postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment
|
||||
private final Set<SignalProtocolAddress> postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup
|
||||
private final AtomicBoolean changeAccessMode = new AtomicBoolean(false);
|
||||
private Set<XmppAxolotlSession> postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment
|
||||
private Set<SignalProtocolAddress> postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup
|
||||
private AtomicBoolean changeAccessMode = new AtomicBoolean(false);
|
||||
|
||||
public AxolotlService(Account account, XmppConnectionService connectionService) {
|
||||
if (account == null || connectionService == null) {
|
||||
|
@ -738,62 +724,58 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
axolotlStore.setFingerprintStatus(fingerprint, status);
|
||||
}
|
||||
|
||||
private ListenableFuture<XmppAxolotlSession> verifySessionWithPEP(final XmppAxolotlSession session) {
|
||||
private void 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 {
|
||||
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)) {
|
||||
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 {
|
||||
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
|
||||
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");
|
||||
}
|
||||
}
|
||||
finishBuildingSessionsFromPEP(address);
|
||||
future.set(session);
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "could not verify certificate");
|
||||
Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "no verification found");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
|
||||
fetchStatusMap.put(address, FetchStatus.SUCCESS);
|
||||
finishBuildingSessionsFromPEP(address);
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "no verification found");
|
||||
}
|
||||
});
|
||||
} catch (IllegalArgumentException e) {
|
||||
fetchStatusMap.put(address, FetchStatus.SUCCESS);
|
||||
finishBuildingSessionsFromPEP(address);
|
||||
future.set(session);
|
||||
});
|
||||
return future;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) {
|
||||
|
@ -909,23 +891,22 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
private ListenableFuture<XmppAxolotlSession> buildSessionFromPEP(final SignalProtocolAddress address) {
|
||||
return buildSessionFromPEP(address, null);
|
||||
private void buildSessionFromPEP(final SignalProtocolAddress address) {
|
||||
buildSessionFromPEP(address, null);
|
||||
}
|
||||
|
||||
private ListenableFuture<XmppAxolotlSession> buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) {
|
||||
final SettableFuture<XmppAxolotlSession> sessionSettableFuture = SettableFuture.create();
|
||||
private void buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) {
|
||||
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();
|
||||
|
@ -938,7 +919,6 @@ 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();
|
||||
|
@ -950,7 +930,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
if (callback != null) {
|
||||
callback.onSessionBuildFailed();
|
||||
}
|
||||
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. No suitable PreKey found"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -965,7 +944,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey());
|
||||
sessions.put(address, session);
|
||||
if (Config.X509_VERIFICATION) {
|
||||
sessionSettableFuture.setFuture(verifySessionWithPEP(session)); //TODO; maybe inject callback in here too
|
||||
verifySessionWithPEP(session); //TODO; maybe inject callback in here too
|
||||
} else {
|
||||
FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize()));
|
||||
FetchStatus fetchStatus;
|
||||
|
@ -981,7 +960,6 @@ 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 + ": "
|
||||
|
@ -994,7 +972,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
if (callback != null) {
|
||||
callback.onSessionBuildFailed();
|
||||
}
|
||||
sessionSettableFuture.setException(new CryptoFailedException(e));
|
||||
}
|
||||
} else {
|
||||
fetchStatusMap.put(address, FetchStatus.ERROR);
|
||||
|
@ -1008,10 +985,8 @@ 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) {
|
||||
|
@ -1185,7 +1160,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;
|
||||
content = message.getFileParams().url.toString();
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
|
@ -1222,154 +1197,6 @@ 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
|
||||
|
@ -1439,7 +1266,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);
|
||||
|
@ -1490,7 +1317,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": nothing to flush. Not republishing key");
|
||||
}
|
||||
if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) {
|
||||
if (trustedOrPreviouslyResponded(session)) {
|
||||
completeSession(session);
|
||||
}
|
||||
}
|
||||
|
@ -1505,7 +1332,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
final Iterator<XmppAxolotlSession> iterator = postponedSessions.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
final XmppAxolotlSession session = iterator.next();
|
||||
if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) {
|
||||
if (trustedOrPreviouslyResponded(session)) {
|
||||
completeSession(session);
|
||||
}
|
||||
iterator.remove();
|
||||
|
@ -1567,16 +1394,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return keyTransportMessage;
|
||||
}
|
||||
|
||||
private ListenableFuture<XmppAxolotlSession> putFreshSession(XmppAxolotlSession session) {
|
||||
private void putFreshSession(XmppAxolotlSession session) {
|
||||
sessions.put(session);
|
||||
if (Config.X509_VERIFICATION) {
|
||||
if (session.getIdentityKey() != null) {
|
||||
return verifySessionWithPEP(session);
|
||||
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 {
|
||||
|
@ -1738,36 +1564,4 @@ 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,15 +3,15 @@ package eu.siacs.conversations.crypto.axolotl;
|
|||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.KeyHelper;
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ public class XmppAxolotlMessage {
|
|||
switch (keyElement.getName()) {
|
||||
case KEYTAG:
|
||||
try {
|
||||
int recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
|
||||
Integer 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(final String plaintext) throws CryptoFailedException {
|
||||
void encrypt(String plaintext) throws CryptoFailedException {
|
||||
try {
|
||||
SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.DuplicateMessageException;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
|
@ -14,7 +14,6 @@ import org.whispersystems.libsignal.InvalidVersionException;
|
|||
import org.whispersystems.libsignal.LegacyMessageException;
|
||||
import org.whispersystems.libsignal.NoSessionException;
|
||||
import org.whispersystems.libsignal.SessionCipher;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.UntrustedIdentityException;
|
||||
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
|
||||
|
|
|
@ -7,24 +7,22 @@ import eu.siacs.conversations.xml.TagWriter;
|
|||
|
||||
public class Anonymous extends SaslMechanism {
|
||||
|
||||
public static final String MECHANISM = "ANONYMOUS";
|
||||
public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 0;
|
||||
}
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return "ANONYMOUS";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return MECHANISM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
return "";
|
||||
}
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,82 +12,79 @@ import eu.siacs.conversations.utils.CryptoHelper;
|
|||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
public class DigestMd5 extends SaslMechanism {
|
||||
public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
public static final String MECHANISM = "DIGEST-MD5";
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return "DIGEST-MD5";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 10;
|
||||
}
|
||||
private State state = State.INITIAL;
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return MECHANISM;
|
||||
}
|
||||
@Override
|
||||
public String getResponse(final String challenge) throws AuthenticationException {
|
||||
switch (state) {
|
||||
case INITIAL:
|
||||
state = State.RESPONSE_SENT;
|
||||
final String encodedResponse;
|
||||
try {
|
||||
final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
|
||||
String nonce = "";
|
||||
for (final String token : tokenizer) {
|
||||
final String[] parts = token.split("=", 2);
|
||||
if (parts[0].equals("nonce")) {
|
||||
nonce = parts[1].replace("\"", "");
|
||||
} else if (parts[0].equals("rspauth")) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
final String digestUri = "xmpp/" + account.getServer();
|
||||
final String nonceCount = "00000001";
|
||||
final String x = account.getUsername() + ":" + account.getServer() + ":"
|
||||
+ account.getPassword();
|
||||
final MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
|
||||
final String cNonce = CryptoHelper.random(100,rng);
|
||||
final byte[] a1 = CryptoHelper.concatenateByteArrays(y,
|
||||
(":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset()));
|
||||
final String a2 = "AUTHENTICATE:" + digestUri;
|
||||
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
|
||||
final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset
|
||||
.defaultCharset())));
|
||||
final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
|
||||
+ ":auth:" + ha2;
|
||||
final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset
|
||||
.defaultCharset())));
|
||||
final String saslString = "username=\"" + account.getUsername()
|
||||
+ "\",realm=\"" + account.getServer() + "\",nonce=\""
|
||||
+ nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
|
||||
+ ",qop=auth,digest-uri=\"" + digestUri + "\",response="
|
||||
+ response + ",charset=utf-8";
|
||||
encodedResponse = Base64.encodeToString(
|
||||
saslString.getBytes(Charset.defaultCharset()),
|
||||
Base64.NO_WRAP);
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
|
||||
private State state = State.INITIAL;
|
||||
|
||||
@Override
|
||||
public String getResponse(final String challenge) throws AuthenticationException {
|
||||
switch (state) {
|
||||
case INITIAL:
|
||||
state = State.RESPONSE_SENT;
|
||||
final String encodedResponse;
|
||||
try {
|
||||
final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
|
||||
String nonce = "";
|
||||
for (final String token : tokenizer) {
|
||||
final String[] parts = token.split("=", 2);
|
||||
if (parts[0].equals("nonce")) {
|
||||
nonce = parts[1].replace("\"", "");
|
||||
} else if (parts[0].equals("rspauth")) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
final String digestUri = "xmpp/" + account.getServer();
|
||||
final String nonceCount = "00000001";
|
||||
final String x = account.getUsername() + ":" + account.getServer() + ":"
|
||||
+ account.getPassword();
|
||||
final MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
|
||||
final String cNonce = CryptoHelper.random(100, rng);
|
||||
final byte[] a1 = CryptoHelper.concatenateByteArrays(y,
|
||||
(":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset()));
|
||||
final String a2 = "AUTHENTICATE:" + digestUri;
|
||||
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
|
||||
final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset
|
||||
.defaultCharset())));
|
||||
final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
|
||||
+ ":auth:" + ha2;
|
||||
final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset
|
||||
.defaultCharset())));
|
||||
final String saslString = "username=\"" + account.getUsername()
|
||||
+ "\",realm=\"" + account.getServer() + "\",nonce=\""
|
||||
+ nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
|
||||
+ ",qop=auth,digest-uri=\"" + digestUri + "\",response="
|
||||
+ response + ",charset=utf-8";
|
||||
encodedResponse = Base64.encodeToString(
|
||||
saslString.getBytes(Charset.defaultCharset()),
|
||||
Base64.NO_WRAP);
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
|
||||
return encodedResponse;
|
||||
case RESPONSE_SENT:
|
||||
state = State.VALID_SERVER_RESPONSE;
|
||||
break;
|
||||
case VALID_SERVER_RESPONSE:
|
||||
if (challenge == null) {
|
||||
return null; //everything is fine
|
||||
}
|
||||
default:
|
||||
throw new InvalidStateException(state);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return encodedResponse;
|
||||
case RESPONSE_SENT:
|
||||
state = State.VALID_SERVER_RESPONSE;
|
||||
break;
|
||||
case VALID_SERVER_RESPONSE:
|
||||
if (challenge==null) {
|
||||
return null; //everything is fine
|
||||
}
|
||||
default:
|
||||
throw new InvalidStateException(state);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
|
@ -9,24 +8,22 @@ import eu.siacs.conversations.xml.TagWriter;
|
|||
|
||||
public class External extends SaslMechanism {
|
||||
|
||||
public static final String MECHANISM = "EXTERNAL";
|
||||
public External(TagWriter tagWriter, Account account, SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
public External(TagWriter tagWriter, Account account, SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 25;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 25;
|
||||
}
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return "EXTERNAL";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return MECHANISM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP);
|
||||
}
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(),Base64.NO_WRAP);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,30 +8,27 @@ import eu.siacs.conversations.entities.Account;
|
|||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
public class Plain extends SaslMechanism {
|
||||
public Plain(final TagWriter tagWriter, final Account account) {
|
||||
super(tagWriter, account, null);
|
||||
}
|
||||
|
||||
public static final String MECHANISM = "PLAIN";
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
public Plain(final TagWriter tagWriter, final Account account) {
|
||||
super(tagWriter, account, null);
|
||||
}
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return "PLAIN";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 10;
|
||||
}
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
return getMessage(account.getUsername(), account.getPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return MECHANISM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
return getMessage(account.getUsername(), account.getPassword());
|
||||
}
|
||||
|
||||
public static String getMessage(String username, String password) {
|
||||
final String message = '\u0000' + username + '\u0000' + password;
|
||||
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
|
||||
}
|
||||
public static String getMessage(String username, String password) {
|
||||
final String message = '\u0000' + username + '\u0000' + password;
|
||||
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,63 +7,60 @@ import eu.siacs.conversations.xml.TagWriter;
|
|||
|
||||
public abstract class SaslMechanism {
|
||||
|
||||
final protected TagWriter tagWriter;
|
||||
final protected Account account;
|
||||
final protected SecureRandom rng;
|
||||
final protected TagWriter tagWriter;
|
||||
final protected Account account;
|
||||
final protected SecureRandom rng;
|
||||
|
||||
protected enum State {
|
||||
INITIAL,
|
||||
AUTH_TEXT_SENT,
|
||||
RESPONSE_SENT,
|
||||
VALID_SERVER_RESPONSE,
|
||||
}
|
||||
protected enum State {
|
||||
INITIAL,
|
||||
AUTH_TEXT_SENT,
|
||||
RESPONSE_SENT,
|
||||
VALID_SERVER_RESPONSE,
|
||||
}
|
||||
|
||||
public static class AuthenticationException extends Exception {
|
||||
public AuthenticationException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
public static class AuthenticationException extends Exception {
|
||||
public AuthenticationException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthenticationException(final Exception inner) {
|
||||
super(inner);
|
||||
}
|
||||
public AuthenticationException(final Exception inner) {
|
||||
super(inner);
|
||||
}
|
||||
|
||||
public AuthenticationException(final String message, final Exception exception) {
|
||||
super(message, exception);
|
||||
}
|
||||
}
|
||||
public AuthenticationException(final String message, final Exception exception) {
|
||||
super(message,exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidStateException extends AuthenticationException {
|
||||
public InvalidStateException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
public static class InvalidStateException extends AuthenticationException {
|
||||
public InvalidStateException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidStateException(final State state) {
|
||||
this("Invalid state: " + state.toString());
|
||||
}
|
||||
}
|
||||
public InvalidStateException(final State state) {
|
||||
this("Invalid state: " + state.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
this.tagWriter = tagWriter;
|
||||
this.account = account;
|
||||
this.rng = rng;
|
||||
}
|
||||
public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
this.tagWriter = tagWriter;
|
||||
this.account = account;
|
||||
this.rng = rng;
|
||||
}
|
||||
|
||||
/**
|
||||
* The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another
|
||||
* mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade
|
||||
* attacks).
|
||||
*
|
||||
* @return An arbitrary int representing the priority
|
||||
*/
|
||||
public abstract int getPriority();
|
||||
/**
|
||||
* The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another
|
||||
* mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade
|
||||
* attacks).
|
||||
* @return An arbitrary int representing the priority
|
||||
*/
|
||||
public abstract int getPriority();
|
||||
|
||||
public abstract String getMechanism();
|
||||
|
||||
public String getClientFirstMessage() {
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getResponse(final String challenge) throws AuthenticationException {
|
||||
return "";
|
||||
}
|
||||
public abstract String getMechanism();
|
||||
public String getClientFirstMessage() {
|
||||
return "";
|
||||
}
|
||||
public String getResponse(final String challenge) throws AuthenticationException {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,74 +1,53 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import android.util.LruCache;
|
||||
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
|
||||
abstract class ScramMechanism extends SaslMechanism {
|
||||
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
|
||||
private final static String GS2_HEADER = "n,,";
|
||||
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
|
||||
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
|
||||
private static final LruCache<String, KeyPair> CACHE;
|
||||
static HMac HMAC;
|
||||
static Digest DIGEST;
|
||||
|
||||
protected abstract HMac getHMAC();
|
||||
static {
|
||||
CACHE = new LruCache<String, KeyPair>(10) {
|
||||
protected KeyPair create(final String k) {
|
||||
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism".
|
||||
// Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()'
|
||||
// is applied to prevent commas in the strings breaking things.
|
||||
final String[] kParts = k.split(",", 5);
|
||||
try {
|
||||
final byte[] saltedPassword, serverKey, clientKey;
|
||||
saltedPassword = hi(CryptoHelper.hexToString(kParts[1]).getBytes(),
|
||||
Base64.decode(CryptoHelper.hexToString(kParts[2]), Base64.DEFAULT), Integer.parseInt(kParts[3]));
|
||||
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
|
||||
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
|
||||
|
||||
protected abstract Digest getDigest();
|
||||
|
||||
private static final Cache<CacheKey, KeyPair> CACHE = CacheBuilder.newBuilder().maximumSize(10).build();
|
||||
|
||||
private static class CacheKey {
|
||||
final String algorithm;
|
||||
final String password;
|
||||
final String salt;
|
||||
final int iterations;
|
||||
|
||||
private CacheKey(String algorithm, String password, String salt, int iterations) {
|
||||
this.algorithm = algorithm;
|
||||
this.password = password;
|
||||
this.salt = salt;
|
||||
this.iterations = iterations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CacheKey cacheKey = (CacheKey) o;
|
||||
return iterations == cacheKey.iterations &&
|
||||
Objects.equal(algorithm, cacheKey.algorithm) &&
|
||||
Objects.equal(password, cacheKey.password) &&
|
||||
Objects.equal(salt, cacheKey.salt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(algorithm, password, salt, iterations);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException {
|
||||
return CACHE.get(new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), () -> {
|
||||
final byte[] saltedPassword, serverKey, clientKey;
|
||||
saltedPassword = hi(password.getBytes(), Base64.decode(salt, Base64.DEFAULT), iterations);
|
||||
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
|
||||
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
|
||||
return new KeyPair(clientKey, serverKey);
|
||||
});
|
||||
return new KeyPair(clientKey, serverKey);
|
||||
} catch (final InvalidKeyException | NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private final String clientNonce;
|
||||
|
@ -84,21 +63,20 @@ abstract class ScramMechanism extends SaslMechanism {
|
|||
clientFirstMessageBare = "";
|
||||
}
|
||||
|
||||
private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException {
|
||||
final HMac hMac = getHMAC();
|
||||
hMac.init(new KeyParameter(key));
|
||||
hMac.update(input, 0, input.length);
|
||||
final byte[] out = new byte[hMac.getMacSize()];
|
||||
hMac.doFinal(out, 0);
|
||||
private static synchronized byte[] hmac(final byte[] key, final byte[] input)
|
||||
throws InvalidKeyException {
|
||||
HMAC.init(new KeyParameter(key));
|
||||
HMAC.update(input, 0, input.length);
|
||||
final byte[] out = new byte[HMAC.getMacSize()];
|
||||
HMAC.doFinal(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
public byte[] digest(byte[] bytes) {
|
||||
final Digest digest = getDigest();
|
||||
digest.reset();
|
||||
digest.update(bytes, 0, bytes.length);
|
||||
final byte[] out = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(out, 0);
|
||||
public static synchronized byte[] digest(byte[] bytes) {
|
||||
DIGEST.reset();
|
||||
DIGEST.update(bytes, 0, bytes.length);
|
||||
final byte[] out = new byte[DIGEST.getDigestSize()];
|
||||
DIGEST.doFinal(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
@ -107,7 +85,7 @@ abstract class ScramMechanism extends SaslMechanism {
|
|||
* pseudorandom function (PRF) and with dkLen == output length of
|
||||
* HMAC() == output length of H().
|
||||
*/
|
||||
private byte[] hi(final byte[] key, final byte[] salt, final int iterations)
|
||||
private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations)
|
||||
throws InvalidKeyException {
|
||||
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
|
||||
byte[] out = u.clone();
|
||||
|
@ -193,10 +171,15 @@ abstract class ScramMechanism extends SaslMechanism {
|
|||
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
|
||||
+ clientFinalMessageWithoutProof).getBytes();
|
||||
|
||||
final KeyPair keys;
|
||||
try {
|
||||
keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount);
|
||||
} catch (ExecutionException e) {
|
||||
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism".
|
||||
final KeyPair keys = CACHE.get(
|
||||
CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getJid().asBareJid().toEscapedString()).getBytes()) + ","
|
||||
+ CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getPassword()).getBytes()) + ","
|
||||
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
|
||||
+ iterationCount + ","
|
||||
+ getMechanism()
|
||||
);
|
||||
if (keys == null) {
|
||||
throw new AuthenticationException("Invalid keys generated");
|
||||
}
|
||||
final byte[] clientSignature;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.digests.SHA1Digest;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
|
||||
|
@ -10,30 +9,22 @@ import eu.siacs.conversations.entities.Account;
|
|||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
public class ScramSha1 extends ScramMechanism {
|
||||
static {
|
||||
DIGEST = new SHA1Digest();
|
||||
HMAC = new HMac(new SHA1Digest());
|
||||
}
|
||||
|
||||
public static final String MECHANISM = "SCRAM-SHA-1";
|
||||
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HMac getHMAC() {
|
||||
return new HMac(new SHA1Digest());
|
||||
}
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Digest getDigest() {
|
||||
return new SHA1Digest();
|
||||
}
|
||||
|
||||
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return MECHANISM;
|
||||
}
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return "SCRAM-SHA-1";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
|
||||
|
@ -10,30 +9,22 @@ import eu.siacs.conversations.entities.Account;
|
|||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
public class ScramSha256 extends ScramMechanism {
|
||||
static {
|
||||
DIGEST = new SHA256Digest();
|
||||
HMAC = new HMac(new SHA256Digest());
|
||||
}
|
||||
|
||||
public static final String MECHANISM = "SCRAM-SHA-256";
|
||||
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HMac getHMAC() {
|
||||
return new HMac(new SHA256Digest());
|
||||
}
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 25;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Digest getDigest() {
|
||||
return new SHA256Digest();
|
||||
}
|
||||
|
||||
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 25;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return MECHANISM;
|
||||
}
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return "SCRAM-SHA-256";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.digests.SHA512Digest;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
public class ScramSha512 extends ScramMechanism {
|
||||
|
||||
public static final String MECHANISM = "SCRAM-SHA-512";
|
||||
|
||||
@Override
|
||||
protected HMac getHMAC() {
|
||||
return new HMac(new SHA512Digest());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Digest getDigest() {
|
||||
return new SHA512Digest();
|
||||
}
|
||||
|
||||
public ScramSha512(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 30;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return MECHANISM;
|
||||
}
|
||||
}
|
|
@ -10,69 +10,69 @@ import java.util.NoSuchElementException;
|
|||
* A tokenizer for GS2 header strings
|
||||
*/
|
||||
public final class Tokenizer implements Iterator<String>, Iterable<String> {
|
||||
private final List<String> parts;
|
||||
private int index;
|
||||
private final List<String> parts;
|
||||
private int index;
|
||||
|
||||
public Tokenizer(final byte[] challenge) {
|
||||
final String challengeString = new String(challenge);
|
||||
parts = new ArrayList<>(Arrays.asList(challengeString.split(",")));
|
||||
// Trim parts.
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
parts.set(i, parts.get(i).trim());
|
||||
}
|
||||
index = 0;
|
||||
}
|
||||
public Tokenizer(final byte[] challenge) {
|
||||
final String challengeString = new String(challenge);
|
||||
parts = new ArrayList<>(Arrays.asList(challengeString.split(",")));
|
||||
// Trim parts.
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
parts.set(i, parts.get(i).trim());
|
||||
}
|
||||
index = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is at least one more element, false otherwise.
|
||||
*
|
||||
* @see #next
|
||||
*/
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return parts.size() != index + 1;
|
||||
}
|
||||
/**
|
||||
* Returns true if there is at least one more element, false otherwise.
|
||||
*
|
||||
* @see #next
|
||||
*/
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return parts.size() != index + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next object and advances the iterator.
|
||||
*
|
||||
* @return the next object.
|
||||
* @throws java.util.NoSuchElementException if there are no more elements.
|
||||
* @see #hasNext
|
||||
*/
|
||||
@Override
|
||||
public String next() {
|
||||
if (hasNext()) {
|
||||
return parts.get(index++);
|
||||
} else {
|
||||
throw new NoSuchElementException("No such element. Size is: " + parts.size());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the next object and advances the iterator.
|
||||
*
|
||||
* @return the next object.
|
||||
* @throws java.util.NoSuchElementException if there are no more elements.
|
||||
* @see #hasNext
|
||||
*/
|
||||
@Override
|
||||
public String next() {
|
||||
if (hasNext()) {
|
||||
return parts.get(index++);
|
||||
} else {
|
||||
throw new NoSuchElementException("No such element. Size is: " + parts.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the last object returned by {@code next} from the collection.
|
||||
* This method can only be called once between each call to {@code next}.
|
||||
*
|
||||
* @throws UnsupportedOperationException if removing is not supported by the collection being
|
||||
* iterated.
|
||||
* @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
|
||||
* already been called after the last call to {@code next}.
|
||||
*/
|
||||
@Override
|
||||
public void remove() {
|
||||
if (index <= 0) {
|
||||
throw new IllegalStateException("You can't delete an element before first next() method call");
|
||||
}
|
||||
parts.remove(--index);
|
||||
}
|
||||
/**
|
||||
* Removes the last object returned by {@code next} from the collection.
|
||||
* This method can only be called once between each call to {@code next}.
|
||||
*
|
||||
* @throws UnsupportedOperationException if removing is not supported by the collection being
|
||||
* iterated.
|
||||
* @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
|
||||
* already been called after the last call to {@code next}.
|
||||
*/
|
||||
@Override
|
||||
public void remove() {
|
||||
if(index <= 0) {
|
||||
throw new IllegalStateException("You can't delete an element before first next() method call");
|
||||
}
|
||||
parts.remove(--index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link java.util.Iterator} for the elements in this object.
|
||||
*
|
||||
* @return An {@code Iterator} instance.
|
||||
*/
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
return parts.iterator();
|
||||
}
|
||||
/**
|
||||
* Returns an {@link java.util.Iterator} for the elements in this object.
|
||||
*
|
||||
* @return An {@code Iterator} instance.
|
||||
*/
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
return parts.iterator();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@ import android.content.ContentValues;
|
|||
import android.database.Cursor;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -28,9 +27,9 @@ import eu.siacs.conversations.services.AvatarService;
|
|||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
import eu.siacs.conversations.utils.XmppUri;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class Account extends AbstractEntity implements AvatarService.Avatarable {
|
||||
|
||||
|
@ -65,6 +64,7 @@ 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);
|
||||
return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer());
|
||||
}
|
||||
|
||||
public boolean httpUploadAvailable() {
|
||||
|
@ -249,7 +249,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
|
||||
public String getHostname() {
|
||||
return Strings.nullToEmpty(this.hostname);
|
||||
return this.hostname == null ? "" : this.hostname;
|
||||
}
|
||||
|
||||
public void setHostname(String hostname) {
|
||||
|
@ -615,11 +615,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
return UIHelper.getColorForName(jid.asBareJid().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAvatarName() {
|
||||
throw new IllegalStateException("This method should not be called");
|
||||
}
|
||||
|
||||
public enum State {
|
||||
DISABLED(false, false),
|
||||
OFFLINE(false),
|
||||
|
@ -637,7 +632,6 @@ 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,
|
||||
|
@ -704,8 +698,6 @@ 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:
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
|
@ -22,7 +21,7 @@ import eu.siacs.conversations.xmpp.Jid;
|
|||
|
||||
public class Bookmark extends Element implements ListItem {
|
||||
|
||||
private final Account account;
|
||||
private Account account;
|
||||
private WeakReference<Conversation> conversation;
|
||||
private Jid jid;
|
||||
|
||||
|
@ -249,9 +248,4 @@ public class Bookmark extends Element implements ListItem {
|
|||
public int getAvatarBackgroundColor() {
|
||||
return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAvatarName() {
|
||||
return getDisplayName();
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,11 +2,10 @@ package eu.siacs.conversations.entities;
|
|||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.collect.ComparisonChain;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
|
@ -28,11 +27,10 @@ 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;
|
||||
import eu.siacs.conversations.xmpp.mam.MamReference;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
import static eu.siacs.conversations.entities.Bookmark.printableValue;
|
||||
|
||||
|
@ -70,12 +68,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
|
||||
protected Account account = null;
|
||||
private String draftMessage;
|
||||
private final String name;
|
||||
private final String contactUuid;
|
||||
private final String accountUuid;
|
||||
private String name;
|
||||
private String contactUuid;
|
||||
private String accountUuid;
|
||||
private Jid contactJid;
|
||||
private int status;
|
||||
private final long created;
|
||||
private long created;
|
||||
private int mode;
|
||||
private JSONObject attributes;
|
||||
private Jid nextCounterpart;
|
||||
|
@ -144,7 +142,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.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
|
||||
if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
|
||||
return false;
|
||||
}
|
||||
return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
|
||||
|
@ -188,18 +186,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
return null;
|
||||
}
|
||||
|
||||
public int countFailedDeliveries() {
|
||||
int count = 0;
|
||||
synchronized (this.messages) {
|
||||
for(final Message message : this.messages) {
|
||||
if (message.getStatus() == Message.STATUS_SEND_FAILED) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public Message getLastEditableMessage() {
|
||||
synchronized (this.messages) {
|
||||
for (final Message message : Lists.reverse(this.messages)) {
|
||||
|
@ -259,22 +245,9 @@ 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() || 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)) {
|
||||
&& (message.isFileOrImage() || message.treatAsDownloadable())) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
@ -502,7 +475,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
|
||||
public void setLastClearHistory(long time, String reference) {
|
||||
if (reference != null) {
|
||||
setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time + ":" + reference);
|
||||
setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
|
||||
} else {
|
||||
setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
|
||||
}
|
||||
|
@ -565,15 +538,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
|
||||
public boolean isRead() {
|
||||
synchronized (this.messages) {
|
||||
for(final Message message : Lists.reverse(this.messages)) {
|
||||
if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) {
|
||||
continue;
|
||||
}
|
||||
return message.isRead();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
|
||||
}
|
||||
|
||||
public List<Message> markRead(String upToUuid) {
|
||||
|
@ -802,7 +767,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;
|
||||
otherBody = message.getFileParams().url.toString();
|
||||
} else {
|
||||
otherBody = message.body;
|
||||
}
|
||||
|
@ -1033,11 +998,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
public int unreadCount() {
|
||||
synchronized (this.messages) {
|
||||
int count = 0;
|
||||
for(final Message message : Lists.reverse(this.messages)) {
|
||||
if (message.isRead()) {
|
||||
if (message.getType() == Message.TYPE_RTP_SESSION) {
|
||||
continue;
|
||||
}
|
||||
for (int i = this.messages.size() - 1; i >= 0; --i) {
|
||||
if (this.messages.get(i).isRead()) {
|
||||
return count;
|
||||
}
|
||||
++count;
|
||||
|
@ -1104,11 +1066,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
return UIHelper.getColorForName(getName().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAvatarName() {
|
||||
return getName().toString();
|
||||
}
|
||||
|
||||
public interface OnMessageFound {
|
||||
void onMessageFound(final Message message);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue