Compare commits
No commits in common. "develop" and "2.9.11-sum7" have entirely different histories.
develop
...
2.9.11-sum
|
@ -1,38 +0,0 @@
|
||||||
name: Android CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: set up JDK 11
|
|
||||||
uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
java-version: '11'
|
|
||||||
distribution: 'adopt'
|
|
||||||
- name: Download WebRTC
|
|
||||||
run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar
|
|
||||||
- name: Grant execute permission for gradlew
|
|
||||||
run: chmod +x gradlew
|
|
||||||
- name: Build Quicksy (Compat)
|
|
||||||
run: ./gradlew assembleQuicksyFreeCompatDebug
|
|
||||||
- name: Build Quicksy (System)
|
|
||||||
run: ./gradlew assembleQuicksyFreeSystemDebug
|
|
||||||
- name: Build Conversations (Compat)
|
|
||||||
run: ./gradlew assembleConversationsFreeCompatDebug
|
|
||||||
- name: Build Conversations (System)
|
|
||||||
run: ./gradlew assembleConversationsFreeSystemDebug
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: Conversations all-flavors (debug)
|
|
||||||
path: ./build/outputs/apk/**/debug/Conversations-*.apk
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
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-m89.aar https://gultsch.de/files/libwebrtc-m89.aar
|
||||||
|
script:
|
||||||
|
- ./gradlew assembleQuicksyFreeCompatDebug
|
||||||
|
- ./gradlew assembleQuicksyFreeSystemDebug
|
||||||
|
- ./gradlew assembleConversationsFreeCompatDebug
|
||||||
|
- ./gradlew assembleConversationsFreeSystemDebug
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- yes | sdkmanager "platforms;android-28"
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,30 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
### Version 2.10.2
|
|
||||||
|
|
||||||
* Fix crash when rendering some quotes
|
|
||||||
* Fix crash in welcome screen
|
|
||||||
|
|
||||||
### Version 2.10.1
|
|
||||||
|
|
||||||
* Fix issue with some videos not being compressed
|
|
||||||
* Fix rare crash when opening notification
|
|
||||||
|
|
||||||
### Version 2.10.0
|
|
||||||
|
|
||||||
* Show black bars when remote video does not match aspect ratio of screen
|
|
||||||
* Improve search performance
|
|
||||||
* Add setting to prevent screenshots
|
|
||||||
|
|
||||||
### Version 2.9.13
|
|
||||||
|
|
||||||
* minor A/V improvements
|
|
||||||
|
|
||||||
### Version 2.9.12
|
|
||||||
|
|
||||||
* Always verify domain name. No user overwrite
|
|
||||||
* Support roster pre authentication
|
|
||||||
|
|
||||||
### Version 2.9.11
|
### Version 2.9.11
|
||||||
|
|
||||||
* Fixed 'No Connectivity' issues on Android 7.1
|
* Fixed 'No Connectivity' issues on Android 7.1
|
||||||
|
|
|
@ -371,7 +371,7 @@ Unfortunately we don‘t have a recommendation for iPhones right now. There are
|
||||||
**Note:** Starting with version 2.8.0 you will need to compile libwebrtc.
|
**Note:** Starting with version 2.8.0 you will need to compile libwebrtc.
|
||||||
[Instructions](https://webrtc.github.io/webrtc-org/native-code/android/) can be found on the WebRTC
|
[Instructions](https://webrtc.github.io/webrtc-org/native-code/android/) can be found on the WebRTC
|
||||||
website. Place the resulting libwebrtc.aar in the `libs/` directory. The PlayStore release currently
|
website. Place the resulting libwebrtc.aar in the `libs/` directory. The PlayStore release currently
|
||||||
uses the stable M90 release and renamed the file name to `libwebrtc-m90.aar` put potentially you can
|
uses the stable M81 release and renamed the file name to `libwebrtc-m81.aar` put potentially you can
|
||||||
reference any file name by modifying `build.gradle`.
|
reference any file name by modifying `build.gradle`.
|
||||||
|
|
||||||
Make sure to have ANDROID_HOME point to your Android SDK. Use the Android SDK Manager to install missing dependencies.
|
Make sure to have ANDROID_HOME point to your Android SDK. Use the Android SDK Manager to install missing dependencies.
|
||||||
|
|
71
build.gradle
71
build.gradle
|
@ -1,12 +1,14 @@
|
||||||
|
import com.android.build.OutputFile
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all
|
// Top-level build file where you can add configuration options common to all
|
||||||
// sub-projects/modules.
|
// sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,8 +16,8 @@ apply plugin: 'com.android.application'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
|
||||||
jcenter()
|
jcenter()
|
||||||
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
|
@ -33,23 +35,23 @@ configurations {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.viewpager:viewpager:1.0.0'
|
implementation 'androidx.viewpager:viewpager:1.0.0'
|
||||||
|
|
||||||
playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') {
|
playstoreImplementation('com.google.firebase:firebase-messaging:21.0.1') {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||||
}
|
}
|
||||||
conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2")
|
conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2")
|
||||||
conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2")
|
conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2")
|
||||||
quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.1'
|
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.1'
|
quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0'
|
||||||
implementation 'org.sufficientlysecure:openpgp-api:10.0'
|
implementation 'org.sufficientlysecure:openpgp-api:10.0'
|
||||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.emoji:emoji:1.1.0'
|
implementation 'androidx.emoji:emoji:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
compatImplementation 'androidx.emoji:emoji-appcompat:1.1.0'
|
compatImplementation 'androidx.emoji:emoji-appcompat:1.1.0'
|
||||||
conversationsFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
|
conversationsFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
|
||||||
quicksyFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
|
quicksyFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
|
||||||
|
@ -62,22 +64,20 @@ dependencies {
|
||||||
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
|
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
|
||||||
implementation 'com.makeramen:roundedimageview:2.3.0'
|
implementation 'com.makeramen:roundedimageview:2.3.0'
|
||||||
implementation "com.wefika:flowlayout:0.4.1"
|
implementation "com.wefika:flowlayout:0.4.1"
|
||||||
implementation 'com.otaliastudios:transcoder:0.10.4'
|
implementation 'net.ypresto.androidtranscoder:android-transcoder:0.3.0'
|
||||||
|
implementation 'org.jxmpp:jxmpp-jid:0.6.4'
|
||||||
implementation 'org.jxmpp:jxmpp-jid:1.0.2'
|
|
||||||
implementation 'org.osmdroid:osmdroid-android:6.1.10'
|
implementation 'org.osmdroid:osmdroid-android:6.1.10'
|
||||||
implementation 'org.hsluv:hsluv:0.2'
|
implementation 'org.hsluv:hsluv:0.2'
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
implementation 'org.conscrypt:conscrypt-android:2.2.1'
|
||||||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||||
implementation "com.leinardi.android:speed-dial:3.2.0"
|
implementation "com.leinardi.android:speed-dial:2.0.1"
|
||||||
|
|
||||||
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
|
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.9.2"
|
implementation "com.squareup.okhttp3:okhttp:4.9.1"
|
||||||
|
|
||||||
implementation 'com.google.guava:guava:30.1.1-android'
|
implementation 'com.google.guava:guava:30.1-android'
|
||||||
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36'
|
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
|
||||||
// implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs')
|
// implementation fileTree(include: ['libwebrtc-m89.aar'], dir: 'libs')
|
||||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,25 +93,30 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 4202301
|
versionCode 42013
|
||||||
versionName "2.10.2"
|
versionName "2.9.11"
|
||||||
archivesBaseName += "-$versionName"
|
archivesBaseName += "-$versionName"
|
||||||
applicationId "eu.sum7.conversations"
|
applicationId "eu.sum7.conversations"
|
||||||
resValue "string", "applicationId", applicationId
|
resValue "string", "applicationId", applicationId
|
||||||
def appName = "Conv6ations"
|
resValue "string", "app_name", "Conv6ations"
|
||||||
resValue "string", "app_name", appName
|
buildConfigField "String", "LOGTAG", "\"conver6ations\""
|
||||||
buildConfigField "String", "APP_NAME", "\"$appName\"";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
implementation.exclude group: 'org.jetbrains' , module:'annotations'
|
compile.exclude group: 'org.jetbrains' , module:'annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
dataBinding {
|
dataBinding {
|
||||||
enabled true
|
enabled true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dexOptions {
|
||||||
|
// Skip pre-dexing when running on Travis CI or when disabled via -Dpre-dex=false.
|
||||||
|
preDexLibraries = preDexEnabled && !travisBuild
|
||||||
|
jumboMode true
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
@ -124,11 +129,9 @@ android {
|
||||||
quicksy {
|
quicksy {
|
||||||
dimension "mode"
|
dimension "mode"
|
||||||
applicationId = "im.quicksy.client"
|
applicationId = "im.quicksy.client"
|
||||||
|
resValue "string", "app_name", "Quicksy"
|
||||||
resValue "string", "applicationId", applicationId
|
resValue "string", "applicationId", applicationId
|
||||||
|
buildConfigField "String", "LOGTAG", "\"quicksy\""
|
||||||
def appName = "Quicksy"
|
|
||||||
resValue "string", "app_name", appName
|
|
||||||
buildConfigField "String", "APP_NAME", "\"$appName\"";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conversations {
|
conversations {
|
||||||
|
@ -262,4 +265,14 @@ android {
|
||||||
exclude 'META-INF/BCKEY.DSA'
|
exclude 'META-INF/BCKEY.DSA'
|
||||||
exclude 'META-INF/BCKEY.SF'
|
exclude 'META-INF/BCKEY.SF'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android.applicationVariants.all { variant ->
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
|
||||||
|
if (baseAbiVersionCode != null) {
|
||||||
|
output.versionCodeOverride = (100 * variant.versionCode) + baseAbiVersionCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -453,19 +453,12 @@
|
||||||
<xmpp:version>0.2.0</xmpp:version>
|
<xmpp:version>0.2.0</xmpp:version>
|
||||||
</xmpp:SupportedXep>
|
</xmpp:SupportedXep>
|
||||||
</implements>
|
</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>
|
<release>
|
||||||
<Version>
|
<Version>
|
||||||
<revision>2.9.13</revision>
|
<revision>2.5.8</revision>
|
||||||
<created>2021-05-03</created>
|
<created>2019-09-12</created>
|
||||||
<file-release rdf:resource="https://github.com/iNPUTmice/Conversations/archive/2.9.13.tar.gz"/>
|
<file-release rdf:resource="https://github.com/iNPUTmice/Conversations/archive/2.5.8.tar.gz"/>
|
||||||
</Version>
|
</Version>
|
||||||
</release>
|
</release>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
• Always verify domain name. No user overwrite
|
|
||||||
• Support roster pre authentication
|
|
||||||
• minor A/V improvements
|
|
|
@ -1,3 +0,0 @@
|
||||||
• Show black bars when remote video does not match aspect ratio of screen
|
|
||||||
• Improve search performance
|
|
||||||
• Add setting to prevent screenshots
|
|
|
@ -1,2 +0,0 @@
|
||||||
• Fix issue with some videos not being compressed
|
|
||||||
• Fix rare crash when opening notification
|
|
|
@ -1,2 +0,0 @@
|
||||||
• Fix crash when rendering some quotes
|
|
||||||
• Fix crash in welcome screen
|
|
|
@ -1 +0,0 @@
|
||||||
• Fix usage directTLS of manuelle enter an address
|
|
|
@ -26,15 +26,6 @@
|
||||||
-dontwarn java.lang.**
|
-dontwarn java.lang.**
|
||||||
-dontwarn javax.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.** {
|
-keepclassmembers class eu.siacs.conversations.http.services.** {
|
||||||
!transient <fields>;
|
!transient <fields>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -29,7 +30,6 @@ import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
|
||||||
import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
|
import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
|
||||||
import eu.siacs.conversations.services.ImportBackupService;
|
import eu.siacs.conversations.services.ImportBackupService;
|
||||||
import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
|
import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
import eu.siacs.conversations.utils.ThemeHelper;
|
import eu.siacs.conversations.utils.ThemeHelper;
|
||||||
|
|
||||||
public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
|
public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
|
||||||
|
@ -56,12 +56,6 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
this.backupFileAdapter.setOnItemClickedListener(this);
|
this.backupFileAdapter.setOnItemClickedListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume(){
|
|
||||||
super.onResume();
|
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
getMenuInflater().inflate(R.menu.import_backup, menu);
|
getMenuInflater().inflate(R.menu.import_backup, menu);
|
||||||
|
@ -131,8 +125,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
try {
|
try {
|
||||||
final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
|
final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
|
||||||
showEnterPasswordDialog(backupFile, finishOnCancel);
|
showEnterPasswordDialog(backupFile, finishOnCancel);
|
||||||
} catch (final IOException | IllegalArgumentException e) {
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
Log.d(Config.LOGTAG, "unable to open backup file " + uri, e);
|
|
||||||
Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
|
Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,7 +181,6 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||||
super.onActivityResult(requestCode, resultCode, intent);
|
|
||||||
if (resultCode == RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
if (requestCode == 0xbac) {
|
if (requestCode == 0xbac) {
|
||||||
openBackupFileFromUri(intent.getData(), false);
|
openBackupFileFromUri(intent.getData(), false);
|
||||||
|
@ -233,17 +225,15 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
if (item.getItemId() == R.id.action_open_backup_file) {
|
if (item.getItemId() == R.id.action_open_backup_file) {
|
||||||
openBackupFile();
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
intent.setType("*/*");
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
|
||||||
|
}
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openBackupFile() {
|
|
||||||
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
||||||
intent.setType("*/*");
|
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
||||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,7 +228,6 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
if (grantResults.length > 0) {
|
if (grantResults.length > 0) {
|
||||||
if (allGranted(grantResults)) {
|
if (allGranted(grantResults)) {
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
|
|
|
@ -106,8 +106,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewIntent(final Intent intent) {
|
public void onNewIntent(Intent intent) {
|
||||||
super.onNewIntent(intent);
|
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
setIntent(intent);
|
setIntent(intent);
|
||||||
}
|
}
|
||||||
|
@ -202,7 +201,6 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
|
UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
|
||||||
if (grantResults.length > 0) {
|
if (grantResults.length > 0) {
|
||||||
if (allGranted(grantResults)) {
|
if (allGranted(grantResults)) {
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package eu.siacs.conversations.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
|
||||||
|
|
||||||
public class PhoneNumberUtilWrapper {
|
|
||||||
public static String toFormattedPhoneNumber(Context context, Jid jid) {
|
|
||||||
throw new AssertionError("This method is not implemented in Conversations");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,18 +3,4 @@
|
||||||
<string name="pick_a_server">اختر مزود خدمة XMPP الخاص بك</string>
|
<string name="pick_a_server">اختر مزود خدمة XMPP الخاص بك</string>
|
||||||
<string name="use_chat.sum7.eu">استخدِم chat.sum7.eu</string>
|
<string name="use_chat.sum7.eu">استخدِم chat.sum7.eu</string>
|
||||||
<string name="create_new_account">أنشئ حسابًا جديدًا</string>
|
<string name="create_new_account">أنشئ حسابًا جديدًا</string>
|
||||||
<string name="do_you_have_an_account">هل تملك حساب XMPP؟؟ قد يكون ذلك ممكنا لو كنت تستعمل خدمة XMPP أخرى أو إستعملت تطبيق كونفرسايشنز سابقا. أو يمكنك صنع حساب XMPP جديد الآن.
|
</resources>
|
||||||
ملاحظة: بعض خدمات البريد الإلكتروني تقدم حسابات 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>
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="pick_a_server">XMPP সার্ভার নির্বাচন করুন</string>
|
<string name="pick_a_server">XMPP সার্ভার নির্বাচন করুন</string>
|
||||||
<string name="use_chat.sum7.eu">chat.sum7.eu ব্যবহার করা যাক</string>
|
<string name="use_conversations.im">conversations.im-ই ব্যবহার করা যাক</string>
|
||||||
<string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করা যাক</string>
|
<string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করা যাক</string>
|
||||||
<string name="do_you_have_an_account">আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</string>
|
<string name="do_you_have_an_account">আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</string>
|
||||||
<string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই chat.sum7.eu -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
|
<string name="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_on_x">আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
|
||||||
<string name="magic_create_text_fixed">আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
|
<string name="magic_create_text_fixed">আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
|
||||||
<string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string>
|
<string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="pick_a_server">Vælg din XMPP-udbyder</string>
|
<string name="pick_a_server">Vælg din XMPP-udbyder</string>
|
||||||
<string name="use_chat.sum7.eu">Brug chat.sum7.eu</string>
|
<string name="use_conversations.im">Brug conversations.im</string>
|
||||||
<string name="create_new_account">Opret ny konto</string>
|
<string name="create_new_account">Opret ny konto</string>
|
||||||
<string name="do_you_have_an_account">Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti.</string>
|
<string name="do_you_have_an_account">Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti.</string>
|
||||||
<string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på chat.sum7.eu; en udbyder, der er specielt velegnet til brug med Conversations.</string>
|
<string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im¹; en udbyder, der er specielt velegnet til brug med Conversations.</string>
|
||||||
<string name="magic_create_text_on_x">Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
|
<string name="magic_create_text_on_x">Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
|
||||||
<string name="magic_create_text_fixed">Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
|
<string name="magic_create_text_fixed">Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
|
||||||
<string name="your_server_invitation">Din server invitation</string>
|
<string name="your_server_invitation">Din server invitation</string>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<string name="your_server_invitation">Η πρόσκλησή σας στον διακομιστή</string>
|
<string name="your_server_invitation">Η πρόσκλησή σας στον διακομιστή</string>
|
||||||
<string name="improperly_formatted_provisioning">Λάθος μορφοποίηση κώδικα παροχής</string>
|
<string name="improperly_formatted_provisioning">Λάθος μορφοποίηση κώδικα παροχής</string>
|
||||||
<string name="tap_share_button_send_invite">Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s.</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="easy_invite_share_text">Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s</string>
|
||||||
<string name="share_invite_with">Διαμοιρασμός πρόσκλησης με...</string>
|
<string name="share_invite_with">Διαμοιρασμός πρόσκλησης με...</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="pick_a_server">Scegli il tuo fornitore XMPP</string>
|
<string name="pick_a_server">Scegli il tuo provider XMPP</string>
|
||||||
<string name="use_chat.sum7.eu">Usa chat.sum7.eu</string>
|
<string name="use_chat.sum7.eu">Usa chat.sum7.eu</string>
|
||||||
<string name="create_new_account">Crea un nuovo profilo</string>
|
<string name="create_new_account">Crea un nuovo account</string>
|
||||||
<string name="do_you_have_an_account">Possiedi già un profilo XMPP? Questo succede se stai già usando un diverso client XMPP o hai già usato prima Conversations. In caso negativo puoi creare un profilo XMPP adesso.
|
<string name="do_you_have_an_account">Possiedi già un account XMPP? Questo succede se stai già usando un diverso client XMPP o hai già usato prima Conversations. In caso negativo puoi creare un account XMPP adesso.
|
||||||
Suggerimento: alcuni provider di email forniscono anche un account XMPP.</string>
|
Suggerimento: alcuni provider di email forniscono anche un account XMPP.</string>
|
||||||
<string name="server_select_text">XMPP è una rete di messaggistica istantanea indipendente dal fornitore. Puoi usare questo client con qualsiasi server XMPP.
|
<string name="server_select_text">XMPP è una rete di instant messaging indipendente dal provider. Puoi usare questo client con qualsiasi server XMPP.
|
||||||
In ogni caso per facilitare puoi creare facilmente un account su chat.sum7.eu, un fornitore pensato apposta per essere usato con Conversations.</string>
|
In ogni caso per facilitare puoi creare facilmente un account su chat.sum7.eu, un provider pensato apposta per essere usato con Conversations.</string>
|
||||||
<string name="magic_create_text_on_x">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_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">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="magic_create_text_fixed">Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
|
||||||
<string name="your_server_invitation">Il tuo invito al server</string>
|
<string name="your_server_invitation">Il tuo invito al server</string>
|
||||||
<string name="improperly_formatted_provisioning">Codice di approvvigionamento formattato male</string>
|
<string name="improperly_formatted_provisioning">Codice di approvvigionamento formattato male</string>
|
||||||
<string name="tap_share_button_send_invite">Tocca il pulsante condividi per inviare al contatto un invito per %1$s.</string>
|
<string name="tap_share_button_send_invite">Tocca il pulsante condividi per inviare al contatto un invito per %1$s.</string>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="pick_a_server">XMPP プロバイダーを選択してください</string>
|
<string name="pick_a_server">XMPPプロバイダーを選択してください</string>
|
||||||
<string name="use_chat.sum7.eu">chat.sum7.eu を利用する</string>
|
<string name="use_chat.sum7.eu">chat.sum7.euを利用する</string>
|
||||||
<string name="create_new_account">新規アカウントを作成</string>
|
<string name="create_new_account">アカウントを作成</string>
|
||||||
<string name="do_you_have_an_account">XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。</string>
|
<string name="do_you_have_an_account">XMPPアカウントをお持ちですか?既にほかのXMPPクライアントを利用しているか、Conversationsを利用したことがある場合はこちら。初めての方は、今すぐ新しいXMPPアカウントを作成できます。\nヒント: eメールのプロバイダーがXMPPアカウントも提供している場合があります。</string>
|
||||||
<string name="server_select_text">XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。\nよろしければ、 Conversations に最適化されたプロバイダー chat.sum7.eu で簡単にアカウントを作成することもできます。</string>
|
<string name="server_select_text">XMPPは、プロバイダーに依存しないインスタントメッセージのプロトコルです。XMPPサーバーならどこでも、このクライアントを使用することができます。\nよろしければ、Conversationsに最適化されたプロバイダーchat.sum7.euで簡単にアカウントを作成することもできます。</string>
|
||||||
<string name="magic_create_text_on_x">%1$s へ招待されました。アカウント作成手順をご案内します。 \n%1$s をプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。</string>
|
<string name="magic_create_text_on_x">%1$sへ招待されました。アカウント作成手順をご案内します。 \n%1$sをプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。</string>
|
||||||
<string name="magic_create_text_fixed">%1$s へ招待されました。ユーザー名は既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。</string>
|
<string name="magic_create_text_fixed">%1$sへ招待されました。ユーザーネームは既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。</string>
|
||||||
<string name="your_server_invitation">サーバーの招待</string>
|
<string name="your_server_invitation">サーバーの招待</string>
|
||||||
<string name="improperly_formatted_provisioning">仮コードの書式が不正です</string>
|
<string name="improperly_formatted_provisioning">仮コードの書式が不正です</string>
|
||||||
<string name="tap_share_button_send_invite">共有ボタンを叩いて、連絡先の %1$s に招待を送信する。</string>
|
<string name="tap_share_button_send_invite">共有ボタンを叩いて、連絡先の %1$s に招待を送信する。</string>
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="pick_a_server">Vyberte si svojho XMPP poskytovateľa</string>
|
|
||||||
<string name="use_chat.sum7.eu">Použiť chat.sum7.eu</string>
|
|
||||||
<string name="create_new_account">Vytvoriť nové konto</string>
|
|
||||||
<string name="do_you_have_an_account">Máte už svoje XMPP konto? Môže to tak byť v prípade, že už používate iného klienta XMPP alebo ste predtým používali Conversations. Ak nie, môžete si vytvoriť nové XMPP konto práve teraz.\nHint: Niektorí poskytovatelia emailu zároveň poskytujú aj XMPP kontá.</string>
|
|
||||||
<string name="server_select_text">XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na chat.sum7.eu; poskytovateľ špeciálne vhodný na používanie s Conversations.</string>
|
|
||||||
<string name="magic_create_text_on_x">Boli ste pozvaný do %1$s. Prevedieme vás procesom vytvorenia konta..\nPo výbere %1$s ako poskytovateľa, budete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu.</string>
|
|
||||||
<string name="magic_create_text_fixed">Boli ste pozvaný do %1$s . Užívateľské meno vám už bolo vopred vybrané. Prevedieme vás procesom vytvorenia konta..\nBudete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu.</string>
|
|
||||||
<string name="tap_share_button_send_invite">Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu.</string>
|
|
||||||
<string name="if_contact_is_nearby_use_qr">Ak je váš kontakt blízko, na prijatie vašej pozvánky si môže nasnímať kód nižšie.</string>
|
|
||||||
<string name="easy_invite_share_text">Pripojte sa k %1$sa rozprávajte sa so mnou: %2$s</string>
|
|
||||||
<string name="share_invite_with">Zdieľať pozvánku s...</string>
|
|
||||||
</resources>
|
|
|
@ -1,8 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="pick_a_server">Välj din XMPP leverantör</string>
|
|
||||||
<string name="use_chat.sum7.eu">Använd chat.sum7.eu</string>
|
<string name="use_chat.sum7.eu">Använd chat.sum7.eu</string>
|
||||||
<string name="create_new_account">Skapa nytt konto</string>
|
<string name="create_new_account">Skapa nytt konto</string>
|
||||||
<string name="your_server_invitation">Din server inbjudan</string>
|
</resources>
|
||||||
<string name="share_invite_with">Dela inbjudan med...</string>
|
|
||||||
</resources>
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="pick_a_server">Chọn nhà cung cấp XMPP của bạn</string>
|
|
||||||
<string name="use_chat.sum7.eu">Sử dụng chat.sum7.eu</string>
|
|
||||||
<string name="create_new_account">Tạo tài khoản mới</string>
|
|
||||||
<string name="do_you_have_an_account">Bạn đã có tài khoản XMPP chưa? Điều này có thể đúng nếu bạn đang dùng một ứng dụng khách cho XMPP khác hoặc đã sử dụng Conversations trước đó. Nếu không, bạn có thể tạo tài khoản XMPP mới ngay bây giờ.\nGợi ý: Một số nhà cung cấp email cũng cung cấp tài khoản XMPP.</string>
|
|
||||||
<string name="server_select_text">XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên chat.sum7.eu được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations.</string>
|
|
||||||
<string name="magic_create_text_on_x">Bạn đã được mời vào %1$s. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nKhi chọn %1$s là nhà cung cấp, bạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn.</string>
|
|
||||||
<string name="magic_create_text_fixed">Bạn đã được mời vào %1$s. Một tên người dùng đã được chọn sẵn cho bạn. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nBạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn.</string>
|
|
||||||
<string name="your_server_invitation">Lời mời vào máy chủ của bạn</string>
|
|
||||||
<string name="improperly_formatted_provisioning">Mã cung cấp không được định dạng đúng</string>
|
|
||||||
<string name="tap_share_button_send_invite">Nhấn nút chia sẻ để gửi lời mời vào %1$s đến liên hệ của bạn.</string>
|
|
||||||
<string name="if_contact_is_nearby_use_qr">Nếu liên hệ của bạn ở gần đây, họ cũng có thể quét mã ở dưới để chấp nhận lời mời của bạn.</string>
|
|
||||||
<string name="easy_invite_share_text">Hãy tham gia vào %1$s và trò chuyện với tôi: %2$s</string>
|
|
||||||
<string name="share_invite_with">Chia sẻ lời mời với...</string>
|
|
||||||
</resources>
|
|
|
@ -39,6 +39,8 @@
|
||||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
|
<uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
@ -52,8 +54,7 @@
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="@xml/backup_content"
|
|
||||||
android:appCategory="social"
|
android:appCategory="social"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/new_launcher"
|
android:icon="@mipmap/new_launcher"
|
||||||
|
@ -143,14 +144,6 @@
|
||||||
<data android:scheme="imto" />
|
<data android:scheme="imto" />
|
||||||
<data android:host="jabber" />
|
<data android:host="jabber" />
|
||||||
</intent-filter>
|
</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>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.StartConversationActivity"
|
android:name=".ui.StartConversationActivity"
|
||||||
|
|
|
@ -6,7 +6,6 @@ import android.net.Uri;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
@ -36,7 +35,7 @@ public final class Config {
|
||||||
return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
|
return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US);
|
public static final String LOGTAG = BuildConfig.LOGTAG;
|
||||||
|
|
||||||
public static final Jid BUG_REPORTS = Jid.of("bugs@chat.sum7.eu");
|
public static final Jid BUG_REPORTS = Jid.of("bugs@chat.sum7.eu");
|
||||||
public static final Uri HELP = Uri.parse("https://sum7.eu/chat");
|
public static final Uri HELP = Uri.parse("https://sum7.eu/chat");
|
||||||
|
@ -108,6 +107,7 @@ public final class Config {
|
||||||
|
|
||||||
public static final boolean USE_BOOKMARKS2 = false;
|
public static final boolean USE_BOOKMARKS2 = false;
|
||||||
|
|
||||||
|
public static final boolean PROCESS_EXTMAP_ALLOW_MIXED = false;
|
||||||
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
|
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
|
||||||
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
|
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
|
||||||
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
||||||
|
@ -118,7 +118,6 @@ public final class Config {
|
||||||
public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
|
public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
|
||||||
|
|
||||||
public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
|
public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
|
||||||
public static final boolean REQUIRE_RTP_VERIFICATION = false; //require a/v calls to be verified with OMEMO
|
|
||||||
|
|
||||||
public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments
|
public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments
|
||||||
|
|
||||||
|
@ -200,9 +199,4 @@ public final class Config {
|
||||||
public final static float LOCATION_FIX_SPACE_DELTA = 10; // m
|
public final static float LOCATION_FIX_SPACE_DELTA = 10; // m
|
||||||
public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
|
public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
|
||||||
}
|
}
|
||||||
|
|
||||||
// How deep nested quotes should be displayed. '2' means one quote nested in another.
|
|
||||||
public static final int QUOTE_MAX_DEPTH = 7;
|
|
||||||
// How deep nested quotes should be created on quoting a message.
|
|
||||||
public static final int QUOTING_MAX_DEPTH = 1;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,21 +17,6 @@ import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
public class JabberIdContact extends AbstractPhoneContact {
|
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 final Jid jid;
|
||||||
|
|
||||||
private JabberIdContact(Cursor cursor) throws IllegalArgumentException {
|
private JabberIdContact(Cursor cursor) throws IllegalArgumentException {
|
||||||
|
@ -51,26 +36,38 @@ public class JabberIdContact extends AbstractPhoneContact {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
try (final Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, SELECTION_ARGS, null)) {
|
final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
|
||||||
if (cursor == null) {
|
ContactsContract.Data.DISPLAY_NAME,
|
||||||
return Collections.emptyMap();
|
ContactsContract.Data.PHOTO_URI,
|
||||||
}
|
ContactsContract.Data.LOOKUP_KEY,
|
||||||
final HashMap<Jid, JabberIdContact> contacts = new HashMap<>();
|
ContactsContract.CommonDataKinds.Im.DATA};
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
try {
|
final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
|
||||||
final JabberIdContact contact = new JabberIdContact(cursor);
|
+ ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
|
||||||
final JabberIdContact preexisting = contacts.put(contact.getJid(), contact);
|
+ "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
|
||||||
if (preexisting == null || preexisting.rating() < contact.rating()) {
|
+ "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
|
||||||
contacts.put(contact.getJid(), contact);
|
+ "\")";
|
||||||
}
|
final Cursor cursor;
|
||||||
} catch (final IllegalArgumentException e) {
|
try {
|
||||||
Log.d(Config.LOGTAG, "unable to create jabber id contact");
|
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, null);
|
||||||
}
|
} catch (Exception e) {
|
||||||
}
|
|
||||||
return contacts;
|
|
||||||
} catch (final Exception e) {
|
|
||||||
Log.d(Config.LOGTAG, "unable to query", e);
|
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
|
final HashMap<Jid, JabberIdContact> contacts = new HashMap<>();
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
try {
|
||||||
|
final JabberIdContact contact = new JabberIdContact(cursor);
|
||||||
|
final JabberIdContact preexisting = contacts.put(contact.getJid(), contact);
|
||||||
|
if (preexisting == null || preexisting.rating() < contact.rating()) {
|
||||||
|
contacts.put(contact.getJid(), contact);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.d(Config.LOGTAG,"unable to create jabber id contact");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return contacts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
package eu.siacs.conversations.crypto;
|
package eu.siacs.conversations.crypto;
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
|
||||||
import javax.net.ssl.SSLSession;
|
import javax.net.ssl.SSLSession;
|
||||||
|
|
||||||
public interface DomainHostnameVerifier extends HostnameVerifier {
|
public interface DomainHostnameVerifier extends HostnameVerifier {
|
||||||
|
|
||||||
boolean verify(String domain, String hostname, SSLSession sslSession) throws SSLPeerUnverifiedException;
|
boolean verify(String domain, String hostname, SSLSession sslSession);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ package eu.siacs.conversations.crypto;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1Primitive;
|
import org.bouncycastle.asn1.ASN1Primitive;
|
||||||
import org.bouncycastle.asn1.DERIA5String;
|
import org.bouncycastle.asn1.DERIA5String;
|
||||||
import org.bouncycastle.asn1.DERTaggedObject;
|
import org.bouncycastle.asn1.DERTaggedObject;
|
||||||
|
@ -20,17 +18,15 @@ import java.io.IOException;
|
||||||
import java.net.IDN;
|
import java.net.IDN;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.CertificateParsingException;
|
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
|
||||||
import javax.net.ssl.SSLSession;
|
import javax.net.ssl.SSLSession;
|
||||||
|
|
||||||
public class XmppDomainVerifier {
|
public class XmppDomainVerifier implements DomainHostnameVerifier {
|
||||||
|
|
||||||
private static final String LOGTAG = "XmppDomainVerifier";
|
private static final String LOGTAG = "XmppDomainVerifier";
|
||||||
|
|
||||||
|
@ -98,90 +94,65 @@ public class XmppDomainVerifier {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean verify(final String unicodeDomain, final String unicodeHostname, SSLSession sslSession) throws SSLPeerUnverifiedException {
|
@Override
|
||||||
|
public boolean verify(final String unicodeDomain,final String unicodeHostname, SSLSession sslSession) {
|
||||||
final String domain = IDN.toASCII(unicodeDomain);
|
final String domain = IDN.toASCII(unicodeDomain);
|
||||||
final String hostname = unicodeHostname == null ? null : IDN.toASCII(unicodeHostname);
|
final String hostname = unicodeHostname == null ? null : IDN.toASCII(unicodeHostname);
|
||||||
final Certificate[] chain = sslSession.getPeerCertificates();
|
|
||||||
if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final X509Certificate certificate = (X509Certificate) chain[0];
|
|
||||||
final List<String> commonNames = getCommonNames(certificate);
|
|
||||||
if (isSelfSigned(certificate)) {
|
|
||||||
if (commonNames.size() == 1 && matchDomain(domain, commonNames)) {
|
|
||||||
Log.d(LOGTAG, "accepted CN in self signed cert as work around for " + domain);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
final ValidDomains validDomains = parseValidDomains(certificate);
|
final Certificate[] chain = sslSession.getPeerCertificates();
|
||||||
Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + validDomains.srvNames + " xmppAddrs: " + validDomains.xmppAddrs + " domains:" + validDomains.domains);
|
if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
|
||||||
if (hostname != null) {
|
return false;
|
||||||
Log.d(LOGTAG, "also trying to verify hostname " + hostname);
|
|
||||||
}
|
}
|
||||||
return validDomains.xmppAddrs.contains(domain)
|
final X509Certificate certificate = (X509Certificate) chain[0];
|
||||||
|| validDomains.srvNames.contains("_xmpp-client." + domain)
|
final List<String> commonNames = getCommonNames(certificate);
|
||||||
|| matchDomain(domain, validDomains.domains)
|
if (isSelfSigned(certificate)) {
|
||||||
|| (hostname != null && matchDomain(hostname, validDomains.domains));
|
if (commonNames.size() == 1 && matchDomain(domain, commonNames)) {
|
||||||
} catch (final Exception e) {
|
Log.d(LOGTAG, "accepted CN in self signed cert as work around for " + domain);
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
final Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
|
||||||
public static ValidDomains parseValidDomains(final X509Certificate certificate) throws CertificateParsingException {
|
final List<String> xmppAddrs = new ArrayList<>();
|
||||||
final List<String> commonNames = getCommonNames(certificate);
|
final List<String> srvNames = new ArrayList<>();
|
||||||
final Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
|
final List<String> domains = new ArrayList<>();
|
||||||
final List<String> xmppAddrs = new ArrayList<>();
|
if (alternativeNames != null) {
|
||||||
final List<String> srvNames = new ArrayList<>();
|
for (List<?> san : alternativeNames) {
|
||||||
final List<String> domains = new ArrayList<>();
|
final Integer type = (Integer) san.get(0);
|
||||||
if (alternativeNames != null) {
|
if (type == 0) {
|
||||||
for (List<?> san : alternativeNames) {
|
final Pair<String, String> otherName = parseOtherName((byte[]) san.get(1));
|
||||||
final Integer type = (Integer) san.get(0);
|
if (otherName != null && otherName.first != null && otherName.second != null) {
|
||||||
if (type == 0) {
|
switch (otherName.first) {
|
||||||
final Pair<String, String> otherName = parseOtherName((byte[]) san.get(1));
|
case SRV_NAME:
|
||||||
if (otherName != null && otherName.first != null && otherName.second != null) {
|
srvNames.add(otherName.second.toLowerCase(Locale.US));
|
||||||
switch (otherName.first) {
|
break;
|
||||||
case SRV_NAME:
|
case XMPP_ADDR:
|
||||||
srvNames.add(otherName.second.toLowerCase(Locale.US));
|
xmppAddrs.add(otherName.second.toLowerCase(Locale.US));
|
||||||
break;
|
break;
|
||||||
case XMPP_ADDR:
|
default:
|
||||||
xmppAddrs.add(otherName.second.toLowerCase(Locale.US));
|
Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
|
||||||
break;
|
}
|
||||||
default:
|
}
|
||||||
Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
|
} else if (type == 2) {
|
||||||
|
final Object value = san.get(1);
|
||||||
|
if (value instanceof String) {
|
||||||
|
domains.add(((String) value).toLowerCase(Locale.US));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if (type == 2) {
|
|
||||||
final Object value = san.get(1);
|
|
||||||
if (value instanceof String) {
|
|
||||||
domains.add(((String) value).toLowerCase(Locale.US));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) {
|
||||||
if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) {
|
domains.addAll(commonNames);
|
||||||
domains.addAll(commonNames);
|
}
|
||||||
}
|
Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + srvNames + " xmppAddrs: " + xmppAddrs + " domains:" + domains);
|
||||||
return new ValidDomains(xmppAddrs, srvNames, domains);
|
if (hostname != null) {
|
||||||
}
|
Log.d(LOGTAG, "also trying to verify hostname " + hostname);
|
||||||
|
}
|
||||||
public static final class ValidDomains {
|
return xmppAddrs.contains(domain)
|
||||||
final List<String> xmppAddrs;
|
|| srvNames.contains("_xmpp-client." + domain)
|
||||||
final List<String> srvNames;
|
|| matchDomain(domain, domains)
|
||||||
final List<String> domains;
|
|| (hostname != null && matchDomain(hostname, domains));
|
||||||
|
} catch (final Exception e) {
|
||||||
private ValidDomains(List<String> xmppAddrs, List<String> srvNames, List<String> domains) {
|
return false;
|
||||||
this.xmppAddrs = xmppAddrs;
|
|
||||||
this.srvNames = srvNames;
|
|
||||||
this.domains = domains;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> all() {
|
|
||||||
ImmutableList.Builder<String> all = new ImmutableList.Builder<>();
|
|
||||||
all.addAll(xmppAddrs);
|
|
||||||
all.addAll(srvNames);
|
|
||||||
all.addAll(domains);
|
|
||||||
return all.build();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,4 +164,9 @@ public class XmppDomainVerifier {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verify(String domain, SSLSession sslSession) {
|
||||||
|
return verify(domain, null, sslSession);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,7 @@ import android.util.Pair;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.util.concurrent.Futures;
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
|
||||||
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.whispersystems.libsignal.IdentityKey;
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
@ -738,62 +733,58 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
axolotlStore.setFingerprintStatus(fingerprint, status);
|
axolotlStore.setFingerprintStatus(fingerprint, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListenableFuture<XmppAxolotlSession> verifySessionWithPEP(final XmppAxolotlSession session) {
|
private void verifySessionWithPEP(final XmppAxolotlSession session) {
|
||||||
Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep");
|
Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep");
|
||||||
final SignalProtocolAddress address = session.getRemoteAddress();
|
final SignalProtocolAddress address = session.getRemoteAddress();
|
||||||
final IdentityKey identityKey = session.getIdentityKey();
|
final IdentityKey identityKey = session.getIdentityKey();
|
||||||
final Jid jid;
|
|
||||||
try {
|
try {
|
||||||
jid = Jid.of(address.getName());
|
IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.of(address.getName()), address.getDeviceId());
|
||||||
} catch (final IllegalArgumentException e) {
|
mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
|
||||||
fetchStatusMap.put(address, FetchStatus.SUCCESS);
|
@Override
|
||||||
finishBuildingSessionsFromPEP(address);
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
return Futures.immediateFuture(session);
|
Pair<X509Certificate[], byte[]> verification = mXmppConnectionService.getIqParser().verification(packet);
|
||||||
}
|
if (verification != null) {
|
||||||
final SettableFuture<XmppAxolotlSession> future = SettableFuture.create();
|
|
||||||
final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId());
|
|
||||||
mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> {
|
|
||||||
Pair<X509Certificate[], byte[]> verification = mXmppConnectionService.getIqParser().verification(response);
|
|
||||||
if (verification != null) {
|
|
||||||
try {
|
|
||||||
Signature verifier = Signature.getInstance("sha256WithRSA");
|
|
||||||
verifier.initVerify(verification.first[0]);
|
|
||||||
verifier.update(identityKey.serialize());
|
|
||||||
if (verifier.verify(verification.second)) {
|
|
||||||
try {
|
try {
|
||||||
mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
|
Signature verifier = Signature.getInstance("sha256WithRSA");
|
||||||
String fingerprint = session.getFingerprint();
|
verifier.initVerify(verification.first[0]);
|
||||||
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint);
|
verifier.update(identityKey.serialize());
|
||||||
setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
|
if (verifier.verify(verification.second)) {
|
||||||
axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
|
try {
|
||||||
fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
|
mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
|
||||||
Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
|
String fingerprint = session.getFingerprint();
|
||||||
try {
|
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint);
|
||||||
final String cn = information.getString("subject_cn");
|
setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
|
||||||
final Jid jid1 = Jid.of(address.getName());
|
axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
|
||||||
Log.d(Config.LOGTAG, "setting common name for " + jid1 + " to " + cn);
|
fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
|
||||||
account.getRoster().getContact(jid1).setCommonName(cn);
|
Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
|
||||||
} catch (final IllegalArgumentException ignored) {
|
try {
|
||||||
//ignored
|
final String cn = information.getString("subject_cn");
|
||||||
|
final Jid jid = Jid.of(address.getName());
|
||||||
|
Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn);
|
||||||
|
account.getRoster().getContact(jid).setCommonName(cn);
|
||||||
|
} catch (final IllegalArgumentException ignored) {
|
||||||
|
//ignored
|
||||||
|
}
|
||||||
|
finishBuildingSessionsFromPEP(address);
|
||||||
|
return;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d(Config.LOGTAG, "could not verify certificate");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finishBuildingSessionsFromPEP(address);
|
|
||||||
future.set(session);
|
|
||||||
return;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.d(Config.LOGTAG, "could not verify certificate");
|
Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, "no verification found");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
fetchStatusMap.put(address, FetchStatus.SUCCESS);
|
||||||
Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
|
finishBuildingSessionsFromPEP(address);
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
Log.d(Config.LOGTAG, "no verification found");
|
} catch (IllegalArgumentException e) {
|
||||||
}
|
|
||||||
fetchStatusMap.put(address, FetchStatus.SUCCESS);
|
fetchStatusMap.put(address, FetchStatus.SUCCESS);
|
||||||
finishBuildingSessionsFromPEP(address);
|
finishBuildingSessionsFromPEP(address);
|
||||||
future.set(session);
|
}
|
||||||
});
|
|
||||||
return future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) {
|
private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) {
|
||||||
|
@ -909,23 +900,22 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListenableFuture<XmppAxolotlSession> buildSessionFromPEP(final SignalProtocolAddress address) {
|
private void buildSessionFromPEP(final SignalProtocolAddress address) {
|
||||||
return buildSessionFromPEP(address, null);
|
buildSessionFromPEP(address, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListenableFuture<XmppAxolotlSession> buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) {
|
private void buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) {
|
||||||
final SettableFuture<XmppAxolotlSession> sessionSettableFuture = SettableFuture.create();
|
|
||||||
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString());
|
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString());
|
||||||
if (address.equals(getOwnAxolotlAddress())) {
|
if (address.equals(getOwnAxolotlAddress())) {
|
||||||
throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
|
throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Jid jid = Jid.of(address.getName());
|
final Jid jid = Jid.of(address.getName());
|
||||||
final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid());
|
final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid());
|
||||||
IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId());
|
IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId());
|
||||||
mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> {
|
mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> {
|
||||||
if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
|
if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
|
||||||
fetchStatusMap.put(address, FetchStatus.TIMEOUT);
|
fetchStatusMap.put(address, FetchStatus.TIMEOUT);
|
||||||
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout"));
|
|
||||||
} else if (packet.getType() == IqPacket.TYPE.RESULT) {
|
} else if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
|
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
|
||||||
final IqParser parser = mXmppConnectionService.getIqParser();
|
final IqParser parser = mXmppConnectionService.getIqParser();
|
||||||
|
@ -938,7 +928,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onSessionBuildFailed();
|
callback.onSessionBuildFailed();
|
||||||
}
|
}
|
||||||
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Invalid"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
|
@ -950,7 +939,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onSessionBuildFailed();
|
callback.onSessionBuildFailed();
|
||||||
}
|
}
|
||||||
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. No suitable PreKey found"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -965,7 +953,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey());
|
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey());
|
||||||
sessions.put(address, session);
|
sessions.put(address, session);
|
||||||
if (Config.X509_VERIFICATION) {
|
if (Config.X509_VERIFICATION) {
|
||||||
sessionSettableFuture.setFuture(verifySessionWithPEP(session)); //TODO; maybe inject callback in here too
|
verifySessionWithPEP(session); //TODO; maybe inject callback in here too
|
||||||
} else {
|
} else {
|
||||||
FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize()));
|
FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize()));
|
||||||
FetchStatus fetchStatus;
|
FetchStatus fetchStatus;
|
||||||
|
@ -981,7 +969,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onSessionBuildSuccessful();
|
callback.onSessionBuildSuccessful();
|
||||||
}
|
}
|
||||||
sessionSettableFuture.set(session);
|
|
||||||
}
|
}
|
||||||
} catch (UntrustedIdentityException | InvalidKeyException e) {
|
} catch (UntrustedIdentityException | InvalidKeyException e) {
|
||||||
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
|
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
|
||||||
|
@ -994,7 +981,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onSessionBuildFailed();
|
callback.onSessionBuildFailed();
|
||||||
}
|
}
|
||||||
sessionSettableFuture.setException(new CryptoFailedException(e));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fetchStatusMap.put(address, FetchStatus.ERROR);
|
fetchStatusMap.put(address, FetchStatus.ERROR);
|
||||||
|
@ -1008,10 +994,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onSessionBuildFailed();
|
callback.onSessionBuildFailed();
|
||||||
}
|
}
|
||||||
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Error"));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return sessionSettableFuture;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeFromDeviceAnnouncement(Integer id) {
|
private void removeFromDeviceAnnouncement(Integer id) {
|
||||||
|
@ -1233,7 +1217,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||||
final String content = child.getContent();
|
final String content = child.getContent();
|
||||||
axolotlMessage.encrypt(content);
|
axolotlMessage.encrypt(content);
|
||||||
axolotlMessage.addDevice(session, true);
|
axolotlMessage.addDevice(session);
|
||||||
fingerprint.addChild(axolotlMessage.toElement());
|
fingerprint.addChild(axolotlMessage.toElement());
|
||||||
transportInfo.addChild(fingerprint);
|
transportInfo.addChild(fingerprint);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1244,63 +1228,36 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public ListenableFuture<OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) {
|
public OmemoVerifiedPayload<OmemoVerifiedRtpContentMap> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) throws CryptoFailedException {
|
||||||
return Futures.transformAsync(
|
final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
|
||||||
getSession(jid, deviceId),
|
final XmppAxolotlSession session = sessions.get(address);
|
||||||
session -> encrypt(rtpContentMap, session),
|
if (session == null) {
|
||||||
MoreExecutors.directExecutor()
|
throw new CryptoFailedException(String.format("No session found for %d", deviceId));
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ListenableFuture<OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) {
|
|
||||||
if (Config.REQUIRE_RTP_VERIFICATION) {
|
|
||||||
requireVerification(session);
|
|
||||||
}
|
}
|
||||||
final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
|
final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
|
||||||
final OmemoVerification omemoVerification = new OmemoVerification();
|
final OmemoVerification omemoVerification = new OmemoVerification();
|
||||||
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
|
omemoVerification.setDeviceId(deviceId);
|
||||||
omemoVerification.setSessionFingerprint(session.getFingerprint());
|
omemoVerification.setSessionFingerprint(session.getFingerprint());
|
||||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : rtpContentMap.contents.entrySet()) {
|
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : rtpContentMap.contents.entrySet()) {
|
||||||
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
|
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
|
||||||
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
|
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
|
||||||
try {
|
|
||||||
encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
|
|
||||||
} catch (final CryptoFailedException e) {
|
|
||||||
return Futures.immediateFailedFuture(e);
|
|
||||||
}
|
|
||||||
descriptionTransportBuilder.put(
|
descriptionTransportBuilder.put(
|
||||||
content.getKey(),
|
content.getKey(),
|
||||||
new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo)
|
new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Futures.immediateFuture(
|
return new OmemoVerifiedPayload<>(
|
||||||
new OmemoVerifiedPayload<>(
|
omemoVerification,
|
||||||
omemoVerification,
|
new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build())
|
||||||
new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build())
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListenableFuture<XmppAxolotlSession> getSession(final Jid jid, final int deviceId) {
|
public OmemoVerifiedPayload<RtpContentMap> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) throws CryptoFailedException {
|
||||||
final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
|
|
||||||
final XmppAxolotlSession session = sessions.get(address);
|
|
||||||
if (session == null) {
|
|
||||||
return buildSessionFromPEP(address);
|
|
||||||
}
|
|
||||||
return Futures.immediateFuture(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListenableFuture<OmemoVerifiedPayload<RtpContentMap>> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) {
|
|
||||||
final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
|
final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
|
||||||
final OmemoVerification omemoVerification = new OmemoVerification();
|
final OmemoVerification omemoVerification = new OmemoVerification();
|
||||||
final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>();
|
|
||||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
|
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
|
||||||
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
|
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
|
||||||
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport;
|
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from);
|
||||||
try {
|
|
||||||
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
|
|
||||||
} catch (CryptoFailedException e) {
|
|
||||||
return Futures.immediateFailedFuture(e);
|
|
||||||
}
|
|
||||||
omemoVerification.setOrEnsureEqual(decryptedTransport);
|
omemoVerification.setOrEnsureEqual(decryptedTransport);
|
||||||
descriptionTransportBuilder.put(
|
descriptionTransportBuilder.put(
|
||||||
content.getKey(),
|
content.getKey(),
|
||||||
|
@ -1308,26 +1265,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
processPostponed();
|
processPostponed();
|
||||||
final ImmutableList<ListenableFuture<XmppAxolotlSession>> sessionFutures = pepVerificationFutures.build();
|
return new OmemoVerifiedPayload<>(
|
||||||
return Futures.transform(
|
omemoVerification,
|
||||||
Futures.allAsList(sessionFutures),
|
new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build())
|
||||||
sessions -> {
|
|
||||||
if (Config.REQUIRE_RTP_VERIFICATION) {
|
|
||||||
for (XmppAxolotlSession session : sessions) {
|
|
||||||
requireVerification(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new OmemoVerifiedPayload<>(
|
|
||||||
omemoVerification,
|
|
||||||
new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build())
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
MoreExecutors.directExecutor()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private OmemoVerifiedPayload<IceUdpTransportInfo> decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures) throws CryptoFailedException {
|
private OmemoVerifiedPayload<IceUdpTransportInfo> decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from) throws CryptoFailedException {
|
||||||
final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
|
final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
|
||||||
transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes());
|
transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes());
|
||||||
final OmemoVerification omemoVerification = new OmemoVerification();
|
final OmemoVerification omemoVerification = new OmemoVerification();
|
||||||
|
@ -1344,11 +1288,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
if (preKeyId != null) {
|
if (preKeyId != null) {
|
||||||
postponedSessions.add(session);
|
postponedSessions.add(session);
|
||||||
}
|
}
|
||||||
if (session.isFresh()) {
|
|
||||||
pepVerificationFutures.add(putFreshSession(session));
|
|
||||||
} else if (Config.REQUIRE_RTP_VERIFICATION) {
|
|
||||||
pepVerificationFutures.add(Futures.immediateFuture(session));
|
|
||||||
}
|
|
||||||
fingerprint.setContent(plaintext.getPlaintext());
|
fingerprint.setContent(plaintext.getPlaintext());
|
||||||
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
|
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
|
||||||
omemoVerification.setSessionFingerprint(plaintext.getFingerprint());
|
omemoVerification.setSessionFingerprint(plaintext.getFingerprint());
|
||||||
|
@ -1360,16 +1299,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
return new OmemoVerifiedPayload<>(omemoVerification, transportInfo);
|
return new OmemoVerifiedPayload<>(omemoVerification, transportInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void requireVerification(final XmppAxolotlSession session) {
|
|
||||||
if (session.getTrust().isVerified()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new NotVerifiedException(String.format(
|
|
||||||
"session with %s was not verified",
|
|
||||||
session.getFingerprint()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
|
public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
|
||||||
executor.execute(new Runnable() {
|
executor.execute(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -1567,16 +1496,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
return keyTransportMessage;
|
return keyTransportMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListenableFuture<XmppAxolotlSession> putFreshSession(XmppAxolotlSession session) {
|
private void putFreshSession(XmppAxolotlSession session) {
|
||||||
sessions.put(session);
|
sessions.put(session);
|
||||||
if (Config.X509_VERIFICATION) {
|
if (Config.X509_VERIFICATION) {
|
||||||
if (session.getIdentityKey() != null) {
|
if (session.getIdentityKey() != null) {
|
||||||
return verifySessionWithPEP(session);
|
verifySessionWithPEP(session);
|
||||||
} else {
|
} else {
|
||||||
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification");
|
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Futures.immediateFuture(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FetchStatus {
|
public enum FetchStatus {
|
||||||
|
@ -1762,12 +1690,4 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class NotVerifiedException extends SecurityException {
|
|
||||||
|
|
||||||
public NotVerifiedException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import android.database.Cursor;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
@ -249,7 +247,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getHostname() {
|
public String getHostname() {
|
||||||
return Strings.nullToEmpty(this.hostname);
|
return this.hostname == null ? "" : this.hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHostname(String hostname) {
|
public void setHostname(String hostname) {
|
||||||
|
@ -637,7 +635,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
REGISTRATION_INVALID_TOKEN(true,false),
|
REGISTRATION_INVALID_TOKEN(true,false),
|
||||||
REGISTRATION_PASSWORD_TOO_WEAK(true, false),
|
REGISTRATION_PASSWORD_TOO_WEAK(true, false),
|
||||||
TLS_ERROR,
|
TLS_ERROR,
|
||||||
TLS_ERROR_DOMAIN,
|
|
||||||
INCOMPATIBLE_SERVER,
|
INCOMPATIBLE_SERVER,
|
||||||
TOR_NOT_AVAILABLE,
|
TOR_NOT_AVAILABLE,
|
||||||
DOWNGRADE_ATTACK,
|
DOWNGRADE_ATTACK,
|
||||||
|
@ -704,8 +701,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
return R.string.account_status_regis_invalid_token;
|
return R.string.account_status_regis_invalid_token;
|
||||||
case TLS_ERROR:
|
case TLS_ERROR:
|
||||||
return R.string.account_status_tls_error;
|
return R.string.account_status_tls_error;
|
||||||
case TLS_ERROR_DOMAIN:
|
|
||||||
return R.string.account_status_tls_error_domain;
|
|
||||||
case INCOMPATIBLE_SERVER:
|
case INCOMPATIBLE_SERVER:
|
||||||
return R.string.account_status_incompatible_server;
|
return R.string.account_status_incompatible_server;
|
||||||
case TOR_NOT_AVAILABLE:
|
case TOR_NOT_AVAILABLE:
|
||||||
|
|
|
@ -28,7 +28,6 @@ import eu.siacs.conversations.persistance.DatabaseBackend;
|
||||||
import eu.siacs.conversations.services.AvatarService;
|
import eu.siacs.conversations.services.AvatarService;
|
||||||
import eu.siacs.conversations.services.QuickConversationsService;
|
import eu.siacs.conversations.services.QuickConversationsService;
|
||||||
import eu.siacs.conversations.utils.JidHelper;
|
import eu.siacs.conversations.utils.JidHelper;
|
||||||
import eu.siacs.conversations.utils.MessageUtils;
|
|
||||||
import eu.siacs.conversations.utils.UIHelper;
|
import eu.siacs.conversations.utils.UIHelper;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
|
@ -259,22 +258,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
public Message findMessageWithFileAndUuid(final String uuid) {
|
public Message findMessageWithFileAndUuid(final String uuid) {
|
||||||
synchronized (this.messages) {
|
synchronized (this.messages) {
|
||||||
for (final Message message : this.messages) {
|
for (final Message message : this.messages) {
|
||||||
final Transferable transferable = message.getTransferable();
|
|
||||||
final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
|
|
||||||
if (message.getUuid().equals(uuid)
|
if (message.getUuid().equals(uuid)
|
||||||
&& message.getEncryption() != Message.ENCRYPTION_PGP
|
&& message.getEncryption() != Message.ENCRYPTION_PGP
|
||||||
&& (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
|
&& (message.isFileOrImage() || message.treatAsDownloadable())) {
|
||||||
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;
|
return message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.primitives.Longs;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
@ -850,10 +849,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
fileParams.height = parseInt(parts[3]);
|
fileParams.height = parseInt(parts[3]);
|
||||||
case 2:
|
case 2:
|
||||||
fileParams.url = URL.tryParse(parts[0]);
|
fileParams.url = URL.tryParse(parts[0]);
|
||||||
fileParams.size = Longs.tryParse(parts[1]);
|
fileParams.size = parseLong(parts[1]);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
fileParams.size = Longs.tryParse(parts[0]);
|
fileParams.size = parseLong(parts[0]);
|
||||||
fileParams.width = parseInt(parts[1]);
|
fileParams.width = parseInt(parts[1]);
|
||||||
fileParams.height = parseInt(parts[2]);
|
fileParams.height = parseInt(parts[2]);
|
||||||
break;
|
break;
|
||||||
|
@ -862,6 +861,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
return fileParams;
|
return fileParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long parseLong(String value) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int parseInt(String value) {
|
private static int parseInt(String value) {
|
||||||
try {
|
try {
|
||||||
return Integer.parseInt(value);
|
return Integer.parseInt(value);
|
||||||
|
@ -898,14 +905,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
|
|
||||||
public static class FileParams {
|
public static class FileParams {
|
||||||
public String url;
|
public String url;
|
||||||
public Long size = null;
|
public long size = 0;
|
||||||
public int width = 0;
|
public int width = 0;
|
||||||
public int height = 0;
|
public int height = 0;
|
||||||
public int runtime = 0;
|
public int runtime = 0;
|
||||||
|
|
||||||
public long getSize() {
|
|
||||||
return size == null ? 0 : size;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFingerprint(String fingerprint) {
|
public void setFingerprint(String fingerprint) {
|
||||||
|
@ -984,28 +987,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||||
final Jid nextCounterpart = conversation.getNextCounterpart();
|
final Jid nextCounterpart = conversation.getNextCounterpart();
|
||||||
return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
|
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 false;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ public interface Transferable {
|
||||||
|
|
||||||
int getStatus();
|
int getStatus();
|
||||||
|
|
||||||
Long getFileSize();
|
long getFileSize();
|
||||||
|
|
||||||
int getProgress();
|
int getProgress();
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@ public class TransferablePlaceholder implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getFileSize() {
|
public long getFileSize() {
|
||||||
return null;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -12,7 +12,6 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import eu.siacs.conversations.BuildConfig;
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
|
@ -66,6 +65,7 @@ public abstract class AbstractGenerator {
|
||||||
Namespace.JINGLE_MESSAGE
|
Namespace.JINGLE_MESSAGE
|
||||||
};
|
};
|
||||||
protected XmppConnectionService mXmppConnectionService;
|
protected XmppConnectionService mXmppConnectionService;
|
||||||
|
private String mVersion = null;
|
||||||
|
|
||||||
AbstractGenerator(XmppConnectionService service) {
|
AbstractGenerator(XmppConnectionService service) {
|
||||||
this.mXmppConnectionService = service;
|
this.mXmppConnectionService = service;
|
||||||
|
@ -77,11 +77,18 @@ public abstract class AbstractGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
String getIdentityVersion() {
|
String getIdentityVersion() {
|
||||||
return BuildConfig.VERSION_NAME;
|
if (mVersion == null) {
|
||||||
|
this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService);
|
||||||
|
}
|
||||||
|
return this.mVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getIdentityName() {
|
String getIdentityName() {
|
||||||
return BuildConfig.APP_NAME;
|
return mXmppConnectionService.getString(R.string.app_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserAgent() {
|
||||||
|
return mXmppConnectionService.getString(R.string.app_name) + '/' + getIdentityVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
String getIdentityType() {
|
String getIdentityType() {
|
||||||
|
|
|
@ -25,19 +25,12 @@ public class PresenceGenerator extends AbstractGenerator {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PresencePacket requestPresenceUpdatesFrom(final Contact contact) {
|
public PresencePacket requestPresenceUpdatesFrom(Contact contact) {
|
||||||
return requestPresenceUpdatesFrom(contact, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PresencePacket requestPresenceUpdatesFrom(final Contact contact, final String preAuth) {
|
|
||||||
PresencePacket packet = subscription("subscribe", contact);
|
PresencePacket packet = subscription("subscribe", contact);
|
||||||
String displayName = contact.getAccount().getDisplayName();
|
String displayName = contact.getAccount().getDisplayName();
|
||||||
if (!TextUtils.isEmpty(displayName)) {
|
if (!TextUtils.isEmpty(displayName)) {
|
||||||
packet.addChild("nick", Namespace.NICK).setContent(displayName);
|
packet.addChild("nick", Namespace.NICK).setContent(displayName);
|
||||||
}
|
}
|
||||||
if (preAuth != null) {
|
|
||||||
packet.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
|
|
||||||
}
|
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,10 @@ import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
import eu.siacs.conversations.BuildConfig;
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
|
@ -41,24 +41,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
|
|
||||||
public static final Executor EXECUTOR = Executors.newFixedThreadPool(4);
|
public static final Executor EXECUTOR = Executors.newFixedThreadPool(4);
|
||||||
|
|
||||||
public static final OkHttpClient OK_HTTP_CLIENT;
|
private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient();
|
||||||
|
|
||||||
static {
|
|
||||||
OK_HTTP_CLIENT = new OkHttpClient.Builder()
|
|
||||||
.addInterceptor(chain -> {
|
|
||||||
final Request original = chain.request();
|
|
||||||
final Request modified = original.newBuilder()
|
|
||||||
.header("User-Agent", getUserAgent())
|
|
||||||
.build();
|
|
||||||
return chain.proceed(modified);
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static String getUserAgent() {
|
|
||||||
return String.format("%s/%s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpConnectionManager(XmppConnectionService service) {
|
public HttpConnectionManager(XmppConnectionService service) {
|
||||||
super(service);
|
super(service);
|
||||||
|
@ -141,6 +124,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
|
|
||||||
private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) {
|
private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) {
|
||||||
final X509TrustManager trustManager;
|
final X509TrustManager trustManager;
|
||||||
|
final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
|
trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
|
||||||
} else {
|
} else {
|
||||||
|
@ -149,7 +133,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
try {
|
try {
|
||||||
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
||||||
builder.sslSocketFactory(sf, trustManager);
|
builder.sslSocketFactory(sf, trustManager);
|
||||||
builder.hostnameVerifier(new StrictHostnameVerifier());
|
builder.hostnameVerifier(hostnameVerifier);
|
||||||
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
|
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
final Message.FileParams fileParams = message.getFileParams();
|
final Message.FileParams fileParams = message.getFileParams();
|
||||||
if (message.hasFileOnRemoteHost()) {
|
if (message.hasFileOnRemoteHost()) {
|
||||||
mUrl = AesGcmURL.of(fileParams.url);
|
mUrl = AesGcmURL.of(fileParams.url);
|
||||||
} else if (message.isOOb() && fileParams.url != null && fileParams.size != null) {
|
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
|
||||||
mUrl = AesGcmURL.of(fileParams.url);
|
mUrl = AesGcmURL.of(fileParams.url);
|
||||||
} else {
|
} else {
|
||||||
mUrl = AesGcmURL.of(message.getBody().split("\n")[0]);
|
mUrl = AesGcmURL.of(message.getBody().split("\n")[0]);
|
||||||
|
@ -106,9 +106,8 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
||||||
}
|
}
|
||||||
//TODO add auth tag size to knownFileSize
|
//TODO add auth tag size to knownFileSize
|
||||||
final Long knownFileSize = message.getFileParams().size;
|
final long knownFileSize = message.getFileParams().size;
|
||||||
Log.d(Config.LOGTAG,"knownFileSize: "+knownFileSize+", body="+message.getBody());
|
if (knownFileSize > 0 && interactive) {
|
||||||
if (knownFileSize != null && interactive) {
|
|
||||||
this.file.setExpectedSize(knownFileSize);
|
this.file.setExpectedSize(knownFileSize);
|
||||||
download(true);
|
download(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -235,11 +234,11 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getFileSize() {
|
public long getFileSize() {
|
||||||
if (this.file != null) {
|
if (this.file != null) {
|
||||||
return this.file.getExpectedSize();
|
return this.file.getExpectedSize();
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,7 +318,6 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
mostRecentCall = client.newCall(request);
|
mostRecentCall = client.newCall(request);
|
||||||
try {
|
try {
|
||||||
final Response response = mostRecentCall.execute();
|
final Response response = mostRecentCall.execute();
|
||||||
throwOnInvalidCode(response);
|
|
||||||
final String contentLength = response.header("Content-Length");
|
final String contentLength = response.header("Content-Length");
|
||||||
final String contentType = response.header("Content-Type");
|
final String contentType = response.header("Content-Type");
|
||||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
|
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
|
||||||
|
@ -334,11 +332,7 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
if (Strings.isNullOrEmpty(contentLength)) {
|
if (Strings.isNullOrEmpty(contentLength)) {
|
||||||
throw new IOException("no content-length found in HEAD response");
|
throw new IOException("no content-length found in HEAD response");
|
||||||
}
|
}
|
||||||
final long size = Long.parseLong(contentLength, 10);
|
return Long.parseLong(contentLength, 10);
|
||||||
if (size < 0) {
|
|
||||||
throw new IOException("Server reported negative file size");
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage());
|
Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage());
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -401,41 +395,45 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
final Request request = requestBuilder.build();
|
final Request request = requestBuilder.build();
|
||||||
mostRecentCall = client.newCall(request);
|
mostRecentCall = client.newCall(request);
|
||||||
final Response response = mostRecentCall.execute();
|
final Response response = mostRecentCall.execute();
|
||||||
throwOnInvalidCode(response);
|
final int code = response.code();
|
||||||
final String contentRange = response.header("Content-Range");
|
if (code >= 200 && code <= 299) {
|
||||||
final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
|
final String contentRange = response.header("Content-Range");
|
||||||
final InputStream inputStream = response.body().byteStream();
|
final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
|
||||||
final OutputStream outputStream;
|
final InputStream inputStream = response.body().byteStream();
|
||||||
long transmitted = 0;
|
final OutputStream outputStream;
|
||||||
if (tryResume && serverResumed) {
|
long transmitted = 0;
|
||||||
Log.d(Config.LOGTAG, "server resumed");
|
if (tryResume && serverResumed) {
|
||||||
transmitted = file.getSize();
|
Log.d(Config.LOGTAG, "server resumed");
|
||||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
transmitted = file.getSize();
|
||||||
outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
|
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||||
|
outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
|
||||||
|
} else {
|
||||||
|
final String contentLength = response.header("Content-Length");
|
||||||
|
final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
|
||||||
|
if (expected != size) {
|
||||||
|
Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
||||||
|
}
|
||||||
|
file.getParentFile().mkdirs();
|
||||||
|
if (!file.exists() && !file.createNewFile()) {
|
||||||
|
throw new FileWriterException();
|
||||||
|
}
|
||||||
|
outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
|
||||||
|
}
|
||||||
|
int count;
|
||||||
|
final byte[] buffer = new byte[4096];
|
||||||
|
while ((count = inputStream.read(buffer)) != -1) {
|
||||||
|
transmitted += count;
|
||||||
|
try {
|
||||||
|
outputStream.write(buffer, 0, count);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileWriterException();
|
||||||
|
}
|
||||||
|
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||||
|
}
|
||||||
|
outputStream.flush();
|
||||||
} else {
|
} else {
|
||||||
final String contentLength = response.header("Content-Length");
|
throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
|
||||||
final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
|
|
||||||
if (expected != size) {
|
|
||||||
Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
|
|
||||||
}
|
|
||||||
file.getParentFile().mkdirs();
|
|
||||||
if (!file.exists() && !file.createNewFile()) {
|
|
||||||
throw new FileWriterException();
|
|
||||||
}
|
|
||||||
outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
|
|
||||||
}
|
}
|
||||||
int count;
|
|
||||||
final byte[] buffer = new byte[4096];
|
|
||||||
while ((count = inputStream.read(buffer)) != -1) {
|
|
||||||
transmitted += count;
|
|
||||||
try {
|
|
||||||
outputStream.write(buffer, 0, count);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FileWriterException();
|
|
||||||
}
|
|
||||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
|
||||||
}
|
|
||||||
outputStream.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateImageBounds() {
|
private void updateImageBounds() {
|
||||||
|
@ -453,11 +451,4 @@ public class HttpDownloadConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void throwOnInvalidCode(final Response response) throws IOException {
|
|
||||||
final int code = response.code();
|
|
||||||
if (code < 200 || code >= 300) {
|
|
||||||
throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,8 +69,8 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getFileSize() {
|
public long getFileSize() {
|
||||||
return file == null ? null : file.getExpectedSize();
|
return file == null ? 0 : file.getExpectedSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -11,8 +11,6 @@ import android.os.SystemClock;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.base.Stopwatch;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.whispersystems.libsignal.IdentityKey;
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
@ -64,9 +62,7 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
|
||||||
public class DatabaseBackend extends SQLiteOpenHelper {
|
public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
|
|
||||||
private static final String DATABASE_NAME = "history";
|
private static final String DATABASE_NAME = "history";
|
||||||
private static final int DATABASE_VERSION = 50;
|
private static final int DATABASE_VERSION = 49;
|
||||||
|
|
||||||
private static boolean requiresMessageIndexRebuild = false;
|
|
||||||
private static DatabaseBackend instance = null;
|
private static DatabaseBackend instance = null;
|
||||||
private static final String CREATE_CONTATCS_STATEMENT = "create table "
|
private static final String CREATE_CONTATCS_STATEMENT = "create table "
|
||||||
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
|
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
|
||||||
|
@ -169,17 +165,16 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
+ "UNIQUE(" + Resolver.Result.DOMAIN + ") ON CONFLICT REPLACE"
|
+ "UNIQUE(" + Resolver.Result.DOMAIN + ") ON CONFLICT REPLACE"
|
||||||
+ ");";
|
+ ");";
|
||||||
|
|
||||||
private static final String CREATE_MESSAGE_TIME_INDEX = "CREATE INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")";
|
private static final String CREATE_MESSAGE_TIME_INDEX = "create INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")";
|
||||||
private static final String CREATE_MESSAGE_CONVERSATION_INDEX = "CREATE INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")";
|
private static final String CREATE_MESSAGE_CONVERSATION_INDEX = "create INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")";
|
||||||
private static final String CREATE_MESSAGE_DELETED_INDEX = "CREATE INDEX message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")";
|
private static final String CREATE_MESSAGE_DELETED_INDEX = "create index message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")";
|
||||||
private static final String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "CREATE INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")";
|
private static final String CREATE_MESSAGE_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_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,body,notindexed=\"uuid\",content=\"" + Message.TABLENAME + "\",tokenize='unicode61')";
|
private static final String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING FTS4(uuid TEXT PRIMARY KEY, body TEXT)";
|
||||||
private static final String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index(rowid,uuid,body) VALUES(NEW.rowid,NEW.uuid,NEW.body); END;";
|
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 rowid=OLD.rowid; 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 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(uuid,body) select uuid,body FROM " + Message.TABLENAME + ";";
|
||||||
private static final String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(messages_index) VALUES('rebuild');";
|
|
||||||
|
|
||||||
private DatabaseBackend(Context context) {
|
private DatabaseBackend(Context context) {
|
||||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||||
|
@ -192,17 +187,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
return values;
|
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) {
|
public static synchronized DatabaseBackend getInstance(Context context) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new DatabaseBackend(context);
|
instance = new DatabaseBackend(context);
|
||||||
|
@ -279,7 +263,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
|
db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
|
||||||
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
|
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
|
||||||
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
|
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
|
||||||
db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -532,10 +515,16 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0");
|
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldVersion < 39 && newVersion >= 39) {
|
if (oldVersion < 41 && newVersion >= 41) {
|
||||||
db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
|
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) {
|
if (QuickConversationsService.isQuicksy() && oldVersion < 43 && newVersion >= 43) {
|
||||||
List<Account> accounts = getAccounts(db);
|
List<Account> accounts = getAccounts(db);
|
||||||
for (Account account : accounts) {
|
for (Account account : accounts) {
|
||||||
|
@ -559,10 +548,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
if (oldVersion < 46 && newVersion >= 46) {
|
if (oldVersion < 46 && newVersion >= 46) {
|
||||||
final long start = SystemClock.elapsedRealtime();
|
final long start = SystemClock.elapsedRealtime();
|
||||||
db.rawQuery("PRAGMA secure_delete = FALSE", null).close();
|
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();
|
db.rawQuery("PRAGMA secure_delete=ON", null).close();
|
||||||
final long diff = SystemClock.elapsedRealtime() - start;
|
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) {
|
if (oldVersion < 47 && newVersion >= 47) {
|
||||||
|
@ -579,26 +568,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
db.execSQL("IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" + Contact.TABLENAME + "' AND COLUMN_NAME = '" + Contact.RTP_CAPABILITY + "') THEN"
|
db.execSQL("IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" + Contact.TABLENAME + "' AND COLUMN_NAME = '" + Contact.RTP_CAPABILITY + "') THEN"
|
||||||
+ "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT");
|
+ "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT");
|
||||||
}
|
}
|
||||||
if (oldVersion < 50 && newVersion >= 50) {
|
|
||||||
db.beginTransaction();
|
|
||||||
db.execSQL("DROP TRIGGER IF EXISTS after_message_insert;");
|
|
||||||
db.execSQL("DROP TRIGGER IF EXISTS after_message_update;");
|
|
||||||
db.execSQL("DROP TRIGGER IF EXISTS after_message_delete;");
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS messages_index;");
|
|
||||||
// a hack that should not be necessary, but
|
|
||||||
// there was at least one occurence when SQLite failed at this
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS messages_index_docsize;");
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS messages_index_segdir;");
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS messages_index_segments;");
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS messages_index_stat;");
|
|
||||||
db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
|
|
||||||
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
|
|
||||||
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
|
|
||||||
db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
|
|
||||||
db.setTransactionSuccessful();
|
|
||||||
db.endTransaction();
|
|
||||||
requiresMessageIndexRebuild = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void canonicalizeJids(SQLiteDatabase db) {
|
private void canonicalizeJids(SQLiteDatabase db) {
|
||||||
|
@ -813,7 +782,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
try {
|
try {
|
||||||
list.add(0, Message.fromCursor(cursor, conversation));
|
list.add(0, Message.fromCursor(cursor, conversation));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(Config.LOGTAG, "unable to restore message");
|
Log.e(Config.LOGTAG,"unable to restore message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor.close();
|
cursor.close();
|
||||||
|
@ -824,12 +793,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
final SQLiteDatabase db = this.getReadableDatabase();
|
final SQLiteDatabase db = this.getReadableDatabase();
|
||||||
final StringBuilder SQL = new StringBuilder();
|
final StringBuilder SQL = new StringBuilder();
|
||||||
final String[] selectionArgs;
|
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.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 ?");
|
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 ?");
|
||||||
if (uuid == null) {
|
if (uuid == null) {
|
||||||
selectionArgs = new String[]{FtsUtils.toMatchString(term)};
|
selectionArgs = new String[]{FtsUtils.toMatchString(term)};
|
||||||
} else {
|
} else {
|
||||||
selectionArgs = new String[]{FtsUtils.toMatchString(term), uuid};
|
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);
|
SQL.append(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS);
|
||||||
Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term));
|
Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term));
|
||||||
|
@ -893,7 +862,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
|
|
||||||
public List<FilePathInfo> getFilePathInfo() {
|
public List<FilePathInfo> getFilePathInfo() {
|
||||||
final SQLiteDatabase db = this.getReadableDatabase();
|
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<>();
|
final List<FilePathInfo> list = new ArrayList<>();
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
list.add(new FilePathInfo(cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0));
|
list.add(new FilePathInfo(cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0));
|
||||||
|
@ -906,7 +875,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
|
|
||||||
public List<FilePath> getRelativeFilePaths(String account, Jid jid, int limit) {
|
public List<FilePath> getRelativeFilePaths(String account, Jid jid, int limit) {
|
||||||
SQLiteDatabase db = this.getReadableDatabase();
|
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() + "/%"};
|
final String[] args = {account, jid.toString(), jid.toString() + "/%"};
|
||||||
Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + limit : ""), args);
|
Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + limit : ""), args);
|
||||||
List<FilePath> filesPaths = new ArrayList<>();
|
List<FilePath> filesPaths = new ArrayList<>();
|
||||||
|
@ -931,7 +900,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
public boolean deleted;
|
public boolean deleted;
|
||||||
|
|
||||||
private FilePathInfo(String uuid, String path, boolean deleted) {
|
private FilePathInfo(String uuid, String path, boolean deleted) {
|
||||||
super(uuid, path);
|
super(uuid,path);
|
||||||
this.deleted = deleted;
|
this.deleted = deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1075,7 +1044,8 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
long start = SystemClock.elapsedRealtime();
|
long start = SystemClock.elapsedRealtime();
|
||||||
final SQLiteDatabase db = this.getWritableDatabase();
|
final SQLiteDatabase db = this.getWritableDatabase();
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
final String[] args = {conversation.getUuid()};
|
String[] args = {conversation.getUuid()};
|
||||||
|
db.delete("messages_index", "uuid in (select uuid from messages where conversationUuid=?)", args);
|
||||||
int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
|
int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
@ -1086,6 +1056,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
final String[] args = {String.valueOf(timestamp)};
|
final String[] args = {String.valueOf(timestamp)};
|
||||||
SQLiteDatabase db = this.getReadableDatabase();
|
SQLiteDatabase db = this.getReadableDatabase();
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
|
db.delete("messages_index", "uuid in (select uuid from messages where timeSent<?)", args);
|
||||||
db.delete(Message.TABLENAME, "timeSent<?", args);
|
db.delete(Message.TABLENAME, "timeSent<?", args);
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
|
|
@ -31,9 +31,6 @@ import android.util.LruCache;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
import androidx.exifinterface.media.ExifInterface;
|
|
||||||
|
|
||||||
import com.google.common.io.ByteStreams;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
@ -67,6 +64,7 @@ import eu.siacs.conversations.ui.RecordingActivity;
|
||||||
import eu.siacs.conversations.ui.util.Attachment;
|
import eu.siacs.conversations.ui.util.Attachment;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
import eu.siacs.conversations.utils.ExifHelper;
|
||||||
import eu.siacs.conversations.utils.FileUtils;
|
import eu.siacs.conversations.utils.FileUtils;
|
||||||
import eu.siacs.conversations.utils.FileWriterException;
|
import eu.siacs.conversations.utils.FileWriterException;
|
||||||
import eu.siacs.conversations.utils.MimeUtils;
|
import eu.siacs.conversations.utils.MimeUtils;
|
||||||
|
@ -629,20 +627,20 @@ public class FileBackend {
|
||||||
private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
|
private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
|
||||||
Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath());
|
Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath());
|
||||||
file.getParentFile().mkdirs();
|
file.getParentFile().mkdirs();
|
||||||
|
OutputStream os = null;
|
||||||
|
InputStream is = null;
|
||||||
try {
|
try {
|
||||||
file.createNewFile();
|
file.createNewFile();
|
||||||
} catch (IOException e) {
|
os = new FileOutputStream(file);
|
||||||
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
|
is = mXmppConnectionService.getContentResolver().openInputStream(uri);
|
||||||
}
|
byte[] buffer = new byte[1024];
|
||||||
try (final OutputStream os = new FileOutputStream(file);
|
int length;
|
||||||
final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri)) {
|
while ((length = is.read(buffer)) > 0) {
|
||||||
if (is == null) {
|
try {
|
||||||
throw new FileCopyException(R.string.error_file_not_found);
|
os.write(buffer, 0, length);
|
||||||
}
|
} catch (IOException e) {
|
||||||
try {
|
throw new FileWriterException();
|
||||||
ByteStreams.copy(is, os);
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FileWriterException();
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
os.flush();
|
os.flush();
|
||||||
|
@ -650,17 +648,16 @@ public class FileBackend {
|
||||||
throw new FileWriterException();
|
throw new FileWriterException();
|
||||||
}
|
}
|
||||||
} catch (final FileNotFoundException e) {
|
} catch (final FileNotFoundException e) {
|
||||||
cleanup(file);
|
|
||||||
throw new FileCopyException(R.string.error_file_not_found);
|
throw new FileCopyException(R.string.error_file_not_found);
|
||||||
} catch (final FileWriterException e) {
|
} catch (final FileWriterException e) {
|
||||||
cleanup(file);
|
|
||||||
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
|
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
|
||||||
} catch (final SecurityException e) {
|
} catch (final SecurityException e) {
|
||||||
cleanup(file);
|
|
||||||
throw new FileCopyException(R.string.error_security_exception);
|
throw new FileCopyException(R.string.error_security_exception);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
cleanup(file);
|
|
||||||
throw new FileCopyException(R.string.error_io_exception);
|
throw new FileCopyException(R.string.error_io_exception);
|
||||||
|
} finally {
|
||||||
|
close(os);
|
||||||
|
close(is);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -711,7 +708,7 @@ public class FileBackend {
|
||||||
|
|
||||||
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException {
|
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException {
|
||||||
final File parent = file.getParentFile();
|
final File parent = file.getParentFile();
|
||||||
if (parent != null && parent.mkdirs()) {
|
if (parent.mkdirs()) {
|
||||||
Log.d(Config.LOGTAG, "created parent directory");
|
Log.d(Config.LOGTAG, "created parent directory");
|
||||||
}
|
}
|
||||||
InputStream is = null;
|
InputStream is = null;
|
||||||
|
@ -756,15 +753,13 @@ public class FileBackend {
|
||||||
}
|
}
|
||||||
scaledBitmap.recycle();
|
scaledBitmap.recycle();
|
||||||
} catch (final FileNotFoundException e) {
|
} catch (final FileNotFoundException e) {
|
||||||
cleanup(file);
|
|
||||||
throw new FileCopyException(R.string.error_file_not_found);
|
throw new FileCopyException(R.string.error_file_not_found);
|
||||||
} catch (final IOException e) {
|
} catch (IOException e) {
|
||||||
cleanup(file);
|
e.printStackTrace();
|
||||||
throw new FileCopyException(R.string.error_io_exception);
|
throw new FileCopyException(R.string.error_io_exception);
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
cleanup(file);
|
|
||||||
throw new FileCopyException(R.string.error_security_exception_during_image_copy);
|
throw new FileCopyException(R.string.error_security_exception_during_image_copy);
|
||||||
} catch (final OutOfMemoryError e) {
|
} catch (OutOfMemoryError e) {
|
||||||
++sampleSize;
|
++sampleSize;
|
||||||
if (sampleSize <= 3) {
|
if (sampleSize <= 3) {
|
||||||
copyImageToPrivateStorage(file, image, sampleSize);
|
copyImageToPrivateStorage(file, image, sampleSize);
|
||||||
|
@ -777,14 +772,6 @@ 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 {
|
public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, ImageCompressionException {
|
||||||
Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath());
|
Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath());
|
||||||
copyImageToPrivateStorage(file, image, 0);
|
copyImageToPrivateStorage(file, image, 0);
|
||||||
|
@ -821,34 +808,19 @@ public class FileBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getRotation(final File file) {
|
private int getRotation(File file) {
|
||||||
try (final InputStream inputStream = new FileInputStream(file)) {
|
return getRotation(Uri.parse("file://" + file.getAbsolutePath()));
|
||||||
return getRotation(inputStream);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getRotation(final Uri image) {
|
private int getRotation(Uri image) {
|
||||||
try (final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(image)) {
|
InputStream is = null;
|
||||||
return is == null ? 0 : getRotation(is);
|
try {
|
||||||
} catch (final Exception e) {
|
is = mXmppConnectionService.getContentResolver().openInputStream(image);
|
||||||
|
return ExifHelper.getOrientation(is);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
return 0;
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1496,8 +1468,7 @@ public class FileBackend {
|
||||||
this.resId = resId;
|
this.resId = resId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @StringRes
|
public @StringRes int getResId() {
|
||||||
int getResId() {
|
|
||||||
return resId;
|
return resId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import android.os.PowerManager;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.engines.AESEngine;
|
import org.bouncycastle.crypto.engines.AESEngine;
|
||||||
import org.bouncycastle.crypto.io.CipherInputStream;
|
import org.bouncycastle.crypto.io.CipherInputStream;
|
||||||
import org.bouncycastle.crypto.io.CipherOutputStream;
|
import org.bouncycastle.crypto.io.CipherOutputStream;
|
||||||
|
@ -120,8 +118,7 @@ public class AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getAutoAcceptFileSize() {
|
public long getAutoAcceptFileSize() {
|
||||||
final long autoAcceptFileSize = this.mXmppConnectionService.getLongPreference("auto_accept_file_size", R.integer.auto_accept_filesize);
|
return this.mXmppConnectionService.getLongPreference("auto_accept_file_size", R.integer.auto_accept_filesize);
|
||||||
return autoAcceptFileSize <= 0 ? -1 : autoAcceptFileSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasStoragePermission() {
|
public boolean hasStoragePermission() {
|
||||||
|
@ -137,8 +134,12 @@ public class AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PowerManager.WakeLock createWakeLock(final Thread thread) {
|
||||||
|
return createWakeLock("conversations:" + thread.getName());
|
||||||
|
}
|
||||||
|
|
||||||
public PowerManager.WakeLock createWakeLock(final String name) {
|
public PowerManager.WakeLock createWakeLock(final String name) {
|
||||||
final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class);
|
final PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
|
||||||
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
|
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,19 +3,16 @@ package eu.siacs.conversations.services;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import net.ypresto.androidtranscoder.MediaTranscoder;
|
||||||
|
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
|
||||||
import com.otaliastudios.transcoder.Transcoder;
|
|
||||||
import com.otaliastudios.transcoder.TranscoderListener;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileDescriptor;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
@ -26,164 +23,161 @@ import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
import eu.siacs.conversations.ui.UiCallback;
|
import eu.siacs.conversations.ui.UiCallback;
|
||||||
|
import eu.siacs.conversations.utils.Android360pFormatStrategy;
|
||||||
|
import eu.siacs.conversations.utils.Android720pFormatStrategy;
|
||||||
import eu.siacs.conversations.utils.MimeUtils;
|
import eu.siacs.conversations.utils.MimeUtils;
|
||||||
import eu.siacs.conversations.utils.TranscoderStrategies;
|
|
||||||
|
|
||||||
public class AttachFileToConversationRunnable implements Runnable, TranscoderListener {
|
public class AttachFileToConversationRunnable implements Runnable, MediaTranscoder.Listener {
|
||||||
|
|
||||||
private final XmppConnectionService mXmppConnectionService;
|
private final XmppConnectionService mXmppConnectionService;
|
||||||
private final Message message;
|
private final Message message;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
private final String type;
|
private final String type;
|
||||||
private final UiCallback<Message> callback;
|
private final UiCallback<Message> callback;
|
||||||
private final boolean isVideoMessage;
|
private final boolean isVideoMessage;
|
||||||
private final long originalFileSize;
|
private final long originalFileSize;
|
||||||
private int currentProgress = -1;
|
private int currentProgress = -1;
|
||||||
|
|
||||||
AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback<Message> callback) {
|
AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback<Message> callback) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.mXmppConnectionService = xmppConnectionService;
|
this.mXmppConnectionService = xmppConnectionService;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
|
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
|
||||||
final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
|
final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
|
||||||
this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri);
|
this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService,uri);
|
||||||
this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression());
|
this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression());
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isVideoMessage() {
|
boolean isVideoMessage() {
|
||||||
return this.isVideoMessage;
|
return this.isVideoMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processAsFile() {
|
private void processAsFile() {
|
||||||
final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri);
|
final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri);
|
||||||
if (path != null && !FileBackend.isPathBlacklisted(path)) {
|
if (path != null && !FileBackend.isPathBlacklisted(path)) {
|
||||||
message.setRelativeFilePath(path);
|
message.setRelativeFilePath(path);
|
||||||
mXmppConnectionService.getFileBackend().updateFileParams(message);
|
mXmppConnectionService.getFileBackend().updateFileParams(message);
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
||||||
mXmppConnectionService.getPgpEngine().encrypt(message, callback);
|
mXmppConnectionService.getPgpEngine().encrypt(message, callback);
|
||||||
} else {
|
} else {
|
||||||
mXmppConnectionService.sendMessage(message);
|
mXmppConnectionService.sendMessage(message);
|
||||||
callback.success(message);
|
callback.success(message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
|
mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
|
||||||
mXmppConnectionService.getFileBackend().updateFileParams(message);
|
mXmppConnectionService.getFileBackend().updateFileParams(message);
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
||||||
final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
|
final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
|
||||||
if (pgpEngine != null) {
|
if (pgpEngine != null) {
|
||||||
pgpEngine.encrypt(message, callback);
|
pgpEngine.encrypt(message, callback);
|
||||||
} else if (callback != null) {
|
} else if (callback != null) {
|
||||||
callback.error(R.string.unable_to_connect_to_keychain, null);
|
callback.error(R.string.unable_to_connect_to_keychain, null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mXmppConnectionService.sendMessage(message);
|
mXmppConnectionService.sendMessage(message);
|
||||||
callback.success(message);
|
callback.success(message);
|
||||||
}
|
}
|
||||||
} catch (FileBackend.FileCopyException e) {
|
} catch (FileBackend.FileCopyException e) {
|
||||||
callback.error(e.getResId(), message);
|
callback.error(e.getResId(), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processAsVideo() throws FileNotFoundException {
|
private void processAsVideo() throws FileNotFoundException {
|
||||||
Log.d(Config.LOGTAG, "processing file as video");
|
Log.d(Config.LOGTAG,"processing file as video");
|
||||||
mXmppConnectionService.startForcingForegroundNotification();
|
mXmppConnectionService.startForcingForegroundNotification();
|
||||||
message.setRelativeFilePath(message.getUuid() + ".mp4");
|
message.setRelativeFilePath(message.getUuid() + ".mp4");
|
||||||
final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
|
final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
|
||||||
if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
|
final MediaFormatStrategy formatStrategy = "720".equals(getVideoCompression()) ? new Android720pFormatStrategy() : new Android360pFormatStrategy();
|
||||||
Log.d(Config.LOGTAG, "created parent directory for video file");
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final boolean highQuality = "720".equals(getVideoCompression());
|
@Override
|
||||||
|
public void onTranscodeProgress(double progress) {
|
||||||
|
final int p = (int) Math.round(progress * 100);
|
||||||
|
if (p > currentProgress) {
|
||||||
|
currentProgress = p;
|
||||||
|
mXmppConnectionService.getNotificationService().updateFileAddingNotification(p,message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final Future<Void> future = Transcoder.into(file.getAbsolutePath()).
|
@Override
|
||||||
addDataSource(mXmppConnectionService, uri)
|
public void onTranscodeCompleted() {
|
||||||
.setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P)
|
mXmppConnectionService.stopForcingForegroundNotification();
|
||||||
.setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ)
|
final File file = mXmppConnectionService.getFileBackend().getFile(message);
|
||||||
.setListener(this)
|
long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
|
||||||
.transcode();
|
Log.d(Config.LOGTAG,"originalFileSize="+originalFileSize+" convertedFileSize="+convertedFileSize);
|
||||||
try {
|
if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
|
||||||
future.get();
|
if (file.delete()) {
|
||||||
} catch (InterruptedException e) {
|
Log.d(Config.LOGTAG,"original file size was smaller. deleting and processing as file");
|
||||||
throw new AssertionError(e);
|
processAsFile();
|
||||||
} catch (ExecutionException e) {
|
return;
|
||||||
if (e.getCause() instanceof Error) {
|
} else {
|
||||||
mXmppConnectionService.stopForcingForegroundNotification();
|
Log.d(Config.LOGTAG,"unable to delete converted file");
|
||||||
processAsFile();
|
}
|
||||||
} else {
|
}
|
||||||
Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
|
mXmppConnectionService.getFileBackend().updateFileParams(message);
|
||||||
}
|
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
||||||
}
|
mXmppConnectionService.getPgpEngine().encrypt(message, callback);
|
||||||
}
|
} else {
|
||||||
|
mXmppConnectionService.sendMessage(message);
|
||||||
|
callback.success(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTranscodeProgress(double progress) {
|
public void onTranscodeCanceled() {
|
||||||
final int p = (int) Math.round(progress * 100);
|
mXmppConnectionService.stopForcingForegroundNotification();
|
||||||
if (p > currentProgress) {
|
processAsFile();
|
||||||
currentProgress = p;
|
}
|
||||||
mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTranscodeCompleted(int successCode) {
|
public void onTranscodeFailed(Exception e) {
|
||||||
mXmppConnectionService.stopForcingForegroundNotification();
|
mXmppConnectionService.stopForcingForegroundNotification();
|
||||||
final File file = mXmppConnectionService.getFileBackend().getFile(message);
|
Log.d(Config.LOGTAG,"video transcoding failed",e);
|
||||||
long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
|
processAsFile();
|
||||||
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
|
@Override
|
||||||
public void onTranscodeCanceled() {
|
public void run() {
|
||||||
mXmppConnectionService.stopForcingForegroundNotification();
|
if (this.isVideoMessage()) {
|
||||||
processAsFile();
|
try {
|
||||||
}
|
processAsVideo();
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
processAsFile();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
processAsFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
private String getVideoCompression() {
|
||||||
public void onTranscodeFailed(@NonNull @NotNull Throwable exception) {
|
return getVideoCompression(mXmppConnectionService);
|
||||||
mXmppConnectionService.stopForcingForegroundNotification();
|
}
|
||||||
Log.d(Config.LOGTAG, "video transcoding failed", exception);
|
|
||||||
processAsFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public static String getVideoCompression(final Context context) {
|
||||||
public void run() {
|
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
if (this.isVideoMessage()) {
|
return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
|
||||||
try {
|
}
|
||||||
processAsVideo();
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
processAsFile();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
processAsFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getVideoCompression() {
|
|
||||||
return getVideoCompression(mXmppConnectionService);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getVideoCompression(final Context context) {
|
|
||||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ public class ChannelDiscoveryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeMuclumbusService() {
|
void initializeMuclumbusService() {
|
||||||
final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
|
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||||
if (service.useTorToConnect()) {
|
if (service.useTorToConnect()) {
|
||||||
builder.proxy(HttpConnectionManager.getProxy());
|
builder.proxy(HttpConnectionManager.getProxy());
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,15 +42,16 @@ import android.util.SparseArray;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
import com.google.common.base.Joiner;
|
|
||||||
import com.google.common.io.CharStreams;
|
import com.google.common.io.CharStreams;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -62,10 +63,12 @@ import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateExpiredException;
|
||||||
import java.security.cert.CertificateParsingException;
|
import java.security.cert.CertificateParsingException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -73,12 +76,14 @@ import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
import eu.siacs.conversations.crypto.DomainHostnameVerifier;
|
||||||
import eu.siacs.conversations.entities.MTMDecision;
|
import eu.siacs.conversations.entities.MTMDecision;
|
||||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
|
@ -96,12 +101,12 @@ import eu.siacs.conversations.ui.MemorizingActivity;
|
||||||
*/
|
*/
|
||||||
public class MemorizingTrustManager {
|
public class MemorizingTrustManager {
|
||||||
|
|
||||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
|
||||||
|
|
||||||
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
|
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
|
||||||
public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
|
public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
|
||||||
public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
|
public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
|
||||||
public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
|
public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
|
||||||
|
final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
|
||||||
final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
|
final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
|
||||||
private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||||
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||||
|
@ -109,6 +114,7 @@ public class MemorizingTrustManager {
|
||||||
private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
|
private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
|
||||||
private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
|
private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
|
||||||
private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
|
private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
|
||||||
|
private final static int NOTIFICATION_ID = 100509;
|
||||||
static String KEYSTORE_DIR = "KeyStore";
|
static String KEYSTORE_DIR = "KeyStore";
|
||||||
static String KEYSTORE_FILE = "KeyStore.bks";
|
static String KEYSTORE_FILE = "KeyStore.bks";
|
||||||
private static int decisionId = 0;
|
private static int decisionId = 0;
|
||||||
|
@ -162,6 +168,20 @@ public class MemorizingTrustManager {
|
||||||
this.defaultTrustManager = getTrustManager(null);
|
this.defaultTrustManager = getTrustManager(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the path for the KeyStore file.
|
||||||
|
* <p>
|
||||||
|
* The actual filename relative to the app's directory will be
|
||||||
|
* <code>app_<i>dirname</i>/<i>filename</i></code>.
|
||||||
|
*
|
||||||
|
* @param dirname directory to store the KeyStore.
|
||||||
|
* @param filename file name for the KeyStore.
|
||||||
|
*/
|
||||||
|
public static void setKeyStoreFile(String dirname, String filename) {
|
||||||
|
KEYSTORE_DIR = dirname;
|
||||||
|
KEYSTORE_FILE = filename;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isIp(final String server) {
|
private static boolean isIp(final String server) {
|
||||||
return server != null && (
|
return server != null && (
|
||||||
PATTERN_IPV4.matcher(server).matches()
|
PATTERN_IPV4.matcher(server).matches()
|
||||||
|
@ -197,7 +217,9 @@ public class MemorizingTrustManager {
|
||||||
MessageDigest md = MessageDigest.getInstance(digest);
|
MessageDigest md = MessageDigest.getInstance(digest);
|
||||||
md.update(cert.getEncoded());
|
md.update(cert.getEncoded());
|
||||||
return hexString(md.digest());
|
return hexString(md.digest());
|
||||||
} catch (CertificateEncodingException | NoSuchAlgorithmException e) {
|
} catch (java.security.cert.CertificateEncodingException e) {
|
||||||
|
return e.getMessage();
|
||||||
|
} catch (java.security.NoSuchAlgorithmException e) {
|
||||||
return e.getMessage();
|
return e.getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,7 +240,7 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void init(final Context m) {
|
void init(Context m) {
|
||||||
master = m;
|
master = m;
|
||||||
masterHandler = new Handler(m.getMainLooper());
|
masterHandler = new Handler(m.getMainLooper());
|
||||||
notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
|
notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
@ -241,6 +263,36 @@ public class MemorizingTrustManager {
|
||||||
appKeyStore = loadAppKeyStore();
|
appKeyStore = loadAppKeyStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds an Activity to the MTM for displaying the query dialog.
|
||||||
|
* <p>
|
||||||
|
* This is useful if your connection is run from a service that is
|
||||||
|
* triggered by user interaction -- in such cases the activity is
|
||||||
|
* visible and the user tends to ignore the service notification.
|
||||||
|
* <p>
|
||||||
|
* You should never have a hidden activity bound to MTM! Use this
|
||||||
|
* function in onResume() and @see unbindDisplayActivity in onPause().
|
||||||
|
*
|
||||||
|
* @param act Activity to be bound
|
||||||
|
*/
|
||||||
|
public void bindDisplayActivity(AppCompatActivity act) {
|
||||||
|
foregroundAct = act;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an Activity from the MTM display stack.
|
||||||
|
* <p>
|
||||||
|
* Always call this function when the Activity added with
|
||||||
|
* {@link #bindDisplayActivity(AppCompatActivity)} is hidden.
|
||||||
|
*
|
||||||
|
* @param act Activity to be unbound
|
||||||
|
*/
|
||||||
|
public void unbindDisplayActivity(AppCompatActivity act) {
|
||||||
|
// do not remove if it was overridden by a different activity
|
||||||
|
if (foregroundAct == act)
|
||||||
|
foregroundAct = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of all certificate aliases stored in MTM.
|
* Get a list of all certificate aliases stored in MTM.
|
||||||
*
|
*
|
||||||
|
@ -255,6 +307,21 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a certificate for a given alias.
|
||||||
|
*
|
||||||
|
* @param alias the certificate's alias as returned by {@link #getCertificates()}.
|
||||||
|
* @return the certificate associated with the alias or <tt>null</tt> if none found.
|
||||||
|
*/
|
||||||
|
public Certificate getCertificate(String alias) {
|
||||||
|
try {
|
||||||
|
return appKeyStore.getCertificate(alias);
|
||||||
|
} catch (KeyStoreException e) {
|
||||||
|
// this should never happen, however...
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the given certificate from MTMs key store.
|
* Removes the given certificate from MTMs key store.
|
||||||
*
|
*
|
||||||
|
@ -273,6 +340,32 @@ public class MemorizingTrustManager {
|
||||||
keyStoreUpdated();
|
keyStoreUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new hostname verifier supporting user interaction.
|
||||||
|
*
|
||||||
|
* <p>This method creates a new {@link HostnameVerifier} that is bound to
|
||||||
|
* the given instance of {@link MemorizingTrustManager}, and leverages an
|
||||||
|
* existing {@link HostnameVerifier}. The returned verifier performs the
|
||||||
|
* following steps, returning as soon as one of them succeeds:
|
||||||
|
* /p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Success, if the wrapped defaultVerifier accepts the certificate.</li>
|
||||||
|
* <li>Success, if the server certificate is stored in the keystore under the given hostname.</li>
|
||||||
|
* <li>Ask the user and return accordingly.</li>
|
||||||
|
* <li>Failure on exception.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @param defaultVerifier the {@link HostnameVerifier} that should perform the actual check
|
||||||
|
* @return a new hostname verifier using the MTM's key store
|
||||||
|
* @throws IllegalArgumentException if the defaultVerifier parameter is null
|
||||||
|
*/
|
||||||
|
public DomainHostnameVerifier wrapHostnameVerifier(final HostnameVerifier defaultVerifier, final boolean interactive) {
|
||||||
|
if (defaultVerifier == null)
|
||||||
|
throw new IllegalArgumentException("The default verifier may not be null");
|
||||||
|
|
||||||
|
return new MemorizingHostnameVerifier(defaultVerifier, interactive);
|
||||||
|
}
|
||||||
|
|
||||||
X509TrustManager getTrustManager(KeyStore ks) {
|
X509TrustManager getTrustManager(KeyStore ks) {
|
||||||
try {
|
try {
|
||||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
|
||||||
|
@ -359,8 +452,16 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isExpiredException(Throwable e) {
|
||||||
|
do {
|
||||||
|
if (e instanceof CertificateExpiredException)
|
||||||
|
return true;
|
||||||
|
e = e.getCause();
|
||||||
|
} while (e != null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
|
public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
|
||||||
throws CertificateException {
|
throws CertificateException {
|
||||||
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
|
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
|
||||||
try {
|
try {
|
||||||
|
@ -369,8 +470,13 @@ public class MemorizingTrustManager {
|
||||||
appTrustManager.checkServerTrusted(chain, authType);
|
appTrustManager.checkServerTrusted(chain, authType);
|
||||||
else
|
else
|
||||||
appTrustManager.checkClientTrusted(chain, authType);
|
appTrustManager.checkClientTrusted(chain, authType);
|
||||||
} catch (final CertificateException ae) {
|
} catch (CertificateException ae) {
|
||||||
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
|
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
|
||||||
|
// if the cert is stored in our appTrustManager, we ignore expiredness
|
||||||
|
if (isExpiredException(ae)) {
|
||||||
|
LOGGER.log(Level.INFO, "checkCertTrusted: accepting expired certificate from keystore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isCertKnown(chain[0])) {
|
if (isCertKnown(chain[0])) {
|
||||||
LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
|
LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
|
||||||
return;
|
return;
|
||||||
|
@ -526,24 +632,14 @@ public class MemorizingTrustManager {
|
||||||
return myId;
|
return myId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void certDetails(final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
|
private void certDetails(StringBuffer si, X509Certificate c) {
|
||||||
|
SimpleDateFormat validityDateFormater = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
si.append("\n");
|
si.append("\n");
|
||||||
if (showValidFor) {
|
si.append(c.getSubjectDN().toString());
|
||||||
try {
|
|
||||||
si.append("Valid for: ");
|
|
||||||
si.append(Joiner.on(", ").join(XmppDomainVerifier.parseValidDomains(c).all()));
|
|
||||||
} catch (final CertificateParsingException e) {
|
|
||||||
si.append("Unable to parse Certificate");
|
|
||||||
}
|
|
||||||
si.append("\n");
|
|
||||||
} else {
|
|
||||||
si.append(c.getSubjectDN());
|
|
||||||
}
|
|
||||||
si.append("\n");
|
si.append("\n");
|
||||||
si.append(DATE_FORMAT.format(c.getNotBefore()));
|
si.append(validityDateFormater.format(c.getNotBefore()));
|
||||||
si.append(" - ");
|
si.append(" - ");
|
||||||
si.append(DATE_FORMAT.format(c.getNotAfter()));
|
si.append(validityDateFormater.format(c.getNotAfter()));
|
||||||
si.append("\nSHA-256: ");
|
si.append("\nSHA-256: ");
|
||||||
si.append(certHash(c, "SHA-256"));
|
si.append(certHash(c, "SHA-256"));
|
||||||
si.append("\nSHA-1: ");
|
si.append("\nSHA-1: ");
|
||||||
|
@ -556,7 +652,7 @@ public class MemorizingTrustManager {
|
||||||
private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
|
private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
|
||||||
Throwable e = cause;
|
Throwable e = cause;
|
||||||
LOGGER.log(Level.FINE, "certChainMessage for " + e);
|
LOGGER.log(Level.FINE, "certChainMessage for " + e);
|
||||||
final StringBuffer si = new StringBuffer();
|
StringBuffer si = new StringBuffer();
|
||||||
if (e.getCause() != null) {
|
if (e.getCause() != null) {
|
||||||
e = e.getCause();
|
e = e.getCause();
|
||||||
// HACK: there is no sane way to check if the error is a "trust anchor
|
// HACK: there is no sane way to check if the error is a "trust anchor
|
||||||
|
@ -571,13 +667,46 @@ public class MemorizingTrustManager {
|
||||||
si.append(master.getString(R.string.mtm_connect_anyway));
|
si.append(master.getString(R.string.mtm_connect_anyway));
|
||||||
si.append("\n\n");
|
si.append("\n\n");
|
||||||
si.append(master.getString(R.string.mtm_cert_details));
|
si.append(master.getString(R.string.mtm_cert_details));
|
||||||
si.append('\n');
|
for (X509Certificate c : chain) {
|
||||||
for(int i = 0; i < chain.length; ++i) {
|
certDetails(si, c);
|
||||||
certDetails(si, chain[i], i == 0);
|
|
||||||
}
|
}
|
||||||
return si.toString();
|
return si.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String hostNameMessage(X509Certificate cert, String hostname) {
|
||||||
|
StringBuffer si = new StringBuffer();
|
||||||
|
|
||||||
|
si.append(master.getString(R.string.mtm_hostname_mismatch, hostname));
|
||||||
|
si.append("\n\n");
|
||||||
|
try {
|
||||||
|
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
|
||||||
|
if (sans == null) {
|
||||||
|
si.append(cert.getSubjectDN());
|
||||||
|
si.append("\n");
|
||||||
|
} else for (List<?> altName : sans) {
|
||||||
|
Object name = altName.get(1);
|
||||||
|
if (name instanceof String) {
|
||||||
|
si.append("[");
|
||||||
|
si.append(altName.get(0));
|
||||||
|
si.append("] ");
|
||||||
|
si.append(name);
|
||||||
|
si.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (CertificateParsingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
si.append("<Parsing error: ");
|
||||||
|
si.append(e.getLocalizedMessage());
|
||||||
|
si.append(">\n");
|
||||||
|
}
|
||||||
|
si.append("\n");
|
||||||
|
si.append(master.getString(R.string.mtm_connect_anyway));
|
||||||
|
si.append("\n\n");
|
||||||
|
si.append(master.getString(R.string.mtm_cert_details));
|
||||||
|
certDetails(si, cert);
|
||||||
|
return si.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the top-most entry of the activity stack.
|
* Returns the top-most entry of the activity stack.
|
||||||
*
|
*
|
||||||
|
@ -635,6 +764,17 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean interactHostname(X509Certificate cert, String hostname) {
|
||||||
|
switch (interact(hostNameMessage(cert, hostname), R.string.mtm_accept_servername)) {
|
||||||
|
case MTMDecision.DECISION_ALWAYS:
|
||||||
|
storeCert(hostname, cert);
|
||||||
|
case MTMDecision.DECISION_ONCE:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public X509TrustManager getNonInteractive(String domain) {
|
public X509TrustManager getNonInteractive(String domain) {
|
||||||
return new NonInteractiveMemorizingTrustManager(domain);
|
return new NonInteractiveMemorizingTrustManager(domain);
|
||||||
}
|
}
|
||||||
|
@ -651,6 +791,57 @@ public class MemorizingTrustManager {
|
||||||
return new InteractiveMemorizingTrustManager(null);
|
return new InteractiveMemorizingTrustManager(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MemorizingHostnameVerifier implements DomainHostnameVerifier {
|
||||||
|
private final HostnameVerifier defaultVerifier;
|
||||||
|
private final boolean interactive;
|
||||||
|
|
||||||
|
public MemorizingHostnameVerifier(HostnameVerifier wrapped, boolean interactive) {
|
||||||
|
this.defaultVerifier = wrapped;
|
||||||
|
this.interactive = interactive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verify(String domain, String hostname, SSLSession session) {
|
||||||
|
LOGGER.log(Level.FINE, "hostname verifier for " + domain + ", trying default verifier first");
|
||||||
|
// if the default verifier accepts the hostname, we are done
|
||||||
|
if (defaultVerifier instanceof DomainHostnameVerifier) {
|
||||||
|
if (((DomainHostnameVerifier) defaultVerifier).verify(domain, hostname, session)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (defaultVerifier.verify(domain, session)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// otherwise, we check if the hostname is an alias for this cert in our keystore
|
||||||
|
try {
|
||||||
|
X509Certificate cert = (X509Certificate) session.getPeerCertificates()[0];
|
||||||
|
//Log.d(TAG, "cert: " + cert);
|
||||||
|
if (cert.equals(appKeyStore.getCertificate(domain.toLowerCase(Locale.US)))) {
|
||||||
|
LOGGER.log(Level.FINE, "certificate for " + domain + " is in our keystore. accepting.");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LOGGER.log(Level.FINE, "server " + domain + " provided wrong certificate, asking user.");
|
||||||
|
if (interactive) {
|
||||||
|
return interactHostname(cert, domain);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verify(String domain, SSLSession sslSession) {
|
||||||
|
return verify(domain, null, sslSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
|
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
|
||||||
|
|
||||||
private final String domain;
|
private final String domain;
|
||||||
|
|
|
@ -35,7 +35,6 @@ import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.graphics.drawable.IconCompat;
|
import androidx.core.graphics.drawable.IconCompat;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -398,7 +397,7 @@ public class NotificationService {
|
||||||
notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
|
notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) {
|
public void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) {
|
||||||
showIncomingCallNotification(id, media);
|
showIncomingCallNotification(id, media);
|
||||||
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
|
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
final int currentInterruptionFilter;
|
final int currentInterruptionFilter;
|
||||||
|
@ -408,7 +407,7 @@ public class NotificationService {
|
||||||
currentInterruptionFilter = 1; //INTERRUPTION_FILTER_ALL
|
currentInterruptionFilter = 1; //INTERRUPTION_FILTER_ALL
|
||||||
}
|
}
|
||||||
if (currentInterruptionFilter != 1) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
|
final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
|
||||||
|
@ -425,13 +424,13 @@ public class NotificationService {
|
||||||
final Resources resources = mXmppConnectionService.getResources();
|
final Resources resources = mXmppConnectionService.getResources();
|
||||||
final String ringtonePreference = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone));
|
final String ringtonePreference = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone));
|
||||||
if (Strings.isNullOrEmpty(ringtonePreference)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
final Uri uri = Uri.parse(ringtonePreference);
|
final Uri uri = Uri.parse(ringtonePreference);
|
||||||
this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
|
this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
|
||||||
if (this.currentlyPlayingRingtone == null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
@ -488,23 +487,14 @@ public class NotificationService {
|
||||||
notify(INCOMING_CALL_NOTIFICATION_ID, notification);
|
notify(INCOMING_CALL_NOTIFICATION_ID, notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) {
|
public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
|
||||||
final AbstractJingleConnection.Id id = ongoingCall.id;
|
|
||||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
|
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
|
||||||
if (ongoingCall.media.contains(Media.VIDEO)) {
|
if (media.contains(Media.VIDEO)) {
|
||||||
builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
|
builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
|
||||||
if (ongoingCall.reconnecting) {
|
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
|
||||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call));
|
|
||||||
} else {
|
|
||||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
builder.setSmallIcon(R.drawable.ic_call_white_24dp);
|
builder.setSmallIcon(R.drawable.ic_call_white_24dp);
|
||||||
if (ongoingCall.reconnecting) {
|
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
|
||||||
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.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
|
||||||
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
||||||
|
@ -800,18 +790,17 @@ public class NotificationService {
|
||||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
|
||||||
.setShowsUserInterface(false)
|
.setShowsUserInterface(false)
|
||||||
.build();
|
.build();
|
||||||
final String replyLabel = mXmppConnectionService.getString(R.string.reply);
|
String replyLabel = mXmppConnectionService.getString(R.string.reply);
|
||||||
final String lastMessageUuid = Iterables.getLast(messages).getUuid();
|
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
|
||||||
final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
|
|
||||||
R.drawable.ic_send_text_offline,
|
R.drawable.ic_send_text_offline,
|
||||||
replyLabel,
|
replyLabel,
|
||||||
createReplyIntent(conversation, lastMessageUuid, false))
|
createReplyIntent(conversation, false))
|
||||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||||
.setShowsUserInterface(false)
|
.setShowsUserInterface(false)
|
||||||
.addRemoteInput(remoteInput).build();
|
.addRemoteInput(remoteInput).build();
|
||||||
final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
|
NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
|
||||||
replyLabel,
|
replyLabel,
|
||||||
createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build();
|
createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
|
||||||
mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
|
mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
|
||||||
int addedActionsCount = 1;
|
int addedActionsCount = 1;
|
||||||
mBuilder.addAction(markReadAction);
|
mBuilder.addAction(markReadAction);
|
||||||
|
@ -1077,14 +1066,13 @@ public class NotificationService {
|
||||||
return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
|
return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PendingIntent createReplyIntent(final Conversation conversation, final String lastMessageUuid, final boolean dismissAfterReply) {
|
private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
|
||||||
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
|
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
|
||||||
intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
|
intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
|
||||||
intent.putExtra("uuid", conversation.getUuid());
|
intent.putExtra("uuid", conversation.getUuid());
|
||||||
intent.putExtra("dismiss_notification", dismissAfterReply);
|
intent.putExtra("dismiss_notification", dismissAfterReply);
|
||||||
intent.putExtra("last_message_uuid", lastMessageUuid);
|
|
||||||
final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
|
final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
|
||||||
return PendingIntent.getService(mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PendingIntent createReadPendingIntent(Conversation conversation) {
|
private PendingIntent createReadPendingIntent(Conversation conversation) {
|
||||||
|
|
|
@ -75,8 +75,6 @@ import java.util.Set;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.CountDownLatch;
|
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.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
@ -186,9 +184,8 @@ public class XmppConnectionService extends Service {
|
||||||
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
|
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
|
||||||
|
|
||||||
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
|
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
|
||||||
private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
|
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
|
||||||
private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
|
private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");
|
||||||
private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression");
|
|
||||||
private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
|
private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
|
||||||
private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader");
|
private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader");
|
||||||
private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor");
|
private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor");
|
||||||
|
@ -474,6 +471,7 @@ public class XmppConnectionService extends Service {
|
||||||
private OpenPgpServiceConnection pgpServiceConnection;
|
private OpenPgpServiceConnection pgpServiceConnection;
|
||||||
private PgpEngine mPgpEngine = null;
|
private PgpEngine mPgpEngine = null;
|
||||||
private WakeLock wakeLock;
|
private WakeLock wakeLock;
|
||||||
|
private PowerManager pm;
|
||||||
private LruCache<String, Bitmap> mBitmapCache;
|
private LruCache<String, Bitmap> mBitmapCache;
|
||||||
private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
|
private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
|
||||||
private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
|
private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
|
||||||
|
@ -566,14 +564,14 @@ public class XmppConnectionService extends Service {
|
||||||
Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
|
Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
|
||||||
final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback);
|
final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback);
|
||||||
if (runnable.isVideoMessage()) {
|
if (runnable.isVideoMessage()) {
|
||||||
VIDEO_COMPRESSION_EXECUTOR.execute(runnable);
|
mVideoCompressionExecutor.execute(runnable);
|
||||||
} else {
|
} else {
|
||||||
FILE_ATTACHMENT_EXECUTOR.execute(runnable);
|
mFileAddingExecutor.execute(runnable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
|
public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
|
||||||
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
|
final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
|
||||||
final String compressPictures = getCompressPicturesPreference();
|
final String compressPictures = getCompressPicturesPreference();
|
||||||
|
|
||||||
if ("never".equals(compressPictures)
|
if ("never".equals(compressPictures)
|
||||||
|
@ -595,7 +593,7 @@ public class XmppConnectionService extends Service {
|
||||||
message.setType(Message.TYPE_IMAGE);
|
message.setType(Message.TYPE_IMAGE);
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
|
Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
|
||||||
FILE_ATTACHMENT_EXECUTOR.execute(() -> {
|
mFileAddingExecutor.execute(() -> {
|
||||||
try {
|
try {
|
||||||
getFileBackend().copyImageToPrivateStorage(message, uri);
|
getFileBackend().copyImageToPrivateStorage(message, uri);
|
||||||
} catch (FileBackend.ImageCompressionException e) {
|
} catch (FileBackend.ImageCompressionException e) {
|
||||||
|
@ -726,7 +724,6 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
final CharSequence body = remoteInput.getCharSequence("text_reply");
|
final CharSequence body = remoteInput.getCharSequence("text_reply");
|
||||||
final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
|
final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
|
||||||
final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
|
|
||||||
if (body == null || body.length() <= 0) {
|
if (body == null || body.length() <= 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -735,7 +732,7 @@ public class XmppConnectionService extends Service {
|
||||||
restoredFromDatabaseLatch.await();
|
restoredFromDatabaseLatch.await();
|
||||||
final Conversation c = findConversationByUuid(uuid);
|
final Conversation c = findConversationByUuid(uuid);
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
directReply(c, body.toString(), lastMessageUuid, dismissNotification);
|
directReply(c, body.toString(), dismissNotification);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Log.d(Config.LOGTAG, "unable to process direct reply");
|
Log.d(Config.LOGTAG, "unable to process direct reply");
|
||||||
|
@ -933,12 +930,8 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
|
private void directReply(Conversation conversation, String body, final boolean dismissAfterReply) {
|
||||||
final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
|
Message message = new Message(conversation, body, conversation.getNextEncryption());
|
||||||
final Message message = new Message(conversation, body, conversation.getNextEncryption());
|
|
||||||
if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
|
|
||||||
Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
|
|
||||||
}
|
|
||||||
message.markUnread();
|
message.markUnread();
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
|
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||||
getPgpEngine().encrypt(message, new UiCallback<Message>() {
|
getPgpEngine().encrypt(message, new UiCallback<Message>() {
|
||||||
|
@ -1154,11 +1147,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) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
|
||||||
startContactObserver();
|
startContactObserver();
|
||||||
}
|
}
|
||||||
FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
|
mFileAddingExecutor.execute(fileBackend::deleteHistoricAvatarPath);
|
||||||
if (Compatibility.hasStoragePermission(this)) {
|
if (Compatibility.hasStoragePermission(this)) {
|
||||||
Log.d(Config.LOGTAG, "starting file observer");
|
Log.d(Config.LOGTAG, "starting file observer");
|
||||||
FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching);
|
mFileAddingExecutor.execute(this.fileObserver::startWatching);
|
||||||
FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
|
mFileAddingExecutor.execute(this::checkForDeletedFiles);
|
||||||
}
|
}
|
||||||
if (Config.supportOpenPgp()) {
|
if (Config.supportOpenPgp()) {
|
||||||
this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
|
this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
|
||||||
|
@ -1179,7 +1172,7 @@ public class XmppConnectionService extends Service {
|
||||||
this.pgpServiceConnection.bindToService();
|
this.pgpServiceConnection.bindToService();
|
||||||
}
|
}
|
||||||
|
|
||||||
final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class);
|
this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||||
this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
|
this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
|
||||||
|
|
||||||
toggleForegroundService();
|
toggleForegroundService();
|
||||||
|
@ -1274,8 +1267,8 @@ public class XmppConnectionService extends Service {
|
||||||
|
|
||||||
public void restartFileObserver() {
|
public void restartFileObserver() {
|
||||||
Log.d(Config.LOGTAG, "restarting file observer");
|
Log.d(Config.LOGTAG, "restarting file observer");
|
||||||
FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching);
|
mFileAddingExecutor.execute(this.fileObserver::restartWatching);
|
||||||
FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
|
mFileAddingExecutor.execute(this::checkForDeletedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleScreenEventReceiver() {
|
public void toggleScreenEventReceiver() {
|
||||||
|
@ -1298,8 +1291,8 @@ public class XmppConnectionService extends Service {
|
||||||
toggleForegroundService(false);
|
toggleForegroundService(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
|
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
|
||||||
ongoingCall.set(new OngoingCall(id, media, reconnecting));
|
ongoingCall.set(new OngoingCall(id, media));
|
||||||
toggleForegroundService(false);
|
toggleForegroundService(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1315,7 +1308,7 @@ public class XmppConnectionService extends Service {
|
||||||
final Notification notification;
|
final Notification notification;
|
||||||
final int id;
|
final int id;
|
||||||
if (ongoing != null) {
|
if (ongoing != null) {
|
||||||
notification = this.mNotificationService.getOngoingCallNotification(ongoing);
|
notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media);
|
||||||
id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
|
id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
|
||||||
startForeground(id, notification);
|
startForeground(id, notification);
|
||||||
mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
|
mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
|
||||||
|
@ -1892,10 +1885,7 @@ public class XmppConnectionService extends Service {
|
||||||
long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
|
long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
|
||||||
Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
|
Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
|
||||||
Runnable runnable = () -> {
|
Runnable runnable = () -> {
|
||||||
if (DatabaseBackend.requiresMessageIndexRebuild()) {
|
long deletionDate = getAutomaticMessageDeletionDate();
|
||||||
DatabaseBackend.getInstance(this).rebuildMessagesIndex();
|
|
||||||
}
|
|
||||||
final long deletionDate = getAutomaticMessageDeletionDate();
|
|
||||||
mLastExpiryRun.set(SystemClock.elapsedRealtime());
|
mLastExpiryRun.set(SystemClock.elapsedRealtime());
|
||||||
if (deletionDate > 0) {
|
if (deletionDate > 0) {
|
||||||
Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
|
Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
|
||||||
|
@ -1935,7 +1925,7 @@ public class XmppConnectionService extends Service {
|
||||||
private void restoreMessages(Conversation conversation) {
|
private void restoreMessages(Conversation conversation) {
|
||||||
conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
|
conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
|
||||||
conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
|
conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
|
||||||
conversation.findUnreadMessages(mNotificationService::pushFromBacklog);
|
conversation.findUnreadMessages(message -> mNotificationService.pushFromBacklog(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadPhoneContacts() {
|
public void loadPhoneContacts() {
|
||||||
|
@ -3347,26 +3337,35 @@ public class XmppConnectionService extends Service {
|
||||||
|
|
||||||
public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
|
public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
|
||||||
final Jid jid = user.asBareJid();
|
final Jid jid = user.asBareJid();
|
||||||
final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
|
IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
|
||||||
sendIqPacket(conference.getAccount(), request, (account, response) -> {
|
sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
|
||||||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
@Override
|
||||||
conference.getMucOptions().changeAffiliation(jid, affiliation);
|
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||||
getAvatarService().clear(conference);
|
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||||
if (callback != null) {
|
conference.getMucOptions().changeAffiliation(jid, affiliation);
|
||||||
|
getAvatarService().clear(conference);
|
||||||
callback.onAffiliationChangedSuccessful(jid);
|
callback.onAffiliationChangedSuccessful(jid);
|
||||||
} else {
|
} else {
|
||||||
Log.d(Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation);
|
callback.onAffiliationChangeFailed(jid, R.string.could_not_change_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) {
|
public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) {
|
||||||
IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
|
IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
|
||||||
|
Log.d(Config.LOGTAG, request.toString());
|
||||||
sendIqPacket(conference.getAccount(), request, (account, packet) -> {
|
sendIqPacket(conference.getAccount(), request, (account, packet) -> {
|
||||||
if (packet.getType() != IqPacket.TYPE.RESULT) {
|
if (packet.getType() != IqPacket.TYPE.RESULT) {
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick);
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick);
|
||||||
|
@ -3449,23 +3448,15 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createContact(final Contact contact, final boolean autoGrant) {
|
public void createContact(Contact contact, boolean autoGrant) {
|
||||||
createContact(contact, autoGrant, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createContact(final Contact contact, final boolean autoGrant, final String preAuth) {
|
|
||||||
if (autoGrant) {
|
if (autoGrant) {
|
||||||
contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
|
contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
|
||||||
contact.setOption(Contact.Options.ASKING);
|
contact.setOption(Contact.Options.ASKING);
|
||||||
}
|
}
|
||||||
pushContactToServer(contact, preAuth);
|
pushContactToServer(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pushContactToServer(final Contact contact) {
|
public void pushContactToServer(final Contact contact) {
|
||||||
pushContactToServer(contact, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pushContactToServer(final Contact contact, final String preAuth) {
|
|
||||||
contact.resetOption(Contact.Options.DIRTY_DELETE);
|
contact.resetOption(Contact.Options.DIRTY_DELETE);
|
||||||
contact.setOption(Contact.Options.DIRTY_PUSH);
|
contact.setOption(Contact.Options.DIRTY_PUSH);
|
||||||
final Account account = contact.getAccount();
|
final Account account = contact.getAccount();
|
||||||
|
@ -3481,7 +3472,7 @@ public class XmppConnectionService extends Service {
|
||||||
sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
|
sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
|
||||||
}
|
}
|
||||||
if (ask) {
|
if (ask) {
|
||||||
sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
|
sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
syncRoster(contact.getAccount());
|
syncRoster(contact.getAccount());
|
||||||
|
@ -3929,13 +3920,9 @@ public class XmppConnectionService extends Service {
|
||||||
new Thread(() -> reconnectAccount(account, false, true)).start();
|
new Thread(() -> reconnectAccount(account, false, true)).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void invite(final Conversation conversation, final Jid contact) {
|
public void invite(Conversation conversation, Jid contact) {
|
||||||
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid());
|
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid());
|
||||||
final MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
|
MessagePacket packet = mMessageGenerator.invite(conversation, contact);
|
||||||
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);
|
sendMessagePacket(conversation.getAccount(), packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4869,14 +4856,12 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class OngoingCall {
|
public static class OngoingCall {
|
||||||
public final AbstractJingleConnection.Id id;
|
private final AbstractJingleConnection.Id id;
|
||||||
public final Set<Media> media;
|
private final Set<Media> media;
|
||||||
public final boolean reconnecting;
|
|
||||||
|
|
||||||
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
|
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.media = media;
|
this.media = media;
|
||||||
this.reconnecting = reconnecting;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -4884,12 +4869,12 @@ public class XmppConnectionService extends Service {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
OngoingCall that = (OngoingCall) o;
|
OngoingCall that = (OngoingCall) o;
|
||||||
return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media);
|
return Objects.equal(id, that.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hashCode(id, media, reconnecting);
|
return Objects.hashCode(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,12 @@ import android.os.Bundle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
import eu.siacs.conversations.utils.ThemeHelper;
|
import eu.siacs.conversations.utils.ThemeHelper;
|
||||||
|
|
||||||
import static eu.siacs.conversations.ui.XmppActivity.configureActionBar;
|
import static eu.siacs.conversations.ui.XmppActivity.configureActionBar;
|
||||||
|
|
||||||
public class AboutActivity extends AppCompatActivity {
|
public class AboutActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume(){
|
|
||||||
super.onResume();
|
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
|
@ -5,26 +5,24 @@ import android.content.Intent;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import eu.siacs.conversations.BuildConfig;
|
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.utils.PhoneHelper;
|
import eu.siacs.conversations.utils.PhoneHelper;
|
||||||
|
|
||||||
public class AboutPreference extends Preference {
|
public class AboutPreference extends Preference {
|
||||||
public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) {
|
public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
setSummaryAndTitle(context);
|
final String appName = context.getString(R.string.app_name);
|
||||||
|
setSummary(appName +' '+ PhoneHelper.getVersionName(context));
|
||||||
|
setTitle(context.getString(R.string.title_activity_about_x, appName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AboutPreference(final Context context, final AttributeSet attrs) {
|
public AboutPreference(final Context context, final AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
setSummaryAndTitle(context);
|
final String appName = context.getString(R.string.app_name);
|
||||||
|
setSummary(appName +' '+ PhoneHelper.getVersionName(context));
|
||||||
|
setTitle(context.getString(R.string.title_activity_about_x, appName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSummaryAndTitle(final Context context) {
|
|
||||||
setSummary(String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
|
|
||||||
setTitle(context.getString(R.string.title_activity_about_x, BuildConfig.APP_NAME));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onClick() {
|
protected void onClick() {
|
||||||
super.onClick();
|
super.onClick();
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.siacs.conversations.ui;
|
package eu.siacs.conversations.ui;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
@ -8,7 +7,6 @@ import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -88,13 +86,6 @@ 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() {
|
private final OnClickListener mNotifyStatusClickListener = new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -490,7 +481,6 @@ 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.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.setAutoLinkMask(0);
|
||||||
this.binding.mucSubject.setVisibility(View.VISIBLE);
|
this.binding.mucSubject.setVisibility(View.VISIBLE);
|
||||||
this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
} else {
|
} else {
|
||||||
this.binding.mucSubject.setVisibility(View.GONE);
|
this.binding.mucSubject.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,6 @@ import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.ListItem;
|
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.OnAccountUpdate;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
|
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
|
||||||
import eu.siacs.conversations.ui.adapter.MediaAdapter;
|
import eu.siacs.conversations.ui.adapter.MediaAdapter;
|
||||||
|
@ -59,7 +58,6 @@ import eu.siacs.conversations.utils.AccountUtils;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
import eu.siacs.conversations.utils.Emoticons;
|
import eu.siacs.conversations.utils.Emoticons;
|
||||||
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
|
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
|
||||||
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
|
|
||||||
import eu.siacs.conversations.utils.UIHelper;
|
import eu.siacs.conversations.utils.UIHelper;
|
||||||
import eu.siacs.conversations.utils.XmppUri;
|
import eu.siacs.conversations.utils.XmppUri;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
@ -133,31 +131,15 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showAddToPhoneBookDialog() {
|
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);
|
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
builder.setTitle(getString(R.string.action_add_phone_book));
|
builder.setTitle(getString(R.string.action_add_phone_book));
|
||||||
builder.setMessage(getString(R.string.add_phone_book_text, value));
|
builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toEscapedString()));
|
||||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||||
builder.setPositiveButton(getString(R.string.add), (dialog, which) -> {
|
builder.setPositiveButton(getString(R.string.add), (dialog, which) -> {
|
||||||
final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
|
final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
|
||||||
intent.setType(Contacts.CONTENT_ITEM_TYPE);
|
intent.setType(Contacts.CONTENT_ITEM_TYPE);
|
||||||
if (quicksyContact) {
|
intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toEscapedString());
|
||||||
intent.putExtra(Intents.Insert.PHONE, value);
|
intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
|
||||||
} 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);
|
intent.putExtra("finishActivityOnSaveCompleted", true);
|
||||||
try {
|
try {
|
||||||
startActivityForResult(intent, 0);
|
startActivityForResult(intent, 0);
|
||||||
|
@ -251,7 +233,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
if (grantResults.length > 0)
|
if (grantResults.length > 0)
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
|
if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
|
||||||
|
|
|
@ -6,8 +6,6 @@ import android.os.Bundle;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
|
|
||||||
public class ConversationActivity extends AppCompatActivity {
|
public class ConversationActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -16,10 +14,4 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
startActivity(new Intent(this, ConversationsActivity.class));
|
startActivity(new Intent(this, ConversationsActivity.class));
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume(){
|
|
||||||
super.onResume();
|
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,8 +135,6 @@ import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
|
||||||
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
|
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
|
||||||
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
|
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 {
|
public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {
|
||||||
|
|
||||||
|
@ -188,7 +186,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
ConferenceDetailsActivity.open(getActivity(), conversation);
|
Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
|
||||||
|
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
|
||||||
|
intent.putExtra("uuid", conversation.getUuid());
|
||||||
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private final OnClickListener leaveMuc = new OnClickListener() {
|
private final OnClickListener leaveMuc = new OnClickListener() {
|
||||||
|
@ -688,14 +689,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
toggleInputMethod();
|
toggleInputMethod();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attachImageToConversation(Conversation conversation, Uri uri, String type) {
|
private void attachImageToConversation(Conversation conversation, Uri uri) {
|
||||||
if (conversation == null) {
|
if (conversation == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
|
final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
|
||||||
prepareFileToast.show();
|
prepareFileToast.show();
|
||||||
activity.delegateUriPermissionsToService(uri);
|
activity.delegateUriPermissionsToService(uri);
|
||||||
activity.xmppConnectionService.attachImageToConversation(conversation, uri, type,
|
activity.xmppConnectionService.attachImageToConversation(conversation, uri,
|
||||||
new UiCallback<Message>() {
|
new UiCallback<Message>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -733,7 +734,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
if (body.length() == 0 || conversation == null) {
|
if (body.length() == 0 || conversation == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_TEXT)) {
|
if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(REQUEST_TRUST_KEYS_TEXT)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Message message;
|
final Message message;
|
||||||
|
@ -756,10 +757,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
|
|
||||||
return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(requestCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean trustKeysIfNeeded(int requestCode) {
|
protected boolean trustKeysIfNeeded(int requestCode) {
|
||||||
AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
|
AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
|
||||||
final List<Jid> targets = axolotlService.getCryptoTargets(conversation);
|
final List<Jid> targets = axolotlService.getCryptoTargets(conversation);
|
||||||
|
@ -827,12 +824,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
case REQUEST_TRUST_KEYS_ATTACHMENTS:
|
case REQUEST_TRUST_KEYS_ATTACHMENTS:
|
||||||
commitAttachments();
|
commitAttachments();
|
||||||
break;
|
break;
|
||||||
case REQUEST_START_AUDIO_CALL:
|
|
||||||
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
|
|
||||||
break;
|
|
||||||
case REQUEST_START_VIDEO_CALL:
|
|
||||||
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
|
|
||||||
break;
|
|
||||||
case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
|
case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
|
||||||
final List<Attachment> imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE);
|
final List<Attachment> imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE);
|
||||||
mediaPreviewAdapter.addMediaPreviews(imageUris);
|
mediaPreviewAdapter.addMediaPreviews(imageUris);
|
||||||
|
@ -856,15 +847,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
toggleInputMethod();
|
toggleInputMethod();
|
||||||
break;
|
break;
|
||||||
case ATTACHMENT_CHOICE_LOCATION:
|
case ATTACHMENT_CHOICE_LOCATION:
|
||||||
final double latitude = data.getDoubleExtra("latitude", 0);
|
double latitude = data.getDoubleExtra("latitude", 0);
|
||||||
final double longitude = data.getDoubleExtra("longitude", 0);
|
double longitude = data.getDoubleExtra("longitude", 0);
|
||||||
final int accuracy = data.getIntExtra("accuracy", 0);
|
Uri geo = Uri.parse("geo:" + latitude + "," + longitude);
|
||||||
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));
|
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
|
||||||
toggleInputMethod();
|
toggleInputMethod();
|
||||||
break;
|
break;
|
||||||
|
@ -885,7 +870,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_ATTACHMENTS)) {
|
if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(REQUEST_TRUST_KEYS_ATTACHMENTS)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final PresenceSelector.OnPresenceSelected callback = () -> {
|
final PresenceSelector.OnPresenceSelected callback = () -> {
|
||||||
|
@ -895,7 +880,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
attachLocationToConversation(conversation, attachment.getUri());
|
attachLocationToConversation(conversation, attachment.getUri());
|
||||||
} else if (attachment.getType() == Attachment.Type.IMAGE) {
|
} else if (attachment.getType() == Attachment.Type.IMAGE) {
|
||||||
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
|
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
|
||||||
attachImageToConversation(conversation, attachment.getUri(), attachment.getMime());
|
attachImageToConversation(conversation, attachment.getUri());
|
||||||
} else {
|
} else {
|
||||||
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
|
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
|
||||||
attachFileToConversation(conversation, attachment.getUri(), attachment.getMime());
|
attachFileToConversation(conversation, attachment.getUri(), attachment.getMime());
|
||||||
|
@ -1277,7 +1262,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
activity.switchToContactDetails(conversation.getContact());
|
activity.switchToContactDetails(conversation.getContact());
|
||||||
break;
|
break;
|
||||||
case R.id.action_muc_details:
|
case R.id.action_muc_details:
|
||||||
ConferenceDetailsActivity.open(getActivity(), conversation);
|
Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
|
||||||
|
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
|
||||||
|
intent.putExtra("uuid", conversation.getUuid());
|
||||||
|
startActivity(intent);
|
||||||
break;
|
break;
|
||||||
case R.id.action_invite:
|
case R.id.action_invite:
|
||||||
startActivityForResult(ChooseContactActivity.create(activity, conversation), REQUEST_INVITE_TO_CONVERSATION);
|
startActivityForResult(ChooseContactActivity.create(activity, conversation), REQUEST_INVITE_TO_CONVERSATION);
|
||||||
|
@ -1377,6 +1365,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show();
|
Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Contact contact = conversation.getContact();
|
final Contact contact = conversation.getContact();
|
||||||
if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
|
if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
|
||||||
triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action);
|
triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action);
|
||||||
|
@ -1588,7 +1577,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
if (writeGranted(grantResults, permissions)) {
|
if (writeGranted(grantResults, permissions)) {
|
||||||
if (activity != null && activity.xmppConnectionService != null) {
|
if (activity != null && activity.xmppConnectionService != null) {
|
||||||
activity.xmppConnectionService.getBitmapCache().evictAll();
|
|
||||||
activity.xmppConnectionService.restartFileObserver();
|
activity.xmppConnectionService.restartFileObserver();
|
||||||
}
|
}
|
||||||
refresh();
|
refresh();
|
||||||
|
@ -1627,9 +1615,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
protected void clearHistoryDialog(final Conversation conversation) {
|
protected void clearHistoryDialog(final Conversation conversation) {
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||||
builder.setTitle(getString(R.string.clear_conversation_history));
|
builder.setTitle(getString(R.string.clear_conversation_history));
|
||||||
final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
|
final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
|
||||||
final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox);
|
final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox);
|
||||||
builder.setView(dialogView);
|
builder.setView(dialogView);
|
||||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||||
|
@ -1647,7 +1635,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void muteConversationDialog(final Conversation conversation) {
|
protected void muteConversationDialog(final Conversation conversation) {
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||||
builder.setTitle(R.string.disable_notifications);
|
builder.setTitle(R.string.disable_notifications);
|
||||||
final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
|
final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
|
||||||
final CharSequence[] labels = new CharSequence[durations.length];
|
final CharSequence[] labels = new CharSequence[durations.length];
|
||||||
|
@ -1663,13 +1651,13 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
if (durations[which] == -1) {
|
if (durations[which] == -1) {
|
||||||
till = Long.MAX_VALUE;
|
till = Long.MAX_VALUE;
|
||||||
} else {
|
} else {
|
||||||
till = System.currentTimeMillis() + (durations[which] * 1000L);
|
till = System.currentTimeMillis() + (durations[which] * 1000);
|
||||||
}
|
}
|
||||||
conversation.setMutedTill(till);
|
conversation.setMutedTill(till);
|
||||||
activity.xmppConnectionService.updateConversation(conversation);
|
activity.xmppConnectionService.updateConversation(conversation);
|
||||||
activity.onConversationsListItemUpdated();
|
activity.onConversationsListItemUpdated();
|
||||||
refresh();
|
refresh();
|
||||||
requireActivity().invalidateOptionsMenu();
|
getActivity().invalidateOptionsMenu();
|
||||||
});
|
});
|
||||||
builder.create().show();
|
builder.create().show();
|
||||||
}
|
}
|
||||||
|
@ -1701,7 +1689,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
this.activity.xmppConnectionService.updateConversation(conversation);
|
this.activity.xmppConnectionService.updateConversation(conversation);
|
||||||
this.activity.onConversationsListItemUpdated();
|
this.activity.onConversationsListItemUpdated();
|
||||||
refresh();
|
refresh();
|
||||||
requireActivity().invalidateOptionsMenu();
|
getActivity().invalidateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1711,7 +1699,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
switch (attachmentChoice) {
|
switch (attachmentChoice) {
|
||||||
case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
|
case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
|
||||||
intent.setAction(Intent.ACTION_GET_CONTENT);
|
intent.setAction(Intent.ACTION_GET_CONTENT);
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||||
|
}
|
||||||
intent.setType("image/*");
|
intent.setType("image/*");
|
||||||
chooser = true;
|
chooser = true;
|
||||||
break;
|
break;
|
||||||
|
@ -1729,7 +1719,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
case ATTACHMENT_CHOICE_CHOOSE_FILE:
|
case ATTACHMENT_CHOICE_CHOOSE_FILE:
|
||||||
chooser = true;
|
chooser = true;
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||||
|
}
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
intent.setAction(Intent.ACTION_GET_CONTENT);
|
intent.setAction(Intent.ACTION_GET_CONTENT);
|
||||||
break;
|
break;
|
||||||
|
@ -1812,7 +1804,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showErrorMessage(final Message message) {
|
private void showErrorMessage(final Message message) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||||
builder.setTitle(R.string.error_message);
|
builder.setTitle(R.string.error_message);
|
||||||
final String errorMessage = message.getErrorMessage();
|
final String errorMessage = message.getErrorMessage();
|
||||||
final String[] errorMessageParts = errorMessage == null ? new String[0] : errorMessage.split("\\u001f");
|
final String[] errorMessageParts = errorMessage == null ? new String[0] : errorMessage.split("\\u001f");
|
||||||
|
@ -1833,7 +1825,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
|
|
||||||
|
|
||||||
private void deleteFile(final Message message) {
|
private void deleteFile(final Message message) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||||
builder.setNegativeButton(R.string.cancel, null);
|
builder.setNegativeButton(R.string.cancel, null);
|
||||||
builder.setTitle(R.string.delete_file_dialog);
|
builder.setTitle(R.string.delete_file_dialog);
|
||||||
builder.setMessage(R.string.delete_file_dialog_msg);
|
builder.setMessage(R.string.delete_file_dialog_msg);
|
||||||
|
@ -1862,7 +1854,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
if (!message.hasFileOnRemoteHost()
|
if (!message.hasFileOnRemoteHost()
|
||||||
&& xmppConnection != null
|
&& xmppConnection != null
|
||||||
&& conversation.getMode() == Conversational.MODE_SINGLE
|
&& conversation.getMode() == Conversational.MODE_SINGLE
|
||||||
&& !xmppConnection.getFeatures().httpUpload(message.getFileParams().getSize())) {
|
&& !xmppConnection.getFeatures().httpUpload(message.getFileParams().size)) {
|
||||||
activity.selectPresence(conversation, () -> {
|
activity.selectPresence(conversation, () -> {
|
||||||
message.setCounterpart(conversation.getNextCounterpart());
|
message.setCounterpart(conversation.getNextCounterpart());
|
||||||
activity.xmppConnectionService.resendFailedMessages(message);
|
activity.xmppConnectionService.resendFailedMessages(message);
|
||||||
|
@ -1967,7 +1959,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NotNull Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
if (conversation != null) {
|
if (conversation != null) {
|
||||||
outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid());
|
outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid());
|
||||||
|
@ -2191,14 +2183,13 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE);
|
final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE);
|
||||||
final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
|
final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
|
||||||
final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, 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);
|
final List<Uri> uris = extractUris(extras);
|
||||||
if (uris != null && uris.size() > 0) {
|
if (uris != null && uris.size() > 0) {
|
||||||
if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) {
|
if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) {
|
||||||
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION));
|
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION));
|
||||||
} else {
|
} else {
|
||||||
final List<Uri> cleanedUris = cleanUris(new ArrayList<>(uris));
|
final List<Uri> cleanedUris = cleanUris(new ArrayList<>(uris));
|
||||||
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type));
|
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris));
|
||||||
}
|
}
|
||||||
toggleInputMethod();
|
toggleInputMethod();
|
||||||
return;
|
return;
|
||||||
|
@ -3057,12 +3048,4 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
activity.switchToAccount(message.getConversation().getAccount(), fingerprint);
|
activity.switchToAccount(message.getConversation().getAccount(), fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Activity requireActivity() {
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
if (activity == null) {
|
|
||||||
throw new IllegalStateException("Activity not attached");
|
|
||||||
}
|
|
||||||
return activity;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,6 @@
|
||||||
package eu.siacs.conversations.ui;
|
package eu.siacs.conversations.ui;
|
||||||
|
|
||||||
|
|
||||||
import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
|
@ -67,16 +65,13 @@ import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.crypto.OmemoSetting;
|
import eu.siacs.conversations.crypto.OmemoSetting;
|
||||||
import eu.siacs.conversations.databinding.ActivityConversationsBinding;
|
import eu.siacs.conversations.databinding.ActivityConversationsBinding;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Conversational;
|
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
|
import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
|
||||||
import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
|
import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
|
||||||
import eu.siacs.conversations.ui.interfaces.OnConversationRead;
|
import eu.siacs.conversations.ui.interfaces.OnConversationRead;
|
||||||
import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
|
import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
|
||||||
import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
|
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.ActivityResult;
|
||||||
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
|
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
|
||||||
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
|
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
|
||||||
|
@ -88,6 +83,8 @@ import eu.siacs.conversations.utils.XmppUri;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
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 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";
|
public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
|
||||||
|
@ -99,7 +96,6 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
|
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 EXTRA_POST_INIT_ACTION = "post_init_action";
|
||||||
public static final String POST_ACTION_RECORD_VOICE = "record_voice";
|
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(
|
private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
|
||||||
ACTION_VIEW_CONVERSATION,
|
ACTION_VIEW_CONVERSATION,
|
||||||
|
@ -282,7 +278,6 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
|
UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
|
||||||
if (grantResults.length > 0) {
|
if (grantResults.length > 0) {
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
@ -430,18 +425,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openConversation(Conversation conversation, Bundle extras) {
|
private void openConversation(Conversation conversation, Bundle extras) {
|
||||||
final FragmentManager fragmentManager = getFragmentManager();
|
ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment);
|
||||||
executePendingTransactions(fragmentManager);
|
|
||||||
ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
|
|
||||||
final boolean mainNeedsRefresh;
|
final boolean mainNeedsRefresh;
|
||||||
if (conversationFragment == null) {
|
if (conversationFragment == null) {
|
||||||
mainNeedsRefresh = false;
|
mainNeedsRefresh = false;
|
||||||
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
|
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
|
||||||
if (mainFragment instanceof ConversationFragment) {
|
if (mainFragment instanceof ConversationFragment) {
|
||||||
conversationFragment = (ConversationFragment) mainFragment;
|
conversationFragment = (ConversationFragment) mainFragment;
|
||||||
} else {
|
} else {
|
||||||
conversationFragment = new ConversationFragment();
|
conversationFragment = new ConversationFragment();
|
||||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
|
||||||
fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
|
fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
|
||||||
fragmentTransaction.addToBackStack(null);
|
fragmentTransaction.addToBackStack(null);
|
||||||
try {
|
try {
|
||||||
|
@ -463,14 +456,6 @@ 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) {
|
public boolean onXmppUriClicked(Uri uri) {
|
||||||
XmppUri xmppUri = new XmppUri(uri);
|
XmppUri xmppUri = new XmppUri(uri);
|
||||||
if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
|
if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
|
||||||
|
@ -539,7 +524,6 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
|
||||||
final int theme = findTheme();
|
final int theme = findTheme();
|
||||||
if (this.mTheme != theme) {
|
if (this.mTheme != theme) {
|
||||||
this.mSkipBackgroundBinding = true;
|
this.mSkipBackgroundBinding = true;
|
||||||
|
@ -548,6 +532,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
this.mSkipBackgroundBinding = false;
|
this.mSkipBackgroundBinding = false;
|
||||||
}
|
}
|
||||||
mRedirectInProcess.set(false);
|
mRedirectInProcess.set(false);
|
||||||
|
super.onStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -577,18 +562,17 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeFragments() {
|
private void initializeFragments() {
|
||||||
final FragmentManager fragmentManager = getFragmentManager();
|
FragmentTransaction transaction = getFragmentManager().beginTransaction();
|
||||||
FragmentTransaction transaction = fragmentManager.beginTransaction();
|
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
|
||||||
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
|
Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
|
||||||
final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
|
|
||||||
if (mainFragment != null) {
|
if (mainFragment != null) {
|
||||||
if (binding.secondaryFragment != null) {
|
if (binding.secondaryFragment != null) {
|
||||||
if (mainFragment instanceof ConversationFragment) {
|
if (mainFragment instanceof ConversationFragment) {
|
||||||
getFragmentManager().popBackStack();
|
getFragmentManager().popBackStack();
|
||||||
transaction.remove(mainFragment);
|
transaction.remove(mainFragment);
|
||||||
transaction.commit();
|
transaction.commit();
|
||||||
fragmentManager.executePendingTransactions();
|
getFragmentManager().executePendingTransactions();
|
||||||
transaction = fragmentManager.beginTransaction();
|
transaction = getFragmentManager().beginTransaction();
|
||||||
transaction.replace(R.id.secondary_fragment, mainFragment);
|
transaction.replace(R.id.secondary_fragment, mainFragment);
|
||||||
transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
|
transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
|
||||||
transaction.commit();
|
transaction.commit();
|
||||||
|
@ -599,7 +583,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
transaction.remove(secondaryFragment);
|
transaction.remove(secondaryFragment);
|
||||||
transaction.commit();
|
transaction.commit();
|
||||||
getFragmentManager().executePendingTransactions();
|
getFragmentManager().executePendingTransactions();
|
||||||
transaction = fragmentManager.beginTransaction();
|
transaction = getFragmentManager().beginTransaction();
|
||||||
transaction.replace(R.id.main_fragment, secondaryFragment);
|
transaction.replace(R.id.main_fragment, secondaryFragment);
|
||||||
transaction.addToBackStack(null);
|
transaction.addToBackStack(null);
|
||||||
transaction.commit();
|
transaction.commit();
|
||||||
|
@ -617,38 +601,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
|
|
||||||
private void invalidateActionBarTitle() {
|
private void invalidateActionBarTitle() {
|
||||||
final ActionBar actionBar = getSupportActionBar();
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
if (actionBar == null) {
|
if (actionBar != null) {
|
||||||
return;
|
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
|
||||||
}
|
if (mainFragment instanceof ConversationFragment) {
|
||||||
final FragmentManager fragmentManager = getFragmentManager();
|
final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
|
||||||
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
|
if (conversation != null) {
|
||||||
if (mainFragment instanceof ConversationFragment) {
|
actionBar.setTitle(EmojiWrapper.transform(conversation.getName()));
|
||||||
final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
if (conversation != null) {
|
return;
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -657,18 +621,17 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
if (performRedirectIfNecessary(conversation, false)) {
|
if (performRedirectIfNecessary(conversation, false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final FragmentManager fragmentManager = getFragmentManager();
|
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
|
||||||
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
|
|
||||||
if (mainFragment instanceof ConversationFragment) {
|
if (mainFragment instanceof ConversationFragment) {
|
||||||
try {
|
try {
|
||||||
fragmentManager.popBackStack();
|
getFragmentManager().popBackStack();
|
||||||
} catch (final IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", 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
|
//this usually means activity is no longer active; meaning on the next open we will run through this again
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
|
Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
|
||||||
if (secondaryFragment instanceof ConversationFragment) {
|
if (secondaryFragment instanceof ConversationFragment) {
|
||||||
if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
|
if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
|
||||||
Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);
|
Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);
|
||||||
|
|
|
@ -39,7 +39,6 @@ import eu.siacs.conversations.services.QuickConversationsService;
|
||||||
import eu.siacs.conversations.ui.util.LocationHelper;
|
import eu.siacs.conversations.ui.util.LocationHelper;
|
||||||
import eu.siacs.conversations.ui.widget.Marker;
|
import eu.siacs.conversations.ui.widget.Marker;
|
||||||
import eu.siacs.conversations.ui.widget.MyLocation;
|
import eu.siacs.conversations.ui.widget.MyLocation;
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
import eu.siacs.conversations.utils.ThemeHelper;
|
import eu.siacs.conversations.utils.ThemeHelper;
|
||||||
|
|
||||||
public abstract class LocationActivity extends ActionBarActivity implements LocationListener {
|
public abstract class LocationActivity extends ActionBarActivity implements LocationListener {
|
||||||
|
@ -69,7 +68,6 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void updateLocationMarkers() {
|
protected void updateLocationMarkers() {
|
||||||
clearMarkers();
|
clearMarkers();
|
||||||
}
|
}
|
||||||
|
@ -224,7 +222,6 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
Configuration.getInstance().load(this, getPreferences());
|
Configuration.getInstance().load(this, getPreferences());
|
||||||
map.onResume();
|
map.onResume();
|
||||||
this.setMyLoc(null);
|
this.setMyLoc(null);
|
||||||
|
|
|
@ -39,7 +39,6 @@ import java.util.logging.Logger;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.MTMDecision;
|
import eu.siacs.conversations.entities.MTMDecision;
|
||||||
import eu.siacs.conversations.services.MemorizingTrustManager;
|
import eu.siacs.conversations.services.MemorizingTrustManager;
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
import eu.siacs.conversations.utils.ThemeHelper;
|
import eu.siacs.conversations.utils.ThemeHelper;
|
||||||
|
|
||||||
public class MemorizingActivity extends AppCompatActivity implements OnClickListener, OnCancelListener {
|
public class MemorizingActivity extends AppCompatActivity implements OnClickListener, OnCancelListener {
|
||||||
|
@ -62,8 +61,6 @@ public class MemorizingActivity extends AppCompatActivity implements OnClickList
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
|
|
||||||
Intent i = getIntent();
|
Intent i = getIntent();
|
||||||
decisionId = i.getIntExtra(MemorizingTrustManager.DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
|
decisionId = i.getIntExtra(MemorizingTrustManager.DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
|
||||||
int titleId = i.getIntExtra(MemorizingTrustManager.DECISION_TITLE_ID, R.string.mtm_accept_cert);
|
int titleId = i.getIntExtra(MemorizingTrustManager.DECISION_TITLE_ID, R.string.mtm_accept_cert);
|
||||||
|
|
|
@ -28,7 +28,6 @@ import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.databinding.ActivityRecordingBinding;
|
import eu.siacs.conversations.databinding.ActivityRecordingBinding;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
import eu.siacs.conversations.utils.ThemeHelper;
|
import eu.siacs.conversations.utils.ThemeHelper;
|
||||||
import eu.siacs.conversations.utils.TimeFrameUtils;
|
import eu.siacs.conversations.utils.TimeFrameUtils;
|
||||||
|
|
||||||
|
@ -67,12 +66,6 @@ public class RecordingActivity extends Activity implements View.OnClickListener
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume(){
|
|
||||||
super.onResume();
|
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
package eu.siacs.conversations.ui;
|
package eu.siacs.conversations.ui;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
|
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.PictureInPictureParams;
|
import android.app.PictureInPictureParams;
|
||||||
|
@ -38,7 +35,6 @@ import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
|
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
|
||||||
import org.webrtc.RendererCommon;
|
|
||||||
import org.webrtc.SurfaceViewRenderer;
|
import org.webrtc.SurfaceViewRenderer;
|
||||||
import org.webrtc.VideoTrack;
|
import org.webrtc.VideoTrack;
|
||||||
|
|
||||||
|
@ -58,7 +54,6 @@ import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||||
import eu.siacs.conversations.ui.util.MainThreadExecutor;
|
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.PermissionUtils;
|
||||||
import eu.siacs.conversations.utils.TimeFrameUtils;
|
import eu.siacs.conversations.utils.TimeFrameUtils;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
@ -69,7 +64,10 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
||||||
import eu.siacs.conversations.xmpp.jingle.Media;
|
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||||
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
|
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
|
||||||
|
|
||||||
public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
|
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
|
public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate {
|
||||||
|
|
||||||
public static final String EXTRA_WITH = "with";
|
public static final String EXTRA_WITH = "with";
|
||||||
public static final String EXTRA_SESSION_ID = "session_id";
|
public static final String EXTRA_SESSION_ID = "session_id";
|
||||||
|
@ -83,7 +81,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
|
|
||||||
private static final List<RtpEndUserState> END_CARD = Arrays.asList(
|
private static final List<RtpEndUserState> END_CARD = Arrays.asList(
|
||||||
RtpEndUserState.APPLICATION_ERROR,
|
RtpEndUserState.APPLICATION_ERROR,
|
||||||
RtpEndUserState.SECURITY_ERROR,
|
|
||||||
RtpEndUserState.DECLINED_OR_BUSY,
|
RtpEndUserState.DECLINED_OR_BUSY,
|
||||||
RtpEndUserState.CONNECTIVITY_ERROR,
|
RtpEndUserState.CONNECTIVITY_ERROR,
|
||||||
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
|
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
|
||||||
|
@ -91,22 +88,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
);
|
);
|
||||||
private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON = Arrays.asList(
|
private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON = Arrays.asList(
|
||||||
RtpEndUserState.APPLICATION_ERROR,
|
RtpEndUserState.APPLICATION_ERROR,
|
||||||
RtpEndUserState.CONNECTIVITY_ERROR,
|
RtpEndUserState.CONNECTIVITY_ERROR
|
||||||
RtpEndUserState.SECURITY_ERROR
|
|
||||||
);
|
);
|
||||||
private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
|
private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
|
||||||
RtpEndUserState.CONNECTING,
|
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 String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
|
||||||
private static final int REQUEST_ACCEPT_CALL = 0x1111;
|
private static final int REQUEST_ACCEPT_CALL = 0x1111;
|
||||||
|
@ -457,14 +443,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
|
mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
|
||||||
this.binding.remoteVideo.setOnAspectRatioChanged(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
mHandler.removeCallbacks(mTickExecutor);
|
mHandler.removeCallbacks(mTickExecutor);
|
||||||
binding.remoteVideo.release();
|
binding.remoteVideo.release();
|
||||||
binding.remoteVideo.setOnAspectRatioChanged(null);
|
|
||||||
binding.localVideo.release();
|
binding.localVideo.release();
|
||||||
final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
|
final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
|
||||||
final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get();
|
final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get();
|
||||||
|
@ -512,7 +496,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
|
|
||||||
private boolean isConnected() {
|
private boolean isConnected() {
|
||||||
final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
|
final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
|
||||||
return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
|
return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean switchToPictureInPicture() {
|
private boolean switchToPictureInPicture() {
|
||||||
|
@ -528,12 +512,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
private void startPictureInPicture() {
|
private void startPictureInPicture() {
|
||||||
try {
|
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(
|
enterPictureInPictureMode(
|
||||||
new PictureInPictureParams.Builder()
|
new PictureInPictureParams.Builder()
|
||||||
.setAspectRatio(clippedRational)
|
.setAspectRatio(new Rational(10, 16))
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
} catch (final IllegalStateException e) {
|
} catch (final IllegalStateException e) {
|
||||||
|
@ -542,17 +523,6 @@ 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() {
|
private boolean deviceSupportsPictureInPicture() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||||
|
@ -645,8 +615,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
surfaceViewRenderer.setVisibility(View.VISIBLE);
|
surfaceViewRenderer.setVisibility(View.VISIBLE);
|
||||||
try {
|
try {
|
||||||
surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
|
surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
|
||||||
} catch (final IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
//Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
|
Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
|
||||||
}
|
}
|
||||||
surfaceViewRenderer.setEnableHardwareScaler(true);
|
surfaceViewRenderer.setEnableHardwareScaler(true);
|
||||||
}
|
}
|
||||||
|
@ -671,9 +641,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
case CONNECTED:
|
case CONNECTED:
|
||||||
setTitle(R.string.rtp_state_connected);
|
setTitle(R.string.rtp_state_connected);
|
||||||
break;
|
break;
|
||||||
case RECONNECTING:
|
|
||||||
setTitle(R.string.rtp_state_reconnecting);
|
|
||||||
break;
|
|
||||||
case ACCEPTING_CALL:
|
case ACCEPTING_CALL:
|
||||||
setTitle(R.string.rtp_state_accepting_call);
|
setTitle(R.string.rtp_state_accepting_call);
|
||||||
break;
|
break;
|
||||||
|
@ -701,9 +668,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
case APPLICATION_ERROR:
|
case APPLICATION_ERROR:
|
||||||
setTitle(R.string.rtp_state_application_failure);
|
setTitle(R.string.rtp_state_application_failure);
|
||||||
break;
|
break;
|
||||||
case SECURITY_ERROR:
|
|
||||||
setTitle(R.string.rtp_state_security_error);
|
|
||||||
break;
|
|
||||||
case ENDED:
|
case ENDED:
|
||||||
throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
|
throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
|
||||||
default:
|
default:
|
||||||
|
@ -779,8 +743,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
RtpEndUserState.CONNECTIVITY_ERROR,
|
RtpEndUserState.CONNECTIVITY_ERROR,
|
||||||
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
|
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
|
||||||
RtpEndUserState.APPLICATION_ERROR,
|
RtpEndUserState.APPLICATION_ERROR,
|
||||||
RtpEndUserState.RETRACTED,
|
RtpEndUserState.RETRACTED
|
||||||
RtpEndUserState.SECURITY_ERROR
|
|
||||||
).contains(state)) {
|
).contains(state)) {
|
||||||
this.binding.rejectCall.setContentDescription(getString(R.string.exit));
|
this.binding.rejectCall.setContentDescription(getString(R.string.exit));
|
||||||
this.binding.rejectCall.setOnClickListener(this::exit);
|
this.binding.rejectCall.setOnClickListener(this::exit);
|
||||||
|
@ -816,7 +779,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
|
private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
|
||||||
if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) {
|
if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
|
||||||
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
|
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
|
||||||
if (media.contains(Media.VIDEO)) {
|
if (media.contains(Media.VIDEO)) {
|
||||||
final JingleRtpConnection rtpConnection = requireRtpConnection();
|
final JingleRtpConnection rtpConnection = requireRtpConnection();
|
||||||
|
@ -944,11 +907,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
this.binding.duration.setVisibility(View.GONE);
|
this.binding.duration.setVisibility(View.GONE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (connection.zeroDuration()) {
|
final long rtpConnectionStarted = connection.getRtpConnectionStarted();
|
||||||
this.binding.duration.setVisibility(View.GONE);
|
final long rtpConnectionEnded = connection.getRtpConnectionEnded();
|
||||||
} else {
|
if (rtpConnectionStarted != 0) {
|
||||||
this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
|
final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded;
|
||||||
|
this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false));
|
||||||
this.binding.duration.setVisibility(View.VISIBLE);
|
this.binding.duration.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
this.binding.duration.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -956,17 +922,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
|
if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
|
||||||
binding.localVideo.setVisibility(View.GONE);
|
binding.localVideo.setVisibility(View.GONE);
|
||||||
binding.localVideo.release();
|
binding.localVideo.release();
|
||||||
binding.remoteVideoWrapper.setVisibility(View.GONE);
|
binding.remoteVideo.setVisibility(View.GONE);
|
||||||
binding.remoteVideo.release();
|
binding.remoteVideo.release();
|
||||||
binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
|
binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
|
||||||
if (isPictureInPicture()) {
|
if (isPictureInPicture()) {
|
||||||
binding.appBarLayout.setVisibility(View.GONE);
|
binding.appBarLayout.setVisibility(View.GONE);
|
||||||
binding.pipPlaceholder.setVisibility(View.VISIBLE);
|
binding.pipPlaceholder.setVisibility(View.VISIBLE);
|
||||||
if (Arrays.asList(
|
if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) {
|
||||||
RtpEndUserState.APPLICATION_ERROR,
|
|
||||||
RtpEndUserState.CONNECTIVITY_ERROR,
|
|
||||||
RtpEndUserState.SECURITY_ERROR)
|
|
||||||
.contains(state)) {
|
|
||||||
binding.pipWarning.setVisibility(View.VISIBLE);
|
binding.pipWarning.setVisibility(View.VISIBLE);
|
||||||
binding.pipWaiting.setVisibility(View.GONE);
|
binding.pipWaiting.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
|
@ -980,9 +942,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
|
if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
|
||||||
binding.localVideo.setVisibility(View.GONE);
|
binding.localVideo.setVisibility(View.GONE);
|
||||||
binding.remoteVideoWrapper.setVisibility(View.GONE);
|
binding.remoteVideo.setVisibility(View.GONE);
|
||||||
binding.appBarLayout.setVisibility(View.GONE);
|
binding.appBarLayout.setVisibility(View.GONE);
|
||||||
binding.pipPlaceholder.setVisibility(View.VISIBLE);
|
binding.pipPlaceholder.setVisibility(View.VISIBLE);
|
||||||
binding.pipWarning.setVisibility(View.GONE);
|
binding.pipWarning.setVisibility(View.GONE);
|
||||||
|
@ -1004,18 +966,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
if (remoteVideoTrack.isPresent()) {
|
if (remoteVideoTrack.isPresent()) {
|
||||||
ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
|
ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
|
||||||
addSink(remoteVideoTrack.get(), binding.remoteVideo);
|
addSink(remoteVideoTrack.get(), binding.remoteVideo);
|
||||||
binding.remoteVideo.setScalingType(
|
|
||||||
RendererCommon.ScalingType.SCALE_ASPECT_FILL,
|
|
||||||
RendererCommon.ScalingType.SCALE_ASPECT_FIT
|
|
||||||
);
|
|
||||||
if (state == RtpEndUserState.CONNECTED) {
|
if (state == RtpEndUserState.CONNECTED) {
|
||||||
binding.appBarLayout.setVisibility(View.GONE);
|
binding.appBarLayout.setVisibility(View.GONE);
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
} else {
|
||||||
binding.appBarLayout.setVisibility(View.VISIBLE);
|
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
binding.remoteVideoWrapper.setVisibility(View.GONE);
|
binding.remoteVideo.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
|
if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
|
||||||
binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
|
binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
|
||||||
|
@ -1024,7 +980,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
binding.remoteVideoWrapper.setVisibility(View.GONE);
|
binding.remoteVideo.setVisibility(View.GONE);
|
||||||
binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
|
binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,6 @@ import java.util.Map;
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.ui.service.CameraManager;
|
import eu.siacs.conversations.ui.service.CameraManager;
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
import eu.siacs.conversations.ui.widget.ScannerView;
|
import eu.siacs.conversations.ui.widget.ScannerView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -182,7 +181,6 @@ public final class ScanActivity extends Activity implements SurfaceTextureListen
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
maybeOpenCamera();
|
maybeOpenCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,6 @@ import eu.siacs.conversations.services.MemorizingTrustManager;
|
||||||
import eu.siacs.conversations.services.QuickConversationsService;
|
import eu.siacs.conversations.services.QuickConversationsService;
|
||||||
import eu.siacs.conversations.ui.util.StyledAttributes;
|
import eu.siacs.conversations.ui.util.StyledAttributes;
|
||||||
import eu.siacs.conversations.utils.GeoHelper;
|
import eu.siacs.conversations.utils.GeoHelper;
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
import eu.siacs.conversations.utils.TimeFrameUtils;
|
import eu.siacs.conversations.utils.TimeFrameUtils;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
|
@ -58,10 +57,8 @@ public class SettingsActivity extends XmppActivity implements
|
||||||
public static final String THEME = "theme";
|
public static final String THEME = "theme";
|
||||||
public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
|
public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
|
||||||
public static final String OMEMO_SETTING = "omemo";
|
public static final String OMEMO_SETTING = "omemo";
|
||||||
public static final String PREVENT_SCREENSHOTS = "prevent_screenshots";
|
|
||||||
|
|
||||||
public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
|
public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
|
||||||
|
|
||||||
private SettingsFragment mSettingsFragment;
|
private SettingsFragment mSettingsFragment;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -396,15 +393,8 @@ public class SettingsActivity extends XmppActivity implements
|
||||||
if (this.mTheme != theme) {
|
if (this.mTheme != theme) {
|
||||||
recreate();
|
recreate();
|
||||||
}
|
}
|
||||||
} else if(name.equals(PREVENT_SCREENSHOTS)){
|
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume(){
|
|
||||||
super.onResume();
|
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -13,13 +13,10 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.databinding.DataBindingUtil;
|
import androidx.databinding.DataBindingUtil;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.common.math.DoubleMath;
|
|
||||||
|
|
||||||
import org.osmdroid.api.IGeoPoint;
|
import org.osmdroid.api.IGeoPoint;
|
||||||
import org.osmdroid.util.GeoPoint;
|
import org.osmdroid.util.GeoPoint;
|
||||||
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.databinding.ActivityShareLocationBinding;
|
import eu.siacs.conversations.databinding.ActivityShareLocationBinding;
|
||||||
|
@ -31,213 +28,213 @@ import eu.siacs.conversations.utils.ThemeHelper;
|
||||||
|
|
||||||
public class ShareLocationActivity extends LocationActivity implements LocationListener {
|
public class ShareLocationActivity extends LocationActivity implements LocationListener {
|
||||||
|
|
||||||
private Snackbar snackBar;
|
private Snackbar snackBar;
|
||||||
private ActivityShareLocationBinding binding;
|
private ActivityShareLocationBinding binding;
|
||||||
private boolean marker_fixed_to_loc = false;
|
private boolean marker_fixed_to_loc = false;
|
||||||
private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
|
private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
|
||||||
private Boolean noAskAgain = false;
|
private Boolean noAskAgain = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
|
|
||||||
outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
|
outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
super.onRestoreInstanceState(savedInstanceState);
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
|
|
||||||
if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
|
if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
|
||||||
this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
|
this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_location);
|
this.binding = DataBindingUtil.setContentView(this,R.layout.activity_share_location);
|
||||||
setSupportActionBar(binding.toolbar);
|
setSupportActionBar(binding.toolbar);
|
||||||
configureActionBar(getSupportActionBar());
|
configureActionBar(getSupportActionBar());
|
||||||
setupMapView(binding.map, LocationProvider.getGeoPoint(this));
|
setupMapView(binding.map, LocationProvider.getGeoPoint(this));
|
||||||
|
|
||||||
this.binding.cancelButton.setOnClickListener(view -> {
|
this.binding.cancelButton.setOnClickListener(view -> {
|
||||||
setResult(RESULT_CANCELED);
|
setResult(RESULT_CANCELED);
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
|
this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
|
||||||
this.snackBar.setAction(R.string.enable, view -> {
|
this.snackBar.setAction(R.string.enable, view -> {
|
||||||
if (isLocationEnabledAndAllowed()) {
|
if (isLocationEnabledAndAllowed()) {
|
||||||
updateUi();
|
updateUi();
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
|
||||||
requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
|
requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
|
||||||
} else if (!isLocationEnabled()) {
|
} else if (!isLocationEnabled()) {
|
||||||
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ThemeHelper.fix(this.snackBar);
|
ThemeHelper.fix(this.snackBar);
|
||||||
|
|
||||||
this.binding.shareButton.setOnClickListener(this::shareLocation);
|
this.binding.shareButton.setOnClickListener(view -> {
|
||||||
|
final Intent result = new Intent();
|
||||||
|
|
||||||
this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
|
if (marker_fixed_to_loc && myLoc != null) {
|
||||||
|
result.putExtra("latitude", myLoc.getLatitude());
|
||||||
|
result.putExtra("longitude", myLoc.getLongitude());
|
||||||
|
result.putExtra("altitude", myLoc.getAltitude());
|
||||||
|
result.putExtra("accuracy", (int) myLoc.getAccuracy());
|
||||||
|
} else {
|
||||||
|
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
|
||||||
|
result.putExtra("latitude", markerPoint.getLatitude());
|
||||||
|
result.putExtra("longitude", markerPoint.getLongitude());
|
||||||
|
}
|
||||||
|
|
||||||
this.binding.fab.setOnClickListener(view -> {
|
setResult(RESULT_OK, result);
|
||||||
if (!marker_fixed_to_loc) {
|
finish();
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void shareLocation(final View view) {
|
this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
|
||||||
final Intent result = new Intent();
|
|
||||||
if (marker_fixed_to_loc && myLoc != null) {
|
|
||||||
result.putExtra("latitude", myLoc.getLatitude());
|
|
||||||
result.putExtra("longitude", myLoc.getLongitude());
|
|
||||||
result.putExtra("altitude", myLoc.getAltitude());
|
|
||||||
result.putExtra("accuracy", DoubleMath.roundToInt(myLoc.getAccuracy(), RoundingMode.HALF_UP));
|
|
||||||
} else {
|
|
||||||
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
|
|
||||||
result.putExtra("latitude", markerPoint.getLatitude());
|
|
||||||
result.putExtra("longitude", markerPoint.getLongitude());
|
|
||||||
}
|
|
||||||
setResult(RESULT_OK, result);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
this.binding.fab.setOnClickListener(view -> {
|
||||||
public void onRequestPermissionsResult(final int requestCode,
|
if (!marker_fixed_to_loc) {
|
||||||
@NonNull final String[] permissions,
|
if (!isLocationEnabled()) {
|
||||||
@NonNull final int[] grantResults) {
|
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
requestPermissions(REQUEST_CODE_FAB_PRESSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleFixedLocation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (grantResults.length > 0 &&
|
@Override
|
||||||
grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
public void onRequestPermissionsResult(final int requestCode,
|
||||||
Build.VERSION.SDK_INT >= 23 &&
|
@NonNull final String[] permissions,
|
||||||
permissions.length > 0 &&
|
@NonNull final int[] grantResults) {
|
||||||
(
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
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()) {
|
if (grantResults.length > 0 &&
|
||||||
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||||
}
|
Build.VERSION.SDK_INT >= 23 &&
|
||||||
updateUi();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
|
||||||
protected void gotoLoc(final boolean setZoomLevel) {
|
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||||
if (this.myLoc != null && mapController != null) {
|
}
|
||||||
if (setZoomLevel) {
|
updateUi();
|
||||||
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
|
}
|
||||||
}
|
|
||||||
mapController.animateTo(new GeoPoint(this.myLoc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setMyLoc(final Location location) {
|
protected void gotoLoc(final boolean setZoomLevel) {
|
||||||
this.myLoc = location;
|
if (this.myLoc != null && mapController != null) {
|
||||||
}
|
if (setZoomLevel) {
|
||||||
|
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
|
||||||
|
}
|
||||||
|
mapController.animateTo(new GeoPoint(this.myLoc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void setMyLoc(final Location location) {
|
||||||
super.onPause();
|
this.myLoc = location;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateLocationMarkers() {
|
protected void onPause() {
|
||||||
super.updateLocationMarkers();
|
super.onPause();
|
||||||
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
|
@Override
|
||||||
public void onLocationChanged(final Location location) {
|
protected void updateLocationMarkers() {
|
||||||
if (this.myLoc == null) {
|
super.updateLocationMarkers();
|
||||||
this.marker_fixed_to_loc = true;
|
if (this.myLoc != null) {
|
||||||
}
|
this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
|
||||||
updateUi();
|
if (this.marker_fixed_to_loc) {
|
||||||
if (LocationHelper.isBetterLocation(location, this.myLoc)) {
|
this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
|
||||||
final Location oldLoc = this.myLoc;
|
} else {
|
||||||
this.myLoc = location;
|
this.binding.map.getOverlays().add(new Marker(marker_icon));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.binding.map.getOverlays().add(new Marker(marker_icon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't jump back to the users location if they're not moving (more or less).
|
@Override
|
||||||
if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) {
|
public void onLocationChanged(final Location location) {
|
||||||
gotoLoc();
|
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;
|
||||||
|
|
||||||
updateLocationMarkers();
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
updateLocationMarkers();
|
||||||
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
@Override
|
||||||
|
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
|
||||||
|
|
||||||
@Override
|
}
|
||||||
public void onProviderEnabled(final String provider) {
|
|
||||||
|
|
||||||
}
|
@Override
|
||||||
|
public void onProviderEnabled(final String provider) {
|
||||||
|
|
||||||
@Override
|
}
|
||||||
public void onProviderDisabled(final String provider) {
|
|
||||||
|
|
||||||
}
|
@Override
|
||||||
|
public void onProviderDisabled(final String provider) {
|
||||||
|
|
||||||
private boolean isLocationEnabledAndAllowed() {
|
}
|
||||||
return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleFixedLocation() {
|
private boolean isLocationEnabledAndAllowed() {
|
||||||
this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
|
return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
|
||||||
if (this.marker_fixed_to_loc) {
|
}
|
||||||
gotoLoc(false);
|
|
||||||
}
|
|
||||||
updateLocationMarkers();
|
|
||||||
updateUi();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
private void toggleFixedLocation() {
|
||||||
protected void updateUi() {
|
this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
|
||||||
if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
|
if (this.marker_fixed_to_loc) {
|
||||||
this.snackBar.dismiss();
|
gotoLoc(false);
|
||||||
} else {
|
}
|
||||||
this.snackBar.show();
|
updateLocationMarkers();
|
||||||
}
|
updateUi();
|
||||||
|
}
|
||||||
|
|
||||||
if (isLocationEnabledAndAllowed()) {
|
@Override
|
||||||
this.binding.fab.setVisibility(View.VISIBLE);
|
protected void updateUi() {
|
||||||
runOnUiThread(() -> {
|
if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
|
||||||
this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
|
this.snackBar.dismiss();
|
||||||
R.drawable.ic_gps_not_fixed_white_24dp);
|
} else {
|
||||||
this.binding.fab.setContentDescription(getResources().getString(
|
this.snackBar.show();
|
||||||
marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
|
}
|
||||||
));
|
|
||||||
this.binding.fab.invalidate();
|
if (isLocationEnabledAndAllowed()) {
|
||||||
});
|
this.binding.fab.setVisibility(View.VISIBLE);
|
||||||
} else {
|
runOnUiThread(() -> {
|
||||||
this.binding.fab.setVisibility(View.GONE);
|
this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
|
||||||
}
|
R.drawable.ic_gps_not_fixed_white_24dp);
|
||||||
}
|
this.binding.fab.setContentDescription(getResources().getString(
|
||||||
|
marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
|
||||||
|
));
|
||||||
|
this.binding.fab.invalidate();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.binding.fab.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -33,8 +33,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
||||||
refreshUi();
|
refreshUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Share {
|
private class Share {
|
||||||
public String type;
|
|
||||||
ArrayList<Uri> uris = new ArrayList<>();
|
ArrayList<Uri> uris = new ArrayList<>();
|
||||||
public String account;
|
public String account;
|
||||||
public String contact;
|
public String contact;
|
||||||
|
@ -66,7 +65,6 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
if (grantResults.length > 0)
|
if (grantResults.length > 0)
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
if (requestCode == REQUEST_STORAGE_PERMISSION) {
|
if (requestCode == REQUEST_STORAGE_PERMISSION) {
|
||||||
|
@ -141,7 +139,6 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
||||||
} else if (type != null && uri != null) {
|
} else if (type != null && uri != null) {
|
||||||
this.share.uris.clear();
|
this.share.uris.clear();
|
||||||
this.share.uris.add(uri);
|
this.share.uris.add(uri);
|
||||||
this.share.type = type;
|
|
||||||
} else {
|
} else {
|
||||||
this.share.text = text;
|
this.share.text = text;
|
||||||
this.share.asQuote = asQuote;
|
this.share.asQuote = asQuote;
|
||||||
|
@ -196,9 +193,6 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
||||||
intent.setAction(Intent.ACTION_SEND_MULTIPLE);
|
intent.setAction(Intent.ACTION_SEND_MULTIPLE);
|
||||||
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris);
|
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris);
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
if (share.type != null) {
|
|
||||||
intent.putExtra(ConversationsActivity.EXTRA_TYPE, share.type);
|
|
||||||
}
|
|
||||||
} else if (share.text != null) {
|
} else if (share.text != null) {
|
||||||
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
|
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, share.text);
|
intent.putExtra(Intent.EXTRA_TEXT, share.text);
|
||||||
|
|
|
@ -37,14 +37,10 @@ import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.MenuRes;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.widget.PopupMenu;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.databinding.DataBindingUtil;
|
import androidx.databinding.DataBindingUtil;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
@ -54,8 +50,6 @@ import androidx.viewpager.widget.PagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
import com.leinardi.android.speeddial.SpeedDialActionItem;
|
|
||||||
import com.leinardi.android.speeddial.SpeedDialView;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -271,7 +265,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
|
||||||
setSupportActionBar(binding.toolbar);
|
setSupportActionBar(binding.toolbar);
|
||||||
configureActionBar(getSupportActionBar());
|
configureActionBar(getSupportActionBar());
|
||||||
|
|
||||||
inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu);
|
binding.speedDial.inflate(R.menu.start_conversation_fab_submenu);
|
||||||
|
|
||||||
binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
|
binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
|
||||||
binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
|
binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -342,21 +337,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) {
|
|
||||||
speedDialView.clearActionItems();
|
|
||||||
final PopupMenu popupMenu = new PopupMenu(this, new View(this));
|
|
||||||
popupMenu.inflate(menuRes);
|
|
||||||
final Menu menu = popupMenu.getMenu();
|
|
||||||
for (int i = 0; i < menu.size(); i++) {
|
|
||||||
final MenuItem menuItem = menu.getItem(i);
|
|
||||||
final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
|
|
||||||
.setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null)
|
|
||||||
.setFabImageTintColor(ContextCompat.getColor(this, R.color.white))
|
|
||||||
.create();
|
|
||||||
speedDialView.addActionItem(actionItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isValidJid(String input) {
|
public static boolean isValidJid(String input) {
|
||||||
try {
|
try {
|
||||||
Jid jid = Jid.ofEscaped(input);
|
Jid jid = Jid.ofEscaped(input);
|
||||||
|
@ -545,8 +525,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
|
||||||
} else if (contact.showInRoster()) {
|
} else if (contact.showInRoster()) {
|
||||||
throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
|
throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
|
||||||
} else {
|
} else {
|
||||||
final String preAuth = invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
|
xmppConnectionService.createContact(contact, true);
|
||||||
xmppConnectionService.createContact(contact, true, preAuth);
|
|
||||||
if (invite != null && invite.hasFingerprints()) {
|
if (invite != null && invite.hasFingerprints()) {
|
||||||
xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
|
xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
|
||||||
}
|
}
|
||||||
|
@ -752,7 +731,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
if (mRequestedContactsPermission.compareAndSet(false, true)) {
|
if (mRequestedContactsPermission.compareAndSet(false, true)) {
|
||||||
if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
|
if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
final AtomicBoolean requestPermission = new AtomicBoolean(false);
|
final AtomicBoolean requestPermission = new AtomicBoolean(false);
|
||||||
builder.setTitle(R.string.sync_with_contacts);
|
builder.setTitle(R.string.sync_with_contacts);
|
||||||
|
@ -761,26 +740,20 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
|
||||||
} else {
|
} else {
|
||||||
builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name)));
|
builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name)));
|
||||||
}
|
}
|
||||||
@StringRes int confirmButtonText;
|
builder.setPositiveButton(R.string.next, (dialog, which) -> {
|
||||||
if (QuickConversationsService.isConversations()) {
|
|
||||||
confirmButtonText = R.string.next;
|
|
||||||
} else {
|
|
||||||
confirmButtonText = R.string.confirm;
|
|
||||||
}
|
|
||||||
builder.setPositiveButton(confirmButtonText, (dialog, which) -> {
|
|
||||||
if (requestPermission.compareAndSet(false, true)) {
|
if (requestPermission.compareAndSet(false, true)) {
|
||||||
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
|
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setOnDismissListener(dialog -> {
|
builder.setOnDismissListener(dialog -> {
|
||||||
if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) {
|
if (requestPermission.compareAndSet(false, true)) {
|
||||||
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
|
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setCancelable(QuickConversationsService.isQuicksy());
|
builder.setCancelable(false);
|
||||||
final AlertDialog dialog = builder.create();
|
final AlertDialog dialog = builder.create();
|
||||||
dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy());
|
dialog.setCanceledOnTouchOutside(false);
|
||||||
dialog.setOnShowListener(dialogInterface -> {
|
dialog.setOnShowListener(dialogInterface -> {
|
||||||
final TextView tv = dialog.findViewById(android.R.id.message);
|
final TextView tv = dialog.findViewById(android.R.id.message);
|
||||||
if (tv != null) {
|
if (tv != null) {
|
||||||
|
|
|
@ -7,39 +7,24 @@ import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.databinding.DataBindingUtil;
|
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.databinding.ActivityUriHandlerBinding;
|
|
||||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
|
||||||
import eu.siacs.conversations.persistance.DatabaseBackend;
|
import eu.siacs.conversations.persistance.DatabaseBackend;
|
||||||
import eu.siacs.conversations.services.QuickConversationsService;
|
import eu.siacs.conversations.services.QuickConversationsService;
|
||||||
import eu.siacs.conversations.utils.ProvisioningUtils;
|
import eu.siacs.conversations.utils.ProvisioningUtils;
|
||||||
import eu.siacs.conversations.utils.SignupUtils;
|
import eu.siacs.conversations.utils.SignupUtils;
|
||||||
import eu.siacs.conversations.utils.XmppUri;
|
import eu.siacs.conversations.utils.XmppUri;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import okhttp3.Call;
|
|
||||||
import okhttp3.Callback;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
public class UriHandlerActivity extends AppCompatActivity {
|
public class UriHandlerActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@ -49,9 +34,7 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
|
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
|
||||||
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790;
|
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790;
|
||||||
private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n");
|
private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n");
|
||||||
private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>");
|
private boolean handled = false;
|
||||||
private ActivityUriHandlerBinding binding;
|
|
||||||
private Call call;
|
|
||||||
|
|
||||||
public static void scan(final Activity activity) {
|
public static void scan(final Activity activity) {
|
||||||
scan(activity, false);
|
scan(activity, false);
|
||||||
|
@ -94,7 +77,9 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler);
|
this.handled = savedInstanceState != null && savedInstanceState.getBoolean("handled", false);
|
||||||
|
getLayoutInflater().inflate(R.layout.toolbar, findViewById(android.R.id.content));
|
||||||
|
setSupportActionBar(findViewById(R.id.toolbar));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -104,16 +89,21 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewIntent(final Intent intent) {
|
public void onSaveInstanceState(Bundle savedInstanceState) {
|
||||||
super.onNewIntent(intent);
|
savedInstanceState.putBoolean("handled", this.handled);
|
||||||
|
super.onSaveInstanceState(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewIntent(Intent intent) {
|
||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean handleUri(final Uri uri) {
|
private void handleUri(Uri uri) {
|
||||||
return handleUri(uri, false);
|
handleUri(uri, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean handleUri(final Uri uri, final boolean scanned) {
|
private void handleUri(Uri uri, final boolean scanned) {
|
||||||
final Intent intent;
|
final Intent intent;
|
||||||
final XmppUri xmppUri = new XmppUri(uri);
|
final XmppUri xmppUri = new XmppUri(uri);
|
||||||
final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
|
final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
|
||||||
|
@ -123,22 +113,19 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
final Jid jid = xmppUri.getJid();
|
final Jid jid = xmppUri.getJid();
|
||||||
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
|
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
|
||||||
if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
|
if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
|
||||||
showError(R.string.account_already_exists);
|
Toast.makeText(this, R.string.account_already_exists, Toast.LENGTH_LONG).show();
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
|
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
if (accounts.size() == 0 && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
|
if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
|
||||||
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
|
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
|
||||||
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
|
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
} else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
|
|
||||||
showError(R.string.account_registrations_are_not_supported);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accounts.size() == 0) {
|
if (accounts.size() == 0) {
|
||||||
|
@ -146,19 +133,26 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
intent = SignupUtils.getSignUpIntent(this);
|
intent = SignupUtils.getSignUpIntent(this);
|
||||||
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
|
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
showError(R.string.invalid_jid);
|
Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) {
|
if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) {
|
||||||
|
|
||||||
final Jid jid = xmppUri.getJid();
|
final Jid jid = xmppUri.getJid();
|
||||||
final String body = xmppUri.getBody();
|
final String body = xmppUri.getBody();
|
||||||
|
|
||||||
if (jid != null) {
|
if (jid != null) {
|
||||||
final Class<?> clazz = findShareViaAccountClass();
|
Class clazz;
|
||||||
|
try {
|
||||||
|
clazz = Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
clazz = null;
|
||||||
|
|
||||||
|
}
|
||||||
if (clazz != null) {
|
if (clazz != null) {
|
||||||
intent = new Intent(this, clazz);
|
intent = new Intent(this, clazz);
|
||||||
intent.putExtra("contact", jid.toEscapedString());
|
intent.putExtra("contact", jid.toEscapedString());
|
||||||
|
@ -169,6 +163,7 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
intent.setData(uri);
|
intent.setData(uri);
|
||||||
intent.putExtra("account", accounts.get(0).toEscapedString());
|
intent.putExtra("account", accounts.get(0).toEscapedString());
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
intent = new Intent(this, ShareWithActivity.class);
|
intent = new Intent(this, ShareWithActivity.class);
|
||||||
intent.setAction(Intent.ACTION_SEND);
|
intent.setAction(Intent.ACTION_SEND);
|
||||||
|
@ -188,94 +183,36 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
intent.putExtra("scanned", scanned);
|
intent.putExtra("scanned", scanned);
|
||||||
intent.setData(uri);
|
intent.setData(uri);
|
||||||
} else {
|
} else {
|
||||||
showError(R.string.invalid_jid);
|
Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
startActivity(intent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkForLinkHeader(final HttpUrl url) {
|
|
||||||
Log.d(Config.LOGTAG, "checking for link header on " + url);
|
|
||||||
this.call = HttpConnectionManager.OK_HTTP_CLIENT.newCall(new Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.head()
|
|
||||||
.build());
|
|
||||||
this.call.enqueue(new Callback() {
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NotNull Call call, @NotNull IOException e) {
|
|
||||||
Log.d(Config.LOGTAG, "unable to check HTTP url", e);
|
|
||||||
showError(R.string.no_xmpp_adddress_found);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NotNull Call call, @NotNull Response response) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
final String linkHeader = response.header("Link");
|
|
||||||
if (linkHeader != null && processLinkHeader(linkHeader)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showError(R.string.no_xmpp_adddress_found);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean processLinkHeader(final String header) {
|
|
||||||
final Matcher matcher = LINK_HEADER_PATTERN.matcher(header);
|
|
||||||
if (matcher.find()) {
|
|
||||||
final String group = matcher.group();
|
|
||||||
final String link = group.substring(1, group.length() - 1);
|
|
||||||
if (handleUri(Uri.parse(link))) {
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showError(@StringRes int error) {
|
|
||||||
this.binding.progress.setVisibility(View.INVISIBLE);
|
|
||||||
this.binding.error.setText(error);
|
|
||||||
this.binding.error.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Class<?> findShareViaAccountClass() {
|
|
||||||
try {
|
|
||||||
return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity");
|
|
||||||
} catch (final ClassNotFoundException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleIntent(final Intent data) {
|
|
||||||
final String action = data == null ? null : data.getAction();
|
|
||||||
if (action == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (action) {
|
|
||||||
case Intent.ACTION_MAIN:
|
startActivity(intent);
|
||||||
binding.progress.setVisibility(call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
|
|
||||||
break;
|
|
||||||
case Intent.ACTION_VIEW:
|
|
||||||
case Intent.ACTION_SENDTO:
|
|
||||||
if (handleUri(data.getData())) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ACTION_SCAN_QR_CODE:
|
|
||||||
Log.d(Config.LOGTAG, "scan. allow=" + allowProvisioning());
|
|
||||||
setIntent(createMainIntent());
|
|
||||||
startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN_QR_CODE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent createMainIntent() {
|
private void handleIntent(Intent data) {
|
||||||
final Intent intent = new Intent(Intent.ACTION_MAIN);
|
if (handled) {
|
||||||
intent.putExtra(EXTRA_ALLOW_PROVISIONING, allowProvisioning());
|
return;
|
||||||
return intent;
|
}
|
||||||
|
if (data == null || data.getAction() == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handled = true;
|
||||||
|
|
||||||
|
switch (data.getAction()) {
|
||||||
|
case Intent.ACTION_VIEW:
|
||||||
|
case Intent.ACTION_SENDTO:
|
||||||
|
handleUri(data.getData());
|
||||||
|
break;
|
||||||
|
case ACTION_SCAN_QR_CODE:
|
||||||
|
Intent intent = new Intent(this, ScanActivity.class);
|
||||||
|
startActivityForResult(intent, REQUEST_SCAN_QR_CODE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean allowProvisioning() {
|
private boolean allowProvisioning() {
|
||||||
|
@ -287,7 +224,6 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
|
public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
|
||||||
super.onActivityResult(requestCode, requestCode, intent);
|
super.onActivityResult(requestCode, requestCode, intent);
|
||||||
if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) {
|
if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) {
|
||||||
final boolean allowProvisioning = allowProvisioning();
|
|
||||||
final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
|
final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
|
||||||
if (Strings.isNullOrEmpty(result)) {
|
if (Strings.isNullOrEmpty(result)) {
|
||||||
finish();
|
finish();
|
||||||
|
@ -296,38 +232,22 @@ public class UriHandlerActivity extends AppCompatActivity {
|
||||||
if (result.startsWith("BEGIN:VCARD\n")) {
|
if (result.startsWith("BEGIN:VCARD\n")) {
|
||||||
final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result);
|
final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
if (handleUri(Uri.parse(matcher.group(2)), true)) {
|
handleUri(Uri.parse(matcher.group(2)), true);
|
||||||
finish();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showError(R.string.no_xmpp_adddress_found);
|
|
||||||
}
|
}
|
||||||
|
finish();
|
||||||
return;
|
return;
|
||||||
} else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) {
|
} else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning()) {
|
||||||
ProvisioningUtils.provision(this, result);
|
ProvisioningUtils.provision(this, result);
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Uri uri = Uri.parse(result.trim());
|
handleUri(Uri.parse(result), true);
|
||||||
if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
|
|
||||||
final HttpUrl httpUrl = HttpUrl.parse(uri.toString());
|
|
||||||
if (httpUrl != null) {
|
|
||||||
checkForLinkHeader(httpUrl);
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
} else if (handleUri(uri, true)) {
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
setIntent(new Intent(Intent.ACTION_VIEW, uri));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
}
|
||||||
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean looksLikeJsonObject(final String input) {
|
private static boolean looksLikeJsonObject(final String input) {
|
||||||
final String trimmed = Strings.nullToEmpty(input).trim();
|
final String trimmed = Strings.emptyToNull(input).trim();
|
||||||
return trimmed.charAt(0) == '{' && trimmed.charAt(trimmed.length() - 1) == '}';
|
return trimmed.charAt(0) == '{' && trimmed.charAt(trimmed.length() - 1) == '}';
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -80,7 +80,6 @@ import eu.siacs.conversations.ui.util.PresenceSelector;
|
||||||
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
|
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
|
||||||
import eu.siacs.conversations.utils.AccountUtils;
|
import eu.siacs.conversations.utils.AccountUtils;
|
||||||
import eu.siacs.conversations.utils.ExceptionHelper;
|
import eu.siacs.conversations.utils.ExceptionHelper;
|
||||||
import eu.siacs.conversations.ui.util.SettingsUtils;
|
|
||||||
import eu.siacs.conversations.utils.ThemeHelper;
|
import eu.siacs.conversations.utils.ThemeHelper;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
|
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
|
||||||
|
@ -820,9 +819,8 @@ public abstract class XmppActivity extends ActionBarActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume(){
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int findTheme() {
|
protected int findTheme() {
|
||||||
|
|
|
@ -92,8 +92,6 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
|
||||||
attr = R.attr.media_preview_document;
|
attr = R.attr.media_preview_document;
|
||||||
} else if (mime.equals("application/gpx+xml")) {
|
} else if (mime.equals("application/gpx+xml")) {
|
||||||
attr = R.attr.media_preview_tour;
|
attr = R.attr.media_preview_tour;
|
||||||
} else if (mime.startsWith("image/")) {
|
|
||||||
attr = R.attr.media_preview_image;
|
|
||||||
} else {
|
} else {
|
||||||
attr = R.attr.media_preview_unknown;
|
attr = R.attr.media_preview_unknown;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,10 +75,8 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
|
||||||
view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
try {
|
try {
|
||||||
context.startActivity(view);
|
context.startActivity(view);
|
||||||
} catch (final ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
|
||||||
} catch (final SecurityException e) {
|
|
||||||
Toast.makeText(context, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,6 @@ import eu.siacs.conversations.ui.text.DividerSpan;
|
||||||
import eu.siacs.conversations.ui.text.QuoteSpan;
|
import eu.siacs.conversations.ui.text.QuoteSpan;
|
||||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||||
import eu.siacs.conversations.ui.util.MyLinkify;
|
import eu.siacs.conversations.ui.util.MyLinkify;
|
||||||
import eu.siacs.conversations.ui.util.QuoteHelper;
|
|
||||||
import eu.siacs.conversations.ui.util.ViewUtil;
|
import eu.siacs.conversations.ui.util.ViewUtil;
|
||||||
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
|
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
@ -183,7 +182,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
|
&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
|
||||||
if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) {
|
if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) {
|
||||||
FileParams params = message.getFileParams();
|
FileParams params = message.getFileParams();
|
||||||
filesize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
|
filesize = params.size > 0 ? UIHelper.filesizeToString(params.size) : null;
|
||||||
if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) {
|
if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) {
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
|
@ -358,51 +357,48 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
*/
|
*/
|
||||||
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
|
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
|
||||||
boolean startsWithQuote = false;
|
boolean startsWithQuote = false;
|
||||||
int quoteDepth = 1;
|
char previous = '\n';
|
||||||
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
|
int lineStart = -1;
|
||||||
char previous = '\n';
|
int lineTextStart = -1;
|
||||||
int lineStart = -1;
|
int quoteStart = -1;
|
||||||
int lineTextStart = -1;
|
for (int i = 0; i <= body.length(); i++) {
|
||||||
int quoteStart = -1;
|
char current = body.length() > i ? body.charAt(i) : '\n';
|
||||||
for (int i = 0; i <= body.length(); i++) {
|
if (lineStart == -1) {
|
||||||
char current = body.length() > i ? body.charAt(i) : '\n';
|
if (previous == '\n') {
|
||||||
if (lineStart == -1) {
|
if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i))
|
||||||
if (previous == '\n') {
|
|| current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) {
|
||||||
if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
|
// Line start with quote
|
||||||
// Line start with quote
|
lineStart = i;
|
||||||
lineStart = i;
|
if (quoteStart == -1) quoteStart = i;
|
||||||
if (quoteStart == -1) quoteStart = i;
|
if (i == 0) startsWithQuote = true;
|
||||||
if (i == 0) startsWithQuote = true;
|
} else if (quoteStart >= 0) {
|
||||||
} else if (quoteStart >= 0) {
|
// Line start without quote, apply spans there
|
||||||
// Line start without quote, apply spans there
|
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
|
||||||
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
|
quoteStart = -1;
|
||||||
quoteStart = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Remove extra spaces between > and first character in the line
|
|
||||||
// > character will be removed too
|
|
||||||
if (current != ' ' && lineTextStart == -1) {
|
|
||||||
lineTextStart = i;
|
|
||||||
}
|
|
||||||
if (current == '\n') {
|
|
||||||
body.delete(lineStart, lineTextStart);
|
|
||||||
i -= lineTextStart - lineStart;
|
|
||||||
if (i == lineStart) {
|
|
||||||
// Avoid empty lines because span over empty line can be hidden
|
|
||||||
body.insert(i++, " ");
|
|
||||||
}
|
|
||||||
lineStart = -1;
|
|
||||||
lineTextStart = -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previous = current;
|
} else {
|
||||||
|
// Remove extra spaces between > and first character in the line
|
||||||
|
// > character will be removed too
|
||||||
|
if (current != ' ' && lineTextStart == -1) {
|
||||||
|
lineTextStart = i;
|
||||||
|
}
|
||||||
|
if (current == '\n') {
|
||||||
|
body.delete(lineStart, lineTextStart);
|
||||||
|
i -= lineTextStart - lineStart;
|
||||||
|
if (i == lineStart) {
|
||||||
|
// Avoid empty lines because span over empty line can be hidden
|
||||||
|
body.insert(i++, " ");
|
||||||
|
}
|
||||||
|
lineStart = -1;
|
||||||
|
lineTextStart = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (quoteStart >= 0) {
|
previous = current;
|
||||||
// Apply spans to finishing open quote
|
}
|
||||||
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
|
if (quoteStart >= 0) {
|
||||||
}
|
// Apply spans to finishing open quote
|
||||||
quoteDepth++;
|
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
|
||||||
}
|
}
|
||||||
return startsWithQuote;
|
return startsWithQuote;
|
||||||
}
|
}
|
||||||
|
@ -802,12 +798,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
} else if (message.treatAsDownloadable()) {
|
} else if (message.treatAsDownloadable()) {
|
||||||
try {
|
try {
|
||||||
final URI uri = new URI(message.getBody());
|
final URI uri = new URI(message.getBody());
|
||||||
displayDownloadableMessage(viewHolder,
|
displayDownloadableMessage(viewHolder,
|
||||||
message,
|
message,
|
||||||
activity.getString(R.string.check_x_filesize_on_host,
|
activity.getString(R.string.check_x_filesize_on_host,
|
||||||
UIHelper.getFileDescriptionString(activity, message),
|
UIHelper.getFileDescriptionString(activity, message),
|
||||||
uri.getHost()),
|
uri.getHost()),
|
||||||
darkBackground);
|
darkBackground);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
displayDownloadableMessage(viewHolder,
|
displayDownloadableMessage(viewHolder,
|
||||||
message,
|
message,
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
package eu.siacs.conversations.ui.util;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
|
|
||||||
public class ActionBarUtil {
|
|
||||||
|
|
||||||
public static void resetActionBarOnClickListeners(@NonNull View view) {
|
|
||||||
final View title = findActionBarTitle(view);
|
|
||||||
final View subtitle = findActionBarSubTitle(view);
|
|
||||||
if (title != null) {
|
|
||||||
title.setOnClickListener(null);
|
|
||||||
}
|
|
||||||
if (subtitle != null) {
|
|
||||||
subtitle.setOnClickListener(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setActionBarOnClickListener(@NonNull View view,
|
|
||||||
@NonNull final View.OnClickListener onClickListener) {
|
|
||||||
final View title = findActionBarTitle(view);
|
|
||||||
final View subtitle = findActionBarSubTitle(view);
|
|
||||||
if (title != null) {
|
|
||||||
title.setOnClickListener(onClickListener);
|
|
||||||
}
|
|
||||||
if (subtitle != null) {
|
|
||||||
subtitle.setOnClickListener(onClickListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @Nullable View findActionBarTitle(@NonNull View root) {
|
|
||||||
return findActionBarItem(root, "action_bar_title", "mTitleTextView");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @Nullable
|
|
||||||
View findActionBarSubTitle(@NonNull View root) {
|
|
||||||
return findActionBarItem(root, "action_bar_subtitle", "mSubtitleTextView");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @Nullable View findActionBarItem(@NonNull View root,
|
|
||||||
@NonNull String resourceName,
|
|
||||||
@NonNull String toolbarFieldName) {
|
|
||||||
View result = findViewSupportOrAndroid(root, resourceName);
|
|
||||||
|
|
||||||
if (result == null) {
|
|
||||||
View actionBar = findViewSupportOrAndroid(root, "action_bar");
|
|
||||||
if (actionBar != null) {
|
|
||||||
result = reflectiveRead(actionBar, toolbarFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result == null && root.getClass().getName().endsWith("widget.Toolbar")) {
|
|
||||||
result = reflectiveRead(root, toolbarFieldName);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
private static @Nullable View findViewSupportOrAndroid(@NonNull View root,
|
|
||||||
@NonNull String resourceName) {
|
|
||||||
Context context = root.getContext();
|
|
||||||
View result = null;
|
|
||||||
if (result == null) {
|
|
||||||
int supportID = context.getResources().getIdentifier(resourceName, "id", context.getPackageName());
|
|
||||||
result = root.findViewById(supportID);
|
|
||||||
}
|
|
||||||
if (result == null) {
|
|
||||||
int androidID = context.getResources().getIdentifier(resourceName, "id", "android");
|
|
||||||
result = root.findViewById(androidID);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static <T> T reflectiveRead(@NonNull Object object, @NonNull String fieldName) {
|
|
||||||
try {
|
|
||||||
Field field = object.getClass().getDeclaredField(fieldName);
|
|
||||||
field.setAccessible(true);
|
|
||||||
return (T) field.get(object);
|
|
||||||
} catch (final Exception ex) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -136,10 +136,10 @@ public class Attachment implements Parcelable {
|
||||||
return Collections.singletonList(new Attachment(uri, type, mime));
|
return Collections.singletonList(new Attachment(uri, type, mime));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Attachment> of(final Context context, List<Uri> uris, final String type) {
|
public static List<Attachment> of(final Context context, List<Uri> uris) {
|
||||||
final List<Attachment> attachments = new ArrayList<>();
|
List<Attachment> attachments = new ArrayList<>();
|
||||||
for (final Uri uri : uris) {
|
for (Uri uri : uris) {
|
||||||
final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type);
|
final String mime = MimeUtils.guessMimeTypeFromUri(context, uri);
|
||||||
attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
|
attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
|
||||||
}
|
}
|
||||||
return attachments;
|
return attachments;
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
package eu.siacs.conversations.ui.util;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
import eu.siacs.conversations.utils.UIHelper;
|
|
||||||
|
|
||||||
public class QuoteHelper {
|
|
||||||
|
|
||||||
|
|
||||||
public static final char QUOTE_CHAR = '>';
|
|
||||||
public static final char QUOTE_END_CHAR = '<'; // used for one check, not for actual quoting
|
|
||||||
public static final char QUOTE_ALT_CHAR = '»';
|
|
||||||
public static final char QUOTE_ALT_END_CHAR = '«';
|
|
||||||
|
|
||||||
public static boolean isPositionQuoteCharacter(CharSequence body, int pos) {
|
|
||||||
// second part of logical check actually goes against the logic indicated in the method name, since it also checks for context
|
|
||||||
// but it's very useful
|
|
||||||
return body.charAt(pos) == QUOTE_CHAR || isPositionAltQuoteStart(body, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos) {
|
|
||||||
return body.charAt(pos) == QUOTE_END_CHAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionAltQuoteCharacter(CharSequence body, int pos) {
|
|
||||||
return body.charAt(pos) == QUOTE_ALT_CHAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos) {
|
|
||||||
return body.charAt(pos) == QUOTE_ALT_END_CHAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionAltQuoteStart(CharSequence body, int pos) {
|
|
||||||
return isPositionAltQuoteCharacter(body, pos) && !isPositionFollowedByAltQuoteEnd(body, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionFollowedByQuoteChar(CharSequence body, int pos) {
|
|
||||||
return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'Prequote' means anything we require or can accept in front of a QuoteChar
|
|
||||||
public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) {
|
|
||||||
return UIHelper.isPositionPrecededByLineStart(body, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionQuoteStart(CharSequence body, int pos) {
|
|
||||||
return (isPositionQuoteCharacter(body, pos)
|
|
||||||
&& isPositionPrecededByPreQuote(body, pos)
|
|
||||||
&& (UIHelper.isPositionFollowedByQuoteableCharacter(body, pos)
|
|
||||||
|| isPositionFollowedByQuoteChar(body, pos)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean bodyContainsQuoteStart(CharSequence body) {
|
|
||||||
for (int i = 0; i < body.length(); i++) {
|
|
||||||
if (isPositionQuoteStart(body, i)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionFollowedByAltQuoteEnd(CharSequence body, int pos) {
|
|
||||||
if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
boolean previousWasWhitespace = false;
|
|
||||||
for (int i = pos + 1; i < body.length(); i++) {
|
|
||||||
char c = body.charAt(i);
|
|
||||||
if (c == '\n' || isPositionAltQuoteCharacter(body, i)) {
|
|
||||||
return false;
|
|
||||||
} else if (isPositionAltQuoteEndCharacter(body, i) && !previousWasWhitespace) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
previousWasWhitespace = Character.isWhitespace(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isNestedTooDeeply(CharSequence line) {
|
|
||||||
if (isPositionQuoteStart(line, 0)) {
|
|
||||||
int nestingDepth = 1;
|
|
||||||
for (int i = 1; i < line.length(); i++) {
|
|
||||||
if (isPositionQuoteStart(line, i)) {
|
|
||||||
nestingDepth++;
|
|
||||||
}
|
|
||||||
if (nestingDepth > (Config.QUOTING_MAX_DEPTH - 1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String replaceAltQuoteCharsInText(String text) {
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
|
||||||
if (isPositionAltQuoteStart(text, i)) {
|
|
||||||
text = text.substring(0, i) + QUOTE_CHAR + text.substring(i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package eu.siacs.conversations.ui.util;
|
|
||||||
|
|
||||||
import android.util.Rational;
|
|
||||||
|
|
||||||
public final class Rationals {
|
|
||||||
|
|
||||||
//between 2.39:1 and 1:2.39 (inclusive).
|
|
||||||
private static final Rational MIN = new Rational(100,239);
|
|
||||||
private static final Rational MAX = new Rational(239,100);
|
|
||||||
|
|
||||||
private Rationals() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static Rational clip(final Rational input) {
|
|
||||||
if (input.compareTo(MIN) < 0) {
|
|
||||||
return MIN;
|
|
||||||
}
|
|
||||||
if (input.compareTo(MAX) > 0) {
|
|
||||||
return MAX;
|
|
||||||
}
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package eu.siacs.conversations.ui.util;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
public class SettingsUtils {
|
|
||||||
public static void applyScreenshotPreventionSetting(Activity activity){
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
|
||||||
boolean preventScreenshots = preferences.getBoolean("prevent_screenshots", false);
|
|
||||||
Window activityWindow = activity.getWindow();
|
|
||||||
if(preventScreenshots){
|
|
||||||
activityWindow.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
|
||||||
} else {
|
|
||||||
activityWindow.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,7 +24,6 @@ import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.ui.util.QuoteHelper;
|
|
||||||
|
|
||||||
public class EditMessage extends EmojiWrapperEditText {
|
public class EditMessage extends EmojiWrapperEditText {
|
||||||
|
|
||||||
|
@ -143,8 +142,7 @@ public class EditMessage extends EmojiWrapperEditText {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void insertAsQuote(String text) {
|
public void insertAsQuote(String text) {
|
||||||
text = QuoteHelper.replaceAltQuoteCharsInText(text);
|
text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
|
||||||
text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2").replaceAll("(^|\n)([^" + QuoteHelper.QUOTE_CHAR + "])", "$1> $2").replaceAll("\n$", "");
|
|
||||||
Editable editable = getEditableText();
|
Editable editable = getEditableText();
|
||||||
int position = getSelectionEnd();
|
int position = getSelectionEnd();
|
||||||
if (position == -1) position = editable.length();
|
if (position == -1) position = editable.length();
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
package eu.siacs.conversations.ui.widget;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Rational;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
|
|
||||||
public class SurfaceViewRenderer extends org.webrtc.SurfaceViewRenderer {
|
|
||||||
|
|
||||||
private Rational aspectRatio = new Rational(1,1);
|
|
||||||
|
|
||||||
private OnAspectRatioChanged onAspectRatioChanged;
|
|
||||||
|
|
||||||
public SurfaceViewRenderer(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SurfaceViewRenderer(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) {
|
|
||||||
super.onFrameResolutionChanged(videoWidth, videoHeight, rotation);
|
|
||||||
final int rotatedWidth = rotation != 0 && rotation != 180 ? videoHeight : videoWidth;
|
|
||||||
final int rotatedHeight = rotation != 0 && rotation != 180 ? videoWidth : videoHeight;
|
|
||||||
final Rational currentRational = this.aspectRatio;
|
|
||||||
this.aspectRatio = new Rational(rotatedWidth, rotatedHeight);
|
|
||||||
Log.d(Config.LOGTAG,"onFrameResolutionChanged("+rotatedWidth+","+rotatedHeight+","+aspectRatio+")");
|
|
||||||
if (currentRational.equals(this.aspectRatio) || onAspectRatioChanged == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onAspectRatioChanged.onAspectRatioChanged(this.aspectRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnAspectRatioChanged(final OnAspectRatioChanged onAspectRatioChanged) {
|
|
||||||
this.onAspectRatioChanged = onAspectRatioChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Rational getAspectRatio() {
|
|
||||||
return this.aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnAspectRatioChanged {
|
|
||||||
void onAspectRatioChanged(final Rational rational);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package eu.siacs.conversations.utils;
|
||||||
|
|
||||||
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants;
|
||||||
|
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
|
||||||
|
import net.ypresto.androidtranscoder.format.OutputFormatUnavailableException;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
|
||||||
|
public class Android360pFormatStrategy implements MediaFormatStrategy {
|
||||||
|
|
||||||
|
private static final int LONGER_LENGTH = 640;
|
||||||
|
private static final int SHORTER_LENGTH = 360;
|
||||||
|
private static final int DEFAULT_VIDEO_BITRATE = 1000 * 1000;
|
||||||
|
private static final int DEFAULT_AUDIO_BITRATE = 128 * 1000;
|
||||||
|
private final int mVideoBitrate;
|
||||||
|
private final int mAudioBitrate;
|
||||||
|
private final int mAudioChannels;
|
||||||
|
|
||||||
|
public Android360pFormatStrategy() {
|
||||||
|
mVideoBitrate = DEFAULT_VIDEO_BITRATE;
|
||||||
|
mAudioBitrate = DEFAULT_AUDIO_BITRATE;
|
||||||
|
mAudioChannels = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||||
|
@Override
|
||||||
|
public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) {
|
||||||
|
int width = inputFormat.getInteger(MediaFormat.KEY_WIDTH);
|
||||||
|
int height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT);
|
||||||
|
int longer, shorter, outWidth, outHeight;
|
||||||
|
if (width >= height) {
|
||||||
|
longer = width;
|
||||||
|
shorter = height;
|
||||||
|
outWidth = LONGER_LENGTH;
|
||||||
|
outHeight = SHORTER_LENGTH;
|
||||||
|
} else {
|
||||||
|
shorter = width;
|
||||||
|
longer = height;
|
||||||
|
outWidth = SHORTER_LENGTH;
|
||||||
|
outHeight = LONGER_LENGTH;
|
||||||
|
}
|
||||||
|
if (longer * 9 != shorter * 16) {
|
||||||
|
throw new OutputFormatUnavailableException("This video is not 16:9, and is not able to transcode. (" + width + "x" + height + ")");
|
||||||
|
}
|
||||||
|
if (shorter <= SHORTER_LENGTH) {
|
||||||
|
Log.d(Config.LOGTAG, "This video is less or equal to 360p, pass-through. (" + width + "x" + height + ")");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MediaFormat format = MediaFormat.createVideoFormat("video/avc", outWidth, outHeight);
|
||||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, mVideoBitrate);
|
||||||
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
|
||||||
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3);
|
||||||
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
format.setInteger(MediaFormat.KEY_PROFILE ,MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
|
||||||
|
format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel13);
|
||||||
|
}
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) {
|
||||||
|
final MediaFormat format = MediaFormat.createAudioFormat(MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC, inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), mAudioChannels);
|
||||||
|
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
|
||||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitrate);
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package eu.siacs.conversations.utils;
|
||||||
|
|
||||||
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants;
|
||||||
|
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
|
||||||
|
import net.ypresto.androidtranscoder.format.OutputFormatUnavailableException;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
|
||||||
|
public class Android720pFormatStrategy implements MediaFormatStrategy {
|
||||||
|
|
||||||
|
private static final int LONGER_LENGTH = 1280;
|
||||||
|
private static final int SHORTER_LENGTH = 720;
|
||||||
|
private static final int DEFAULT_VIDEO_BITRATE = 2000 * 1000;
|
||||||
|
private static final int DEFAULT_AUDIO_BITRATE = 192 * 1000;
|
||||||
|
private final int mVideoBitrate;
|
||||||
|
private final int mAudioBitrate;
|
||||||
|
private final int mAudioChannels;
|
||||||
|
|
||||||
|
public Android720pFormatStrategy() {
|
||||||
|
mVideoBitrate = DEFAULT_VIDEO_BITRATE;
|
||||||
|
mAudioBitrate = DEFAULT_AUDIO_BITRATE;
|
||||||
|
mAudioChannels = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||||
|
@Override
|
||||||
|
public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) {
|
||||||
|
int width = inputFormat.getInteger(MediaFormat.KEY_WIDTH);
|
||||||
|
int height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT);
|
||||||
|
int longer, shorter, outWidth, outHeight;
|
||||||
|
if (width >= height) {
|
||||||
|
longer = width;
|
||||||
|
shorter = height;
|
||||||
|
outWidth = LONGER_LENGTH;
|
||||||
|
outHeight = SHORTER_LENGTH;
|
||||||
|
} else {
|
||||||
|
shorter = width;
|
||||||
|
longer = height;
|
||||||
|
outWidth = SHORTER_LENGTH;
|
||||||
|
outHeight = LONGER_LENGTH;
|
||||||
|
}
|
||||||
|
if (longer * 9 != shorter * 16) {
|
||||||
|
throw new OutputFormatUnavailableException("This video is not 16:9, and is not able to transcode. (" + width + "x" + height + ")");
|
||||||
|
}
|
||||||
|
if (shorter <= SHORTER_LENGTH) {
|
||||||
|
Log.d(Config.LOGTAG, "This video is less or equal to 720p, pass-through. (" + width + "x" + height + ")");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MediaFormat format = MediaFormat.createVideoFormat("video/avc", outWidth, outHeight);
|
||||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, mVideoBitrate);
|
||||||
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
|
||||||
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3);
|
||||||
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
format.setInteger(MediaFormat.KEY_PROFILE ,MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
|
||||||
|
format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel13);
|
||||||
|
}
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) {
|
||||||
|
final MediaFormat format = MediaFormat.createAudioFormat(MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC, inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), mAudioChannels);
|
||||||
|
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
|
||||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitrate);
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.siacs.conversations.utils;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class ExifHelper {
|
||||||
|
private static final String TAG = "CameraExif";
|
||||||
|
|
||||||
|
public static int getOrientation(InputStream is) {
|
||||||
|
if (is == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buf = new byte[8];
|
||||||
|
int length = 0;
|
||||||
|
|
||||||
|
// ISO/IEC 10918-1:1993(E)
|
||||||
|
while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) {
|
||||||
|
int marker = buf[1] & 0xFF;
|
||||||
|
|
||||||
|
// Check if the marker is a padding.
|
||||||
|
if (marker == 0xFF) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the marker is SOI or TEM.
|
||||||
|
if (marker == 0xD8 || marker == 0x01) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Check if the marker is EOI or SOS.
|
||||||
|
if (marker == 0xD9 || marker == 0xDA) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the length and check if it is reasonable.
|
||||||
|
if (!read(is, buf, 2)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
length = pack(buf, 0, 2, false);
|
||||||
|
if (length < 2) {
|
||||||
|
Log.e(TAG, "Invalid length");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
length -= 2;
|
||||||
|
|
||||||
|
// Break if the marker is EXIF in APP1.
|
||||||
|
if (marker == 0xE1 && length >= 6) {
|
||||||
|
if (!read(is, buf, 6)) return 0;
|
||||||
|
length -= 6;
|
||||||
|
if (pack(buf, 0, 4, false) == 0x45786966 &&
|
||||||
|
pack(buf, 4, 2, false) == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip other markers.
|
||||||
|
try {
|
||||||
|
is.skip(length);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JEITA CP-3451 Exif Version 2.2
|
||||||
|
if (length > 8) {
|
||||||
|
int offset = 0;
|
||||||
|
byte[] jpeg = new byte[length];
|
||||||
|
if (!read(is, jpeg, length)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify the byte order.
|
||||||
|
int tag = pack(jpeg, offset, 4, false);
|
||||||
|
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
|
||||||
|
Log.e(TAG, "Invalid byte order");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
boolean littleEndian = (tag == 0x49492A00);
|
||||||
|
|
||||||
|
// Get the offset and check if it is reasonable.
|
||||||
|
int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
|
||||||
|
if (count < 10 || count > length) {
|
||||||
|
Log.e(TAG, "Invalid offset");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
offset += count;
|
||||||
|
length -= count;
|
||||||
|
|
||||||
|
// Get the count and go through all the elements.
|
||||||
|
count = pack(jpeg, offset - 2, 2, littleEndian);
|
||||||
|
while (count-- > 0 && length >= 12) {
|
||||||
|
// Get the tag and check if it is orientation.
|
||||||
|
tag = pack(jpeg, offset, 2, littleEndian);
|
||||||
|
if (tag == 0x0112) {
|
||||||
|
// We do not really care about type and count, do we?
|
||||||
|
int orientation = pack(jpeg, offset + 8, 2, littleEndian);
|
||||||
|
switch (orientation) {
|
||||||
|
case 1:
|
||||||
|
return 0;
|
||||||
|
case 3:
|
||||||
|
return 180;
|
||||||
|
case 6:
|
||||||
|
return 90;
|
||||||
|
case 8:
|
||||||
|
return 270;
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Unsupported orientation");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
offset += 12;
|
||||||
|
length -= 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Orientation not found");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int pack(byte[] bytes, int offset, int length,
|
||||||
|
boolean littleEndian) {
|
||||||
|
int step = 1;
|
||||||
|
if (littleEndian) {
|
||||||
|
offset += length - 1;
|
||||||
|
step = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int value = 0;
|
||||||
|
while (length-- > 0) {
|
||||||
|
value = (value << 8) | (bytes[offset] & 0xFF);
|
||||||
|
offset += step;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean read(InputStream is, byte[] buf, int length) {
|
||||||
|
try {
|
||||||
|
return is.read(buf, 0, length) == length;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,6 @@ import android.content.Context;
|
||||||
import android.telephony.TelephonyManager;
|
import android.telephony.TelephonyManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import org.osmdroid.util.GeoPoint;
|
import org.osmdroid.util.GeoPoint;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
@ -18,14 +16,11 @@ import eu.siacs.conversations.R;
|
||||||
|
|
||||||
public class LocationProvider {
|
public class LocationProvider {
|
||||||
|
|
||||||
public static final GeoPoint FALLBACK = new GeoPoint(0.0, 0.0);
|
public static final GeoPoint FALLBACK = new GeoPoint(0.0,0.0);
|
||||||
|
|
||||||
public static String getUserCountry(final Context context) {
|
public static String getUserCountry(Context context) {
|
||||||
try {
|
try {
|
||||||
final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class);
|
final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||||
if (tm == null) {
|
|
||||||
return getUserCountryFallback();
|
|
||||||
}
|
|
||||||
final String simCountry = tm.getSimCountryIso();
|
final String simCountry = tm.getSimCountryIso();
|
||||||
if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
|
if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
|
||||||
return simCountry.toUpperCase(Locale.US);
|
return simCountry.toUpperCase(Locale.US);
|
||||||
|
@ -35,39 +30,38 @@ public class LocationProvider {
|
||||||
return networkCountry.toUpperCase(Locale.US);
|
return networkCountry.toUpperCase(Locale.US);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getUserCountryFallback();
|
} catch (Exception e) {
|
||||||
} catch (final Exception e) {
|
// fallthrough
|
||||||
return getUserCountryFallback();
|
|
||||||
}
|
}
|
||||||
}
|
Locale locale = Locale.getDefault();
|
||||||
|
|
||||||
private static String getUserCountryFallback() {
|
|
||||||
final Locale locale = Locale.getDefault();
|
|
||||||
return locale.getCountry();
|
return locale.getCountry();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GeoPoint getGeoPoint(final Context context) {
|
public static GeoPoint getGeoPoint(Context context) {
|
||||||
return getGeoPoint(context, getUserCountry(context));
|
return getGeoPoint(context, getUserCountry(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static synchronized GeoPoint getGeoPoint(final Context context, final String country) {
|
public static synchronized GeoPoint getGeoPoint(Context context, String country) {
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)))) {
|
try {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)));
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while((line = reader.readLine()) != null) {
|
||||||
final String[] parts = line.split("\\s+", 4);
|
String[] parts = line.split("\\s+",4);
|
||||||
if (parts.length == 4) {
|
if (parts.length == 4) {
|
||||||
if (country.equalsIgnoreCase(parts[0])) {
|
if (country.equalsIgnoreCase(parts[0])) {
|
||||||
try {
|
try {
|
||||||
return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2]));
|
return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2]));
|
||||||
} catch (final NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
return FALLBACK;
|
return FALLBACK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG,"unable to parse line="+line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (final IOException e) {
|
} catch (IOException e) {
|
||||||
Log.d(Config.LOGTAG, "unable to parse country->geo map", e);
|
Log.d(Config.LOGTAG,e.getMessage());
|
||||||
}
|
}
|
||||||
return FALLBACK;
|
return FALLBACK;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,6 @@ import eu.siacs.conversations.entities.Conversational;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.http.AesGcmURL;
|
import eu.siacs.conversations.http.AesGcmURL;
|
||||||
import eu.siacs.conversations.http.URL;
|
import eu.siacs.conversations.http.URL;
|
||||||
import eu.siacs.conversations.ui.util.QuoteHelper;
|
|
||||||
|
|
||||||
public class MessageUtils {
|
public class MessageUtils {
|
||||||
|
|
||||||
|
@ -70,7 +69,8 @@ public class MessageUtils {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final char c = line.charAt(0);
|
final char c = line.charAt(0);
|
||||||
if (QuoteHelper.isNestedTooDeeply(line)) {
|
if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0)
|
||||||
|
|| (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (builder.length() != 0) {
|
if (builder.length() != 0) {
|
||||||
|
@ -115,6 +115,6 @@ public class MessageUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean unInitiatedButKnownSize(Message message) {
|
public static boolean unInitiatedButKnownSize(Message message) {
|
||||||
return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size != null && message.getFileParams().url != null;
|
return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,6 @@ import android.net.Uri;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -249,7 +247,6 @@ public final class MimeUtils {
|
||||||
add("audio/mpeg", "m4a");
|
add("audio/mpeg", "m4a");
|
||||||
add("audio/mpegurl", "m3u");
|
add("audio/mpegurl", "m3u");
|
||||||
add("audio/ogg", "oga");
|
add("audio/ogg", "oga");
|
||||||
add("audio/opus", "opus");
|
|
||||||
add("audio/prs.sid", "sid");
|
add("audio/prs.sid", "sid");
|
||||||
add("audio/x-aiff", "aif");
|
add("audio/x-aiff", "aif");
|
||||||
add("audio/x-aiff", "aiff");
|
add("audio/x-aiff", "aiff");
|
||||||
|
@ -276,8 +273,6 @@ public final class MimeUtils {
|
||||||
add("image/ico", "ico");
|
add("image/ico", "ico");
|
||||||
add("image/ief", "ief");
|
add("image/ief", "ief");
|
||||||
add("image/heic", "heic");
|
add("image/heic", "heic");
|
||||||
add("image/heif", "heif");
|
|
||||||
add("image/avif", "avif");
|
|
||||||
// add ".jpg" first so it will be the default for guessExtensionFromMimeType
|
// add ".jpg" first so it will be the default for guessExtensionFromMimeType
|
||||||
add("image/jpeg", "jpg");
|
add("image/jpeg", "jpg");
|
||||||
add("image/jpeg", "jpeg");
|
add("image/jpeg", "jpeg");
|
||||||
|
@ -572,8 +567,6 @@ public final class MimeUtils {
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -591,33 +584,22 @@ public final class MimeUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String extractRelevantExtension(final String path, final boolean ignoreCryptoExtension) {
|
public static String extractRelevantExtension(final String path, final boolean ignoreCryptoExtension) {
|
||||||
if (Strings.isNullOrEmpty(path)) {
|
if (path == null || path.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String filenameQueryAnchor = path.substring(path.lastIndexOf('/') + 1);
|
String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
|
||||||
final String filenameQuery = cutBefore(filenameQueryAnchor, '#');
|
int dotPosition = filename.lastIndexOf(".");
|
||||||
final String filename = cutBefore(filenameQuery, '?');
|
|
||||||
final int dotPosition = filename.lastIndexOf('.');
|
|
||||||
|
|
||||||
if (dotPosition == -1) {
|
if (dotPosition != -1) {
|
||||||
return null;
|
String extension = filename.substring(dotPosition + 1);
|
||||||
}
|
// we want the real file extension, not the crypto one
|
||||||
final String extension = filename.substring(dotPosition + 1);
|
if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
|
||||||
// we want the real file extension, not the crypto one
|
return extractRelevantExtension(filename.substring(0, dotPosition));
|
||||||
if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
|
} else {
|
||||||
return extractRelevantExtension(filename.substring(0, dotPosition));
|
return extension;
|
||||||
} else {
|
}
|
||||||
return extension;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String cutBefore(final String input, final char c) {
|
|
||||||
final int position = input.indexOf(c);
|
|
||||||
if (position > 0) {
|
|
||||||
return input.substring(0, position);
|
|
||||||
} else {
|
|
||||||
return input;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,39 +254,6 @@ public class Patterns {
|
||||||
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
|
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
|
||||||
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
|
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
|
||||||
+ "|[1-9][0-9]|[0-9]))");
|
+ "|[1-9][0-9]|[0-9]))");
|
||||||
|
|
||||||
/**
|
|
||||||
* IPv6 address matcher for
|
|
||||||
* IPv6 addresses
|
|
||||||
* zero compressed IPv6 addresses (section 2.2 of rfc5952)
|
|
||||||
* link-local IPv6 addresses with zone index (section 11 of rfc4007)
|
|
||||||
* IPv4-Embedded IPv6 Address (section 2 of rfc6052)
|
|
||||||
* IPv4-mapped IPv6 addresses (section 2.1 of rfc2765)
|
|
||||||
* IPv4-translated addresses (section 2.1 of rfc2765)
|
|
||||||
*
|
|
||||||
* Taken from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses/17871737#17871737
|
|
||||||
*/
|
|
||||||
public static final Pattern IP6_ADDRESS
|
|
||||||
= Pattern.compile(
|
|
||||||
"\\[" +
|
|
||||||
"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" +
|
|
||||||
"([0-9a-fA-F]{1,4}:){1,7}:|" +
|
|
||||||
"([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" +
|
|
||||||
"([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" +
|
|
||||||
"([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" +
|
|
||||||
"([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" +
|
|
||||||
"([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" +
|
|
||||||
"[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" +
|
|
||||||
":((:[0-9a-fA-F]{1,4}){1,7}|:)|" +
|
|
||||||
"fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" +
|
|
||||||
"::(ffff(:0{1,4}){0,1}:){0,1}" +
|
|
||||||
"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" +
|
|
||||||
"(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" +
|
|
||||||
"([0-9a-fA-F]{1,4}:){1,4}:" +
|
|
||||||
"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" +
|
|
||||||
"(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))" +
|
|
||||||
"\\]"
|
|
||||||
);
|
|
||||||
/**
|
/**
|
||||||
* Valid UCS characters defined in RFC 3987. Excludes space characters.
|
* Valid UCS characters defined in RFC 3987. Excludes space characters.
|
||||||
*/
|
*/
|
||||||
|
@ -329,7 +296,7 @@ public class Patterns {
|
||||||
private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")";
|
private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")";
|
||||||
private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD;
|
private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD;
|
||||||
public static final Pattern DOMAIN_NAME
|
public static final Pattern DOMAIN_NAME
|
||||||
= Pattern.compile("(" + HOST_NAME + "|" + IP6_ADDRESS + "|" + IP_ADDRESS +")");
|
= Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")");
|
||||||
private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/";
|
private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/";
|
||||||
/* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
|
/* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
|
||||||
private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
|
private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
|
||||||
|
@ -339,7 +306,7 @@ public class Patterns {
|
||||||
private static final String PORT_NUMBER = "\\:\\d{1,5}";
|
private static final String PORT_NUMBER = "\\:\\d{1,5}";
|
||||||
private static final String PATH_AND_QUERY = "\\/(?:(?:[" + LABEL_CHAR
|
private static final String PATH_AND_QUERY = "\\/(?:(?:[" + LABEL_CHAR
|
||||||
+ "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus optional query params
|
+ "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus optional query params
|
||||||
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_\\$])|(?:\\%[a-fA-F0-9]{2}))*";
|
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*";
|
||||||
/**
|
/**
|
||||||
* Regular expression pattern to match most part of RFC 3987
|
* Regular expression pattern to match most part of RFC 3987
|
||||||
* Internationalized URLs, aka IRIs.
|
* Internationalized URLs, aka IRIs.
|
||||||
|
@ -368,12 +335,12 @@ public class Patterns {
|
||||||
* {@link #IP_ADDRESS}
|
* {@link #IP_ADDRESS}
|
||||||
*/
|
*/
|
||||||
private static final Pattern STRICT_DOMAIN_NAME
|
private static final Pattern STRICT_DOMAIN_NAME
|
||||||
= Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")");
|
= Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + ")");
|
||||||
/**
|
/**
|
||||||
* Regular expression that matches domain names without a TLD
|
* Regular expression that matches domain names without a TLD
|
||||||
*/
|
*/
|
||||||
private static final String RELAXED_DOMAIN_NAME =
|
private static final String RELAXED_DOMAIN_NAME =
|
||||||
"(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")";
|
"(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + ")";
|
||||||
/**
|
/**
|
||||||
* Regular expression to match strings that do not start with a supported protocol. The TLDs
|
* Regular expression to match strings that do not start with a supported protocol. The TLDs
|
||||||
* are expected to be one of the known TLDs.
|
* are expected to be one of the known TLDs.
|
||||||
|
|
|
@ -35,4 +35,17 @@ public class PhoneHelper {
|
||||||
cursor.close();
|
cursor.close();
|
||||||
return uri == null ? null : Uri.parse(uri);
|
return uri == null ? null : Uri.parse(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getVersionName(Context context) {
|
||||||
|
final String packageName = context == null ? null : context.getPackageName();
|
||||||
|
if (packageName != null) {
|
||||||
|
try {
|
||||||
|
return context.getPackageManager().getPackageInfo(packageName, 0).versionName;
|
||||||
|
} catch (final PackageManager.NameNotFoundException | RuntimeException e) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -408,7 +408,6 @@ public class Resolver {
|
||||||
Result result = new Result();
|
Result result = new Result();
|
||||||
result.timeRequested = System.currentTimeMillis();
|
result.timeRequested = System.currentTimeMillis();
|
||||||
result.port = port;
|
result.port = port;
|
||||||
result.directTls = useDirectTls(port);
|
|
||||||
result.hostname = hostname;
|
result.hostname = hostname;
|
||||||
result.ip = ip;
|
result.ip = ip;
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package eu.siacs.conversations.utils;
|
package eu.siacs.conversations.utils;
|
||||||
|
|
||||||
import com.google.common.io.ByteStreams;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
@ -14,108 +12,76 @@ import eu.siacs.conversations.Config;
|
||||||
|
|
||||||
public class SocksSocketFactory {
|
public class SocksSocketFactory {
|
||||||
|
|
||||||
private static final byte[] LOCALHOST = new byte[]{127, 0, 0, 1};
|
private static final byte[] LOCALHOST = new byte[]{127,0,0,1};
|
||||||
|
|
||||||
public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException {
|
public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException {
|
||||||
//TODO use different Socks Addr Type if destination is IP or IPv6
|
final InputStream proxyIs = socket.getInputStream();
|
||||||
final InputStream proxyIs = socket.getInputStream();
|
final OutputStream proxyOs = socket.getOutputStream();
|
||||||
final OutputStream proxyOs = socket.getOutputStream();
|
proxyOs.write(new byte[]{0x05, 0x01, 0x00});
|
||||||
proxyOs.write(new byte[]{0x05, 0x01, 0x00});
|
proxyOs.flush();
|
||||||
proxyOs.flush();
|
final byte[] handshake = new byte[2];
|
||||||
final byte[] handshake = new byte[2];
|
proxyIs.read(handshake);
|
||||||
ByteStreams.readFully(proxyIs, handshake);
|
if (handshake[0] != 0x05 || handshake[1] != 0x00) {
|
||||||
if (handshake[0] != 0x05 || handshake[1] != 0x00) {
|
throw new SocksConnectionException("Socks 5 handshake failed");
|
||||||
throw new SocksConnectionException("Socks 5 handshake failed");
|
}
|
||||||
}
|
final byte[] dest = destination.getBytes();
|
||||||
final byte[] dest = destination.getBytes();
|
final ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
|
||||||
final ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
|
request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
|
||||||
request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
|
request.put((byte) dest.length);
|
||||||
request.put((byte) dest.length);
|
request.put(dest);
|
||||||
request.put(dest);
|
request.putShort((short) port);
|
||||||
request.putShort((short) port);
|
proxyOs.write(request.array());
|
||||||
proxyOs.write(request.array());
|
proxyOs.flush();
|
||||||
proxyOs.flush();
|
final byte[] response = new byte[7 + dest.length];
|
||||||
final byte[] response = new byte[4];
|
proxyIs.read(response);
|
||||||
ByteStreams.readFully(proxyIs, response);
|
if (response[1] != 0x00) {
|
||||||
final byte ver = response[0];
|
if (response[1] == 0x04) {
|
||||||
if (ver != 0x05) {
|
throw new HostNotFoundException("Host unreachable");
|
||||||
throw new IOException(String.format("Unknown Socks version %02X ", ver));
|
}
|
||||||
}
|
if (response[1] == 0x05) {
|
||||||
final byte status = response[1];
|
throw new HostNotFoundException("Connection refused");
|
||||||
final byte bndAddrType = response[3];
|
}
|
||||||
final byte[] bndDestination = readDestination(bndAddrType, proxyIs);
|
throw new SocksConnectionException("Unable to connect to destination "+(int) (response[1]));
|
||||||
final byte[] bndPort = new byte[2];
|
}
|
||||||
if (bndAddrType == 0x03) {
|
}
|
||||||
final String receivedDestination = new String(bndDestination);
|
|
||||||
if (!receivedDestination.equalsIgnoreCase(destination)) {
|
|
||||||
throw new IOException(String.format("Destination mismatch. Received %s Expected %s", receivedDestination, destination));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ByteStreams.readFully(proxyIs, bndPort);
|
|
||||||
if (status != 0x00) {
|
|
||||||
if (status == 0x04) {
|
|
||||||
throw new HostNotFoundException("Host unreachable");
|
|
||||||
}
|
|
||||||
if (status == 0x05) {
|
|
||||||
throw new HostNotFoundException("Connection refused");
|
|
||||||
}
|
|
||||||
throw new IOException(String.format("Unknown status code %02X ", status));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] readDestination(final byte type, final InputStream inputStream) throws IOException {
|
public static boolean contains(byte needle, byte[] haystack) {
|
||||||
final byte[] bndDestination;
|
for(byte hay : haystack) {
|
||||||
if (type == 0x01) {
|
if (hay == needle) {
|
||||||
bndDestination = new byte[4];
|
return true;
|
||||||
} else if (type == 0x03) {
|
}
|
||||||
final int length = inputStream.read();
|
}
|
||||||
bndDestination = new byte[length];
|
return false;
|
||||||
} else if (type == 0x04) {
|
}
|
||||||
bndDestination = new byte[16];
|
|
||||||
} else {
|
|
||||||
throw new IOException(String.format("Unknown Socks address type %02X ", type));
|
|
||||||
}
|
|
||||||
ByteStreams.readFully(inputStream, bndDestination);
|
|
||||||
return bndDestination;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean contains(byte needle, byte[] haystack) {
|
private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
|
||||||
for (byte hay : haystack) {
|
Socket socket = new Socket();
|
||||||
if (hay == needle) {
|
try {
|
||||||
return true;
|
socket.connect(address, Config.CONNECT_TIMEOUT * 1000);
|
||||||
}
|
} catch (IOException e) {
|
||||||
}
|
throw new SocksProxyNotFoundException();
|
||||||
return false;
|
}
|
||||||
}
|
createSocksConnection(socket, destination, port);
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
|
public static Socket createSocketOverTor(String destination, int port) throws IOException {
|
||||||
Socket socket = new Socket();
|
return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port);
|
||||||
try {
|
}
|
||||||
socket.connect(address, Config.CONNECT_TIMEOUT * 1000);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SocksProxyNotFoundException();
|
|
||||||
}
|
|
||||||
createSocksConnection(socket, destination, port);
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Socket createSocketOverTor(String destination, int port) throws IOException {
|
private static class SocksConnectionException extends IOException {
|
||||||
return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port);
|
SocksConnectionException(String message) {
|
||||||
}
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class SocksConnectionException extends IOException {
|
public static class SocksProxyNotFoundException extends IOException {
|
||||||
SocksConnectionException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SocksProxyNotFoundException extends IOException {
|
}
|
||||||
|
|
||||||
}
|
public static class HostNotFoundException extends SocksConnectionException {
|
||||||
|
HostNotFoundException(String message) {
|
||||||
public static class HostNotFoundException extends SocksConnectionException {
|
super(message);
|
||||||
HostNotFoundException(String message) {
|
}
|
||||||
super(message);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,14 +71,10 @@ public class TimeFrameUtils {
|
||||||
|
|
||||||
public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) {
|
public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) {
|
||||||
final long passed = (since < 0) ? 0 : (to - since);
|
final long passed = (since < 0) ? 0 : (to - since);
|
||||||
return formatElapsedTime(passed, withMilliseconds);
|
final int hours = (int) (passed / 3600000);
|
||||||
}
|
final int minutes = (int) (passed / 60000) % 60;
|
||||||
|
final int seconds = (int) (passed / 1000) % 60;
|
||||||
public static String formatElapsedTime(final long elapsed, final boolean withMilliseconds) {
|
final int milliseconds = (int) (passed / 100) % 10;
|
||||||
final int hours = (int) (elapsed / 3600000);
|
|
||||||
final int minutes = (int) (elapsed / 60000) % 60;
|
|
||||||
final int seconds = (int) (elapsed / 1000) % 60;
|
|
||||||
final int milliseconds = (int) (elapsed / 100) % 10;
|
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds);
|
return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds);
|
||||||
} else if (withMilliseconds) {
|
} else if (withMilliseconds) {
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
package eu.siacs.conversations.utils;
|
|
||||||
|
|
||||||
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
|
|
||||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy;
|
|
||||||
|
|
||||||
public final class TranscoderStrategies {
|
|
||||||
|
|
||||||
public static final DefaultVideoStrategy VIDEO_720P = DefaultVideoStrategy.atMost(720)
|
|
||||||
.bitRate(2L * 1000 * 1000)
|
|
||||||
.frameRate(30)
|
|
||||||
.keyFrameInterval(3F)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final DefaultVideoStrategy VIDEO_360P = DefaultVideoStrategy.atMost(360)
|
|
||||||
.bitRate(1000 * 1000)
|
|
||||||
.frameRate(30)
|
|
||||||
.keyFrameInterval(3F)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
//TODO do we want to add 240p (@500kbs) and 1080p (@4mbs?) ?
|
|
||||||
// see suggested bit rates on https://www.videoproc.com/media-converter/bitrate-setting-for-h264.htm
|
|
||||||
|
|
||||||
public static final DefaultAudioStrategy AUDIO_HQ = DefaultAudioStrategy.builder()
|
|
||||||
.bitRate(192 * 1000)
|
|
||||||
.channels(2)
|
|
||||||
.sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final DefaultAudioStrategy AUDIO_MQ = DefaultAudioStrategy.builder()
|
|
||||||
.bitRate(128 * 1000)
|
|
||||||
.channels(2)
|
|
||||||
.sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
//TODO if we add 144p we definitely want to add a lower audio bit rate as well
|
|
||||||
|
|
||||||
private TranscoderStrategies() {
|
|
||||||
throw new IllegalStateException("Do not instantiate me");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -32,7 +32,6 @@ import eu.siacs.conversations.entities.Presence;
|
||||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
import eu.siacs.conversations.services.ExportBackupService;
|
import eu.siacs.conversations.services.ExportBackupService;
|
||||||
import eu.siacs.conversations.ui.util.QuoteHelper;
|
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
public class UIHelper {
|
public class UIHelper {
|
||||||
|
@ -329,7 +328,7 @@ public class UIHelper {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
char first = l.charAt(0);
|
char first = l.charAt(0);
|
||||||
if ((!QuoteHelper.isPositionQuoteStart(l, 0))) {
|
if ((first != '>' || !isPositionFollowedByQuoteableCharacter(l, 0)) && first != '\u00bb') {
|
||||||
CharSequence line = CharSequenceUtils.trim(l);
|
CharSequence line = CharSequenceUtils.trim(l);
|
||||||
if (line.length() == 0) {
|
if (line.length() == 0) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -373,23 +372,6 @@ public class UIHelper {
|
||||||
return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input;
|
return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos){
|
|
||||||
// true if not a single linebreak before current position
|
|
||||||
for (int i = pos - 1; i >= 0; i--){
|
|
||||||
if (body.charAt(i) != ' '){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionPrecededByLineStart(CharSequence body, int pos){
|
|
||||||
if (isPositionPrecededByBodyStart(body, pos)){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return body.charAt(pos - 1) == '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPositionFollowedByQuoteableCharacter(CharSequence body, int pos) {
|
public static boolean isPositionFollowedByQuoteableCharacter(CharSequence body, int pos) {
|
||||||
return !isPositionFollowedByNumber(body, pos)
|
return !isPositionFollowedByNumber(body, pos)
|
||||||
&& !isPositionFollowedByEmoticon(body, pos)
|
&& !isPositionFollowedByEmoticon(body, pos)
|
||||||
|
@ -422,7 +404,6 @@ public class UIHelper {
|
||||||
final char first = body.charAt(pos + 1);
|
final char first = body.charAt(pos + 1);
|
||||||
return first == ';'
|
return first == ';'
|
||||||
|| first == ':'
|
|| first == ':'
|
||||||
|| first == '.' // do not quote >.< (but >>.<)
|
|
||||||
|| closingBeforeWhitespace(body, pos + 1);
|
|| closingBeforeWhitespace(body, pos + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -432,13 +413,31 @@ public class UIHelper {
|
||||||
final char c = body.charAt(i);
|
final char c = body.charAt(i);
|
||||||
if (Character.isWhitespace(c)) {
|
if (Character.isWhitespace(c)) {
|
||||||
return false;
|
return false;
|
||||||
} else if (QuoteHelper.isPositionQuoteCharacter(body, pos) || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) {
|
} else if (c == '<' || c == '>') {
|
||||||
return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1));
|
return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isPositionFollowedByQuote(CharSequence body, int pos) {
|
||||||
|
if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean previousWasWhitespace = false;
|
||||||
|
for (int i = pos + 1; i < body.length(); i++) {
|
||||||
|
char c = body.charAt(i);
|
||||||
|
if (c == '\n' || c == '»') {
|
||||||
|
return false;
|
||||||
|
} else if (c == '«' && !previousWasWhitespace) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
previousWasWhitespace = Character.isWhitespace(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static String getDisplayName(MucOptions.User user) {
|
public static String getDisplayName(MucOptions.User user) {
|
||||||
Contact contact = user.getContact();
|
Contact contact = user.getContact();
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
|
@ -476,6 +475,9 @@ public class UIHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFileDescriptionString(final Context context, final Message message) {
|
public static String getFileDescriptionString(final Context context, final Message message) {
|
||||||
|
if (message.getType() == Message.TYPE_IMAGE) {
|
||||||
|
return context.getString(R.string.image);
|
||||||
|
}
|
||||||
final String mime = message.getMimeType();
|
final String mime = message.getMimeType();
|
||||||
if (mime == null) {
|
if (mime == null) {
|
||||||
return context.getString(R.string.file);
|
return context.getString(R.string.file);
|
||||||
|
@ -485,9 +487,7 @@ public class UIHelper {
|
||||||
return context.getString(R.string.video);
|
return context.getString(R.string.video);
|
||||||
} else if (mime.equals("image/gif")) {
|
} else if (mime.equals("image/gif")) {
|
||||||
return context.getString(R.string.gif);
|
return context.getString(R.string.gif);
|
||||||
} else if (mime.equals("image/svg+xml")) {
|
} else if (mime.startsWith("image/")) {
|
||||||
return context.getString(R.string.vector_graphic);
|
|
||||||
} else if (mime.startsWith("image/") || message.getType() == Message.TYPE_IMAGE) {
|
|
||||||
return context.getString(R.string.image);
|
return context.getString(R.string.image);
|
||||||
} else if (mime.contains("pdf")) {
|
} else if (mime.contains("pdf")) {
|
||||||
return context.getString(R.string.pdf_document);
|
return context.getString(R.string.pdf_document);
|
||||||
|
@ -503,8 +503,6 @@ public class UIHelper {
|
||||||
return context.getString(R.string.ebook);
|
return context.getString(R.string.ebook);
|
||||||
} else if (mime.equals("application/gpx+xml")) {
|
} else if (mime.equals("application/gpx+xml")) {
|
||||||
return context.getString(R.string.gpx_track);
|
return context.getString(R.string.gpx_track);
|
||||||
} else if (mime.equals("text/plain")) {
|
|
||||||
return context.getString(R.string.plain_text_document);
|
|
||||||
} else {
|
} else {
|
||||||
return mime;
|
return mime;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,12 @@ import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
|
||||||
import com.google.common.collect.Collections2;
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -35,8 +32,6 @@ public class XmppUri {
|
||||||
private Map<String, String> parameters = Collections.emptyMap();
|
private Map<String, String> parameters = Collections.emptyMap();
|
||||||
private boolean safeSource = true;
|
private boolean safeSource = true;
|
||||||
|
|
||||||
public static final String INVITE_DOMAIN = "conversations.im";
|
|
||||||
|
|
||||||
public XmppUri(final String uri) {
|
public XmppUri(final String uri) {
|
||||||
try {
|
try {
|
||||||
parse(Uri.parse(uri));
|
parse(Uri.parse(uri));
|
||||||
|
@ -138,10 +133,10 @@ public class XmppUri {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
final String scheme = uri.getScheme();
|
String scheme = uri.getScheme();
|
||||||
final String host = uri.getHost();
|
String host = uri.getHost();
|
||||||
List<String> segments = uri.getPathSegments();
|
List<String> segments = uri.getPathSegments();
|
||||||
if ("https".equalsIgnoreCase(scheme) && INVITE_DOMAIN.equalsIgnoreCase(host)) {
|
if ("https".equalsIgnoreCase(scheme) && "conversations.im".equalsIgnoreCase(host)) {
|
||||||
if (segments.size() >= 2 && segments.get(1).contains("@")) {
|
if (segments.size() >= 2 && segments.get(1).contains("@")) {
|
||||||
// sample : https://conversations.im/i/foo@bar.com
|
// sample : https://conversations.im/i/foo@bar.com
|
||||||
try {
|
try {
|
||||||
|
@ -172,7 +167,7 @@ public class XmppUri {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.fingerprints = parseFingerprints(parameters);
|
this.fingerprints = parseFingerprints(parameters);
|
||||||
} else if ("imto".equalsIgnoreCase(scheme) && Arrays.asList("xmpp", "jabber").contains(uri.getHost())) {
|
} else if ("imto".equalsIgnoreCase(scheme)) {
|
||||||
// sample: imto://xmpp/foo@bar.com
|
// sample: imto://xmpp/foo@bar.com
|
||||||
try {
|
try {
|
||||||
jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1].trim();
|
jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1].trim();
|
||||||
|
@ -194,10 +189,7 @@ public class XmppUri {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAction(final String action) {
|
public boolean isAction(final String action) {
|
||||||
return Collections2.transform(
|
return parameters.containsKey(action);
|
||||||
parameters.keySet(),
|
|
||||||
s -> CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('A', 'Z')).retainFrom(s)
|
|
||||||
).contains(action);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Jid getJid() {
|
public Jid getJid() {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package eu.siacs.conversations.xml;
|
package eu.siacs.conversations.xml;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -167,9 +165,8 @@ public class Element {
|
||||||
return this.attributes;
|
return this.attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
final StringBuilder elementOutput = new StringBuilder();
|
StringBuilder elementOutput = new StringBuilder();
|
||||||
if ((content == null) && (children.size() == 0)) {
|
if ((content == null) && (children.size() == 0)) {
|
||||||
Tag emptyTag = Tag.empty(name);
|
Tag emptyTag = Tag.empty(name);
|
||||||
emptyTag.setAtttributes(this.attributes);
|
emptyTag.setAtttributes(this.attributes);
|
||||||
|
|
|
@ -28,7 +28,6 @@ public final class Namespace {
|
||||||
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
|
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
|
||||||
public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0";
|
public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0";
|
||||||
public static final String JINGLE = "urn:xmpp:jingle:1";
|
public static final String JINGLE = "urn:xmpp:jingle:1";
|
||||||
public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1";
|
|
||||||
public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0";
|
public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0";
|
||||||
public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0";
|
public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0";
|
||||||
public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0";
|
public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0";
|
||||||
|
|
|
@ -46,7 +46,6 @@ import java.util.regex.Matcher;
|
||||||
|
|
||||||
import javax.net.ssl.KeyManager;
|
import javax.net.ssl.KeyManager;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
import javax.net.ssl.X509KeyManager;
|
import javax.net.ssl.X509KeyManager;
|
||||||
|
@ -54,6 +53,7 @@ import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
|
import eu.siacs.conversations.crypto.DomainHostnameVerifier;
|
||||||
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
||||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
import eu.siacs.conversations.crypto.sasl.Anonymous;
|
import eu.siacs.conversations.crypto.sasl.Anonymous;
|
||||||
|
@ -401,7 +401,7 @@ public class XmppConnection implements Runnable {
|
||||||
return tag != null && tag.isStart("stream");
|
return tag != null && tag.isStart("stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
|
private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
|
||||||
final SSLContext sc = SSLSocketHelper.getSSLContext();
|
final SSLContext sc = SSLSocketHelper.getSSLContext();
|
||||||
final MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
|
final MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
|
||||||
final KeyManager[] keyManager;
|
final KeyManager[] keyManager;
|
||||||
|
@ -412,7 +412,9 @@ public class XmppConnection implements Runnable {
|
||||||
}
|
}
|
||||||
final String domain = account.getServer();
|
final String domain = account.getServer();
|
||||||
sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
|
sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
|
||||||
return sc.getSocketFactory();
|
final SSLSocketFactory factory = sc.getSocketFactory();
|
||||||
|
final DomainHostnameVerifier verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier(), mInteractive);
|
||||||
|
return new TlsFactoryVerifier(factory, verifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -787,25 +789,19 @@ public class XmppConnection implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
|
private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
|
||||||
final SSLSocketFactory sslSocketFactory;
|
final TlsFactoryVerifier tlsFactoryVerifier;
|
||||||
try {
|
try {
|
||||||
sslSocketFactory = getSSLSocketFactory();
|
tlsFactoryVerifier = getTlsFactoryVerifier();
|
||||||
} catch (final NoSuchAlgorithmException | KeyManagementException e) {
|
} catch (final NoSuchAlgorithmException | KeyManagementException e) {
|
||||||
throw new StateChangingException(Account.State.TLS_ERROR);
|
throw new StateChangingException(Account.State.TLS_ERROR);
|
||||||
}
|
}
|
||||||
final InetAddress address = socket.getInetAddress();
|
final InetAddress address = socket.getInetAddress();
|
||||||
final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
|
final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
|
||||||
SSLSocketHelper.setSecurity(sslSocket);
|
SSLSocketHelper.setSecurity(sslSocket);
|
||||||
SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer()));
|
SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer()));
|
||||||
SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
|
SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
|
||||||
final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
|
if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
|
||||||
try {
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
|
||||||
if (!xmppDomainVerifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
|
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate domain verification failed");
|
|
||||||
FileBackend.close(sslSocket);
|
|
||||||
throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
|
|
||||||
}
|
|
||||||
} catch (final SSLPeerUnverifiedException e) {
|
|
||||||
FileBackend.close(sslSocket);
|
FileBackend.close(sslSocket);
|
||||||
throw new StateChangingException(Account.State.TLS_ERROR);
|
throw new StateChangingException(Account.State.TLS_ERROR);
|
||||||
}
|
}
|
||||||
|
@ -1715,6 +1711,19 @@ public class XmppConnection implements Runnable {
|
||||||
UNKNOWN
|
UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class TlsFactoryVerifier {
|
||||||
|
private final SSLSocketFactory factory;
|
||||||
|
private final DomainHostnameVerifier verifier;
|
||||||
|
|
||||||
|
TlsFactoryVerifier(final SSLSocketFactory factory, final DomainHostnameVerifier verifier) throws IOException {
|
||||||
|
this.factory = factory;
|
||||||
|
this.verifier = verifier;
|
||||||
|
if (factory == null || verifier == null) {
|
||||||
|
throw new IOException("could not setup ssl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class MyKeyManager implements X509KeyManager {
|
private class MyKeyManager implements X509KeyManager {
|
||||||
@Override
|
@Override
|
||||||
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
|
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
|
||||||
|
|
|
@ -136,7 +136,6 @@ public abstract class AbstractJingleConnection {
|
||||||
TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call)
|
TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call)
|
||||||
TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button)
|
TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button)
|
||||||
TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
|
TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
|
||||||
TERMINATED_APPLICATION_FAILURE,
|
TERMINATED_APPLICATION_FAILURE
|
||||||
TERMINATED_SECURITY_ERROR
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,7 +206,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
final Element error = response.addChild("error");
|
final Element error = response.addChild("error");
|
||||||
error.setAttribute("type", conditionType);
|
error.setAttribute("type", conditionType);
|
||||||
error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
|
error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
|
||||||
error.addChild(jingleCondition, Namespace.JINGLE_ERRORS);
|
error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1");
|
||||||
account.getXmppConnection().sendIqPacket(response, null);
|
account.getXmppConnection().sendIqPacket(response, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1239,11 +1239,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getFileSize() {
|
public long getFileSize() {
|
||||||
if (this.file != null) {
|
if (this.file != null) {
|
||||||
return this.file.getExpectedSize();
|
return this.file.getExpectedSize();
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,8 +3,6 @@ package eu.siacs.conversations.xmpp.jingle;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.io.ByteStreams;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
@ -60,12 +58,22 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
} else {
|
} else {
|
||||||
destBuilder.append(this.connection.getTransportId());
|
destBuilder.append(this.connection.getTransportId());
|
||||||
}
|
}
|
||||||
if (candidate.isOurs()) {
|
if (candidate.getType() == JingleCandidate.TYPE_PROXY) {
|
||||||
destBuilder.append(this.account.getJid());
|
if (candidate.isOurs()) {
|
||||||
destBuilder.append(this.connection.getId().with);
|
destBuilder.append(this.account.getJid());
|
||||||
|
destBuilder.append(this.connection.getId().with);
|
||||||
|
} else {
|
||||||
|
destBuilder.append(this.connection.getId().with);
|
||||||
|
destBuilder.append(this.account.getJid());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
destBuilder.append(this.connection.getId().with);
|
if (connection.isInitiator()) {
|
||||||
destBuilder.append(this.account.getJid());
|
destBuilder.append(this.account.getJid());
|
||||||
|
destBuilder.append(this.connection.getId().with);
|
||||||
|
} else {
|
||||||
|
destBuilder.append(this.connection.getId().with);
|
||||||
|
destBuilder.append(this.account.getJid());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
messageDigest.reset();
|
messageDigest.reset();
|
||||||
this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes()));
|
this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes()));
|
||||||
|
@ -106,26 +114,26 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
final byte[] authBegin = new byte[2];
|
final byte[] authBegin = new byte[2];
|
||||||
final InputStream inputStream = socket.getInputStream();
|
final InputStream inputStream = socket.getInputStream();
|
||||||
final OutputStream outputStream = socket.getOutputStream();
|
final OutputStream outputStream = socket.getOutputStream();
|
||||||
ByteStreams.readFully(inputStream, authBegin);
|
inputStream.read(authBegin);
|
||||||
if (authBegin[0] != 0x5) {
|
if (authBegin[0] != 0x5) {
|
||||||
socket.close();
|
socket.close();
|
||||||
}
|
}
|
||||||
final short methodCount = authBegin[1];
|
final short methodCount = authBegin[1];
|
||||||
final byte[] methods = new byte[methodCount];
|
final byte[] methods = new byte[methodCount];
|
||||||
ByteStreams.readFully(inputStream, methods);
|
inputStream.read(methods);
|
||||||
if (SocksSocketFactory.contains((byte) 0x00, methods)) {
|
if (SocksSocketFactory.contains((byte) 0x00, methods)) {
|
||||||
outputStream.write(new byte[]{0x05, 0x00});
|
outputStream.write(new byte[]{0x05, 0x00});
|
||||||
} else {
|
} else {
|
||||||
outputStream.write(new byte[]{0x05, (byte) 0xff});
|
outputStream.write(new byte[]{0x05, (byte) 0xff});
|
||||||
}
|
}
|
||||||
final byte[] connectCommand = new byte[4];
|
byte[] connectCommand = new byte[4];
|
||||||
ByteStreams.readFully(inputStream, connectCommand);
|
inputStream.read(connectCommand);
|
||||||
if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) {
|
if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) {
|
||||||
int destinationCount = inputStream.read();
|
int destinationCount = inputStream.read();
|
||||||
final byte[] destination = new byte[destinationCount];
|
final byte[] destination = new byte[destinationCount];
|
||||||
ByteStreams.readFully(inputStream, destination);
|
inputStream.read(destination);
|
||||||
final byte[] port = new byte[2];
|
final byte[] port = new byte[2];
|
||||||
ByteStreams.readFully(inputStream, port);
|
inputStream.read(port);
|
||||||
final String receivedDestination = new String(destination);
|
final String receivedDestination = new String(destination);
|
||||||
final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
|
final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
|
||||||
final byte[] responseHeader;
|
final byte[] responseHeader;
|
||||||
|
@ -179,7 +187,7 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
socket.setSoTimeout(0);
|
socket.setSoTimeout(0);
|
||||||
isEstablished = true;
|
isEstablished = true;
|
||||||
callback.established();
|
callback.established();
|
||||||
} catch (final IOException e) {
|
} catch (IOException e) {
|
||||||
callback.failed();
|
callback.failed();
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue