~singpolyma/cheogram-android

480902d4574b4a9f28a58da6c74bfeaebd924900 — Stephen Paul Weber 2 years ago 3825e24 + 47cc063
Merge remote-tracking branch 'singpolyma/dtmf' into integration2

* singpolyma/dtmf: (145 commits)
  Detect a video call in a consistent way
  Use a boolean for this state
  Switch onClicks to use DataBinding
  Polyfill to allow use on Android 21
  RtpSessionActivity: Fix NPE from using incorrect view id
  Changed dialpad icon to something more recognizable.
  Cleaned up DTMF code and click handling.
  WIP - dialpad and dtmf sending
  flush stanzas in batches
  code clean up in TagWriter
  Fix #4249.
  Clarify build instructions.
  allow verification of own omemo keys via uri
  bump dependencies
  version bump to 2.10.3-beta
  fix precondition for timeout handling
  bump agp version
  pulled translations from transifex
  add Samsung S4 to hardware aec blacklist
  add additional logging to image compression
  ...
136 files changed, 5913 insertions(+), 2437 deletions(-)

M .builds/debian-stable.yml
A .github/workflows/android.yml
D .travis.yml
M CHANGELOG.md
M build.gradle
M conversations.doap
A fastlane/metadata/android/en-US/changelogs/42018.txt
A fastlane/metadata/android/en-US/changelogs/42022.txt
A fastlane/metadata/android/en-US/changelogs/42023.txt
M gradle/wrapper/gradle-wrapper.properties
M proguard-rules.pro
M src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java
M src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java
M src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java
A src/conversations/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java
M src/conversations/res/values-ar/strings.xml
M src/conversations/res/values-bg/strings.xml
M src/conversations/res/values-el/strings.xml
A src/conversations/res/values-fi/strings.xml
M src/conversations/res/values-it/strings.xml
M src/conversations/res/values-ja/strings.xml
A src/conversations/res/values-sk/strings.xml
M src/conversations/res/values-sv/strings.xml
M src/main/AndroidManifest.xml
M src/main/java/eu/siacs/conversations/Config.java
M src/main/java/eu/siacs/conversations/android/JabberIdContact.java
M src/main/java/eu/siacs/conversations/entities/Account.java
M src/main/java/eu/siacs/conversations/entities/Conversation.java
M src/main/java/eu/siacs/conversations/entities/Message.java
M src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
M src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
M src/main/java/eu/siacs/conversations/persistance/FileBackend.java
M src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java
M src/main/java/eu/siacs/conversations/services/NotificationService.java
M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
M src/main/java/eu/siacs/conversations/ui/AboutActivity.java
M src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
M src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
M src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
M src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
M src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
M src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
M src/main/java/eu/siacs/conversations/ui/LocationActivity.java
M src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java
M src/main/java/eu/siacs/conversations/ui/RecordingActivity.java
M src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
M src/main/java/eu/siacs/conversations/ui/ScanActivity.java
M src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
M src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java
M src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
M src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
M src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
M src/main/java/eu/siacs/conversations/ui/XmppActivity.java
M src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java
M src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
A src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java
M src/main/java/eu/siacs/conversations/ui/util/Attachment.java
A src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java
A src/main/java/eu/siacs/conversations/ui/util/Rationals.java
A src/main/java/eu/siacs/conversations/ui/util/SettingsUtils.java
M src/main/java/eu/siacs/conversations/ui/widget/DialpadView.java
M src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java
A src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java
D src/main/java/eu/siacs/conversations/utils/Android360pFormatStrategy.java
D src/main/java/eu/siacs/conversations/utils/Android720pFormatStrategy.java
D src/main/java/eu/siacs/conversations/utils/ExifHelper.java
M src/main/java/eu/siacs/conversations/utils/LocationProvider.java
M src/main/java/eu/siacs/conversations/utils/MessageUtils.java
M src/main/java/eu/siacs/conversations/utils/MimeUtils.java
M src/main/java/eu/siacs/conversations/utils/Patterns.java
M src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java
M src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java
A src/main/java/eu/siacs/conversations/utils/TranscoderStrategies.java
M src/main/java/eu/siacs/conversations/utils/UIHelper.java
M src/main/java/eu/siacs/conversations/utils/XmppUri.java
M src/main/java/eu/siacs/conversations/xml/Element.java
M src/main/java/eu/siacs/conversations/xml/Namespace.java
M src/main/java/eu/siacs/conversations/xml/TagWriter.java
M src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
M src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java
M src/main/res/layout/activity_rtp_session.xml
A src/main/res/layout/activity_uri_handler.xml
M src/main/res/layout/dialpad.xml
M src/main/res/menu/activity_rtp_session.xml
M src/main/res/menu/start_conversation_fab_submenu.xml
M src/main/res/values-ar/strings.xml
M src/main/res/values-bg/strings.xml
M src/main/res/values-bn-rIN/strings.xml
M src/main/res/values-cs/strings.xml
M src/main/res/values-da-rDK/strings.xml
M src/main/res/values-de/strings.xml
M src/main/res/values-el/strings.xml
M src/main/res/values-es/strings.xml
A src/main/res/values-fi/strings.xml
M src/main/res/values-fr/strings.xml
M src/main/res/values-gl/strings.xml
M src/main/res/values-hu/strings.xml
M src/main/res/values-it/strings.xml
M src/main/res/values-ja/strings.xml
M src/main/res/values-ml/strings.xml
M src/main/res/values-pl/strings.xml
M src/main/res/values-pt-rBR/strings.xml
M src/main/res/values-ro-rRO/strings.xml
M src/main/res/values-ru/strings.xml
M src/main/res/values-sk/strings.xml
M src/main/res/values-sr/strings.xml
M src/main/res/values-sv/strings.xml
M src/main/res/values-tr-rTR/strings.xml
M src/main/res/values-vi/strings.xml
M src/main/res/values-zh-rCN/strings.xml
M src/main/res/values/defaults.xml
M src/main/res/values/dimens.xml
M src/main/res/values/strings.xml
M src/main/res/values/styles.xml
A src/main/res/xml/backup_content.xml
M src/main/res/xml/preferences.xml
M src/quicksy/java/eu/siacs/conversations/android/PhoneNumberContact.java
M src/quicksy/java/eu/siacs/conversations/entities/Entry.java
M src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java
M src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java
M src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java
M src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java
M src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java
M src/quicksy/res/layout/activity_enter_number.xml
M src/quicksy/res/values-ar/strings.xml
M src/quicksy/res/values-bg/strings.xml
A src/quicksy/res/values-fi/strings.xml
A src/quicksy/res/values-sk/strings.xml
M .builds/debian-stable.yml => .builds/debian-stable.yml +5 -0
@@ 22,6 22,11 @@ tasks:
    echo y | android/cmdline-tools/tools/bin/sdkmanager "build-tools;29.0.2"
    touch ~/.android/repositories.cfg
    yes | android/cmdline-tools/tools/bin/sdkmanager --licenses
- libwebrtc: |
    cd cheogram-android
    mkdir libs
    cd libs
    wget -qO libwebtrc.aar https://cloudflare-ipfs.com/ipfs/QmeqMiLxHi8AAjXobxr3QTfa1bSSLyAu86YviAqQnjxCjM/libwebrtc.aar
- build: |
    cd cheogram-android
    ./gradlew assembleCheogramFreeSystemDebug

A .github/workflows/android.yml => .github/workflows/android.yml +38 -0
@@ 0,0 1,38 @@
name: Android CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: '11'
        distribution: 'adopt'
    - name: Download WebRTC
      run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build Quicksy (Compat)
      run: ./gradlew assembleQuicksyFreeCompatDebug
    - name: Build Quicksy (System)
      run: ./gradlew assembleQuicksyFreeSystemDebug
    - name: Build Conversations (Compat)
      run: ./gradlew assembleConversationsFreeCompatDebug
    - name: Build Conversations (System)
      run: ./gradlew assembleConversationsFreeSystemDebug
    - uses: actions/upload-artifact@v2
      with:
        name: Conversations all-flavors (debug)
        path: ./build/outputs/apk/**/debug/Conversations-*.apk

      

D .travis.yml => .travis.yml +0 -22
@@ 1,22 0,0 @@
language: android
jdk:
  - oraclejdk8
android:
  components:
    - platform-tools
    - tools
    - build-tools-28.0.3
    - extra-google-google_play_services
  licenses:
    - '.+'
before_script:
    - mkdir libs
    - wget -O libs/libwebrtc-m90.aar https://gultsch.de/files/libwebrtc-m90.aar
script:
    - ./gradlew assembleQuicksyFreeCompatDebug
    - ./gradlew assembleQuicksyFreeSystemDebug
    - ./gradlew assembleConversationsFreeCompatDebug
    - ./gradlew assembleConversationsFreeSystemDebug

before_install:
    - yes | sdkmanager "platforms;android-28"

M CHANGELOG.md => CHANGELOG.md +16 -0
@@ 1,5 1,21 @@
# Changelog

### Version 2.10.2

* Fix crash when rendering some quotes
* Fix crash in welcome screen

### Version 2.10.1

* Fix issue with some videos not being compressed
* Fix rare crash when opening notification

### Version 2.10.0

* Show black bars when remote video does not match aspect ratio of screen
* Improve search performance
* Add setting to prevent screenshots

### Version 2.9.13

* minor A/V improvements

M build.gradle => build.gradle +35 -27
@@ 1,5 1,3 @@
import com.android.build.OutputFile

// Top-level build file where you can add configuration options common to all
// sub-projects/modules.
buildscript {


@@ 8,7 6,7 @@ buildscript {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.2.1'
        classpath 'com.android.tools.build:gradle:7.1.1'
    }
}



@@ 35,23 33,23 @@ configurations {
dependencies {
    implementation 'androidx.viewpager:viewpager:1.0.0'

    playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') {
    playstoreImplementation('com.google.firebase:firebase-messaging:23.0.0') {
        exclude group: 'com.google.firebase', module: 'firebase-core'
        exclude group: 'com.google.firebase', module: 'firebase-analytics'
        exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
    }
    conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2")
    conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2")
    quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0'
    quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0'
    quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
    quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
    implementation 'org.sufficientlysecure:openpgp-api:10.0'
    implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.exifinterface:exifinterface:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'androidx.exifinterface:exifinterface:1.3.3'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
    implementation 'androidx.emoji:emoji:1.1.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    compatImplementation 'androidx.emoji:emoji-appcompat:1.1.0'
    conversationsFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
    quicksyFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'


@@ 64,21 62,22 @@ dependencies {
    implementation 'org.whispersystems:signal-protocol-java:2.6.2'
    implementation 'com.makeramen:roundedimageview:2.3.0'
    implementation "com.wefika:flowlayout:0.4.1"
    implementation 'net.ypresto.androidtranscoder:android-transcoder:0.3.0'
    implementation 'org.jxmpp:jxmpp-jid:1.0.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 'org.hsluv:hsluv:0.2'
    implementation 'org.conscrypt:conscrypt-android:2.5.2'
    implementation 'me.drakeet.support:toastcompat:1.1.0'
    implementation "com.leinardi.android:speed-dial:2.0.1"
    implementation "com.leinardi.android:speed-dial:3.2.0"

    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
    implementation "com.squareup.okhttp3:okhttp:4.9.1"
    implementation "com.squareup.okhttp3:okhttp:4.9.3"

    implementation 'com.google.guava:guava:30.1.1-android'
    quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
    implementation 'org.webrtc:google-webrtc:1.0.32006'
    quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36'
    implementation fileTree(include: ['libwebrtc.aar'], dir: 'libs')
}

ext {


@@ 93,8 92,8 @@ android {
    defaultConfig {
        minSdkVersion 24
        targetSdkVersion 29
        versionCode 42015
        versionName "2.9.13"
        versionCode 42024
        versionName "2.10.3-beta"
        archivesBaseName += "-$versionName"
        applicationId "eu.siacs.conversations"
        resValue "string", "applicationId", applicationId


@@ 105,19 104,13 @@ android {


    configurations {
        compile.exclude group: 'org.jetbrains' , module:'annotations'
        implementation.exclude group: 'org.jetbrains' , module:'annotations'
    }

    dataBinding {
        enabled true
    }

    dexOptions {
        // Skip pre-dexing when running on Travis CI or when disabled via -Dpre-dex=false.
        preDexLibraries = preDexEnabled && !travisBuild
        jumboMode true
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8


@@ 292,10 285,25 @@ android {

        }
    }

    packagingOptions {
        exclude 'META-INF/BCKEY.DSA'
        exclude 'META-INF/BCKEY.SF'
        resources {
            excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF']
        }
    }
    lint {
        disable 'MissingTranslation', 'InvalidPackage', 'AppCompatResource'
    }


    android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
            if (baseAbiVersionCode != null) {
                output.versionCodeOverride = (100 * project.android.defaultConfig.versionCode) + baseAbiVersionCode
            } else {
                output.versionCodeOverride = 100 * project.android.defaultConfig.versionCode
            }
        }

    }
}

M conversations.doap => conversations.doap +10 -3
@@ 453,12 453,19 @@
            <xmpp:version>0.2.0</xmpp:version>
        </xmpp:SupportedXep>
    </implements>
    <implements>
        <xmpp:SupportedXep>
            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
            <xmpp:status>complete</xmpp:status>
            <xmpp:version>0.1.0</xmpp:version>
        </xmpp:SupportedXep>
    </implements>

    <release>
        <Version>
            <revision>2.5.8</revision>
            <created>2019-09-12</created>
            <file-release rdf:resource="https://github.com/iNPUTmice/Conversations/archive/2.5.8.tar.gz"/>
            <revision>2.9.13</revision>
            <created>2021-05-03</created>
            <file-release rdf:resource="https://github.com/iNPUTmice/Conversations/archive/2.9.13.tar.gz"/>
        </Version>
    </release>
</Project>

A fastlane/metadata/android/en-US/changelogs/42018.txt => fastlane/metadata/android/en-US/changelogs/42018.txt +3 -0
@@ 0,0 1,3 @@
* Show black bars when remote video does not match aspect ratio of screen
* Improve search performance
* Add setting to prevent screenshots

A fastlane/metadata/android/en-US/changelogs/42022.txt => fastlane/metadata/android/en-US/changelogs/42022.txt +2 -0
@@ 0,0 1,2 @@
* Fix issue with some videos not being compressed
* Fix rare crash when opening notification

A fastlane/metadata/android/en-US/changelogs/42023.txt => fastlane/metadata/android/en-US/changelogs/42023.txt +2 -0
@@ 0,0 1,2 @@
* Fix crash when rendering some quotes
* Fix crash in welcome screen

M gradle/wrapper/gradle-wrapper.properties => gradle/wrapper/gradle-wrapper.properties +1 -1
@@ 3,4 3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip

M proguard-rules.pro => proguard-rules.pro +9 -0
@@ 26,6 26,15 @@
-dontwarn java.lang.**
-dontwarn javax.lang.**

-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE

-keepclassmembers class eu.siacs.conversations.http.services.** {
  !transient <fields>;
}

M src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java => src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java +7 -0
@@ 29,6 29,7 @@ import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
import eu.siacs.conversations.services.ImportBackupService;
import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;

public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {


@@ 54,6 55,12 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
        this.binding.list.setAdapter(this.backupFileAdapter);
        this.backupFileAdapter.setOnItemClickedListener(this);
    }
    
    @Override
    protected void onResume(){
        super.onResume();
        SettingsUtils.applyScreenshotPreventionSetting(this);
    }

    @Override
    public boolean onCreateOptionsMenu(final Menu menu) {

M src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java => src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java +1 -0
@@ 228,6 228,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (grantResults.length > 0) {
            if (allGranted(grantResults)) {
                switch (requestCode) {

M src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java => src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java +3 -1
@@ 106,7 106,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
    }

    @Override
    public void onNewIntent(Intent intent) {
    public void onNewIntent(final Intent intent) {
        super.onNewIntent(intent);
        if (intent != null) {
            setIntent(intent);
        }


@@ 201,6 202,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
        if (grantResults.length > 0) {
            if (allGranted(grantResults)) {

A src/conversations/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java => src/conversations/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java +11 -0
@@ 0,0 1,11 @@
package eu.siacs.conversations.utils;

import android.content.Context;

import eu.siacs.conversations.xmpp.Jid;

public class PhoneNumberUtilWrapper {
    public static String toFormattedPhoneNumber(Context context, Jid jid) {
        throw new AssertionError("This method is not implemented in Conversations");
    }
}

M src/conversations/res/values-ar/strings.xml => src/conversations/res/values-ar/strings.xml +15 -1
@@ 3,4 3,18 @@
    <string name="pick_a_server">اختر مزود خدمة XMPP الخاص بك</string>
    <string name="use_conversations.im">استخدِم conversations.im</string>
    <string name="create_new_account">أنشئ حسابًا جديدًا</string>
    </resources>
\ No newline at end of file
    <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>
\ No newline at end of file

M src/conversations/res/values-bg/strings.xml => src/conversations/res/values-bg/strings.xml +5 -5
@@ 1,13 1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Изберете вашият XMPP доставчик</string>
    <string name="pick_a_server">Изберете своя XMPP доставчик</string>
    <string name="use_conversations.im">Използвайте conversations.im</string>
    <string name="create_new_account">Създаване не нов профил</string>
    <string name="do_you_have_an_account">Имате ли вече XMPP профил? Това може да се случи, ако вече използвате друг клиент на XMPP или сте използвали преди това Conversations. Ако не, можете да създадете нов XMPP профил в момента.\nСъвет: Някои доставчици на имейл също предоставят XMPP профили.
    <string name="do_you_have_an_account">Имате ли вече XMPP профил? Може да имате, ако вече използвате друг клиент на XMPP или сте използвали Conversations и преди. Ако не, можете да създадете нов XMPP профил сега.\nСъвет: някои доставчици на е-поща също предоставят XMPP профили.
 </string>
    <string name="server_select_text">XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи добре с 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="server_select_text">XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, обаче, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи най-добре с 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>

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

A src/conversations/res/values-fi/strings.xml => src/conversations/res/values-fi/strings.xml +14 -0
@@ 0,0 1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Valitse XMPP-palveluntarjoaja</string>
    <string name="use_conversations.im">Käytä conversations.im:ää</string>
    <string name="create_new_account">Luo uusi tili</string>
    <string name="do_you_have_an_account">Onko sinulla jo XMPP-tunnus? Jos käytät jo toista XMPP-sovellusta tai olet käyttänyt Conversationsia aiemmin, niin voi olla. Jos ei, voit tehdä uuden XMPP-tilin saman tien.\nVinkki: Jotkin sähköpostipalvelut tarjoavat myös XMPP-tilin.</string>
    <string name="server_select_text">XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä asiakasohjelmaa minkä tahansa haluamasi XMPP-palvelimen kanssa.\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin.</string>
    <string name="magic_create_text_on_x">Sinut on kutsuttu %1$s:iin. Opastamme sinua tilin luomisen kanssa.\nValitessasi palvelimen %1$s palveluntarjoajaksesi voit jutella muiden palveluntajoajien käyttäjien kanssa kertomalla heille koko XMPP-osoitteesi.</string>
    <string name="magic_create_text_fixed">Sinut on kutsuttu palvelimelle %1$s. Käyttäjänimesi on valittu valmiiksi puolestasi. Opastamme sinua tilin luomisen kanssa.\nVoit jutella muiden palveluntarjoajien käyttäjien kanssa kertomalle heille koko XMPP-osoitteesi.</string>
    <string name="your_server_invitation">Kutsusi palvelimelle</string>
    <string name="improperly_formatted_provisioning">Virheellisesti muotoiltu koodi</string>
    <string name="if_contact_is_nearby_use_qr">Jos henkilö on lähellä, hän voi myös hyväksyä kutsun lukemalla allaolevan koodin.</string>
    <string name="share_invite_with">Jaa kutsu sovelluksella...</string>
</resources>
\ No newline at end of file

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

M src/conversations/res/values-ja/strings.xml => src/conversations/res/values-ja/strings.xml +7 -7
@@ 1,12 1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">XMPPプロバイダーを選択してください</string>
    <string name="use_conversations.im">conversations.imを利用する</string>
    <string name="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に最適化されたプロバイダーconversations.im¹で簡単にアカウントを作成することもできます。</string>
    <string name="magic_create_text_on_x">%1$sへ招待されました。アカウント作成手順をご案内します。 \n%1$sをプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。</string>
    <string name="magic_create_text_fixed">%1$sへ招待されました。ユーザーネームは既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。</string>
    <string name="pick_a_server">XMPP プロバイダーを選択してください</string>
    <string name="use_conversations.im">conversations.im を利用する</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 に最適化されたプロバイダー conversations.im¹ で簡単にアカウントを作成することもできます。</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>

A src/conversations/res/values-sk/strings.xml => src/conversations/res/values-sk/strings.xml +14 -0
@@ 0,0 1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Vyberte si svojho XMPP poskytovateľa</string>
    <string name="use_conversations.im">Použiť conversations.im</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 conversations.im¹; 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>
\ No newline at end of file

M src/conversations/res/values-sv/strings.xml => src/conversations/res/values-sv/strings.xml +10 -2
@@ 1,5 1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Välj din XMPP-leverantör</string>
    <string name="use_conversations.im">Använd conversations.im</string>
    <string name="create_new_account">Skapa nytt konto</string>
    </resources>
\ No newline at end of file
    <string name="create_new_account">Skapa ett nytt konto</string>
    <string name="do_you_have_an_account">Har du redan ett XMPP-konto? Detta kan vara fallet om du redan använder en annan XMPP-klient eller om du har använt Conversations tidigare. Om inte, kan du skapa ett nytt XMPP-konto på en gång.\nTips: Vissa e-postleverantörer tillhandahåller även XMPP-konton.</string>
    <string name="your_server_invitation">Din serverinbjudan</string>
    <string name="improperly_formatted_provisioning">Felaktigt formaterad provisioneringskod</string>
    <string name="tap_share_button_send_invite">Tryck på dela-knappen för att skicka en inbjudan till din kontakt till %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Om din kontakt är i närheten, kan de också skanna koden nedan för att acceptera din inbjudan.</string>
    <string name="easy_invite_share_text">Gå med %1$s och chatta med mig: %2$s</string>
    <string name="share_invite_with">Dela inbjudan med...</string>
</resources>
\ No newline at end of file

M src/main/AndroidManifest.xml => src/main/AndroidManifest.xml +11 -3
@@ 39,8 39,6 @@
    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    <uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />

    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />


@@ 54,7 52,8 @@


    <application
        android:allowBackup="false"
        android:allowBackup="true"
        android:fullBackupContent="@xml/backup_content"
        android:appCategory="social"
        android:hardwareAccelerated="true"
        android:icon="@mipmap/new_launcher"


@@ 152,6 151,14 @@
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="tel" />
            </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"


@@ 195,6 202,7 @@
            android:launchMode="singleTop" />
        <activity
            android:name=".ui.EditAccountActivity"
            android:exported="false"
            android:launchMode="singleTop"
            android:windowSoftInputMode="stateHidden|adjustResize" />
        <activity

M src/main/java/eu/siacs/conversations/Config.java => src/main/java/eu/siacs/conversations/Config.java +5 -0
@@ 204,4 204,9 @@ public final class Config {
        public final static float LOCATION_FIX_SPACE_DELTA = 10; // m
        public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
    }

    // How deep nested quotes should be displayed. '2' means one quote nested in another.
    public static final int QUOTE_MAX_DEPTH = 7;
    // How deep nested quotes should be created on quoting a message.
    public static final int QUOTING_MAX_DEPTH = 1;
}

M src/main/java/eu/siacs/conversations/android/JabberIdContact.java => src/main/java/eu/siacs/conversations/android/JabberIdContact.java +33 -30
@@ 17,6 17,21 @@ import eu.siacs.conversations.xmpp.Jid;

public class JabberIdContact extends AbstractPhoneContact {

    private static final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
            ContactsContract.Data.DISPLAY_NAME,
            ContactsContract.Data.PHOTO_URI,
            ContactsContract.Data.LOOKUP_KEY,
            ContactsContract.CommonDataKinds.Im.DATA
    };
    private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and lower(" + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + ")=?))";

    private static final String[] SELECTION_ARGS = {
            ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE,
            String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER),
            String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM),
            "xmpp"
    };

    private final Jid jid;

    private JabberIdContact(Cursor cursor) throws IllegalArgumentException {


@@ 36,38 51,26 @@ public class JabberIdContact extends AbstractPhoneContact {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            return Collections.emptyMap();
        }
        final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
                ContactsContract.Data.DISPLAY_NAME,
                ContactsContract.Data.PHOTO_URI,
                ContactsContract.Data.LOOKUP_KEY,
                ContactsContract.CommonDataKinds.Im.DATA};

        final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
                + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
                + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
                + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
                + "\")";
        final Cursor cursor;
        try {
            cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, null);
        } catch (Exception e) {
            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);
        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");
                }
            } catch (IllegalArgumentException e) {
                Log.d(Config.LOGTAG,"unable to create jabber id contact");
            }
            return contacts;
        } catch (final Exception e) {
            Log.d(Config.LOGTAG, "unable to query", e);
            return Collections.emptyMap();
        }
        if (cursor != null) {
            cursor.close();
        }
        return contacts;
    }
}

M src/main/java/eu/siacs/conversations/entities/Account.java => src/main/java/eu/siacs/conversations/entities/Account.java +3 -1
@@ 5,6 5,8 @@ import android.database.Cursor;
import android.os.SystemClock;
import android.util.Log;

import com.google.common.base.Strings;

import org.json.JSONException;
import org.json.JSONObject;



@@ 247,7 249,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable 
    }

    public String getHostname() {
        return this.hostname == null ? "" : this.hostname;
        return Strings.nullToEmpty(this.hostname);
    }

    public void setHostname(String hostname) {

M src/main/java/eu/siacs/conversations/entities/Conversation.java => src/main/java/eu/siacs/conversations/entities/Conversation.java +15 -1
@@ 28,6 28,7 @@ import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;


@@ 258,9 259,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
    public Message findMessageWithFileAndUuid(final String uuid) {
        synchronized (this.messages) {
            for (final Message message : this.messages) {
                final Transferable transferable = message.getTransferable();
                final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
                if (message.getUuid().equals(uuid)
                        && message.getEncryption() != Message.ENCRYPTION_PGP
                        && (message.isFileOrImage() || message.treatAsDownloadable())) {
                        && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
                    return message;
                }
            }
        }
        return null;
    }

    public Message findMessageWithUuid(final String uuid) {
        synchronized (this.messages) {
            for (final Message message : this.messages) {
                if (message.getUuid().equals(uuid)) {
                    return message;
                }
            }

M src/main/java/eu/siacs/conversations/entities/Message.java => src/main/java/eu/siacs/conversations/entities/Message.java +21 -6
@@ 984,13 984,28 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable 
        }
        if (conversation.getMode() == Conversation.MODE_MULTI) {
            final Jid nextCounterpart = conversation.getNextCounterpart();
            if (nextCounterpart != null) {
                message.setCounterpart(nextCounterpart);
                message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
                message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
                return true;
            }
            return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
        }
        return false;
    }

    public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
        final Conversation conversation;
        if (message.conversation instanceof Conversation) {
            conversation = (Conversation) message.conversation;
        } else {
            return false;
        }
        return configurePrivateMessage(conversation, message, counterpart, false);
    }

    private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
        if (counterpart == null) {
            return false;
        }
        message.setCounterpart(counterpart);
        message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
        message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
        return true;
    }
}

M src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java => src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +0 -1
@@ 131,7 131,6 @@ public class HttpDownloadConnection implements Transferable {
    }

    private void download(final boolean interactive) {
        Log.d(Config.LOGTAG,"download()",new Exception());
        EXECUTOR.execute(new FileDownloader(interactive));
    }


M src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java => src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +51 -26
@@ 11,6 11,8 @@ import android.os.SystemClock;
import android.util.Base64;
import android.util.Log;

import com.google.common.base.Stopwatch;

import org.json.JSONException;
import org.json.JSONObject;
import org.whispersystems.libsignal.IdentityKey;


@@ 62,7 64,9 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
public class DatabaseBackend extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "history";
    private static final int DATABASE_VERSION = 48;
    private static final int DATABASE_VERSION = 49;

    private static boolean requiresMessageIndexRebuild = false;
    private static DatabaseBackend instance = null;
    private static final String CREATE_CONTATCS_STATEMENT = "create table "
            + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "


@@ 170,10 174,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
    private static final String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "CREATE INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")";
    private static final String CREATE_MESSAGE_TYPE_INDEX = "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")";

    private static final String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid TEXT PRIMARY KEY, body TEXT, tokenize = 'unicode61')";
    private static final String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index (uuid,body) VALUES (new.uuid,new.body); END;";
    private static final String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE of uuid,body ON " + Message.TABLENAME + " BEGIN update messages_index set body=new.body,uuid=new.uuid WHERE uuid=old.uuid; END;";
    private static final String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(uuid,body) SELECT uuid,body FROM " + Message.TABLENAME + ";";
    private static final String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid,body,notindexed=\"uuid\",content=\"" + Message.TABLENAME + "\",tokenize='unicode61')";
    private static final String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index(rowid,uuid,body) VALUES(NEW.rowid,NEW.uuid,NEW.body); END;";
    private static final String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE OF uuid,body ON " + Message.TABLENAME + " BEGIN UPDATE messages_index SET body=NEW.body,uuid=NEW.uuid WHERE rowid=OLD.rowid; END;";
    private static final String CREATE_MESSAGE_DELETE_TRIGGER = "CREATE TRIGGER after_message_delete AFTER DELETE ON " + Message.TABLENAME + " BEGIN DELETE FROM messages_index WHERE rowid=OLD.rowid; END;";
    private static final String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(messages_index) VALUES('rebuild');";

    private DatabaseBackend(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);


@@ 186,6 191,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
        return values;
    }

    public static boolean requiresMessageIndexRebuild() {
        return requiresMessageIndexRebuild;
    }

    public void rebuildMessagesIndex() {
        final SQLiteDatabase db = getWritableDatabase();
        final Stopwatch stopwatch = Stopwatch.createStarted();
        db.execSQL(COPY_PREEXISTING_ENTRIES);
        Log.d(Config.LOGTAG,"rebuilt message index in "+ stopwatch.stop().toString());
    }

    public static synchronized DatabaseBackend getInstance(Context context) {
        if (instance == null) {
            instance = new DatabaseBackend(context);


@@ 262,6 278,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
        db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
        db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
        db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
        db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
    }

    @Override


@@ 518,16 535,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
            db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
        }

        if (oldVersion < 41 && newVersion >= 41) {
            db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
            db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
            db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
            db.execSQL(COPY_PREEXISTING_ENTRIES);
        }

        if (oldVersion < 42 && newVersion >= 42) {
            db.execSQL("DROP TRIGGER IF EXISTS after_message_delete");
        }
        if (QuickConversationsService.isQuicksy() && oldVersion < 43 && newVersion >= 43) {
            List<Account> accounts = getAccounts(db);
            for (Account account : accounts) {


@@ 551,10 558,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
        if (oldVersion < 46 && newVersion >= 46) {
            final long start = SystemClock.elapsedRealtime();
            db.rawQuery("PRAGMA secure_delete = FALSE", null).close();
            db.execSQL("update "+Message.TABLENAME+" set "+Message.EDITED+"=NULL");
            db.execSQL("update " + Message.TABLENAME + " set " + Message.EDITED + "=NULL");
            db.rawQuery("PRAGMA secure_delete=ON", null).close();
            final long diff = SystemClock.elapsedRealtime() - start;
            Log.d(Config.LOGTAG,"deleted old edit information in "+diff+"ms");
            Log.d(Config.LOGTAG, "deleted old edit information in " + diff + "ms");
        }
        if (oldVersion < 47 && newVersion >= 47) {
            db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.PRESENCE_NAME + " TEXT");


@@ 562,6 569,26 @@ public class DatabaseBackend extends SQLiteOpenHelper {
        if (oldVersion < 48 && newVersion >= 48) {
            db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT");
        }
        if (oldVersion < 49 && newVersion >= 49) {
            db.beginTransaction();
            db.execSQL("DROP TRIGGER IF EXISTS after_message_insert;");
            db.execSQL("DROP TRIGGER IF EXISTS after_message_update;");
            db.execSQL("DROP TRIGGER IF EXISTS after_message_delete;");
            db.execSQL("DROP TABLE IF EXISTS messages_index;");
            // a hack that should not be necessary, but
            // there was at least one occurence when SQLite failed at this
            db.execSQL("DROP TABLE IF EXISTS messages_index_docsize;");
            db.execSQL("DROP TABLE IF EXISTS messages_index_segdir;");
            db.execSQL("DROP TABLE IF EXISTS messages_index_segments;");
            db.execSQL("DROP TABLE IF EXISTS messages_index_stat;");
            db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
            db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
            db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
            db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
            db.setTransactionSuccessful();
            db.endTransaction();
            requiresMessageIndexRebuild = true;
        }
    }

    private void canonicalizeJids(SQLiteDatabase db) {


@@ 776,7 803,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
            try {
                list.add(0, Message.fromCursor(cursor, conversation));
            } catch (Exception e) {
                Log.e(Config.LOGTAG,"unable to restore message");
                Log.e(Config.LOGTAG, "unable to restore message");
            }
        }
        cursor.close();


@@ 787,12 814,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
        final SQLiteDatabase db = this.getReadableDatabase();
        final StringBuilder SQL = new StringBuilder();
        final String[] selectionArgs;
        SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + '.' + Conversation.CONTACTJID + ',' + Conversation.TABLENAME + '.' + Conversation.ACCOUNT + ',' + Conversation.TABLENAME + '.' + Conversation.MODE + " FROM " + Message.TABLENAME + " join " + Conversation.TABLENAME + " on " + Message.TABLENAME + '.' + Message.CONVERSATION + '=' + Conversation.TABLENAME + '.' + Conversation.UUID + " join messages_index ON messages_index.uuid=messages.uuid where " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + ',' + Message.ENCRYPTION_PGP + ',' + Message.ENCRYPTION_DECRYPTION_FAILED + ',' + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + ',' + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?");
        SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + "." + Conversation.CONTACTJID + "," + Conversation.TABLENAME + "." + Conversation.ACCOUNT + "," + Conversation.TABLENAME + "." + Conversation.MODE + " FROM " + Message.TABLENAME + " JOIN " + Conversation.TABLENAME + " ON " + Message.TABLENAME + "." + Message.CONVERSATION + "=" + Conversation.TABLENAME + "." + Conversation.UUID + " JOIN messages_index ON messages_index.rowid=messages.rowid WHERE " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + "," + Message.ENCRYPTION_PGP + "," + Message.ENCRYPTION_DECRYPTION_FAILED + "," + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + "," + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?");
        if (uuid == null) {
            selectionArgs = new String[]{FtsUtils.toMatchString(term)};
        } else {
            selectionArgs = new String[]{FtsUtils.toMatchString(term), uuid};
            SQL.append(" AND "+Conversation.TABLENAME+'.'+Conversation.UUID+"=?");
            SQL.append(" AND " + Conversation.TABLENAME + '.' + Conversation.UUID + "=?");
        }
        SQL.append(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS);
        Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term));


@@ 856,7 883,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {

    public List<FilePathInfo> getFilePathInfo() {
        final SQLiteDatabase db = this.getReadableDatabase();
        final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2,5) and "+Message.RELATIVE_FILE_PATH+" is not null", null, null, null, null);
        final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2,5) and " + Message.RELATIVE_FILE_PATH + " is not null", null, null, null, null);
        final List<FilePathInfo> list = new ArrayList<>();
        while (cursor != null && cursor.moveToNext()) {
            list.add(new FilePathInfo(cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0));


@@ 869,7 896,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {

    public List<FilePath> getRelativeFilePaths(String account, Jid jid, int limit) {
        SQLiteDatabase db = this.getReadableDatabase();
        final String SQL = "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and "+Message.RELATIVE_FILE_PATH+" is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc";
        final String SQL = "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and " + Message.RELATIVE_FILE_PATH + " is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc";
        final String[] args = {account, jid.toString(), jid.toString() + "/%"};
        Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + limit : ""), args);
        List<FilePath> filesPaths = new ArrayList<>();


@@ 894,7 921,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
        public boolean deleted;

        private FilePathInfo(String uuid, String path, boolean deleted) {
            super(uuid,path);
            super(uuid, path);
            this.deleted = deleted;
        }



@@ 1038,8 1065,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
        long start = SystemClock.elapsedRealtime();
        final SQLiteDatabase db = this.getWritableDatabase();
        db.beginTransaction();
        String[] args = {conversation.getUuid()};
        db.delete("messages_index", "uuid in (select uuid from messages where conversationUuid=?)", args);
        final String[] args = {conversation.getUuid()};
        int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
        db.setTransactionSuccessful();
        db.endTransaction();


@@ 1050,7 1076,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
        final String[] args = {String.valueOf(timestamp)};
        SQLiteDatabase db = this.getReadableDatabase();
        db.beginTransaction();
        db.delete("messages_index", "uuid in (select uuid from messages where timeSent<?)", args);
        db.delete(Message.TABLENAME, "timeSent<?", args);
        db.setTransactionSuccessful();
        db.endTransaction();

M src/main/java/eu/siacs/conversations/persistance/FileBackend.java => src/main/java/eu/siacs/conversations/persistance/FileBackend.java +71 -39
@@ 31,6 31,9 @@ import android.util.LruCache;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.core.content.FileProvider;
import androidx.exifinterface.media.ExifInterface;

import com.google.common.io.ByteStreams;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;


@@ 64,7 67,6 @@ import eu.siacs.conversations.ui.RecordingActivity;
import eu.siacs.conversations.ui.util.Attachment;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.FileWriterException;
import eu.siacs.conversations.utils.MimeUtils;


@@ 162,16 164,16 @@ public class FileBackend {
        return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + app + "/Backup/";
    }

    private static Bitmap rotate(Bitmap bitmap, int degree) {
    private static Bitmap rotate(final Bitmap bitmap, final int degree) {
        if (degree == 0) {
            return bitmap;
        }
        int w = bitmap.getWidth();
        int h = bitmap.getHeight();
        Matrix mtx = new Matrix();
        mtx.postRotate(degree);
        Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
        if (bitmap != null && !bitmap.isRecycled()) {
        final int w = bitmap.getWidth();
        final int h = bitmap.getHeight();
        final Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        final Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true);
        if (!bitmap.isRecycled()) {
            bitmap.recycle();
        }
        return result;


@@ 627,20 629,20 @@ public class FileBackend {
    private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
        Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath());
        file.getParentFile().mkdirs();
        OutputStream os = null;
        InputStream is = null;
        try {
            file.createNewFile();
            os = new FileOutputStream(file);
            is = mXmppConnectionService.getContentResolver().openInputStream(uri);
            byte[] buffer = new byte[1024];
            int length;
            while ((length = is.read(buffer)) > 0) {
                try {
                    os.write(buffer, 0, length);
                } catch (IOException e) {
                    throw new FileWriterException();
                }
        } catch (IOException e) {
            throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
        }
        try (final OutputStream os = new FileOutputStream(file);
             final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri)) {
            if (is == null) {
                throw new FileCopyException(R.string.error_file_not_found);
            }
            try {
                ByteStreams.copy(is, os);
            } catch (IOException e) {
                throw new FileWriterException();
            }
            try {
                os.flush();


@@ 648,16 650,17 @@ public class FileBackend {
                throw new FileWriterException();
            }
        } catch (final FileNotFoundException e) {
            cleanup(file);
            throw new FileCopyException(R.string.error_file_not_found);
        } catch (final FileWriterException e) {
            cleanup(file);
            throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
        } catch (final SecurityException e) {
            cleanup(file);
            throw new FileCopyException(R.string.error_security_exception);
        } catch (final IOException e) {
            cleanup(file);
            throw new FileCopyException(R.string.error_io_exception);
        } finally {
            close(os);
            close(is);
        }
    }



@@ 708,7 711,7 @@ public class FileBackend {

    private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException {
        final File parent = file.getParentFile();
        if (parent.mkdirs()) {
        if (parent != null && parent.mkdirs()) {
            Log.d(Config.LOGTAG, "created parent directory");
        }
        InputStream is = null;


@@ 743,23 746,28 @@ public class FileBackend {
            final int imageMaxSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
            while (!targetSizeReached) {
                os = new FileOutputStream(file);
                Log.d(Config.LOGTAG, "compressing image with quality " + quality);
                boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os);
                if (!success) {
                    throw new FileCopyException(R.string.error_compressing_image);
                }
                os.flush();
                targetSizeReached = file.length() <= imageMaxSize || quality <= 50;
                final long fileSize = file.length();
                Log.d(Config.LOGTAG, "achieved file size of " + fileSize);
                targetSizeReached = fileSize <= imageMaxSize || quality <= 50;
                quality -= 5;
            }
            scaledBitmap.recycle();
        } catch (final FileNotFoundException e) {
            cleanup(file);
            throw new FileCopyException(R.string.error_file_not_found);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (final IOException e) {
            cleanup(file);
            throw new FileCopyException(R.string.error_io_exception);
        } catch (SecurityException e) {
            cleanup(file);
            throw new FileCopyException(R.string.error_security_exception_during_image_copy);
        } catch (OutOfMemoryError e) {
        } catch (final OutOfMemoryError e) {
            ++sampleSize;
            if (sampleSize <= 3) {
                copyImageToPrivateStorage(file, image, sampleSize);


@@ 772,6 780,14 @@ public class FileBackend {
        }
    }

    private static void cleanup(final File file) {
        try {
            file.delete();
        } catch (Exception e) {

        }
    }

    public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, ImageCompressionException {
        Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath());
        copyImageToPrivateStorage(file, image, 0);


@@ 808,19 824,34 @@ public class FileBackend {
        }
    }

    private int getRotation(File file) {
        return getRotation(Uri.parse("file://" + file.getAbsolutePath()));
    private int getRotation(final File file) {
        try (final InputStream inputStream = new FileInputStream(file)) {
            return getRotation(inputStream);
        } catch (Exception e) {
            return 0;
        }
    }

    private int getRotation(Uri image) {
        InputStream is = null;
        try {
            is = mXmppConnectionService.getContentResolver().openInputStream(image);
            return ExifHelper.getOrientation(is);
        } catch (FileNotFoundException e) {
    private int getRotation(final Uri image) {
        try (final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(image)) {
            return is == null ? 0 : getRotation(is);
        } catch (final Exception e) {
            return 0;
        } finally {
            close(is);
        }
    }

    private static int getRotation(final InputStream inputStream) throws IOException {
        final ExifInterface exif = new ExifInterface(inputStream);
        final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
        switch (orientation) {
            case ExifInterface.ORIENTATION_ROTATE_180:
                return 180;
            case ExifInterface.ORIENTATION_ROTATE_90:
                return 90;
            case ExifInterface.ORIENTATION_ROTATE_270:
                return 270;
            default:
                return 0;
        }
    }



@@ 1468,7 1499,8 @@ public class FileBackend {
            this.resId = resId;
        }

        public @StringRes int getResId() {
        public @StringRes
        int getResId() {
            return resId;
        }
    }

M src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java => src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +166 -160
@@ 3,16 3,19 @@ package eu.siacs.conversations.services;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.util.Log;

import net.ypresto.androidtranscoder.MediaTranscoder;
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
import androidx.annotation.NonNull;

import com.otaliastudios.transcoder.Transcoder;
import com.otaliastudios.transcoder.TranscoderListener;

import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;



@@ 23,161 26,164 @@ import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.Android360pFormatStrategy;
import eu.siacs.conversations.utils.Android720pFormatStrategy;
import eu.siacs.conversations.utils.MimeUtils;

public class AttachFileToConversationRunnable implements Runnable, MediaTranscoder.Listener {

	private final XmppConnectionService mXmppConnectionService;
	private final Message message;
	private final Uri uri;
	private final String type;
	private final UiCallback<Message> callback;
	private final boolean isVideoMessage;
	private final long originalFileSize;
	private int currentProgress = -1;

	AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback<Message> callback) {
		this.uri = uri;
		this.type = type;
		this.mXmppConnectionService = xmppConnectionService;
		this.message = message;
		this.callback = callback;
		final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
		final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
		this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService,uri);
		this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression());
	}

	boolean isVideoMessage() {
		return this.isVideoMessage;
	}

	private void processAsFile() {
		final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri);
		if (path != null && !FileBackend.isPathBlacklisted(path)) {
			message.setRelativeFilePath(path);
			mXmppConnectionService.getFileBackend().updateFileParams(message);
			if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
				mXmppConnectionService.getPgpEngine().encrypt(message, callback);
			} else {
				mXmppConnectionService.sendMessage(message);
				callback.success(message);
			}
		} else {
			try {
				mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
				mXmppConnectionService.getFileBackend().updateFileParams(message);
				if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
					final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
					if (pgpEngine != null) {
						pgpEngine.encrypt(message, callback);
					} else if (callback != null) {
						callback.error(R.string.unable_to_connect_to_keychain, null);
					}
				} else {
					mXmppConnectionService.sendMessage(message);
					callback.success(message);
				}
			} catch (FileBackend.FileCopyException e) {
				callback.error(e.getResId(), message);
			}
		}
	}

	private void processAsVideo() throws FileNotFoundException {
		Log.d(Config.LOGTAG,"processing file as video");
		mXmppConnectionService.startForcingForegroundNotification();
		message.setRelativeFilePath(message.getUuid() + ".mp4");
		final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
		final MediaFormatStrategy formatStrategy = "720".equals(getVideoCompression()) ? new Android720pFormatStrategy() : new Android360pFormatStrategy();
		file.getParentFile().mkdirs();
		final ParcelFileDescriptor parcelFileDescriptor = mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r");
		if (parcelFileDescriptor == null) {
			throw new FileNotFoundException("Parcel File Descriptor was null");
		}
		FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
		Future<Void> future = MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), formatStrategy, this);
		try {
			future.get();
		} catch (InterruptedException e) {
			throw new AssertionError(e);
		} catch (ExecutionException e) {
			if (e.getCause() instanceof Error) {
				mXmppConnectionService.stopForcingForegroundNotification();
				processAsFile();
			} else {
				Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
			}
		}
	}

	@Override
	public void onTranscodeProgress(double progress) {
		final int p = (int) Math.round(progress * 100);
		if (p > currentProgress) {
			currentProgress = p;
			mXmppConnectionService.getNotificationService().updateFileAddingNotification(p,message);
		}
	}

	@Override
	public void onTranscodeCompleted() {
		mXmppConnectionService.stopForcingForegroundNotification();
		final File file = mXmppConnectionService.getFileBackend().getFile(message);
		long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
		Log.d(Config.LOGTAG,"originalFileSize="+originalFileSize+" convertedFileSize="+convertedFileSize);
		if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
			if (file.delete()) {
				Log.d(Config.LOGTAG,"original file size was smaller. deleting and processing as file");
				processAsFile();
				return;
			} else {
				Log.d(Config.LOGTAG,"unable to delete converted file");
			}
		}
		mXmppConnectionService.getFileBackend().updateFileParams(message);
		if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
			mXmppConnectionService.getPgpEngine().encrypt(message, callback);
		} else {
			mXmppConnectionService.sendMessage(message);
			callback.success(message);
		}
	}

	@Override
	public void onTranscodeCanceled() {
		mXmppConnectionService.stopForcingForegroundNotification();
		processAsFile();
	}

	@Override
	public void onTranscodeFailed(Exception e) {
		mXmppConnectionService.stopForcingForegroundNotification();
		Log.d(Config.LOGTAG,"video transcoding failed",e);
		processAsFile();
	}

	@Override
	public void run() {
		if (this.isVideoMessage()) {
			try {
				processAsVideo();
			} catch (FileNotFoundException e) {
				processAsFile();
			}
		} else {
			processAsFile();
		}
	}

	private String getVideoCompression() {
		return getVideoCompression(mXmppConnectionService);
	}

	public static String getVideoCompression(final Context context) {
		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
		return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
	}
import eu.siacs.conversations.utils.TranscoderStrategies;

public class AttachFileToConversationRunnable implements Runnable, TranscoderListener {

    private final XmppConnectionService mXmppConnectionService;
    private final Message message;
    private final Uri uri;
    private final String type;
    private final UiCallback<Message> callback;
    private final boolean isVideoMessage;
    private final long originalFileSize;
    private int currentProgress = -1;

    AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback<Message> callback) {
        this.uri = uri;
        this.type = type;
        this.mXmppConnectionService = xmppConnectionService;
        this.message = message;
        this.callback = callback;
        final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
        final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
        this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri);
        this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression());
    }

    boolean isVideoMessage() {
        return this.isVideoMessage;
    }

    private void processAsFile() {
        final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri);
        if (path != null && !FileBackend.isPathBlacklisted(path)) {
            message.setRelativeFilePath(path);
            mXmppConnectionService.getFileBackend().updateFileParams(message);
            if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
                mXmppConnectionService.getPgpEngine().encrypt(message, callback);
            } else {
                mXmppConnectionService.sendMessage(message);
                callback.success(message);
            }
        } else {
            try {
                mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
                mXmppConnectionService.getFileBackend().updateFileParams(message);
                if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
                    final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
                    if (pgpEngine != null) {
                        pgpEngine.encrypt(message, callback);
                    } else if (callback != null) {
                        callback.error(R.string.unable_to_connect_to_keychain, null);
                    }
                } else {
                    mXmppConnectionService.sendMessage(message);
                    callback.success(message);
                }
            } catch (FileBackend.FileCopyException e) {
                callback.error(e.getResId(), message);
            }
        }
    }

    private void processAsVideo() throws FileNotFoundException {
        Log.d(Config.LOGTAG, "processing file as video");
        mXmppConnectionService.startForcingForegroundNotification();
        message.setRelativeFilePath(message.getUuid() + ".mp4");
        final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
        if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
            Log.d(Config.LOGTAG, "created parent directory for video file");
        }

        final boolean highQuality = "720".equals(getVideoCompression());

        final Future<Void> future = Transcoder.into(file.getAbsolutePath()).
                addDataSource(mXmppConnectionService, uri)
                .setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P)
                .setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ)
                .setListener(this)
                .transcode();
        try {
            future.get();
        } catch (InterruptedException e) {
            throw new AssertionError(e);
        } catch (ExecutionException e) {
            if (e.getCause() instanceof Error) {
                mXmppConnectionService.stopForcingForegroundNotification();
                processAsFile();
            } else {
                Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
            }
        }
    }

    @Override
    public void onTranscodeProgress(double progress) {
        final int p = (int) Math.round(progress * 100);
        if (p > currentProgress) {
            currentProgress = p;
            mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message);
        }
    }

    @Override
    public void onTranscodeCompleted(int successCode) {
        mXmppConnectionService.stopForcingForegroundNotification();
        final File file = mXmppConnectionService.getFileBackend().getFile(message);
        long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
        Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize);
        if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
            if (file.delete()) {
                Log.d(Config.LOGTAG, "original file size was smaller. deleting and processing as file");
                processAsFile();
                return;
            } else {
                Log.d(Config.LOGTAG, "unable to delete converted file");
            }
        }
        mXmppConnectionService.getFileBackend().updateFileParams(message);
        if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
            mXmppConnectionService.getPgpEngine().encrypt(message, callback);
        } else {
            mXmppConnectionService.sendMessage(message);
            callback.success(message);
        }
    }

    @Override
    public void onTranscodeCanceled() {
        mXmppConnectionService.stopForcingForegroundNotification();
        processAsFile();
    }

    @Override
    public void onTranscodeFailed(@NonNull @NotNull Throwable exception) {
        mXmppConnectionService.stopForcingForegroundNotification();
        Log.d(Config.LOGTAG, "video transcoding failed", exception);
        processAsFile();
    }

    @Override
    public void run() {
        if (this.isVideoMessage()) {
            try {
                processAsVideo();
            } catch (FileNotFoundException e) {
                processAsFile();
            }
        } else {
            processAsFile();
        }
    }

    private String getVideoCompression() {
        return getVideoCompression(mXmppConnectionService);
    }

    public static String getVideoCompression(final Context context) {
        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
    }
}

M src/main/java/eu/siacs/conversations/services/NotificationService.java => src/main/java/eu/siacs/conversations/services/NotificationService.java +26 -14
@@ 35,6 35,7 @@ import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.IconCompat;

import com.google.common.base.Strings;
import com.google.common.collect.Iterables;

import java.io.File;
import java.io.IOException;


@@ 407,7 408,7 @@ public class NotificationService {
            currentInterruptionFilter = 1; //INTERRUPTION_FILTER_ALL
        }
        if (currentInterruptionFilter != 1) {
            Log.d(Config.LOGTAG,"do not ring or vibrate because interruption filter has been set to "+currentInterruptionFilter);
            Log.d(Config.LOGTAG, "do not ring or vibrate because interruption filter has been set to " + currentInterruptionFilter);
            return;
        }
        final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;


@@ 424,13 425,13 @@ public class NotificationService {
        final Resources resources = mXmppConnectionService.getResources();
        final String ringtonePreference = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone));
        if (Strings.isNullOrEmpty(ringtonePreference)) {
            Log.d(Config.LOGTAG,"ringtone has been set to none");
            Log.d(Config.LOGTAG, "ringtone has been set to none");
            return;
        }
        final Uri uri = Uri.parse(ringtonePreference);
        this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
        if (this.currentlyPlayingRingtone == null) {
            Log.d(Config.LOGTAG,"unable to find ringtone for uri "+uri);
            Log.d(Config.LOGTAG, "unable to find ringtone for uri " + uri);
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {


@@ 487,14 488,23 @@ public class NotificationService {
        notify(INCOMING_CALL_NOTIFICATION_ID, notification);
    }

    public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
    public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) {
        final AbstractJingleConnection.Id id = ongoingCall.id;
        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
        if (media.contains(Media.VIDEO)) {
        if (ongoingCall.media.contains(Media.VIDEO)) {
            builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
            builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
            if (ongoingCall.reconnecting) {
                builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call));
            } else {
                builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
            }
        } else {
            builder.setSmallIcon(R.drawable.ic_call_white_24dp);
            builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
            if (ongoingCall.reconnecting) {
                builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call));
            } else {
                builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
            }
        }
        builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);


@@ 790,17 800,18 @@ public class NotificationService {
                        .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
                        .setShowsUserInterface(false)
                        .build();
                String replyLabel = mXmppConnectionService.getString(R.string.reply);
                NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
                final String replyLabel = mXmppConnectionService.getString(R.string.reply);
                final String lastMessageUuid = Iterables.getLast(messages).getUuid();
                final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
                        R.drawable.ic_send_text_offline,
                        replyLabel,
                        createReplyIntent(conversation, false))
                        createReplyIntent(conversation, lastMessageUuid, false))
                        .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
                        .setShowsUserInterface(false)
                        .addRemoteInput(remoteInput).build();
                NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
                final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
                        replyLabel,
                        createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
                        createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build();
                mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
                int addedActionsCount = 1;
                mBuilder.addAction(markReadAction);


@@ 1066,13 1077,14 @@ public class NotificationService {
        return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
    }

    private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
    private PendingIntent createReplyIntent(final Conversation conversation, final String lastMessageUuid, final boolean dismissAfterReply) {
        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
        intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
        intent.putExtra("uuid", conversation.getUuid());
        intent.putExtra("dismiss_notification", dismissAfterReply);
        intent.putExtra("last_message_uuid", lastMessageUuid);
        final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
        return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
        return PendingIntent.getService(mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private PendingIntent createReadPendingIntent(Conversation conversation) {

M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +56 -49
@@ 75,6 75,8 @@ import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;


@@ 184,8 186,9 @@ public class XmppConnectionService extends Service {
    private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";

    public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
    private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
    private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");
    private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
    private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
    private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression");
    private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
    private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader");
    private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor");


@@ 471,7 474,6 @@ public class XmppConnectionService extends Service {
    private OpenPgpServiceConnection pgpServiceConnection;
    private PgpEngine mPgpEngine = null;
    private WakeLock wakeLock;
    private PowerManager pm;
    private LruCache<String, Bitmap> mBitmapCache;
    private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
    private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();


@@ 564,14 566,14 @@ public class XmppConnectionService extends Service {
        Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
        final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback);
        if (runnable.isVideoMessage()) {
            mVideoCompressionExecutor.execute(runnable);
            VIDEO_COMPRESSION_EXECUTOR.execute(runnable);
        } else {
            mFileAddingExecutor.execute(runnable);
            FILE_ATTACHMENT_EXECUTOR.execute(runnable);
        }
    }

    public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
        final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
    public void attachImageToConversation(final Conversation conversation, final Uri uri,  final String type, final UiCallback<Message> callback) {
        final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
        final String compressPictures = getCompressPicturesPreference();

        if ("never".equals(compressPictures)


@@ 593,7 595,7 @@ public class XmppConnectionService extends Service {
            message.setType(Message.TYPE_IMAGE);
        }
        Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
        mFileAddingExecutor.execute(() -> {
        FILE_ATTACHMENT_EXECUTOR.execute(() -> {
            try {
                getFileBackend().copyImageToPrivateStorage(message, uri);
            } catch (FileBackend.ImageCompressionException e) {


@@ 724,6 726,7 @@ public class XmppConnectionService extends Service {
                    }
                    final CharSequence body = remoteInput.getCharSequence("text_reply");
                    final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
                    final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
                    if (body == null || body.length() <= 0) {
                        break;
                    }


@@ 732,7 735,7 @@ public class XmppConnectionService extends Service {
                            restoredFromDatabaseLatch.await();
                            final Conversation c = findConversationByUuid(uuid);
                            if (c != null) {
                                directReply(c, body.toString(), dismissNotification);
                                directReply(c, body.toString(), lastMessageUuid, dismissNotification);
                            }
                        } catch (InterruptedException e) {
                            Log.d(Config.LOGTAG, "unable to process direct reply");


@@ 930,8 933,12 @@ public class XmppConnectionService extends Service {
        }
    }

    private void directReply(Conversation conversation, String body, final boolean dismissAfterReply) {
        Message message = new Message(conversation, body, conversation.getNextEncryption());
    private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
        final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
        final Message message = new Message(conversation, body, conversation.getNextEncryption());
        if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
            Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
        }
        message.markUnread();
        if (message.getEncryption() == Message.ENCRYPTION_PGP) {
            getPgpEngine().encrypt(message, new UiCallback<Message>() {


@@ 1147,11 1154,11 @@ public class XmppConnectionService extends Service {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
            startContactObserver();
        }
        mFileAddingExecutor.execute(fileBackend::deleteHistoricAvatarPath);
        FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
        if (Compatibility.hasStoragePermission(this)) {
            Log.d(Config.LOGTAG, "starting file observer");
            mFileAddingExecutor.execute(this.fileObserver::startWatching);
            mFileAddingExecutor.execute(this::checkForDeletedFiles);
            FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching);
            FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
        }
        if (Config.supportOpenPgp()) {
            this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {


@@ 1172,7 1179,7 @@ public class XmppConnectionService extends Service {
            this.pgpServiceConnection.bindToService();
        }

        this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class);
        this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");

        toggleForegroundService();


@@ 1267,8 1274,8 @@ public class XmppConnectionService extends Service {

    public void restartFileObserver() {
        Log.d(Config.LOGTAG, "restarting file observer");
        mFileAddingExecutor.execute(this.fileObserver::restartWatching);
        mFileAddingExecutor.execute(this::checkForDeletedFiles);
        FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching);
        FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
    }

    public void toggleScreenEventReceiver() {


@@ 1291,8 1298,8 @@ public class XmppConnectionService extends Service {
        toggleForegroundService(false);
    }

    public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
        ongoingCall.set(new OngoingCall(id, media));
    public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
        ongoingCall.set(new OngoingCall(id, media, reconnecting));
        toggleForegroundService(false);
    }



@@ 1308,7 1315,7 @@ public class XmppConnectionService extends Service {
            final Notification notification;
            final int id;
            if (ongoing != null) {
                notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media);
                notification = this.mNotificationService.getOngoingCallNotification(ongoing);
                id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
                startForeground(id, notification);
                mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);


@@ 1885,7 1892,10 @@ public class XmppConnectionService extends Service {
            long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
            Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
            Runnable runnable = () -> {
                long deletionDate = getAutomaticMessageDeletionDate();
                if (DatabaseBackend.requiresMessageIndexRebuild()) {
                    DatabaseBackend.getInstance(this).rebuildMessagesIndex();
                }
                final long deletionDate = getAutomaticMessageDeletionDate();
                mLastExpiryRun.set(SystemClock.elapsedRealtime());
                if (deletionDate > 0) {
                    Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));


@@ 1925,7 1935,7 @@ public class XmppConnectionService extends Service {
    private void restoreMessages(Conversation conversation) {
        conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
        conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
        conversation.findUnreadMessages(message -> mNotificationService.pushFromBacklog(message));
        conversation.findUnreadMessages(mNotificationService::pushFromBacklog);
    }

    public void loadPhoneContacts() {


@@ 3337,35 3347,26 @@ public class XmppConnectionService extends Service {

    public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
        final Jid jid = user.asBareJid();
        IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
        sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
            @Override
            public void onIqPacketReceived(Account account, IqPacket packet) {
                if (packet.getType() == IqPacket.TYPE.RESULT) {
                    conference.getMucOptions().changeAffiliation(jid, affiliation);
                    getAvatarService().clear(conference);
        final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
        sendIqPacket(conference.getAccount(), request, (account, response) -> {
            if (response.getType() == IqPacket.TYPE.RESULT) {
                conference.getMucOptions().changeAffiliation(jid, affiliation);
                getAvatarService().clear(conference);
                if (callback != null) {
                    callback.onAffiliationChangedSuccessful(jid);
                } else {
                    callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
                    Log.d(Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation);
                }
            } else if (callback != null) {
                callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
            } else {
                Log.d(Config.LOGTAG, "unable to change affiliation");
            }
        });
    }

    public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
        List<Jid> jids = new ArrayList<>();
        for (MucOptions.User user : conference.getMucOptions().getUsers()) {
            if (user.getAffiliation() == before && user.getRealJid() != null) {
                jids.add(user.getRealJid());
            }
        }
        IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
        sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
    }

    public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) {
        IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
        Log.d(Config.LOGTAG, request.toString());
        sendIqPacket(conference.getAccount(), request, (account, packet) -> {
            if (packet.getType() != IqPacket.TYPE.RESULT) {
                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick);


@@ 3928,9 3929,13 @@ public class XmppConnectionService extends Service {
        new Thread(() -> reconnectAccount(account, false, true)).start();
    }

    public void invite(Conversation conversation, Jid contact) {
    public void invite(final Conversation conversation, final Jid contact) {
        Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid());
        MessagePacket packet = mMessageGenerator.invite(conversation, contact);
        final MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
        if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
            changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
        }
        final MessagePacket packet = mMessageGenerator.invite(conversation, contact);
        sendMessagePacket(conversation.getAccount(), packet);
    }



@@ 4864,12 4869,14 @@ public class XmppConnectionService extends Service {
    }

    public static class OngoingCall {
        private final AbstractJingleConnection.Id id;
        private final Set<Media> media;
        public final AbstractJingleConnection.Id id;
        public final Set<Media> media;
        public final boolean reconnecting;

        public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
        public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
            this.id = id;
            this.media = media;
            this.reconnecting = reconnecting;
        }

        @Override


@@ 4877,12 4884,12 @@ public class XmppConnectionService extends Service {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            OngoingCall that = (OngoingCall) o;
            return Objects.equal(id, that.id);
            return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media);
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(id);
            return Objects.hashCode(id, media, reconnecting);
        }
    }
}

M src/main/java/eu/siacs/conversations/ui/AboutActivity.java => src/main/java/eu/siacs/conversations/ui/AboutActivity.java +7 -0
@@ 5,6 5,7 @@ import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;

import static eu.siacs.conversations.ui.XmppActivity.configureActionBar;


@@ 12,6 13,12 @@ import static eu.siacs.conversations.ui.XmppActivity.configureActionBar;
public class AboutActivity extends AppCompatActivity {

    @Override
    protected void onResume(){
        super.onResume();
        SettingsUtils.applyScreenshotPreventionSetting(this);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);


M src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java => src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +10 -0
@@ 1,5 1,6 @@
package eu.siacs.conversations.ui;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;


@@ 7,6 8,7 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;


@@ 86,6 88,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
        }
    };

    public static void open(final Activity activity, final Conversation conversation) {
        Intent intent = new Intent(activity, ConferenceDetailsActivity.class);
        intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
        intent.putExtra("uuid", conversation.getUuid());
        activity.startActivity(intent);
    }

    private final OnClickListener mNotifyStatusClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {


@@ 481,6 490,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
            this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead);
            this.binding.mucSubject.setAutoLinkMask(0);
            this.binding.mucSubject.setVisibility(View.VISIBLE);
            this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
        } else {
            this.binding.mucSubject.setVisibility(View.GONE);
        }

M src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java => src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +22 -3
@@ 45,6 45,7 @@ import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.services.AbstractQuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.ui.adapter.MediaAdapter;


@@ 58,6 59,7 @@ import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Namespace;


@@ 131,15 133,31 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
    }

    private void showAddToPhoneBookDialog() {
        final Jid jid = contact.getJid();
        final boolean quicksyContact = AbstractQuickConversationsService.isQuicksy()
                && Config.QUICKSY_DOMAIN.equals(jid.getDomain())
                && jid.getLocal() != null;
        final String value;
        if (quicksyContact) {
            value = PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, jid);
        } else {
            value = jid.toEscapedString();
        }
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(getString(R.string.action_add_phone_book));
        builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toEscapedString()));
        builder.setMessage(getString(R.string.add_phone_book_text, value));
        builder.setNegativeButton(getString(R.string.cancel), null);
        builder.setPositiveButton(getString(R.string.add), (dialog, which) -> {
            final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
            intent.setType(Contacts.CONTENT_ITEM_TYPE);
            intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toEscapedString());
            intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
            if (quicksyContact) {
                intent.putExtra(Intents.Insert.PHONE, value);
            } else {
                intent.putExtra(Intents.Insert.IM_HANDLE, value);
                intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
                //TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP'
                // however we don’t have such a field and thus have to use the legacy PROTOCOL_JABBER
            }
            intent.putExtra("finishActivityOnSaveCompleted", true);
            try {
                startActivityForResult(intent, 0);


@@ 233,6 251,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (grantResults.length > 0)
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {

M src/main/java/eu/siacs/conversations/ui/ConversationActivity.java => src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +8 -0
@@ 6,6 6,8 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import eu.siacs.conversations.ui.util.SettingsUtils;

public class ConversationActivity extends AppCompatActivity {

	@Override


@@ 14,4 16,10 @@ public class ConversationActivity extends AppCompatActivity {
		startActivity(new Intent(this, ConversationsActivity.class));
		finish();
	}

	@Override
	protected void onResume(){
		super.onResume();
		SettingsUtils.applyScreenshotPreventionSetting(this);
	}
}

M src/main/java/eu/siacs/conversations/ui/ConversationFragment.java => src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +38 -30
@@ 135,6 135,8 @@ import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;

import org.jetbrains.annotations.NotNull;


public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {



@@ 186,10 188,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke

        @Override
        public void onClick(View v) {
            Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
            intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
            intent.putExtra("uuid", conversation.getUuid());
            startActivity(intent);
            ConferenceDetailsActivity.open(getActivity(), conversation);
        }
    };
    private final OnClickListener leaveMuc = new OnClickListener() {


@@ 689,14 688,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
        toggleInputMethod();
    }

    private void attachImageToConversation(Conversation conversation, Uri uri) {
    private void attachImageToConversation(Conversation conversation, Uri uri, String type) {
        if (conversation == null) {
            return;
        }
        final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
        prepareFileToast.show();
        activity.delegateUriPermissionsToService(uri);
        activity.xmppConnectionService.attachImageToConversation(conversation, uri,
        activity.xmppConnectionService.attachImageToConversation(conversation, uri, type,
                new UiCallback<Message>() {

                    @Override


@@ 857,9 856,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
                toggleInputMethod();
                break;
            case ATTACHMENT_CHOICE_LOCATION:
                double latitude = data.getDoubleExtra("latitude", 0);
                double longitude = data.getDoubleExtra("longitude", 0);
                Uri geo = Uri.parse("geo:" + latitude + "," + longitude);
                final double latitude = data.getDoubleExtra("latitude", 0);
                final double longitude = data.getDoubleExtra("longitude", 0);
                final int accuracy = data.getIntExtra("accuracy", 0);
                final Uri geo;
                if (accuracy > 0) {
                    geo = Uri.parse(String.format("geo:%s,%s;u=%s", latitude, longitude, accuracy));
                } else {
                    geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude));
                }
                mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
                toggleInputMethod();
                break;


@@ 890,7 895,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
                    attachLocationToConversation(conversation, attachment.getUri());
                } else if (attachment.getType() == Attachment.Type.IMAGE) {
                    Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
                    attachImageToConversation(conversation, attachment.getUri());
                    attachImageToConversation(conversation, attachment.getUri(), attachment.getMime());
                } else {
                    Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
                    attachFileToConversation(conversation, attachment.getUri(), attachment.getMime());


@@ 1272,10 1277,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
                activity.switchToContactDetails(conversation.getContact());
                break;
            case R.id.action_muc_details:
                Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
                intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
                intent.putExtra("uuid", conversation.getUuid());
                startActivity(intent);
                ConferenceDetailsActivity.open(getActivity(), conversation);
                break;
            case R.id.action_invite:
                startActivityForResult(ChooseContactActivity.create(activity, conversation), REQUEST_INVITE_TO_CONVERSATION);


@@ 1586,6 1588,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
        }
        if (writeGranted(grantResults, permissions)) {
            if (activity != null && activity.xmppConnectionService != null) {
                activity.xmppConnectionService.getBitmapCache().evictAll();
                activity.xmppConnectionService.restartFileObserver();
            }
            refresh();


@@ 1624,9 1627,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke

    @SuppressLint("InflateParams")
    protected void clearHistoryDialog(final Conversation conversation) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
        builder.setTitle(getString(R.string.clear_conversation_history));
        final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
        final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
        final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox);
        builder.setView(dialogView);
        builder.setNegativeButton(getString(R.string.cancel), null);


@@ 1644,7 1647,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
    }

    protected void muteConversationDialog(final Conversation conversation) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
        builder.setTitle(R.string.disable_notifications);
        final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
        final CharSequence[] labels = new CharSequence[durations.length];


@@ 1660,13 1663,13 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
            if (durations[which] == -1) {
                till = Long.MAX_VALUE;
            } else {
                till = System.currentTimeMillis() + (durations[which] * 1000);
                till = System.currentTimeMillis() + (durations[which] * 1000L);
            }
            conversation.setMutedTill(till);
            activity.xmppConnectionService.updateConversation(conversation);
            activity.onConversationsListItemUpdated();
            refresh();
            getActivity().invalidateOptionsMenu();
            requireActivity().invalidateOptionsMenu();
        });
        builder.create().show();
    }


@@ 1698,7 1701,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
        this.activity.xmppConnectionService.updateConversation(conversation);
        this.activity.onConversationsListItemUpdated();
        refresh();
        getActivity().invalidateOptionsMenu();
        requireActivity().invalidateOptionsMenu();
    }




@@ 1708,9 1711,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
        switch (attachmentChoice) {
            case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
                intent.setAction(Intent.ACTION_GET_CONTENT);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
                }
                intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
                intent.setType("image/*");
                chooser = true;
                break;


@@ 1728,9 1729,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
            case ATTACHMENT_CHOICE_CHOOSE_FILE:
                chooser = true;
                intent.setType("*/*");
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
                }
                intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setAction(Intent.ACTION_GET_CONTENT);
                break;


@@ 1813,7 1812,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
    }

    private void showErrorMessage(final Message message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
        builder.setTitle(R.string.error_message);
        final String errorMessage = message.getErrorMessage();
        final String[] errorMessageParts = errorMessage == null ? new String[0] : errorMessage.split("\\u001f");


@@ 1834,7 1833,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke


    private void deleteFile(final Message message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
        builder.setNegativeButton(R.string.cancel, null);
        builder.setTitle(R.string.delete_file_dialog);
        builder.setMessage(R.string.delete_file_dialog_msg);


@@ 1968,7 1967,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
    public void onSaveInstanceState(@NotNull Bundle outState) {
        super.onSaveInstanceState(outState);
        if (conversation != null) {
            outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid());


@@ 2193,13 2192,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
        final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE);
        final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
        final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false);
        final String type = extras.getString(ConversationsActivity.EXTRA_TYPE);
        final List<Uri> uris = extractUris(extras);
        if (uris != null && uris.size() > 0) {
            if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) {
                mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION));
            } else {
                final List<Uri> cleanedUris = cleanUris(new ArrayList<>(uris));
                mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris));
                mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type));
            }
            toggleInputMethod();
            return;


@@ 3058,4 3058,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
        }
        activity.switchToAccount(message.getConversation().getAccount(), fingerprint);
    }

    private Activity requireActivity() {
        final Activity activity = getActivity();
        if (activity == null) {
            throw new IllegalStateException("Activity not attached");
        }
        return activity;
    }
}

M src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java => src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +64 -27
@@ 30,6 30,8 @@
package eu.siacs.conversations.ui;


import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;


@@ 65,13 67,16 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.databinding.ActivityConversationsBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
import eu.siacs.conversations.ui.interfaces.OnConversationRead;
import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
import eu.siacs.conversations.ui.util.ActionBarUtil;
import eu.siacs.conversations.ui.util.ActivityResult;
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;


@@ 83,8 88,6 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;

import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;

public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged {

    public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";


@@ 96,6 99,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
    public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
    public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
    public static final String POST_ACTION_RECORD_VOICE = "record_voice";
    public static final String EXTRA_TYPE = "type";

    private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
            ACTION_VIEW_CONVERSATION,


@@ 278,6 282,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
        if (grantResults.length > 0) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {


@@ 425,16 430,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
    }

    private void openConversation(Conversation conversation, Bundle extras) {
        ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment);
        final FragmentManager fragmentManager = getFragmentManager();
        executePendingTransactions(fragmentManager);
        ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
        final boolean mainNeedsRefresh;
        if (conversationFragment == null) {
            mainNeedsRefresh = false;
            Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
            final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
            if (mainFragment instanceof ConversationFragment) {
                conversationFragment = (ConversationFragment) mainFragment;
            } else {
                conversationFragment = new ConversationFragment();
                FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
                FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
                fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
                fragmentTransaction.addToBackStack(null);
                try {


@@ 456,6 463,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
        }
    }

    private static void executePendingTransactions(final FragmentManager fragmentManager) {
        try {
            fragmentManager.executePendingTransactions();
        } catch (final Exception e) {
            Log.e(Config.LOGTAG,"unable to execute pending fragment transactions");
        }
    }

    public boolean onXmppUriClicked(Uri uri) {
        XmppUri xmppUri = new XmppUri(uri);
        if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {


@@ 524,6 539,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio

    @Override
    protected void onStart() {
        super.onStart();
        final int theme = findTheme();
        if (this.mTheme != theme) {
            this.mSkipBackgroundBinding = true;


@@ 532,7 548,6 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
            this.mSkipBackgroundBinding = false;
        }
        mRedirectInProcess.set(false);
        super.onStart();
    }

    @Override


@@ 562,17 577,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
    }

    private void initializeFragments() {
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
        Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
        final FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
        if (mainFragment != null) {
            if (binding.secondaryFragment != null) {
                if (mainFragment instanceof ConversationFragment) {
                    getFragmentManager().popBackStack();
                    transaction.remove(mainFragment);
                    transaction.commit();
                    getFragmentManager().executePendingTransactions();
                    transaction = getFragmentManager().beginTransaction();
                    fragmentManager.executePendingTransactions();
                    transaction = fragmentManager.beginTransaction();
                    transaction.replace(R.id.secondary_fragment, mainFragment);
                    transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
                    transaction.commit();


@@ 583,7 599,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                    transaction.remove(secondaryFragment);
                    transaction.commit();
                    getFragmentManager().executePendingTransactions();
                    transaction = getFragmentManager().beginTransaction();
                    transaction = fragmentManager.beginTransaction();
                    transaction.replace(R.id.main_fragment, secondaryFragment);
                    transaction.addToBackStack(null);
                    transaction.commit();


@@ 601,18 617,38 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio

    private void invalidateActionBarTitle() {
        final ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
            if (mainFragment instanceof ConversationFragment) {
                final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
                if (conversation != null) {
                    actionBar.setTitle(EmojiWrapper.transform(conversation.getName()));
                    actionBar.setDisplayHomeAsUpEnabled(true);
                    return;
                }
        if (actionBar == null) {
            return;
        }
        final FragmentManager fragmentManager = getFragmentManager();
        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
        if (mainFragment instanceof ConversationFragment) {
            final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
            if (conversation != null) {
                actionBar.setTitle(EmojiWrapper.transform(conversation.getName()));
                actionBar.setDisplayHomeAsUpEnabled(true);
                ActionBarUtil.setActionBarOnClickListener(
                        binding.toolbar,
                        (v) -> openConversationDetails(conversation)
                );
                return;
            }
        }
        actionBar.setTitle(R.string.app_name);
        actionBar.setDisplayHomeAsUpEnabled(false);
        ActionBarUtil.resetActionBarOnClickListeners(binding.toolbar);
    }

    private void openConversationDetails(final Conversation conversation) {
        if (conversation.getMode() == Conversational.MODE_MULTI) {
            ConferenceDetailsActivity.open(this, conversation);
        } else {
            final Contact contact = conversation.getContact();
            if (contact.isSelf()) {
                switchToAccount(conversation.getAccount());
            } else {
                switchToContactDetails(contact);
            }
            actionBar.setTitle(R.string.app_name);
            actionBar.setDisplayHomeAsUpEnabled(false);
        }
    }



@@ 621,17 657,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
        if (performRedirectIfNecessary(conversation, false)) {
            return;
        }
        Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
        final FragmentManager fragmentManager = getFragmentManager();
        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
        if (mainFragment instanceof ConversationFragment) {
            try {
                getFragmentManager().popBackStack();
            } catch (IllegalStateException e) {
                fragmentManager.popBackStack();
            } catch (final IllegalStateException e) {
                Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e);
                //this usually means activity is no longer active; meaning on the next open we will run through this again
            }
            return;
        }
        Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
        if (secondaryFragment instanceof ConversationFragment) {
            if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
                Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);

M src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java => src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +37 -6
@@ 23,11 23,14 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.CheckBox;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AlertDialog.Builder;


@@ 693,12 696,18 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
            } catch (final IllegalArgumentException | NullPointerException ignored) {
                this.jidToEdit = null;
            }
            if (jidToEdit != null && intent.getData() != null && intent.getBooleanExtra("scanned", false)) {
                final XmppUri uri = new XmppUri(intent.getData());
                if (xmppConnectionServiceBound) {
                    processFingerprintVerification(uri, false);
            final Uri data = intent.getData();
            final XmppUri xmppUri = data == null ? null : new XmppUri(data);
            final boolean scanned = intent.getBooleanExtra("scanned", false);
            if (jidToEdit != null && xmppUri != null && xmppUri.hasFingerprints()) {
                if (scanned) {
                    if (xmppConnectionServiceBound) {
                        processFingerprintVerification(xmppUri, false);
                    } else {
                        this.pendingUri = xmppUri;
                    }
                } else {
                    this.pendingUri = uri;
                    displayVerificationWarningDialog(xmppUri);
                }
            }
            boolean init = intent.getBooleanExtra("init", false);


@@ 735,6 744,28 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
        }
    }

    private void displayVerificationWarningDialog(final XmppUri xmppUri) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.verify_omemo_keys);
        View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
        final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
        TextView warning = view.findViewById(R.id.warning);
        warning.setText(R.string.verifying_omemo_keys_trusted_source_account);
        builder.setView(view);
        builder.setPositiveButton(R.string.continue_btn, (dialog, which) -> {
            if (isTrustedSource.isChecked()) {
                processFingerprintVerification(xmppUri, false);
            } else {
                finish();
            }
        });
        builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish());
        AlertDialog dialog = builder.create();
        dialog.setCanceledOnTouchOutside(false);
        dialog.setOnCancelListener(d -> finish());
        dialog.show();
    }

    @Override
    public void onNewIntent(final Intent intent) {
        super.onNewIntent(intent);


@@ 749,7 780,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
    }

    @Override
    public void onSaveInstanceState(final Bundle savedInstanceState) {
    public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
        if (mAccount != null) {
            savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString());
            savedInstanceState.putBoolean("initMode", mInitMode);

M src/main/java/eu/siacs/conversations/ui/LocationActivity.java => src/main/java/eu/siacs/conversations/ui/LocationActivity.java +3 -0
@@ 39,6 39,7 @@ import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.ui.util.LocationHelper;
import eu.siacs.conversations.ui.widget.Marker;
import eu.siacs.conversations.ui.widget.MyLocation;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;

public abstract class LocationActivity extends ActionBarActivity implements LocationListener {


@@ 68,6 69,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
		}
	}


	protected void updateLocationMarkers() {
		clearMarkers();
	}


@@ 222,6 224,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
	@Override
	protected void onResume() {
		super.onResume();
		SettingsUtils.applyScreenshotPreventionSetting(this);
		Configuration.getInstance().load(this, getPreferences());
		map.onResume();
		this.setMyLoc(null);

M src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java => src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java +3 -0
@@ 39,6 39,7 @@ import java.util.logging.Logger;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.MTMDecision;
import eu.siacs.conversations.services.MemorizingTrustManager;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;

public class MemorizingActivity extends AppCompatActivity implements OnClickListener, OnCancelListener {


@@ 61,6 62,8 @@ public class MemorizingActivity extends AppCompatActivity implements OnClickList
	@Override
	public void onResume() {
		super.onResume();
		SettingsUtils.applyScreenshotPreventionSetting(this);

		Intent i = getIntent();
		decisionId = i.getIntExtra(MemorizingTrustManager.DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
		int titleId = i.getIntExtra(MemorizingTrustManager.DECISION_TITLE_ID, R.string.mtm_accept_cert);

M src/main/java/eu/siacs/conversations/ui/RecordingActivity.java => src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +7 -0
@@ 28,6 28,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityRecordingBinding;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;



@@ 67,6 68,12 @@ public class RecordingActivity extends Activity implements View.OnClickListener 
    }

    @Override
    protected void onResume(){
        super.onResume();
        SettingsUtils.applyScreenshotPreventionSetting(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (!startRecording()) {

M src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java => src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +52 -26
@@ 1,5 1,8 @@
package eu.siacs.conversations.ui;

import static java.util.Arrays.asList;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;


@@ 57,6 60,7 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.widget.DialpadView;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.ui.util.Rationals;
import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xml.Namespace;


@@ 67,10 71,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;

import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
import static java.util.Arrays.asList;

public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate {
public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {

    public static final String EXTRA_WITH = "with";
    public static final String EXTRA_SESSION_ID = "session_id";


@@ 97,7 98,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
    );
    private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
            RtpEndUserState.CONNECTING,
            RtpEndUserState.CONNECTED
            RtpEndUserState.CONNECTED,
            RtpEndUserState.RECONNECTING
    );
    private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED = Arrays.asList(
            RtpEndUserState.CONNECTED,
            RtpEndUserState.RECONNECTING
    );
    private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList(
            RtpEndUserState.ACCEPTING_CALL,
            RtpEndUserState.CONNECTING,
            RtpEndUserState.RECONNECTING
    );
    private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
    private static final int REQUEST_ACCEPT_CALL = 0x1111;


@@ 141,13 152,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
        setSupportActionBar(binding.toolbar);

        ((DialpadView)findViewById(R.id.dialpad)).setClickConsumer(tag -> {
        binding.dialpad.setClickConsumer(tag -> {
            requireRtpConnection().applyDtmfTone(tag);
        });

        if (savedInstanceState != null) {
            int dialpad_visibility = savedInstanceState.getInt("dialpad_visibility");
            findViewById(R.id.dialpad).setVisibility(dialpad_visibility);
            boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible");
            binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE);
        }
    }



@@ 197,9 208,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
    private boolean isAudioOnlyConversation() {
        final JingleRtpConnection connection =
                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
        return connection != null &&
                connection.getEndUserState() == RtpEndUserState.CONNECTED &&
                !connection.isVideoEnabled();

        return connection != null && !connection.getMedia().contains(Media.VIDEO);
    }

    private void switchToConversation() {


@@ 482,12 492,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
    public void onStart() {
        super.onStart();
        mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
        this.binding.remoteVideo.setOnAspectRatioChanged(this);
    }

    @Override
    public void onStop() {
        mHandler.removeCallbacks(mTickExecutor);
        binding.remoteVideo.release();
        binding.remoteVideo.setOnAspectRatioChanged(null);
        binding.localVideo.release();
        final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
        final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get();


@@ 535,7 547,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe

    private boolean isConnected() {
        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
        return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED;
        return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
    }

    private boolean switchToPictureInPicture() {


@@ 551,9 563,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
    @RequiresApi(api = Build.VERSION_CODES.O)
    private void startPictureInPicture() {
        try {
            final Rational rational = this.binding.remoteVideo.getAspectRatio();
            final Rational clippedRational = Rationals.clip(rational);
            Log.d(Config.LOGTAG, "suggested rational " + rational + ". clipped to " + clippedRational);
            enterPictureInPictureMode(
                    new PictureInPictureParams.Builder()
                            .setAspectRatio(new Rational(10, 16))
                            .setAspectRatio(clippedRational)
                            .build()
            );
        } catch (final IllegalStateException e) {


@@ 562,6 577,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
        }
    }

    @Override
    public void onAspectRatioChanged(final Rational rational) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) {
            final Rational clippedRational = Rationals.clip(rational);
            Log.d(Config.LOGTAG, "suggested rational after aspect ratio change " + rational + ". clipped to " + clippedRational);
            setPictureInPictureParams(new PictureInPictureParams.Builder()
                    .setAspectRatio(clippedRational)
                    .build());
        }
    }

    private boolean deviceSupportsPictureInPicture() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);


@@ 656,8 682,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
        surfaceViewRenderer.setVisibility(View.VISIBLE);
        try {
            surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
        } catch (IllegalStateException e) {
            Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
        } catch (final IllegalStateException e) {
            //Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
        }
        surfaceViewRenderer.setEnableHardwareScaler(true);
    }


@@ 682,6 708,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
            case CONNECTED:
                setTitle(R.string.rtp_state_connected);
                break;
            case RECONNECTING:
                setTitle(R.string.rtp_state_reconnecting);
                break;
            case ACCEPTING_CALL:
                setTitle(R.string.rtp_state_accepting_call);
                break;


@@ 824,7 853,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe

    @SuppressLint("RestrictedApi")
    private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
        if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
        if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) {
            Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
            if (media.contains(Media.VIDEO)) {
                final JingleRtpConnection rtpConnection = requireRtpConnection();


@@ 952,14 981,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
            this.binding.duration.setVisibility(View.GONE);
            return;
        }
        final long rtpConnectionStarted = connection.getRtpConnectionStarted();
        final long rtpConnectionEnded = connection.getRtpConnectionEnded();
        if (rtpConnectionStarted != 0) {
            final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded;
            this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false));
            this.binding.duration.setVisibility(View.VISIBLE);
        } else {
        if (connection.zeroDuration()) {
            this.binding.duration.setVisibility(View.GONE);
        } else {
            this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
            this.binding.duration.setVisibility(View.VISIBLE);
        }
    }



@@ 991,7 1017,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            return;
        }
        if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
        if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
            binding.localVideo.setVisibility(View.GONE);
            binding.remoteVideoWrapper.setVisibility(View.GONE);
            binding.appBarLayout.setVisibility(View.GONE);


@@ 1024,6 1050,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
                binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
            } else {
                binding.appBarLayout.setVisibility(View.VISIBLE);
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
                binding.remoteVideoWrapper.setVisibility(View.GONE);
            }


@@ 1200,8 1227,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
    @Override
    protected void onSaveInstanceState(@NonNull @NotNull Bundle outState) {
        super.onSaveInstanceState(outState);
        int visibility = findViewById(R.id.dialpad).getVisibility();
        outState.putInt("dialpad_visibility", visibility);
        outState.putBoolean("dialpad_visible", binding.dialpad.getVisibility() == View.VISIBLE);
    }

    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {

M src/main/java/eu/siacs/conversations/ui/ScanActivity.java => src/main/java/eu/siacs/conversations/ui/ScanActivity.java +2 -0
@@ 60,6 60,7 @@ import java.util.Map;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.service.CameraManager;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.ui.widget.ScannerView;

/**


@@ 181,6 182,7 @@ public final class ScanActivity extends Activity implements SurfaceTextureListen
	@Override
	protected void onResume() {
		super.onResume();
		SettingsUtils.applyScreenshotPreventionSetting(this);
		maybeOpenCamera();
	}


M src/main/java/eu/siacs/conversations/ui/SettingsActivity.java => src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +10 -0
@@ 40,6 40,7 @@ import eu.siacs.conversations.services.MemorizingTrustManager;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xmpp.Jid;



@@ 57,8 58,10 @@ public class SettingsActivity extends XmppActivity implements
	public static final String THEME = "theme";
	public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
	public static final String OMEMO_SETTING = "omemo";
	public static final String PREVENT_SCREENSHOTS = "prevent_screenshots";

	public static final int REQUEST_CREATE_BACKUP = 0xbf8701;

	private SettingsFragment mSettingsFragment;

	@Override


@@ 393,8 396,15 @@ public class SettingsActivity extends XmppActivity implements
			if (this.mTheme != theme) {
				recreate();
			}
		} else if(name.equals(PREVENT_SCREENSHOTS)){
			SettingsUtils.applyScreenshotPreventionSetting(this);
		}
	}

	@Override
	public void onResume(){
		super.onResume();
		SettingsUtils.applyScreenshotPreventionSetting(this);
	}

	@Override

M src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java => src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java +212 -209
@@ 13,10 13,13 @@ import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;

import com.google.android.material.snackbar.Snackbar;
import com.google.common.math.DoubleMath;

import org.osmdroid.api.IGeoPoint;
import org.osmdroid.util.GeoPoint;

import java.math.RoundingMode;

import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityShareLocationBinding;


@@ 28,213 31,213 @@ import eu.siacs.conversations.utils.ThemeHelper;

public class ShareLocationActivity extends LocationActivity implements LocationListener {

	private Snackbar snackBar;
	private ActivityShareLocationBinding binding;
	private boolean marker_fixed_to_loc = false;
	private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
	private Boolean noAskAgain = false;

	@Override
	protected void onSaveInstanceState(@NonNull final Bundle outState) {
		super.onSaveInstanceState(outState);

		outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
	}

	@Override
	protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
		super.onRestoreInstanceState(savedInstanceState);

		if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
			this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
		}
	}

	@Override
	protected void onCreate(final Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		this.binding = DataBindingUtil.setContentView(this,R.layout.activity_share_location);
		setSupportActionBar(binding.toolbar);
		configureActionBar(getSupportActionBar());
		setupMapView(binding.map, LocationProvider.getGeoPoint(this));

		this.binding.cancelButton.setOnClickListener(view -> {
			setResult(RESULT_CANCELED);
			finish();
		});

		this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
		this.snackBar.setAction(R.string.enable, view -> {
			if (isLocationEnabledAndAllowed()) {
				updateUi();
			} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
				requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
			} else if (!isLocationEnabled()) {
				startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
			}
		});
		ThemeHelper.fix(this.snackBar);

		this.binding.shareButton.setOnClickListener(view -> {
			final Intent result = new Intent();

			if (marker_fixed_to_loc && myLoc != null) {
				result.putExtra("latitude", myLoc.getLatitude());
				result.putExtra("longitude", myLoc.getLongitude());
				result.putExtra("altitude", myLoc.getAltitude());
				result.putExtra("accuracy", (int) myLoc.getAccuracy());
			} else {
				final IGeoPoint markerPoint = this.binding.map.getMapCenter();
				result.putExtra("latitude", markerPoint.getLatitude());
				result.putExtra("longitude", markerPoint.getLongitude());
			}

			setResult(RESULT_OK, result);
			finish();
		});

		this.marker_fixed_to_loc = isLocationEnabledAndAllowed();

		this.binding.fab.setOnClickListener(view -> {
			if (!marker_fixed_to_loc) {
				if (!isLocationEnabled()) {
					startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
				} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
					requestPermissions(REQUEST_CODE_FAB_PRESSED);
				}
			}
			toggleFixedLocation();
		});
	}

	@Override
	public void onRequestPermissionsResult(final int requestCode,
										   @NonNull final String[] permissions,
										   @NonNull final int[] grantResults) {
		super.onRequestPermissionsResult(requestCode, permissions, grantResults);

		if (grantResults.length > 0 &&
				grantResults[0] != PackageManager.PERMISSION_GRANTED &&
				Build.VERSION.SDK_INT >= 23 &&
				permissions.length > 0 &&
				(
						Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) ||
								Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) ||
								Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
				) &&
				!shouldShowRequestPermissionRationale(permissions[0])) {
			noAskAgain = true;
		}

		if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
			startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
		}
		updateUi();
	}

	@Override
	protected void gotoLoc(final boolean setZoomLevel) {
		if (this.myLoc != null && mapController != null) {
			if (setZoomLevel) {
				mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
			}
			mapController.animateTo(new GeoPoint(this.myLoc));
		}
	}

	@Override
	protected void setMyLoc(final Location location) {
		this.myLoc = location;
	}

	@Override
	protected void onPause() {
		super.onPause();
	}

	@Override
	protected void updateLocationMarkers() {
		super.updateLocationMarkers();
		if (this.myLoc != null) {
			this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
			if (this.marker_fixed_to_loc) {
				this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
			} else {
				this.binding.map.getOverlays().add(new Marker(marker_icon));
			}
		} else {
			this.binding.map.getOverlays().add(new Marker(marker_icon));
		}
	}

	@Override
	public void onLocationChanged(final Location location) {
		if (this.myLoc == null) {
			this.marker_fixed_to_loc = true;
		}
		updateUi();
		if (LocationHelper.isBetterLocation(location, this.myLoc)) {
			final Location oldLoc = this.myLoc;
			this.myLoc = location;

			// Don't jump back to the users location if they're not moving (more or less).
			if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) {
				gotoLoc();
			}

			updateLocationMarkers();
		}
	}

	@Override
	public void onStatusChanged(final String provider, final int status, final Bundle extras) {

	}

	@Override
	public void onProviderEnabled(final String provider) {

	}

	@Override
	public void onProviderDisabled(final String provider) {

	}

	private boolean isLocationEnabledAndAllowed() {
		return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
	}

	private void toggleFixedLocation() {
		this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
		if (this.marker_fixed_to_loc) {
			gotoLoc(false);
		}
		updateLocationMarkers();
		updateUi();
	}

	@Override
	protected void updateUi() {
		if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
			this.snackBar.dismiss();
		} else {
			this.snackBar.show();
		}
<