1
0
Fork 1

removed OTR (for now..), cherry picked blabber.im updates (Christan Schneppe, Daniel Gultsch)

This commit is contained in:
Arne 2021-12-29 22:34:29 +01:00
parent 792e58c570
commit eca64c9362
315 changed files with 31885 additions and 4561 deletions

View file

@ -8,7 +8,7 @@ jobs:
- restore_cache:
key: gradle-{{ checksum "build.gradle" }}-{{ checksum ".circleci/config.yml" }}
- run: export GRADLE_USER_HOME=$PWD/.gradle
- run: wget -O libs/libwebrtc-m90.aar https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar
- run: wget -O libs/libwebrtc-m85.aar https://www.pix-art.de/files/libwebrtc-m85.aar
- run: echo y | sdkmanager "platforms;android-$(sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' build.gradle)" > /dev/null
- run: ./gradlew lintGitDebug
- save_cache:
@ -27,7 +27,7 @@ jobs:
- restore_cache:
key: android
- run: export GRADLE_USER_HOME=$PWD/.gradle
- run: wget -O libs/libwebrtc-m90.aar https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar
- run: wget -O libs/libwebrtc-m85.aar https://www.pix-art.de/files/libwebrtc-m85.aar
- run: echo y | sdkmanager "platforms;android-$(sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' build.gradle)" > /dev/null
# build
- run: ./gradlew assembleGit
@ -46,10 +46,10 @@ jobs:
- restore_cache:
key: android
- run: export GRADLE_USER_HOME=$PWD/.gradle
- run: wget -O libs/libwebrtc-m90.aar https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar
- run: wget -O libs/libwebrtc-m85.aar https://www.pix-art.de/files/libwebrtc-m85.aar
- run: echo y | sdkmanager "platforms;android-$(sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' build.gradle)" > /dev/null
# workaround for fdroid nightly circleci bug
- run: sed -i "s/os.getenv('CIRCLE_REPOSITORY_URL')/\"https:\/\/github.com\/kriztan\/Monocles-Messenger\"/" /usr/lib/python3/dist-packages/fdroidserver/nightly.py
- run: sed -i "s/os.getenv('CIRCLE_REPOSITORY_URL')/\"https:\/\/github.com\/kriztan\/Pix-Art-Messenger\"/" /usr/lib/python3/dist-packages/fdroidserver/nightly.py
# generate version number
- run: sed -i "s/^\(\s*versionCode\s*\).*$/\1$(git rev-list --first-parent --count HEAD)/" build.gradle
- run: sed -i "0,/versionName/s/^\(\s*versionName\).*/\1 \"$(printf '%s-%05d' $(git describe --tag --abbrev=0) $(git rev-list --first-parent --count HEAD))\"/" build.gradle
@ -67,11 +67,11 @@ workflows:
version: 2
test_build:
jobs:
- build:
filters:
branches:
ignore: master
- publish:
filters:
branches:
only: master
- build:
filters:
branches:
ignore: master
- publish:
filters:
branches:
only: master

34
.github/workflows/android.yml vendored Normal file
View file

@ -0,0 +1,34 @@
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

View file

@ -15,19 +15,18 @@ apply plugin: 'com.android.application'
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven { url "https://jitpack.io" }
maven { url "https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar" }
maven { url "https://raw.github.com/abdularis/libwebrtc-android/repo/" }
}
}
repositories {
google()
mavenCentral()
jcenter()
maven { url "https://jitpack.io" }
maven { url "https://github.com/robjperez/libwebrtc_maven/raw/master/libwebrtc.aar" }
maven { url "https://raw.github.com/abdularis/libwebrtc-android/repo/" }
jcenter()
}
configurations {
@ -37,9 +36,9 @@ configurations {
}
dependencies {
implementation 'org.webrtc:google-webrtc:1.+'
implementation 'com.github.webrtc-sdk:android:93.4577.01'
implementation project(':libs:android-transcoder')
playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') { ///higher versions are causing crashes due to missing project IDs
playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@ -51,7 +50,6 @@ dependencies {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'exifinterface'
}
implementation 'org.jitsi:org.otr4j:0.23'
implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
implementation 'org.gnu.inet:libidn:1.15'
implementation 'com.google.zxing:core:3.3.3' // > 3.3.x not working below SDK 24
@ -69,28 +67,30 @@ dependencies {
implementation 'androidx.emoji:emoji:1.1.0'
gitImplementation 'androidx.emoji:emoji-appcompat:1.1.0'
gitImplementation 'androidx.emoji:emoji-bundled:1.1.0'
implementation 'com.google.android.material:material:1.0.0' // higher versions than 1.0.0 cause strange fab design
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.cardview:cardview:1.0.0' // for compatibility
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.google.android.exoplayer:exoplayer-core:2.15.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.15.0'
implementation 'com.wefika:flowlayout:0.4.1'
implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.5'
implementation 'org.jxmpp:jxmpp-jid:1.0.1'
implementation 'org.jxmpp:jxmpp-jid:1.0.2'
implementation 'org.hsluv:hsluv:0.2'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15' // 1.2.15 is last working version for minSDK 16
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.23'
implementation 'me.drakeet.support:toastcompat:1.1.0'
implementation 'org.osmdroid:osmdroid-android:6.1.5'
implementation 'com.leinardi.android:speed-dial:3.1.1'
implementation 'org.osmdroid:osmdroid-android:6.1.11'
implementation 'com.leinardi.android:speed-dial:3.2.0'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.squareup.okhttp3:okhttp:4.9.2' // versions > 3.12.x don't support API level < 21 anymore
implementation 'com.squareup.retrofit2:retrofit:2.9.0' //retrofit needs to stick with 2.6.x for SDK < 21 (https://github.com/square/retrofit/blob/master/CHANGELOG.md)
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.guava:guava:30.1.1-android'
implementation 'com.google.guava:guava:31.0.1-android'
implementation 'com.github.AppIntro:AppIntro:6.1.0'
implementation "androidx.browser:browser:1.3.0"
implementation fileTree(include: ['libwebrtc-m90.aar'], dir: 'libs')
implementation 'androidx.browser:browser:1.3.0'
implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy
implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs')
}
ext {
@ -100,36 +100,29 @@ ext {
android {
signingConfigs {
'monocles chat' {
}
}
//noinspection GradleCompatible
compileSdkVersion 30
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionNameSuffix " beta_(2021-12-26)" // " beta_(XXXX-XX-XX)" // activate for beta versions
versionNameSuffix " beta_(2021-12-19)" // " beta_(XXXX-XX-XX)" // activate for beta versions
versionCode 110
versionName "1.4.4"
//resConfigs "en"
archivesBaseName += "-$versionName"
//archivesBaseName += "$versionNameSuffix" // activate for beta versions
archivesBaseName += "$versionNameSuffix" // activate for beta versions
applicationId "de.monocles.chat"
multiDexEnabled true
buildConfigField("String", "LOGTAG", '"monocles chat"')
buildConfigField("String", "DOMAIN_LOCK", 'null')
buildConfigField("String", "MAGIC_CREATE_DOMAIN", '"monocles.de"')
buildConfigField("boolean", "SHOW_INTRO", 'true')
//buildConfigField("String", "UPDATE_URL", '"https://monocles.de/chat/update/"')
buildConfigField("String", "UPDATE_URL", '"https://monocles.de/chat/update/"')
resValue "string", "applicationId", applicationId
signingConfig signingConfigs.'monocles chat'
def appName = "monocles chat"
resValue "string", "app_name", appName
buildConfigField "String", "APP_NAME", "\"$appName\"";
resValue "string", "app_name", "monocles chat"
}
dataBinding {
@ -159,13 +152,10 @@ android {
applicationId "de.monocles.chat"
buildConfigField("boolean", "SHOW_MIGRATION_INFO", 'false')
resValue "string", "applicationId", applicationId
signingConfig signingConfigs.'monocles chat'
}
git {
dimension "distribution"
buildConfigField("boolean", "SHOW_MIGRATION_INFO", 'true')
applicationId 'de.monocles.chat'
signingConfig signingConfigs.'monocles chat'
}
}
if (project.hasProperty('mStoreFile') &&
@ -205,7 +195,8 @@ android {
lintOptions {
error 'StringFormatInvalid' , 'StringFormatMatches'
disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource', 'RestrictedApi'
abortOnError true
checkReleaseBuilds true
abortOnError false
}
subprojects {
@ -226,4 +217,4 @@ android {
exclude 'META-INF/BCKEY.DSA'
exclude 'META-INF/BCKEY.SF'
}
}
}

2
proguard-rules.pro vendored
View file

@ -1,6 +1,7 @@
-dontobfuscate
-keep class de.monocles.chat.**
-keep class de.pixart.messenger.**
-keep class eu.siacs.conversations.**
-keep class org.whispersystems.**
-keep class com.kyleduo.switchbutton.Configuration
@ -38,7 +39,6 @@
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
-keepclassmembers class eu.siacs.conversations.http.services.** {
!transient <fields>;
}

View file

@ -8,7 +8,7 @@
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@ -81,9 +81,7 @@
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data
android:path="${applicationId}"
android:scheme="package" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_RESTARTED" />
@ -92,7 +90,7 @@
android:scheme="package" />
</intent-filter>
</receiver>
<receiver android:name=".services.AlarmReceiver"></receiver>
<receiver android:name=".services.AlarmReceiver" />
<activity
android:name="de.monocles.chat.ui.StartUI"
@ -110,8 +108,8 @@
android:name=".ui.ConversationsActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:minWidth="300dp"
android:minHeight="300dp"
android:minWidth="336dp"
android:minHeight="480dp"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.ScanActivity"
@ -148,6 +146,14 @@
<data android:scheme="imto" />
<data android:host="jabber" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="imto" />
<data android:host="xmpp" />
</intent-filter>
</activity>
<activity
android:name=".ui.StartConversationActivity"
@ -278,10 +284,6 @@
<activity
android:name=".ui.PublishGroupChatProfilePictureActivity"
android:label="@string/group_chat_avatar" />
<activity
android:name=".ui.VerifyOTRActivity"
android:label="@string/verify_otr"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.ShareWithActivity"
android:label="@string/app_name"
@ -359,7 +361,12 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.SettingsActivity" />
</activity>
<activity
android:name=".ui.UpdaterActivity"
android:configChanges="orientation|screenSize"
android:label="@string/title_activity_updater"
android:launchMode="singleTask"
android:theme="@style/ConversationsTheme" />
<activity
android:name=".ui.ShortcutActivity"
android:label="@string/contact"

View file

@ -84,11 +84,13 @@ public class PermissionsActivity extends AppCompatActivity
dialog.setCancelable(false);
dialog.show();
} else {
StartUI.next(this);
if (Compatibility.hasStoragePermission(PermissionsActivity.this)) {
StartUI.next(this);
}
}
}
public interface OnPermissionGranted {
void onPermissionGranted();
}
}
}

View file

@ -6,16 +6,14 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.util.IntroHelper;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.ThemeHelper;
public class StartUI extends PermissionsActivity
implements PermissionsActivity.OnPermissionGranted {
public class StartUI extends PermissionsActivity
implements PermissionsActivity.OnPermissionGranted {
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -25,22 +23,26 @@ import eu.siacs.conversations.utils.ThemeHelper;
IntroHelper.showIntro(this, false);
}
@Override
protected void onStart() {
super.onStart();
requestNeededPermissions();
protected void onStart() {
super.onStart();
requestNeededPermissions();
}
private void requestNeededPermissions() {
if (Compatibility.runsTwentyThree()) {
if (!checkStoragePermission()) {
requestStoragePermission(this);
}
if (Compatibility.runsAndTargetsThirty(this)) {
requestAllFilesAccess(this);
}
private void requestNeededPermissions() {
if (Compatibility.runsTwentyThree()) {
if (!checkStoragePermission()) {
requestStoragePermission(this);
}
if (Compatibility.runsAndTargetsThirty(this)) {
requestAllFilesAccess(this);
}
if (checkStoragePermission() && !Compatibility.runsAndTargetsThirty(this)) {
next(this);
}
} else {
next(this);
}
}
@Override

View file

@ -2,13 +2,16 @@ package eu.siacs.conversations;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.services.ProviderService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
@ -44,32 +47,36 @@ public final class Config {
}
public static String monocles() {
if (Locale.getDefault().getLanguage().equalsIgnoreCase("de")) {
return "monocles chat";
} else {
return "monocles chat/en";
}
//if (Locale.getDefault().getLanguage().equalsIgnoreCase("de")) {
return "https://ocean.monocles.de/apps/registration/";
/*} else {
return "blabber.im/en.html";
}*/
}
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("support@monocles.de");
public static final Uri HELP = Uri.parse("https://help.conversations.im");
public static final Uri HELP = Uri.parse("https://monocles.de/howto");
public static final String inviteUserURL = "https://ocean.monocles.de/apps/registration/";
public static final String inviteMUCURL = "https://ocean.monocles.de/apps/registration/";
public static final String inviteHostURL = "monocles.de"; // without http(s)
public static final String termsOfUseURL = "https://" + "monocles.de" + "/impressum/";
public static final String privacyURL = "https://" + "monocles.de" + "/impressum/";
public static final String migrationURL = "https://" + "codeberg.org/Arne/monocles_chat/";
public static final String inviteUserURL = "https://" + monocles() + "/i/";
public static final String inviteMUCURL = "https://" + monocles() + "/j/";
public static final String inviteHostURL = monocles(); // without http(s)
public static final String INVITE_DOMAIN = monocles();
public static final String termsOfUseURL = "https://monocles.de/impressum/";
public static final String privacyURL = "https://monocles.de/impressum/";
public static final String migrationURL = Locale.getDefault().getLanguage().equalsIgnoreCase("de") ? "https://codeberg.org/Arne/monocles_chat" : "https://codeberg.org/Arne/monocles_chat";
public static final String CHANGELOG_URL = "https://codeberg.org/Arne/monocles_chat/src/branch/master/CHANGELOG.md";
public static final String GIT_URL = "https://codeberg.org/Arne/monocles_chat/";
public static final String GIT_URL = "https://codeberg.org/Arne/monocles_chat";
public static final String PROVIDER_URL = "https://invent.kde.org/melvo/xmpp-providers/-/raw/master/providers.json"; // https://invent.kde.org/melvo/xmpp-providers
public static final String XMPP_IP = null; //BuildConfig.XMPP_IP; // set to null means disable
public static final Integer[] XMPP_Ports = null; //BuildConfig.XMPP_Ports; // set to null means disable
public static final String DOMAIN_LOCK = BuildConfig.DOMAIN_LOCK; //only allow account creation for this domain
public static final String MAGIC_CREATE_DOMAIN = BuildConfig.MAGIC_CREATE_DOMAIN; //"monocles.de";
public static final String MAGIC_CREATE_DOMAIN = DOMAIN.getRandomServer();
public static final String QUICKSY_DOMAIN = "quicksy.im";
public static final String CHANNEL_DISCOVERY = "https://search.jabber.network";
public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
@ -102,7 +109,7 @@ public final class Config {
public static final boolean XEP_0392 = true; //enables XEP-0392 v0.6.0
public static final int FILE_SIZE = 1048576; // 1 MiB
public static final int VIDEO_FAST_UPLOAD_SIZE = 5 * 1024 * 1024;
public static final int AVATAR_SIZE = 480;
public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.JPEG;
@ -111,11 +118,6 @@ public final class Config {
public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG;
public static final int IMAGE_QUALITY = 65;
public static final int DEFAULT_ZOOM = 15; //for locations
public final static long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms
public final static float LOCATION_FIX_SPACE_DELTA = 10; // m
public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
public static final int MESSAGE_MERGE_WINDOW = 20;
public static final int PAGE_SIZE = 50;
@ -150,8 +152,9 @@ public final class Config {
public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
public static final boolean REQUIRE_RTP_VERIFICATION = false; //require a/v calls to be verified with OMEMO
public static final boolean ONLY_INTERNAL_STORAGE = true; //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
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
public static final boolean MUC_LEAVE_BEFORE_JOIN = false;
@ -166,8 +169,8 @@ public final class Config {
public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
//public static final String UPDATE_URL = BuildConfig.UPDATE_URL;
//public static final long UPDATE_CHECK_TIMER = 24 * 60 * 60; // 24 h in seconds
public static final String UPDATE_URL = BuildConfig.UPDATE_URL;
public static final long UPDATE_CHECK_TIMER = 24 * 60 * 60; // 24 h in seconds
public static final String ISSUE_URL = "xmpp://support@conference.monocles.de?join";
@ -207,6 +210,45 @@ public final class Config {
}
}
public static class DOMAIN {
public static final List<String> DOMAINS = Arrays.asList(
"conversations.im",
"zp1.net"
);
public static final List<String> BLACKLISTED_DOMAINS = Arrays.asList(
"blabber.im"
);
public static String getRandomServer() {
try {
new ProviderService().execute();
final String domain = ProviderService.getProviders().get(new Random().nextInt(ProviderService.getProviders().size()));
Log.d(LOGTAG, "MagicCreate account on domain: " + domain);
return domain;
} catch (Exception e) {
Log.d(LOGTAG, "Error getting random server ", e);
}
return "zp1.net";
}
}
private Config() {
}
public static final class Map {
public final static double INITIAL_ZOOM_LEVEL = 4;
public final static double FINAL_ZOOM_LEVEL = 15;
public final static int MY_LOCATION_INDICATOR_SIZE = 15;
public final static int MY_LOCATION_INDICATOR_OUTLINE_SIZE = 5;
public final static long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms
public final static float LOCATION_FIX_SPACE_DELTA = 10; // m
public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
}
// How deep nested quotes should be displayed. '2' means one quote nested in another.
public static final int QUOTE_MAX_DEPTH = 7;
// How deep nested quotes should be created on quoting a message.
public static final int QUOTING_MAX_DEPTH = 1;
}

View file

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

View file

@ -1,10 +1,11 @@
package eu.siacs.conversations.crypto;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
public interface DomainHostnameVerifier extends HostnameVerifier {
boolean verify(String domain, String hostname, SSLSession sslSession) throws SSLPeerUnverifiedException;
}

View file

@ -1,312 +0,0 @@
package eu.siacs.conversations.crypto;
import android.util.Log;
import net.java.otr4j.OtrEngineHost;
import net.java.otr4j.OtrException;
import net.java.otr4j.OtrPolicy;
import net.java.otr4j.OtrPolicyImpl;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
import net.java.otr4j.crypto.OtrCryptoException;
import net.java.otr4j.session.FragmenterInstructions;
import net.java.otr4j.session.InstanceTag;
import net.java.otr4j.session.SessionID;
import org.json.JSONException;
import org.json.JSONObject;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
private Account account;
private OtrPolicy otrPolicy;
private KeyPair keyPair;
private XmppConnectionService mXmppConnectionService;
public OtrService(XmppConnectionService service, Account account) {
this.account = account;
this.otrPolicy = new OtrPolicyImpl();
this.otrPolicy.setAllowV1(false);
this.otrPolicy.setAllowV2(true);
this.otrPolicy.setAllowV3(true);
this.keyPair = loadKey(account.getKeys());
this.mXmppConnectionService = service;
}
private KeyPair loadKey(final JSONObject keys) {
if (keys == null) {
return null;
}
synchronized (keys) {
try {
BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
return new KeyPair(publicKey, privateKey);
} catch (JSONException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (InvalidKeySpecException e) {
return null;
}
}
}
private void saveKey() {
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
KeyFactory keyFactory;
try {
keyFactory = KeyFactory.getInstance("DSA");
DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(
privateKey, DSAPrivateKeySpec.class);
DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey,
DSAPublicKeySpec.class);
this.account.setKey("otr_x", privateKeySpec.getX().toString(16));
this.account.setKey("otr_g", privateKeySpec.getG().toString(16));
this.account.setKey("otr_p", privateKeySpec.getP().toString(16));
this.account.setKey("otr_q", privateKeySpec.getQ().toString(16));
this.account.setKey("otr_y", publicKeySpec.getY().toString(16));
} catch (final NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (final InvalidKeySpecException e) {
e.printStackTrace();
}
}
@Override
public void askForSecret(SessionID id, InstanceTag instanceTag, String question) {
try {
final Jid jid = OtrJidHelper.fromSessionID(id);
Conversation conversation = this.mXmppConnectionService.find(this.account, jid);
if (conversation != null) {
conversation.smp().hint = question;
conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED;
mXmppConnectionService.updateConversationUi();
}
} catch (IllegalArgumentException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": smp in invalid session " + id.toString());
}
}
@Override
public void finishedSessionMessage(SessionID arg0, String arg1)
throws OtrException {
}
@Override
public String getFallbackMessage(SessionID arg0) {
return MessageGenerator.OTR_FALLBACK_MESSAGE;
}
@Override
public byte[] getLocalFingerprintRaw(SessionID arg0) {
try {
return getFingerprintRaw(getPublicKey());
} catch (OtrCryptoException e) {
return null;
}
}
public PublicKey getPublicKey() {
if (this.keyPair == null) {
return null;
}
return this.keyPair.getPublic();
}
@Override
public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException {
if (this.keyPair == null) {
KeyPairGenerator kg;
try {
kg = KeyPairGenerator.getInstance("DSA");
this.keyPair = kg.genKeyPair();
this.saveKey();
mXmppConnectionService.databaseBackend.updateAccount(account);
} catch (NoSuchAlgorithmException e) {
Log.d(Config.LOGTAG,
"error generating key pair " + e.getMessage());
}
}
return this.keyPair;
}
@Override
public String getReplyForUnreadableMessage(SessionID arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public OtrPolicy getSessionPolicy(SessionID arg0) {
return otrPolicy;
}
@Override
public void injectMessage(SessionID session, String body)
throws OtrException {
MessagePacket packet = new MessagePacket();
packet.setFrom(account.getJid());
if (session.getUserID().isEmpty()) {
packet.setAttribute("to", session.getAccountID());
} else {
packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID());
}
packet.setBody(body);
MessageGenerator.addMessageHints(packet);
try {
Jid jid = OtrJidHelper.fromSessionID(session);
Conversation conversation = mXmppConnectionService.find(account, jid);
if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
if (mXmppConnectionService.sendChatStates()) {
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
}
}
} catch (final IllegalArgumentException ignored) {
}
packet.setType(MessagePacket.TYPE_CHAT);
packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0");
account.getXmppConnection().sendMessagePacket(packet);
}
@Override
public void messageFromAnotherInstanceReceived(SessionID session) {
sendOtrErrorMessage(session, "Message from another OTR-instance received");
}
@Override
public void multipleInstancesDetected(SessionID arg0) {
// TODO Auto-generated method stub
}
@Override
public void requireEncryptedMessage(SessionID arg0, String arg1)
throws OtrException {
// TODO Auto-generated method stub
}
@Override
public void showError(SessionID arg0, String arg1) throws OtrException {
Log.d(Config.LOGTAG, "show error");
}
@Override
public void smpAborted(SessionID id) throws OtrException {
setSmpStatus(id, Conversation.Smp.STATUS_NONE);
}
private void setSmpStatus(SessionID id, int status) {
try {
final Jid jid = OtrJidHelper.fromSessionID(id);
Conversation conversation = this.mXmppConnectionService.find(this.account, jid);
if (conversation != null) {
conversation.smp().status = status;
mXmppConnectionService.updateConversationUi();
}
} catch (final IllegalArgumentException ignored) {
}
}
@Override
public void smpError(SessionID id, int arg1, boolean arg2)
throws OtrException {
setSmpStatus(id, Conversation.Smp.STATUS_NONE);
}
@Override
public void unencryptedMessageReceived(SessionID arg0, String arg1)
throws OtrException {
throw new OtrException(new Exception("unencrypted message received"));
}
@Override
public void unreadableMessageReceived(SessionID session) throws OtrException {
Log.d(Config.LOGTAG, "unreadable message received");
sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message");
}
public void sendOtrErrorMessage(SessionID session, String errorText) {
try {
Jid jid = OtrJidHelper.fromSessionID(session);
Conversation conversation = mXmppConnectionService.find(account, jid);
String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId();
if (id != null) {
MessagePacket packet = mXmppConnectionService.getMessageGenerator()
.generateOtrError(jid, id, errorText);
packet.setFrom(account.getJid());
mXmppConnectionService.sendMessagePacket(account, packet);
Log.d(Config.LOGTAG, packet.toString());
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
+ ": unreadable OTR message in " + conversation.getName());
}
} catch (IllegalArgumentException e) {
return;
}
}
@Override
public void unverify(SessionID id, String arg1) {
setSmpStatus(id, Conversation.Smp.STATUS_FAILED);
}
@Override
public void verify(SessionID id, String fingerprint, boolean approved) {
Log.d(Config.LOGTAG, "OtrService.verify(" + id.toString() + "," + fingerprint + "," + String.valueOf(approved) + ")");
try {
final Jid jid = OtrJidHelper.fromSessionID(id);
Conversation conversation = this.mXmppConnectionService.find(this.account, jid);
if (conversation != null) {
if (approved) {
conversation.getContact().addOtrFingerprint(fingerprint);
}
conversation.smp().hint = null;
conversation.smp().status = Conversation.Smp.STATUS_VERIFIED;
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
}
} catch (final IllegalArgumentException ignored) {
}
}
@Override
public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) {
return null;
}
}

View file

@ -209,7 +209,7 @@ public class PgpDecryptionService {
}
}
final String url = message.getFileParams().url;
mXmppConnectionService.getFileBackend().updateFileParams(message, url.toString());
mXmppConnectionService.getFileBackend().updateFileParams(message, url.toString());
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
mXmppConnectionService.updateMessage(message);
if (!inputFile.delete()) {

View file

@ -128,22 +128,22 @@ public class PgpEngine {
api.executeApiAsync(params, is, os, result -> {
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
try {
os.flush();
} catch (IOException ignored) {
//ignored
}
FileBackend.close(os);
mXmppConnectionService.sendMessage(message);
callback.success(message);
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
break;
case OpenPgpApi.RESULT_CODE_ERROR:
logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
callback.error(R.string.openpgp_error, message);
break;
try {
os.flush();
} catch (IOException ignored) {
//ignored
}
FileBackend.close(os);
mXmppConnectionService.sendMessage(message);
callback.success(message);
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
break;
case OpenPgpApi.RESULT_CODE_ERROR:
logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
callback.error(R.string.openpgp_error, message);
break;
}
});
} catch (final IOException e) {

View file

@ -193,4 +193,4 @@ public class XmppDomainVerifier {
return false;
}
}
}
}

View file

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

View file

@ -5,16 +5,10 @@ import android.database.Cursor;
import android.os.SystemClock;
import android.util.Log;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
import net.java.otr4j.crypto.OtrCryptoException;
import com.google.common.base.Strings;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.PublicKey;
import java.security.interfaces.DSAPublicKey;
import java.util.Locale;
import eu.siacs.conversations.crypto.OtrService;
import java.util.ArrayList;
import java.util.Collection;
@ -88,7 +82,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
protected String hostname = null;
protected int port = 5222;
protected boolean online = false;
private OtrService mOtrService = null;
private String rosterVersion;
private String displayName = null;
private AxolotlService axolotlService = null;
@ -403,16 +396,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public void initAccountServices(final XmppConnectionService context) {
this.mOtrService = new OtrService(context, this);
this.axolotlService = new AxolotlService(this, context);
this.pgpDecryptionService = new PgpDecryptionService(context);
if (xmppConnection != null) {
xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
}
}
public OtrService getOtrService() {
return this.mOtrService;
}
public PgpDecryptionService getPgpDecryptionService() {
return this.pgpDecryptionService;
@ -425,25 +414,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public void setXmppConnection(final XmppConnection connection) {
this.xmppConnection = connection;
}
public String getOtrFingerprint() {
if (this.otrFingerprint == null) {
try {
if (this.mOtrService == null) {
return null;
}
final PublicKey publicKey = this.mOtrService.getPublicKey();
if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
return null;
}
this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
return this.otrFingerprint;
} catch (final OtrCryptoException ignored) {
return null;
}
} else {
return this.otrFingerprint;
}
}
public String getRosterVersion() {
if (this.rosterVersion == null) {
@ -608,10 +578,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private List<XmppUri.Fingerprint> getFingerprints() {
ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
final String otr = this.getOtrFingerprint();
if (otr != null) {
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR, otr));
}
if (axolotlService == null) {
return fingerprints;
}

View file

@ -516,32 +516,6 @@ public class Contact implements ListItem, Blockable {
return avatar;
}
public boolean deleteOtrFingerprint(String fingerprint) {
synchronized (this.keys) {
boolean success = false;
try {
if (this.keys.has("otr_fingerprints")) {
JSONArray newPrints = new JSONArray();
JSONArray oldPrints = this.keys
.getJSONArray("otr_fingerprints");
for (int i = 0; i < oldPrints.length(); ++i) {
if (!oldPrints.getString(i).equals(fingerprint)) {
newPrints.put(oldPrints.getString(i));
} else {
success = true;
}
}
this.keys.put("otr_fingerprints", newPrints);
}
return success;
} catch (JSONException e) {
return false;
}
}
}
public boolean mutualPresenceSubscription() {
return getOption(Options.FROM) && getOption(Options.TO);
}

View file

@ -1,20 +1,18 @@
package eu.siacs.conversations.entities;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
import android.content.ContentValues;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import net.java.otr4j.OtrException;
import net.java.otr4j.crypto.OtrCryptoException;
import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import java.security.interfaces.DSAPublicKey;
import java.util.Locale;
import org.json.JSONArray;
import org.json.JSONException;
@ -22,6 +20,7 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
@ -34,13 +33,12 @@ import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.mam.MamReference;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
public static final String TABLENAME = "conversations";
@ -84,15 +82,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
private int mode;
private JSONObject attributes;
private Jid nextCounterpart;
private transient SessionImpl otrSession;
private transient String otrFingerprint = null;
private Smp mSmp = new Smp();
private transient MucOptions mucOptions = null;
private byte[] symmetricKey;
private boolean messagesLeftOnServer = true;
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
private String mLastReceivedOtrMessageId = null;
private String mFirstMamReference = null;
public Conversation(final String name, final Account account, final Jid contactJid,
@ -305,9 +298,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public Message findMessageWithFileAndUuid(final String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
final Transferable transferable = message.getTransferable();
final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
if (message.getUuid().equals(uuid)
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& (message.isFileOrImage() || message.treatAsDownloadable())) {
&& (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
return message;
}
}
}
return null;
}
public Message findMessageWithUuid(final String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
if (message.getUuid().equals(uuid)) {
return message;
}
}
@ -386,7 +392,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public void trim() {
synchronized (this.messages) {
final int size = messages.size();
final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
if (getAccount() != null && getAccount().getXmppConnection() != null && getAccount().getXmppConnection().getXmppConnectionService() != null) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getAccount().getXmppConnection().getXmppConnectionService());
int pagesize = Integer.parseInt(pref.getString("pagesize", String.valueOf(Config.PAGE_SIZE)));
int maxnumpages = Integer.parseInt(pref.getString("max_num_pages", String.valueOf(Config.MAX_NUM_PAGES)));
maxsize = pagesize * maxnumpages;
}
if (size > maxsize) {
List<Message> discards = this.messages.subList(0, size - maxsize);
final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
@ -399,17 +411,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
}
public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
synchronized (this.messages) {
for (Message message : this.messages) {
if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
&& (message.getEncryption() == encryptionType)) {
onMessageFound.onMessageFound(message);
}
}
}
}
public void findUnsentTextMessages(OnMessageFound onMessageFound) {
final ArrayList<Message> results = new ArrayList<>();
synchronized (this.messages) {
@ -425,13 +426,24 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
public Message findSentMessageWithUuidOrRemoteId(String id) {
return findSentMessageWithUuidOrRemoteId(id, false, false);
}
public Message findSentMessageWithUuidOrRemoteId(String id, boolean ignorestatus, boolean withedits) {
synchronized (this.messages) {
for (Message message : this.messages) {
if (id.equals(message.getUuid())
|| (message.getStatus() >= Message.STATUS_SEND
&& id.equals(message.getRemoteMsgId()))) {
if (id.equals(message.getUuid()) || ((message.getStatus() >= Message.STATUS_SEND || ignorestatus) && id.equals(message.getRemoteMsgId()))) {
return message;
}
if (withedits) {
for (Edit itm : message.edits) {
if (id.equals(itm.getEditedId())) {
return message;
}
}
}
}
}
return null;
@ -450,8 +462,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
return message;
} else {
return null;
}
}
}
@ -504,10 +514,51 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return false;
}
public List<Message> filterDuplicates(List<Message> list) {
HashMap<String, Message> items = new HashMap<String, Message>();
for (Message item : list) {
items.put(item.getUuid(), item);
}
ArrayList<Message> result = new ArrayList<Message>(items.values());
Collections.sort(result, (o1, o2) -> {
if (o1.getTimeSent() < o2.getTimeSent())
return -1;
if (o1.getTimeSent() > o2.getTimeSent())
return 1;
return 0;
});
return result;
}
public void populateWithMessages(final List<Message> messages) {
synchronized (this.messages) {
messages.clear();
messages.addAll(this.messages);
messages.addAll(filterDuplicates(this.messages));
for (int n = 0; n < messages.size(); n++) {
if (messages.get(n).isMessageDeleted()) {
messages.remove(n);
n--;
continue;
}
if (messages.get(n).getRetractId() != null) {
if (messages.get(n).getStatus() != Message.STATUS_RECEIVED) {
messages.remove(n);
n--;
continue;
}
}
}
for (Message itm : messages) {
if (itm.isMessageDeleted()) {
if (itm.getEditedList().size() > 0) {
itm.setTime(itm.getEditedList().get(0).getTimeSent());
}
}
}
}
for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
if (iterator.next().wasMergedIntoPrevious()) {
@ -530,13 +581,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public Jid getBlockedJid() {
return getContact().getBlockedJid();
}
public String getLastReceivedOtrMessageId() {
return this.mLastReceivedOtrMessageId;
}
public void setLastReceivedOtrMessageId(String id) {
this.mLastReceivedOtrMessageId = id;
}
public int countMessages() {
synchronized (this.messages) {
@ -734,112 +778,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public void setMode(int mode) {
this.mode = mode;
}
public SessionImpl startOtrSession(String presence, boolean sendStart) {
if (this.otrSession != null) {
return this.otrSession;
} else {
final SessionID sessionId = new SessionID(this.getJid().asBareJid().toString(),
presence,
"xmpp");
this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
try {
if (sendStart) {
this.otrSession.startSession();
return this.otrSession;
}
return this.otrSession;
} catch (OtrException e) {
return null;
}
}
}
public SessionImpl getOtrSession() {
return this.otrSession;
}
public void resetOtrSession() {
this.otrFingerprint = null;
this.otrSession = null;
this.mSmp.hint = null;
this.mSmp.secret = null;
this.mSmp.status = Smp.STATUS_NONE;
}
public Smp smp() {
return mSmp;
}
public boolean startOtrIfNeeded() {
if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
try {
this.otrSession.startSession();
return true;
} catch (OtrException e) {
this.resetOtrSession();
return false;
}
} else {
return true;
}
}
public boolean endOtrIfNeeded() {
if (this.otrSession != null) {
if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
try {
this.otrSession.endSession();
this.resetOtrSession();
return true;
} catch (OtrException e) {
this.resetOtrSession();
return false;
}
} else {
this.resetOtrSession();
return false;
}
} else {
return false;
}
}
public boolean hasValidOtrSession() {
return this.otrSession != null;
}
public synchronized String getOtrFingerprint() {
if (this.otrFingerprint == null) {
try {
if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
return null;
}
DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
} catch (final OtrCryptoException ignored) {
return null;
} catch (final UnsupportedOperationException ignored) {
return null;
}
}
return this.otrFingerprint;
}
public boolean verifyOtrFingerprint() {
final String fingerprint = getOtrFingerprint();
if (fingerprint != null) {
getContact().addOtrFingerprint(fingerprint);
return true;
} else {
return false;
}
}
public boolean isOtrFingerprintVerified() {
return getContact().getOtrFingerprints().contains(getOtrFingerprint());
}
/**
* short for is Private and Non-anonymous
@ -876,7 +814,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
public int getNextEncryption() {
if (!Config.supportOmemo() && !Config.supportOpenPgp() && !Config.supportOtr()) {
if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
return Message.ENCRYPTION_NONE;
}
if (OmemoSetting.isAlways()) {
@ -892,10 +830,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
defaultEncryption = Message.ENCRYPTION_NONE;
}
int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
if (encryption < 0) {
if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
return defaultEncryption;
} else if (encryption == Message.ENCRYPTION_OTR) {
return encryption;
} else {
return encryption;
}
@ -909,9 +845,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
return nextMessage == null ? "" : nextMessage;
}
public boolean smpRequested() {
return smp().status == Smp.STATUS_CONTACT_REQUESTED;
}
public @Nullable
Draft getDraft() {
@ -934,24 +867,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
return changed;
}
public void setSymmetricKey(byte[] key) {
this.symmetricKey = key;
}
public byte[] getSymmetricKey() {
return this.symmetricKey;
}
public Bookmark getBookmark() {
return this.account.getBookmark(this.contactJid);
}
public Message findDuplicateMessage(Message message) {
public Message findDuplicateMessage(Message message, boolean withremoteid) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
if (this.messages.get(i).similar(message)) {
return this.messages.get(i);
}
if (withremoteid) {
if (this.messages.get(i).remoteMsgId != null && message.getRemoteMsgId() != null && this.messages.get(i).remoteMsgId.equals(message.getRemoteMsgId())) {
return this.messages.get(i);
}
}
}
}
return null;
@ -1308,15 +1239,23 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return message;
}
}
public class Smp {
public static final int STATUS_NONE = 0;
public static final int STATUS_CONTACT_REQUESTED = 1;
public static final int STATUS_WE_REQUESTED = 2;
public static final int STATUS_FAILED = 3;
public static final int STATUS_VERIFIED = 4;
public String secret = null;
public String hint = null;
public int status = 0;
public Message findDuplicateMessage(Message message) {
return findDuplicateMessage(message, false);
}
}
public boolean hasDuplicateMessage(Message message, boolean withremoteid) {
return findDuplicateMessage(message, withremoteid) != null;
}
public Message findMessageWithUuidOrRemoteId(final String id) {
synchronized (this.messages) {
for (final Message message : this.messages) {
if (message.getRemoteMsgId() != null && message.getRemoteMsgId().equals(id) || message.getUuid().equals(id)) {
return message;
}
}
}
return null;
}
}

View file

@ -11,16 +11,20 @@ public class Edit {
private final String editedId;
private final String serverMsgId;
private String body;
private final long timeSent;
Edit(String editedId, String serverMsgId) {
Edit(String editedId, String serverMsgId, String body, long timeSent) {
this.editedId = editedId;
this.serverMsgId = serverMsgId;
this.body = body;
this.timeSent = timeSent;
}
static String toJson(List<Edit> edits) throws JSONException {
static String toJson(List<Edit> edits, boolean hidebody) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (Edit edit : edits) {
jsonArray.put(edit.toJson());
jsonArray.put(edit.toJson(hidebody));
}
return jsonArray.toString();
}
@ -46,7 +50,9 @@ public class Edit {
private static Edit fromJson(JSONObject jsonObject) throws JSONException {
String edited = jsonObject.has("edited_id") ? jsonObject.getString("edited_id") : null;
String serverMsgId = jsonObject.has("server_msg_id") ? jsonObject.getString("server_msg_id") : null;
return new Edit(edited, serverMsgId);
String body = jsonObject.has("body") ? jsonObject.getString("body") : null;
long timeSent = jsonObject.has("timeSent") ? jsonObject.getLong("timeSent") : null;
return new Edit(edited, serverMsgId, body, timeSent);
}
static List<Edit> fromJson(String input) {
@ -65,17 +71,15 @@ public class Edit {
}
}
private JSONObject toJson() throws JSONException {
private JSONObject toJson(boolean hidebody) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("edited_id", editedId);
jsonObject.put("server_msg_id", serverMsgId);
jsonObject.put("body", hidebody ? "" : body);
jsonObject.put("timeSent", timeSent);
return jsonObject;
}
String getEditedId() {
return editedId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -94,4 +98,18 @@ public class Edit {
result = 31 * result + (serverMsgId != null ? serverMsgId.hashCode() : 0);
return result;
}
public String getServerMsgId() {
return serverMsgId;
}
public String getBody() {
return body;
}
public String getEditedId() { return editedId; }
public long getTimeSent() {
return timeSent;
}
}

View file

@ -41,8 +41,8 @@ public class IndividualMessage extends Message {
super(conversation);
}
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, boolean deleted, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean file_deleted, String bodyLanguage) {
super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, deleted, edited, oob, errorMessage, readByMarkers, markable, file_deleted, bodyLanguage);
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, boolean deleted, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean file_deleted, String bodyLanguage, String retractId) {
super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, deleted, edited, oob, errorMessage, readByMarkers, markable, file_deleted, bodyLanguage,retractId);
}
public static Message createDateSeparator(Message message) {
@ -101,7 +101,8 @@ public class IndividualMessage extends Message {
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0,
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
cursor.getString(cursor.getColumnIndex(RETRACT_ID))
);
}

View file

@ -64,7 +64,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public static final int TYPE_STATUS = 3;
public static final int TYPE_PRIVATE = 4;
public static final int TYPE_PRIVATE_FILE = 5;
public static final int TYPE_RTP_SESSION = 6;
public static final int TYPE_RTP_SESSION = 6;
public static final String CONVERSATION = "conversationUuid";
public static final String COUNTERPART = "counterpart";
@ -91,7 +91,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public static final String ME_COMMAND = "/me";
public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
public static final String DELETED_MESSAGE_BODY = "eu.siacs.conversations.message_deleted";
public static final String DELETED_MESSAGE_BODY_OLD = "de.monocles.chat.message_deleted";
public static final String DELETED_MESSAGE_BODY_OLD = "de.pixart.messenger.message_deleted";
public static final String RETRACT_ID = "retractId";
public boolean markable = false;
protected String conversationUuid;
@ -111,6 +112,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
protected boolean read = true;
protected boolean deleted = false;
protected String remoteMsgId = null;
private String bodyLanguage = null;
protected String serverMsgId = null;
private final Conversational conversation;
@ -120,6 +122,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
private String axolotlFingerprint = null;
private String errorMessage = null;
private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
private String retractId = null;
private Boolean isGeoUri = null;
private Boolean isXmppUri = null;
@ -162,21 +165,22 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
null,
false,
false,
null,
null);
}
public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
this(conversation, java.util.UUID.randomUUID().toString(),
conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
null,
null,
System.currentTimeMillis(),
Message.ENCRYPTION_NONE,
status,
type,
false,
remoteMsgId,
public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
this(conversation, java.util.UUID.randomUUID().toString(),
conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
null,
null,
System.currentTimeMillis(),
Message.ENCRYPTION_NONE,
status,
type,
false,
remoteMsgId,
null,
null,
null,
@ -188,6 +192,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
null,
false,
false,
null,
null);
}
@ -197,7 +202,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint, final boolean read, final boolean deleted,
final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
final boolean markable, final boolean file_deleted, final String bodyLanguage) {
final boolean markable, final boolean file_deleted, final String bodyLanguage, final String retractId) {
this.conversation = conversation;
this.uuid = uuid;
this.conversationUuid = conversationUUid;
@ -222,6 +227,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.markable = markable;
this.file_deleted = file_deleted;
this.bodyLanguage = bodyLanguage;
this.retractId = retractId;
}
public static Message fromCursor(Cursor cursor, Conversation conversation) {
@ -248,7 +254,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0,
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
cursor.getString(cursor.getColumnIndex(RETRACT_ID))
);
}
@ -306,7 +313,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
values.put(READ, read ? 1 : 0);
values.put(DELETED, deleted ? 1 : 0);
try {
values.put(EDITED, Edit.toJson(edits));
values.put(EDITED, Edit.toJson(edits, retractId != null || deleted));
} catch (JSONException e) {
Log.e(Config.LOGTAG, "error persisting json for edits", e);
}
@ -316,6 +323,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
values.put(MARKABLE, markable ? 1 : 0);
values.put(FILE_DELETED, file_deleted ? 1 : 0);
values.put(BODY_LANGUAGE, bodyLanguage);
values.put(RETRACT_ID, retractId);
return values;
}
@ -486,8 +494,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.carbon = carbon;
}
public void putEdited(String edited, String serverMsgId) {
final Edit edit = new Edit(edited, serverMsgId);
public void putEdited(String edited, String serverMsgId, String body, long timeSent) {
final Edit edit = new Edit(edited, serverMsgId, body, timeSent);
if (this.edits.size() < 128 && !this.edits.contains(edit)) {
this.edits.add(edit);
}
@ -526,6 +534,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return this.transferable;
}
public String getRetractId() {
return this.retractId;
}
public void setRetractId(String id) {
this.retractId = id;
}
public synchronized void setTransferable(Transferable transferable) {
this.fileParams = null;
this.transferable = transferable;
@ -638,54 +654,62 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public boolean isLastCorrectableMessage() {
Message next = next();
while (next != null) {
if (next.isEditable()) {
if (next.isEditable()) {
return false;
}
next = next.next();
}
return isEditable();
return isEditable();
}
public boolean isEditable() {
return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
return status != STATUS_RECEIVED && type != Message.TYPE_RTP_SESSION;
}
public boolean mergeable(final Message message) {
return message != null &&
(message.getType() == Message.TYPE_TEXT &&
this.getTransferable() == null &&
message.getTransferable() == null &&
message.getEncryption() != Message.ENCRYPTION_PGP &&
message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
this.getType() == message.getType() &&
//this.getStatus() == message.getStatus() &&
isStatusMergeable(this.getStatus(), message.getStatus()) &&
this.getEncryption() == message.getEncryption() &&
this.getCounterpart() != null &&
this.getCounterpart().equals(message.getCounterpart()) &&
this.edited() == message.edited() &&
!this.isMessageDeleted() == !message.isMessageDeleted() &&
(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
!message.isGeoUri() &&
!this.isGeoUri() &&
!message.isWebUri() &&
!this.isWebUri() &&
!message.isOOb() &&
!this.isOOb() &&
!message.treatAsDownloadable() &&
!this.treatAsDownloadable() &&
!message.hasMeCommand() &&
!this.hasMeCommand() &&
!message.bodyIsOnlyEmojis() &&
!this.bodyIsOnlyEmojis() &&
!message.isXmppUri() &&
!this.isXmppUri() &&
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
this.getReadByMarkers().equals(message.getReadByMarkers()) &&
!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
);
try {
boolean mergeAllowed = conversation.getAccount().getXmppConnection().getXmppConnectionService().allowMergeMessages();
return mergeAllowed && message != null &&
(message.getType() == Message.TYPE_TEXT &&
this.getTransferable() == null &&
message.getTransferable() == null &&
message.getEncryption() != Message.ENCRYPTION_PGP &&
message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
this.getType() == message.getType() &&
//this.getStatus() == message.getStatus() &&
isStatusMergeable(this.getStatus(), message.getStatus()) &&
this.getEncryption() == message.getEncryption() &&
this.getCounterpart() != null &&
this.getCounterpart().equals(message.getCounterpart()) &&
this.edited() == message.edited() &&
!this.isMessageDeleted() == !message.isMessageDeleted() &&
(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
!message.isGeoUri() &&
!this.isGeoUri() &&
!message.isWebUri() &&
!this.isWebUri() &&
!message.isOOb() &&
!this.isOOb() &&
!message.treatAsDownloadable() &&
!this.treatAsDownloadable() &&
!message.hasMeCommand() &&
!this.hasMeCommand() &&
!message.bodyIsOnlyEmojis() &&
!this.bodyIsOnlyEmojis() &&
!message.isXmppUri() &&
!this.isXmppUri() &&
!message.hasDeletedBody() &&
!this.hasDeletedBody() &&
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
this.getReadByMarkers().equals(message.getReadByMarkers()) &&
!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private static boolean isStatusMergeable(int a, int b) {
@ -746,6 +770,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return this.body.trim().startsWith(ME_COMMAND);
}
public boolean hasDeletedBody() {
return this.body.trim().equals(DELETED_MESSAGE_BODY) || this.body.trim().equals(DELETED_MESSAGE_BODY_OLD);
}
public int getMergedStatus() {
int status = this.status;
Message current = this;
@ -783,8 +811,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public boolean fixCounterpart() {
final Presences presences = conversation.getContact().getPresences();
if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
final Presences presences = conversation.getContact().getPresences();
if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
return true;
} else if (presences.size() >= 1) {
counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]);
@ -807,6 +835,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
}
public List<Edit> getEditedList() {
return edits;
}
public String getEditedIdWireFormat() {
if (edits.size() > 0) {
return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
@ -824,16 +856,16 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
if (relativeFilePath != null) {
extension = MimeUtils.extractRelevantExtension(relativeFilePath);
} else {
try {
final String url = URL.tryParse(body.split("\n")[0]);
if (url == null) {
return null;
}
extension = MimeUtils.extractRelevantExtension(url);
} catch (Exception e) {
return null;
}
}
try {
final String url = URL.tryParse(body.split("\n")[0]);
if (url == null) {
return null;
}
extension = MimeUtils.extractRelevantExtension(url);
} catch (Exception e) {
return null;
}
}
return MimeUtils.guessMimeTypeFromExtension(extension);
}
@ -977,6 +1009,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return isFileOrImage() && getFileParams().url == null;
}
public boolean fileIsTransferring() {
return transferable.getStatus() == Transferable.STATUS_DOWNLOADING || transferable.getStatus() == Transferable.STATUS_UPLOADING || transferable.getStatus() == Transferable.STATUS_WAITING;
}
public static class FileParams {
public String url;
public Long size = null;
@ -984,7 +1020,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public int height = 0;
public int runtime = 0;
public String subject = "";
public long getSize() {
return size == null ? 0 : size;
}
@ -1066,13 +1101,28 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
if (conversation.getMode() == Conversation.MODE_MULTI) {
final Jid nextCounterpart = conversation.getNextCounterpart();
if (nextCounterpart != null) {
message.setCounterpart(nextCounterpart);
message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
return true;
}
return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
}
return false;
}
}
public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
final Conversation conversation;
if (message.conversation instanceof Conversation) {
conversation = (Conversation) message.conversation;
} else {
return false;
}
return configurePrivateMessage(conversation, message, counterpart, false);
}
private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
if (counterpart == null) {
return false;
}
message.setCounterpart(counterpart);
message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
return true;
}
}

View file

@ -1,6 +1,5 @@
package eu.siacs.conversations.entities;
import android.annotation.SuppressLint;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@ -25,7 +24,6 @@ import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Field;
import eu.siacs.conversations.xmpp.pep.Avatar;
@SuppressLint("DefaultLocale")
public class MucOptions {
public static final String STATUS_CODE_SELF_PRESENCE = "110";
@ -213,6 +211,10 @@ public class MucOptions {
return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false);
}
public boolean stableId() {
return getFeatures().contains("http://jabber.org/protocol/muc#stable_id");
}
public User deleteUser(Jid jid) {
User user = findUserByFullJid(jid);
if (user != null) {
@ -630,8 +632,8 @@ public class MucOptions {
OUTCAST(0, R.string.outcast),
NONE(1, R.string.no_affiliation);
private int resId;
private int rank;
private final int resId;
private final int rank;
Affiliation(int rank, int resId) {
this.resId = resId;
@ -673,8 +675,8 @@ public class MucOptions {
PARTICIPANT(R.string.participant, 2),
NONE(R.string.no_role, 0);
private int resId;
private int rank;
private final int resId;
private final int rank;
Role(int resId, int rank) {
this.resId = resId;
@ -741,7 +743,7 @@ public class MucOptions {
private Jid fullJid;
private long pgpKeyId = 0;
private Avatar avatar;
private MucOptions options;
private final MucOptions options;
private ChatState chatState = Config.DEFAULT_CHAT_STATE;
public User(MucOptions options, Jid fullJid) {
@ -852,7 +854,7 @@ public class MucOptions {
@Override
public String toString() {
return "[fulljid:" + String.valueOf(fullJid) + ",realjid:" + String.valueOf(realJid) + ",affiliation" + affiliation.toString() + "]";
return "[fulljid:" + fullJid + ",realjid:" + realJid + ",affiliation" + affiliation.toString() + "]";
}
public boolean realJidMatchesAccount() {

View file

@ -223,9 +223,9 @@ public class ServiceDiscoveryResult {
for (Data form : forms) {
s.append(clean(form.getFormType())).append("<");
List<Field> fields = form.getFields();
Collections.sort(fields, (lhs, rhs) -> Strings.nullToEmpty(lhs.getFieldName()).compareTo(Strings.nullToEmpty(rhs.getFieldName())));
Collections.sort(fields, (lhs, rhs) -> Strings.nullToEmpty(lhs.getFieldName()).compareTo(Strings.nullToEmpty(rhs.getFieldName())));
for (Field field : fields) {
s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
List<String> values = field.getValues();
Collections.sort(values);
for (String value : values) {

View file

@ -12,7 +12,6 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@ -54,12 +53,12 @@ public abstract class AbstractGenerator {
private final String[] MESSAGE_CORRECTION_FEATURES = {
"urn:xmpp:message-correct:0"
};
private final String[] MESSAGE_RETRACTION_FEATURES = {
"urn:xmpp:message-retract:0"
};
private final String[] PRIVACY_SENSITIVE = {
"urn:xmpp:time" //XEP-0202: Entity Time leaks time zone
};
private final String[] OTR = {
"urn:xmpp:otr:0"
};
private final String[] VOIP_NAMESPACES = {
Namespace.JINGLE_TRANSPORT_ICE_UDP,
Namespace.JINGLE_FEATURE_AUDIO,
@ -70,6 +69,7 @@ public abstract class AbstractGenerator {
};
protected XmppConnectionService mXmppConnectionService;
private String mVersion = null;
AbstractGenerator(XmppConnectionService service) {
this.mXmppConnectionService = service;
@ -81,11 +81,18 @@ public abstract class AbstractGenerator {
}
String getIdentityVersion() {
return BuildConfig.VERSION_NAME;
if (mVersion == null) {
this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService);
}
return this.mVersion;
}
public String getIdentityName() {
return BuildConfig.APP_NAME;
return mXmppConnectionService.getString(R.string.app_name) + ' ' + getIdentityVersion();
}
public String getUserAgent() {
return System.getProperty("http.agent");
}
String getIdentityType() {
@ -122,6 +129,9 @@ public abstract class AbstractGenerator {
if (mXmppConnectionService.allowMessageCorrection()) {
features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES));
}
if (mXmppConnectionService.allowMessageRetraction()) {
features.addAll(Arrays.asList(MESSAGE_RETRACTION_FEATURES));
}
if (Config.supportOmemo()) {
features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
}
@ -129,9 +139,6 @@ public abstract class AbstractGenerator {
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
features.addAll(Arrays.asList(VOIP_NAMESPACES));
}
if (Config.supportOtr()) {
features.addAll(Arrays.asList(OTR));
}
if (mXmppConnectionService.broadcastLastActivity()) {
features.add(Namespace.IDLE);
}

View file

@ -22,11 +22,8 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
public class MessageGenerator extends AbstractGenerator {
public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesnt seem to support that";
private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesnt seem to support that. Find more information on https://conversations.im/omemo";
private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesnt seem to support that.";
@ -61,9 +58,17 @@ public class MessageGenerator extends AbstractGenerator {
}
packet.setFrom(account.getJid());
packet.setId(message.getUuid());
packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid());
if (message.edited()) {
if (conversation.getMode() == Conversational.MODE_SINGLE || message.isPrivateMessage() || !conversation.getMucOptions().stableId()) {
packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid());
}
if (message.edited() && !message.isMessageDeleted()) {
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
} else if (message.isMessageDeleted()) {
Element apply = packet.addChild("apply-to", "urn:xmpp:fasten:0").setAttribute("id", (message.getRetractId() != null ? message.getRetractId() : (message.getRemoteMsgId() != null ? message.getRemoteMsgId() : (message.getEditedIdWireFormat() != null ? message.getEditedIdWireFormat() : message.getUuid()))));
apply.addChild("retract", "urn:xmpp:message-retract:0");
packet.addChild("fallback", "urn:xmpp:fallback:0");
packet.addChild("store", "urn:xmpp:hints");
packet.setBody("This person attempted to retract a previous message, but it's unsupported by your client.");
}
return packet;
}
@ -107,29 +112,6 @@ public class MessageGenerator extends AbstractGenerator {
packet.addChild("no-permanent-store", "urn:xmpp:hints");
packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store*
}
public MessagePacket generateOtrChat(Message message) {
Conversation conversation = (Conversation) message.getConversation();
Session otrSession = conversation.getOtrSession();
if (otrSession == null) {
return null;
}
MessagePacket packet = preparePacket(message);
addMessageHints(packet);
try {
String content;
if (message.hasFileOnRemoteHost()) {
content = message.getFileParams().url.toString();
} else {
content = message.getBody();
}
packet.setBody(otrSession.transformSending(content)[0]);
packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0");
return packet;
} catch (OtrException e) {
return null;
}
}
public MessagePacket generateChat(Message message) {
MessagePacket packet = preparePacket(message);
@ -141,7 +123,8 @@ public class MessageGenerator extends AbstractGenerator {
} else {
content = message.getBody();
}
packet.setBody(content);
if (!message.isMessageDeleted())
packet.setBody(content);
return packet;
}
@ -274,7 +257,7 @@ public class MessageGenerator extends AbstractGenerator {
error.addChild("text").setContent("?OTR Error:" + errorText);
return packet;
}
public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
final MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those

View file

@ -77,7 +77,7 @@ public class PresenceGenerator extends AbstractGenerator {
Element cap = packet.addChild("c",
"http://jabber.org/protocol/caps");
cap.setAttribute("hash", "sha-1");
cap.setAttribute("node", "http://monocles.de");
cap.setAttribute("node", "http://blabber.im");
cap.setAttribute("ver", capHash);
}
return packet;

View file

@ -22,7 +22,6 @@ import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message;
@ -55,10 +54,10 @@ public class HttpConnectionManager extends AbstractConnectionManager {
.build();
}
public static String getUserAgent() {
return String.format("%s/%s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME);
return System.getProperty("http.agent");
}
public HttpConnectionManager(XmppConnectionService service) {
super(service);
}
@ -149,7 +148,8 @@ public class HttpConnectionManager extends AbstractConnectionManager {
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
builder.sslSocketFactory(sf, trustManager);
builder.hostnameVerifier(new StrictHostnameVerifier());
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
} catch (final KeyManagementException ignored) {
} catch (final NoSuchAlgorithmException ignored) {
}
}

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.http;
import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor;
import android.util.Log;
import androidx.annotation.Nullable;
@ -35,8 +37,6 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor;
public class HttpDownloadConnection implements Transferable {
private final Message message;
@ -114,13 +114,12 @@ public class HttpDownloadConnection implements Transferable {
}
}
setupFile();
if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
|| this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
&& this.file.getKey() == null) {
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
//TODO add auth tag size to knownFileSize
final Long knownFileSize = message.getFileParams().size;
Log.d(Config.LOGTAG, "knownFileSize: " + knownFileSize + ", body=" + message.getBody());
if (knownFileSize != null && interactive) {
this.file.setExpectedSize(knownFileSize);
download(true);
@ -145,7 +144,7 @@ public class HttpDownloadConnection implements Transferable {
private void download(final boolean interactive) {
changeStatus(STATUS_WAITING);
Log.d(Config.LOGTAG,"download()",new Exception());
Log.d(Config.LOGTAG, "download()", new Exception());
FileTransferExecutor.execute(new FileDownloader(interactive));
}

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.http;
import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor;
import android.util.Log;
import com.google.common.util.concurrent.FutureCallback;
@ -30,8 +32,6 @@ import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor;
public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
@ -185,26 +185,26 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) {
final int code = response.code();
if (code == 200 || code == 201) {
Log.d(Config.LOGTAG, "finished uploading file");
final String get;
if (key != null) {
if (code == 200 || code == 201) {
Log.d(Config.LOGTAG, "finished uploading file");
final String get;
if (key != null) {
get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
} else {
get = slot.get.toString();
}
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
finish();
if (!message.isPrivateMessage()) {
message.setCounterpart(message.getConversation().getJid().asBareJid());
}
mXmppConnectionService.resendMessage(message, delayed);
} else {
Log.d(Config.LOGTAG, "http upload failed because response code was " + code);
fail("http upload failed because response code was " + code);
}
mXmppConnectionService.getFileBackend().updateFileParams(message, get);
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
finish();
if (!message.isPrivateMessage()) {
message.setCounterpart(message.getConversation().getJid().asBareJid());
}
mXmppConnectionService.resendMessage(message, delayed);
} else {
Log.d(Config.LOGTAG, "http upload failed because response code was " + code);
fail("http upload failed because response code was " + code);
}
}
});
}

View file

@ -33,7 +33,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xmpp.XmppConnection;
public enum Method {
HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
public static Method determine(Account account) {
XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();

View file

@ -114,7 +114,7 @@ public class SlotRequester {
final String name = child.getAttribute("name");
final String value = child.getContent();
if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
headers.put(name, value.trim());
headers.put(name, value.trim());
}
}
}

View file

@ -139,19 +139,19 @@ public abstract class AbstractParser {
return null;
}
public static String errorMessage(Element packet) {
final Element error = packet.findChild("error");
if (error != null && error.getChildren().size() > 0) {
final List<String> errorNames = orderedElementNames(error.getChildren());
final String text = error.findChildContent("text");
if (text != null && !text.trim().isEmpty()) {
return text;
} else if (errorNames.size() > 0){
return errorNames.get(0).replace("-"," ");
}
}
return null;
}
public static String errorMessage(Element packet) {
final Element error = packet.findChild("error");
if (error != null && error.getChildren().size() > 0) {
final List<String> errorNames = orderedElementNames(error.getChildren());
final String text = error.findChildContent("text");
if (text != null && !text.trim().isEmpty()) {
return text;
} else if (errorNames.size() > 0){
return errorNames.get(0).replace("-"," ");
}
}
return null;
}
private static String prefixError(List<String> errorNames) {
if (errorNames.size() > 0) {

View file

@ -463,5 +463,5 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
}
;
;
}

View file

@ -1,8 +1,5 @@
package eu.siacs.conversations.parser;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD;
import android.util.Log;
import android.util.Pair;
@ -16,14 +13,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import android.os.Build;
import android.text.Html;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@ -37,6 +26,7 @@ import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Edit;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.ReadByMarker;
@ -58,15 +48,12 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD;
public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
private static final List<String> CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian");
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract");
public MessageParser(XmppConnectionService service) {
@ -108,29 +95,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
return result != null ? result : fallback;
}
private static boolean clientMightSendHtml(Account account, Jid from) {
String resource = from.getResource();
if (resource == null) {
return false;
}
Presence presence = account.getRoster().getContact(from).getPresences().getPresencesMap().get(resource);
ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult();
if (disco == null) {
return false;
}
return hasIdentityKnowForSendingHtml(disco.getIdentities());
}
private static boolean hasIdentityKnowForSendingHtml(List<ServiceDiscoveryResult.Identity> identities) {
for (ServiceDiscoveryResult.Identity identity : identities) {
if (identity.getName() != null) {
if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) {
return true;
}
}
}
return false;
}
private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
ChatState state = ChatState.parse(packet);
@ -163,66 +127,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return false;
}
private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) {
String presence;
if (from.isBareJid()) {
presence = "";
} else {
presence = from.getResource();
}
if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) {
conversation.endOtrIfNeeded();
}
if (!conversation.hasValidOtrSession()) {
conversation.startOtrSession(presence, false);
} else {
String foreignPresence = conversation.getOtrSession().getSessionID().getUserID();
if (!foreignPresence.equals(presence)) {
conversation.endOtrIfNeeded();
conversation.startOtrSession(presence, false);
}
}
try {
conversation.setLastReceivedOtrMessageId(id);
Session otrSession = conversation.getOtrSession();
body = otrSession.transformReceiving(body);
SessionStatus status = otrSession.getSessionStatus();
if (body == null && status == SessionStatus.ENCRYPTED) {
mXmppConnectionService.onOtrSessionEstablished(conversation);
return null;
} else if (body == null && status == SessionStatus.FINISHED) {
conversation.resetOtrSession();
mXmppConnectionService.updateConversationUi();
return null;
} else if (body == null || (body.isEmpty())) {
return null;
}
if (body.startsWith(CryptoHelper.FILETRANSFER)) {
String key = body.substring(CryptoHelper.FILETRANSFER.length());
conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
return null;
}
if (clientMightSendHtml(conversation.getAccount(), from)) {
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OTR message from bad behaving client. escaping HTML…");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
body = Html.fromHtml(body, Html.FROM_HTML_MODE_LEGACY).toString();
} else {
body = Html.fromHtml(body).toString();
}
}
final OtrService otrService = conversation.getAccount().getOtrService();
Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED);
finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey()));
conversation.setLastReceivedOtrMessageId(null);
return finishedMessage;
} catch (Exception e) {
conversation.resetOtrSession();
return null;
}
}
private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) {
final AxolotlService service = conversation.getAccount().getAxolotlService();
final XmppAxolotlMessage xmppAxolotlMessage;
@ -446,14 +350,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
}
if (message != null) {
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
Conversation conversation = (Conversation) message.getConversation();
conversation.endOtrIfNeeded();
}
}
}
return true;
}
@ -520,6 +416,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
final Element oob = packet.findChild("x", Namespace.OOB);
final String oobUrl = oob != null ? oob.findChildContent("url") : null;
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
final Element applyToElement = packet.findChild("apply-to", "urn:xmpp:fasten:0");
final String retractId = applyToElement != null && applyToElement.findChild("retract", "urn:xmpp:message-retract:0") != null ? applyToElement.getAttribute("id") : null;
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
@ -609,20 +509,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
final Message message;
if (body != null && body.content.startsWith("?OTR") && Config.supportOtr()) {
if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) {
message = parseOtrChat(body.content, from, remoteMsgId, conversation);
if (message == null) {
return;
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed));
message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
if (body.count > 1) {
message.setBodyLanguage(body.language);
}
}
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
if (pgpEncrypted != null && Config.supportOpenPgp()) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
Jid origin;
@ -733,10 +620,14 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
counterpart,
message.getStatus() == Message.STATUS_RECEIVED,
message.isCarbon());
if (replacedMessage == null) {
replacedMessage = conversation.findSentMessageWithUuidOrRemoteId(replacementId, true, true);
}
if (replacedMessage != null) {
final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
|| replacedMessage.getFingerprint().equals(message.getFingerprint());
@ -749,10 +640,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
synchronized (replacedMessage) {
final String uuid = replacedMessage.getUuid();
replacedMessage.putEdited(replacedMessage.getRemoteMsgId() != null ? replacedMessage.getRemoteMsgId() : replacedMessage.getUuid(), replacedMessage.getServerMsgId(), replacedMessage.getBody(), replacedMessage.getTimeSent());
replacedMessage.setUuid(UUID.randomUUID().toString());
replacedMessage.setBody(message.getBody());
replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
replacedMessage.setRemoteMsgId(remoteMsgId);
replacedMessage.setTime(message.getTimeSent());
if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
replacedMessage.setServerMsgId(message.getServerMsgId());
}
@ -781,11 +675,80 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
}
}
} else if (replacementId != null && !mXmppConnectionService.allowMessageCorrection() && (message.getBody().equals(DELETED_MESSAGE_BODY) || message.getBody().equals(DELETED_MESSAGE_BODY_OLD))) {
} else if (replacementId != null && !mXmppConnectionService.allowMessageCorrection() && (message.hasDeletedBody())) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received deleted message but LMC is deactivated");
return;
}
if (retractId != null && mXmppConnectionService.allowMessageRetraction()) {
final Message retractedMessage = conversation.findSentMessageWithUuidOrRemoteId(retractId, true, true);
if (retractedMessage != null) {
final boolean fingerprintsMatch = retractedMessage.getFingerprint() == null
|| retractedMessage.getFingerprint().equals(message.getFingerprint());
final boolean trueCountersMatch = retractedMessage.getTrueCounterpart() != null
&& message.getTrueCounterpart() != null
&& retractedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid());
final boolean mucUserMatches = query == null && retractedMessage.sameMucUser(message); //can not be checked when using mam
final boolean duplicate = conversation.hasDuplicateMessage(message);
List<Account> lAcc = mXmppConnectionService.getAccounts();
boolean activeSelf = false;
if (message.getTrueCounterpart()!=null) {
for (Account a : lAcc) {
if (a.getJid() != null && a.isOnlineAndConnected() && a.getJid().asBareJid().equals(message.getTrueCounterpart().asBareJid())) {
activeSelf = true;
break;
}
}
}
if (fingerprintsMatch && ((trueCountersMatch || !conversationMultiMode || mucUserMatches || (isCarbon&&activeSelf) && !duplicate) || conversationMultiMode)) {
Log.d(Config.LOGTAG, "retracted message '" + retractedMessage.getBody() + "' with '" + message.getBody() + "'");
synchronized (retractedMessage) {
retractedMessage.setBody(mXmppConnectionService.getString(R.string.message_deleted));
retractedMessage.setRetractId(retractId);
extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
message.setMessageDeleted(true);
message.setRetractId(retractId);
if (message.getStatus() > Message.STATUS_RECEIVED) {
retractedMessage.setMessageDeleted(true);
}
for (Edit itm : retractedMessage.getEditedList()) {
Message tmpRetractedMessage = conversation.findMessageWithUuidOrRemoteId(itm.getEditedId());
if (tmpRetractedMessage != null) {
tmpRetractedMessage.setRetractId(retractId);
mXmppConnectionService.updateMessage(tmpRetractedMessage, tmpRetractedMessage.getUuid());
}
}
mXmppConnectionService.updateMessage(retractedMessage, retractedMessage.getUuid());
mXmppConnectionService.databaseBackend.createMessage(message);
if (mXmppConnectionService.confirmMessages()
&& retractedMessage.getStatus() == Message.STATUS_RECEIVED
&& (retractedMessage.trusted() || retractedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs?
&& remoteMsgId != null
&& !selfAddressed
&& !isTypeGroupChat) {
processMessageReceipts(account, packet, remoteMsgId, query);
}
}
mXmppConnectionService.getNotificationService().updateNotification();
return;
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message retraction but checks are not valid");
}
}
else {
//we deleted a carbon from ourself and the dialog allready removed it from ui
message.setMessageDeleted(true);
message.setRetractId(retractId);
mXmppConnectionService.databaseBackend.createMessage(message);
return;
}
}
long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
@ -859,12 +822,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
&& !isTypeGroupChat) {
processMessageReceipts(account, packet, remoteMsgId, query);
}
if (message.getStatus() == Message.STATUS_RECEIVED
&& conversation.getOtrSession() != null
&& !conversation.getOtrSession().getSessionID().getUserID()
.equals(message.getCounterpart().getResource())) {
conversation.endOtrIfNeeded();
}
mXmppConnectionService.databaseBackend.createMessage(message);
final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
@ -1256,4 +1213,4 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return false;
}
}
}
}

View file

@ -313,10 +313,10 @@ public class PresenceParser extends AbstractParser implements
PgpEngine pgp = mXmppConnectionService.getPgpEngine();
Element x = packet.findChild("x", "jabber:x:signed");
if (pgp != null && x != null) {
final String status = packet.findChildContent("status");
final long keyId = pgp.fetchKeyId(account, status, x.getContent());
if (keyId != 0 && contact.setPgpKeyId(keyId)) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": found OpenPGP key id for "+contact.getJid()+" "+OpenPgpUtils.convertKeyIdToHex(keyId));
final String status = packet.findChildContent("status");
final long keyId = pgp.fetchKeyId(account, status, x.getContent());
if (keyId != 0 && contact.setPgpKeyId(keyId)) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": found OpenPGP key id for "+contact.getJid()+" "+OpenPgpUtils.convertKeyIdToHex(keyId));
mXmppConnectionService.syncRoster(account);
}
}

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.persistance;
import static eu.siacs.conversations.ui.util.UpdateHelper.moveData_PAM_monocles;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
@ -11,6 +13,8 @@ import android.os.SystemClock;
import android.util.Base64;
import android.util.Log;
import com.google.common.base.Stopwatch;
import org.json.JSONException;
import org.json.JSONObject;
import org.whispersystems.libsignal.IdentityKey;
@ -58,12 +62,11 @@ import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.mam.MamReference;
import static eu.siacs.conversations.ui.util.UpdateHelper.moveData_PAM_monocles;
public class DatabaseBackend extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "history";
public static final int DATABASE_VERSION = 54; // = Conversations DATABASE_VERSION + 6
public static final int DATABASE_VERSION = 56; // = Conversations DATABASE_VERSION + 7
private static boolean requiresMessageIndexRebuild = false;
private static DatabaseBackend instance = null;
private static final String CREATE_CONTATCS_STATEMENT = "create table "
@ -170,14 +173,15 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static String CREATE_MESSAGE_TIME_INDEX = "CREATE INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")";
private static String CREATE_MESSAGE_CONVERSATION_INDEX = "CREATE INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")";
private static String CREATE_MESSAGE_DELETED_INDEX = "CREATE INDEX message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")";
private static String CREATE_MESSAGE_FILE_DELETED_INDEX = "CREATE INDEX message_file_deleted_index ON " + Message.TABLENAME + "(" + Message.FILE_DELETED + ")";
private static String CREATE_MESSAGE_FILE_DELETED_INDEX = "create index message_file_deleted_index ON " + Message.TABLENAME + "(" + Message.FILE_DELETED + ")";
private static String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "CREATE INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")";
private static String CREATE_MESSAGE_TYPE_INDEX = "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")";
private static String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4(uuid TEXT PRIMARY KEY, body TEXT, tokenize = 'unicode61')";
private static 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 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 String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(uuid,body) SELECT uuid,body FROM " + Message.TABLENAME + ";";
private static String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid,body,notindexed=\"uuid\",content=\"" + Message.TABLENAME + "\",tokenize='unicode61')";
private static 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 String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE OF uuid,body ON " + Message.TABLENAME + " BEGIN UPDATE messages_index SET body=NEW.body,uuid=NEW.uuid WHERE rowid=OLD.rowid; END;";
private static final String CREATE_MESSAGE_DELETE_TRIGGER = "CREATE TRIGGER after_message_delete AFTER DELETE ON " + Message.TABLENAME + " BEGIN DELETE FROM messages_index WHERE rowid=OLD.rowid; END;";
private static String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(messages_index) VALUES('rebuild');";
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@ -190,6 +194,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return values;
}
public static boolean requiresMessageIndexRebuild() {
return requiresMessageIndexRebuild;
}
public void rebuildMessagesIndex() {
final SQLiteDatabase db = getWritableDatabase();
final Stopwatch stopwatch = Stopwatch.createStarted();
db.execSQL(COPY_PREEXISTING_ENTRIES);
Log.d(Config.LOGTAG,"rebuilt message index in "+ stopwatch.stop().toString());
}
public static synchronized DatabaseBackend getInstance(Context context) {
if (instance == null) {
instance = new DatabaseBackend(context);
@ -247,6 +262,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.MARKABLE + " NUMBER DEFAULT 0,"
+ Message.FILE_DELETED + " NUMBER DEFAULT 0,"
+ Message.BODY_LANGUAGE + " TEXT,"
+ Message.RETRACT_ID + " TEXT,"
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
@ -269,6 +285,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
}
@Override
@ -466,13 +483,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
if (oldVersion < 36 && newVersion >= 36) {
// only rename videos, images, audios and other files directories
final File oldPicturesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Images/");
final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Files/");
final File oldAudiosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Audios/");
final File oldVideosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Videos/");
final File oldPicturesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/Images/");
final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/Files/");
final File oldAudiosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/Audios/");
final File oldVideosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/Videos/");
if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) {
final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Images/");
final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Pix-Art Messenger/Media/Pix-Art Messenger Images/");
newPicturesDirectory.getParentFile().mkdirs();
final File[] files = oldPicturesDirectory.listFiles();
if (files == null) {
@ -483,7 +500,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) {
final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Files/");
final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Pix-Art Messenger/Media/Pix-Art Messenger Files/");
newFilesDirectory.mkdirs();
final File[] files = oldFilesDirectory.listFiles();
if (files == null) {
@ -494,7 +511,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
if (oldAudiosDirectory.exists() && oldAudiosDirectory.isDirectory()) {
final File newAudiosDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Audios/");
final File newAudiosDirectory = new File(Environment.getExternalStorageDirectory() + "/Pix-Art Messenger/Media/Pix-Art Messenger Audios/");
newAudiosDirectory.mkdirs();
final File[] files = oldAudiosDirectory.listFiles();
if (files == null) {
@ -505,7 +522,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
if (oldVideosDirectory.exists() && oldVideosDirectory.isDirectory()) {
final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Videos/");
final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Pix-Art Messenger/Media/Pix-Art Messenger Videos/");
newVideosDirectory.mkdirs();
final File[] files = oldVideosDirectory.listFiles();
if (files == null) {
@ -535,13 +552,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0");
}
if (oldVersion < 42 && newVersion >= 42) {
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 < 43 && newVersion >= 43) {
db.execSQL("DROP TRIGGER IF EXISTS after_message_delete");
}
@ -578,9 +588,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
if (oldVersion < 51 && newVersion >= 51) {
// values in resolver_result are cache and not worth to store
db.execSQL("DROP TABLE IF EXISTS " + RESOLVER_RESULTS_TABLENAME);
db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
// values in resolver_result are cache and not worth to store
db.execSQL("DROP TABLE IF EXISTS " + RESOLVER_RESULTS_TABLENAME);
db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
}
if (oldVersion < 52 && newVersion >= 52) {
@ -594,6 +604,33 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (oldVersion < 54 && newVersion >= 54) {
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT");
}
if (oldVersion < 55 && newVersion >= 55) {
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;
}
if (oldVersion < 56 && newVersion >= 56) {
db.beginTransaction();
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.RETRACT_ID + " TEXT;");
db.setTransactionSuccessful();
db.endTransaction();
requiresMessageIndexRebuild = true;
}
}
private boolean isColumnExisting(SQLiteDatabase db, String TableName, String ColumnName) {
@ -832,12 +869,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
final SQLiteDatabase db = this.getReadableDatabase();
final StringBuilder SQL = new StringBuilder();
final String[] selectionArgs;
SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + '.' + Conversation.CONTACTJID + ',' + Conversation.TABLENAME + '.' + Conversation.ACCOUNT + ',' + Conversation.TABLENAME + '.' + Conversation.MODE + " FROM " + Message.TABLENAME + " join " + Conversation.TABLENAME + " on " + Message.TABLENAME + '.' + Message.CONVERSATION + '=' + Conversation.TABLENAME + '.' + Conversation.UUID + " join messages_index ON messages_index.uuid=messages.uuid where " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + ',' + Message.ENCRYPTION_PGP + ',' + Message.ENCRYPTION_DECRYPTION_FAILED + ',' + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + ',' + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?");
SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + "." + Conversation.CONTACTJID + "," + Conversation.TABLENAME + "." + Conversation.ACCOUNT + "," + Conversation.TABLENAME + "." + Conversation.MODE + " FROM " + Message.TABLENAME + " JOIN " + Conversation.TABLENAME + " ON " + Message.TABLENAME + "." + Message.CONVERSATION + "=" + Conversation.TABLENAME + "." + Conversation.UUID + " JOIN messages_index ON messages_index.rowid=messages.rowid WHERE " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + "," + Message.ENCRYPTION_PGP + "," + Message.ENCRYPTION_DECRYPTION_FAILED + "," + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + "," + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?");
if (uuid == null) {
selectionArgs = new String[]{FtsUtils.toMatchString(term)};
} else {
selectionArgs = new String[]{FtsUtils.toMatchString(term), uuid};
SQL.append(" AND "+Conversation.TABLENAME+'.'+Conversation.UUID+"=?");
SQL.append(" AND " + Conversation.TABLENAME + '.' + Conversation.UUID + "=?");
}
SQL.append(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS);
Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term));
@ -1118,23 +1155,23 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public void deleteMessageInConversation(Message message) {
long start = SystemClock.elapsedRealtime();
final String uuid = message.getUuid();
final SQLiteDatabase db = this.getWritableDatabase();
db.beginTransaction();
ContentValues values = new ContentValues();
values.put(Message.DELETED, "1");
String[] args = {message.getUuid()};
String[] args = {uuid};
int rows = db.update("messages", values, "uuid =?", args);
db.setTransactionSuccessful();
db.endTransaction();
Log.d(Config.LOGTAG, "deleted " + rows + " message in " + (SystemClock.elapsedRealtime() - start) + "ms");
Log.d(Config.LOGTAG, "deleted " + rows + " message (" + uuid + ") in " + (SystemClock.elapsedRealtime() - start) + "ms");
}
public void deleteMessagesInConversation(Conversation conversation) {
long start = SystemClock.elapsedRealtime();
final SQLiteDatabase db = this.getWritableDatabase();
db.beginTransaction();
String[] args = {conversation.getUuid()};
db.delete("messages_index", "uuid in (select uuid from messages where conversationUuid=?)", args);
final String[] args = {conversation.getUuid()};
int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
db.setTransactionSuccessful();
db.endTransaction();
@ -1183,8 +1220,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
final String[] args = {String.valueOf(timestamp)};
SQLiteDatabase db = this.getReadableDatabase();
db.beginTransaction();
db.delete("messages_index", "uuid in (select uuid from messages where timeSent<?)", args);
num = db.delete(Message.TABLENAME, "timeSent<?", args);
db.delete(Message.TABLENAME, "timeSent<?", args);
db.setTransactionSuccessful();
db.endTransaction();
}

View file

@ -1,6 +1,5 @@
package eu.siacs.conversations.persistance;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
@ -25,6 +24,7 @@ import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.StatFs;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.system.Os;
@ -35,14 +35,12 @@ import android.util.DisplayMetrics;
import android.util.Log;
import android.util.LruCache;
import androidx.exifinterface.media.ExifInterface;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.core.content.FileProvider;
import com.google.common.io.ByteStreams;
import androidx.exifinterface.media.ExifInterface;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
@ -64,21 +62,27 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Stack;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AttachFileToConversationRunnable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.util.Attachment;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.FileWriterException;
@ -104,6 +108,11 @@ public class FileBackend {
public static final String SENT_IMAGES = "Images/Sent";
public static final String VIDEOS = "Videos";
public static final String SENT_VIDEOS = "Videos/Sent";
public static final String INNER_APP_DIR[] = new String[]{
Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator ,
Environment.getDataDirectory() + File.separator + "data" + File.separator + BuildConfig.APPLICATION_ID + File.separator + "files" + File.separator,
};
public static final AtomicInteger STORAGE_INDEX = new AtomicInteger(0);
private final XmppConnectionService mXmppConnectionService;
@ -111,6 +120,10 @@ public class FileBackend {
this.mXmppConnectionService = service;
}
public static void switchStorage(boolean checked) {
STORAGE_INDEX.set(checked?1:0);
}
private void createNoMedia() {
final File nomedia_files = new File(getConversationsDirectory(FILES) + ".nomedia");
final File nomedia_audios = new File(getConversationsDirectory(AUDIOS) + ".nomedia");
@ -288,6 +301,77 @@ public class FileBackend {
}
}
public static void deleteOldBackups(File dir, List<Account> mAccounts) {
try {
long start = SystemClock.elapsedRealtime();
int num = 0;
if (dir == null) {
return;
}
Stack<File> dirlist = new Stack<File>();
dirlist.clear();
dirlist.push(dir);
File dirCurrent = dirlist.pop();
File[] fileList = dirCurrent.listFiles();
while (!dirlist.isEmpty()) {
if (fileList != null) {
for (File file : fileList) {
if (file.isDirectory()) {
dirlist.push(file);
}
}
}
}
if (fileList != null) {
ArrayList<File> fileListByAccount = new ArrayList<File>();
ArrayList<File> simpleFileList = new ArrayList<File>(Arrays.asList(fileList));
for (Account account : mAccounts) {
String jid = account.getJid().asBareJid().toString();
for (int i = 0; i < simpleFileList.size(); i++) {
File currentFile = simpleFileList.get(i);
String fileName = currentFile.getName();
if (fileName.startsWith(jid) && fileName.endsWith(".ceb")) {
fileListByAccount.add(currentFile);
simpleFileList.remove(currentFile);
i--;
}
}
if (fileListByAccount.size() > 2) {
num += expireOldBackups(fileListByAccount);
}
fileListByAccount.clear();
}
} else {
return;
}
Log.d(Config.LOGTAG, "deleted " + num + " old backup files in " + (SystemClock.elapsedRealtime() - start) + "ms");
} catch (Exception e) {
e.printStackTrace();
}
}
private static int expireOldBackups(ArrayList<File> fileListByAccount) {
int num = 0;
try {
Collections.sort(fileListByAccount, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return Long.compare(f2.lastModified(), f1.lastModified());
}
});
fileListByAccount.subList(0, 2).clear();
for (File currentFile : fileListByAccount) {
if (currentFile.delete()) {
num++;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return num;
}
public void deleteFilesInDir(File dir) {
long start = SystemClock.elapsedRealtime();
int num = 0;
@ -295,12 +379,11 @@ public class FileBackend {
return;
}
Stack<File> dirlist = new Stack<>();
dirlist.clear();
dirlist.push(dir);
while (!dirlist.isEmpty()) {
File dirCurrent = dirlist.pop();
File[] fileList = dirCurrent.listFiles();
if (fileList.length > 0) {
if (fileList != null && fileList.length > 0) {
for (File file : fileList) {
if (file.isDirectory()) {
dirlist.push(file);
@ -462,20 +545,24 @@ public class FileBackend {
public static String getConversationsDirectory(final String type) {
if (type.equalsIgnoreCase("null")) {
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator;
// return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator;
return INNER_APP_DIR[STORAGE_INDEX.get()]
+ APP_DIRECTORY + File.separator;
} else {
return getAppMediaDirectory() + APP_DIRECTORY + " " + type + File.separator;
}
}
public static String getAppMediaDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Media" + File.separator;
// return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Media" + File.separator;
return INNER_APP_DIR[STORAGE_INDEX.get()]
+ APP_DIRECTORY + File.separator + "Media" + File.separator;
}
public static String getBackupDirectory(@Nullable String app) {
if (app != null && (app.equalsIgnoreCase("conversations") || app.equalsIgnoreCase("Quicksy"))) {
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + app + "/Backup/";
} else if (app != null && (app.equalsIgnoreCase("Monocles Messenger"))) {
} else if (app != null && (app.equalsIgnoreCase("Pix-Art Messenger"))) {
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + app + File.separator + "Database" + File.separator;
} else {
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Database" + File.separator;
@ -600,6 +687,8 @@ public class FileBackend {
ByteStreams.copy(is, os);
} catch (IOException e) {
throw new FileWriterException();
} catch (Exception e) {
throw new FileWriterException();
}
try {
os.flush();
@ -863,7 +952,7 @@ public class FileBackend {
}
DownloadableFile file = getFile(message);
final String mime = file.getMimeType();
if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) {
if ("application/pdf".equals(mime)) {
thumbnail = getPDFPreview(file, size);
} else if (mime.startsWith("video/")) {
thumbnail = getVideoPreview(file, size);
@ -887,7 +976,6 @@ public class FileBackend {
return thumbnail;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private Bitmap getPDFPreview(final File file, int size) {
try {
final ParcelFileDescriptor mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
@ -1049,7 +1137,22 @@ public class FileBackend {
return getUriForFile(mXmppConnectionService, file);
}
public static Uri getUriForUri(Context context, Uri uri) {
if ("file".equals(uri.getScheme())) {
return getUriForFile(context, new File(uri.getPath()));
} else {
return uri;
}
}
public static Uri getUriForFile(Context context, File file) {
if (PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(SettingsActivity.USE_INNER_STORAGE, true)) {
File dataUser0File = new File(file.getAbsolutePath().replace("/data/data", "/data/user/0"));
return FileProvider.getUriForFile(context
, getAuthority(context)
, dataUser0File.exists() ? dataUser0File : file);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
return FileProvider.getUriForFile(context, getAuthority(context), file);
@ -1437,53 +1540,63 @@ public class FileBackend {
final boolean apk = mime != null && mime.equals("application/vnd.android.package-archive");
final boolean pdf = "application/pdf".equals(mime);
/* file params:
1 | 2 | 3 | 4 | 5 | 6
| image/video/pdf | a/v/gif | vcard/apk
url | filesize | width | height | runtime | name
1 | 2 | 3 | 4 | 5 | 6 |
| image/video/pdf | a/v/gif | vcard/apk/audio |
url | filesize | width | height | runtime | name |
*/
final StringBuilder body = new StringBuilder();
if (url != null) {
body.append(url);
body.append(url); // 1
}
body.append('|').append(file.getSize());
if (image || video || (pdf && Compatibility.runsTwentyOne())) {
body.append('|').append(file.getSize()); // 2
if (image || video || pdf) {
try {
final Dimensions dimensions;
if (video) {
dimensions = getVideoDimensions(file);
} else if (pdf && Compatibility.runsTwentyOne()) {
} else if (pdf) {
dimensions = getPDFDimensions(file);
} else {
dimensions = getImageDimensions(file);
}
if (dimensions.valid()) {
body.append('|').append(dimensions.width).append('|').append(dimensions.height);
body.append('|')
.append(dimensions.width) // 3
.append('|')
.append(dimensions.height); // 4
if (isGif || video) {
body.append("|").append(getMediaRuntime(file, isGif));
body.append("|").append(getMediaRuntime(file, isGif)); // 5
}
}
} catch (NotAVideoFile notAVideoFile) {
Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file, trying to handle it as audio file");
try {
body.append("|0|0|").append(getMediaRuntime(file, false)).append('|').append(getAudioTitleArtist(file));
body.append("|0|0|") // 3, 4
.append(getMediaRuntime(file, false)) // 5
.append('|')
.append(getAudioTitleArtist(file)); // 6
} catch (Exception e) {
Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was neither a video file nor an audio file");
//fall threw
}
}
} else if (audio) {
body.append("|0|0|").append(getMediaRuntime(file, false)).append('|').append(getAudioTitleArtist(file));
body.append("|0|0|") // 3, 4
.append(getMediaRuntime(file, false)) // 5
.append('|')
.append(getAudioTitleArtist(file)); // 6
} else if (vcard) {
body.append("|0|0|0|").append(getVCard(file));
body.append("|0|0|0|") // 3, 4, 5
.append(getVCard(file)); // 6
} else if (apk) {
body.append("|0|0|0|").append(getAPK(file, mXmppConnectionService.getApplicationContext()));
body.append("|0|0|0|") // 3, 4, 5
.append(getAPK(file, mXmppConnectionService.getApplicationContext())); // 6
}
message.setBody(body.toString());
message.setFileDeleted(false);
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE));
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private Dimensions getPDFDimensions(final File file) {
final ParcelFileDescriptor fileDescriptor;
try {
@ -1516,16 +1629,16 @@ public class FileBackend {
public int getMediaRuntime(File file, boolean isGif) {
if (isGif) {
try {
final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(getUriForFile(mXmppConnectionService, file));
Movie movie = Movie.decodeStream(inputStream);
int duration = movie.duration();
close(inputStream);
return duration;
} catch (FileNotFoundException e) {
Log.d(Config.LOGTAG, "unable to get image dimensions", e);
return 0;
}
try {
final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(getUriForFile(mXmppConnectionService, file));
Movie movie = Movie.decodeStream(inputStream);
int duration = movie.duration();
close(inputStream);
return duration;
} catch (FileNotFoundException e) {
Log.d(Config.LOGTAG, "unable to get image dimensions", e);
return 0;
}
} else {
try {
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
@ -1661,7 +1774,7 @@ public class FileBackend {
return bitmap;
}
DownloadableFile file = new DownloadableFile(attachment.getUri().getPath());
if ("application/pdf".equals(attachment.getMime()) && Compatibility.runsTwentyOne()) {
if ("application/pdf".equals(attachment.getMime())) {
bitmap = cropCenterSquare(getPDFPreview(file, size), size);
} else if (attachment.getMime() != null && attachment.getMime().startsWith("video/")) {
bitmap = cropCenterSquareVideo(attachment.getUri(), size);
@ -1719,11 +1832,7 @@ public class FileBackend {
return dimensions;
}
final int rotation;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
rotation = extractRotationFromMediaRetriever(metadataRetriever);
} else {
rotation = 0;
}
rotation = extractRotationFromMediaRetriever(metadataRetriever);
boolean rotated = rotation == 90 || rotation == 270;
int height;
try {
@ -1744,7 +1853,6 @@ public class FileBackend {
return rotated ? new Dimensions(width, height) : new Dimensions(height, width);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) {
String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
try {
@ -1851,30 +1959,11 @@ public class FileBackend {
public static boolean weOwnFile(Context context, Uri uri) {
if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
return false;
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return fileIsInFilesDir(context, uri);
} else {
return weOwnFileLollipop(uri);
}
}
/**
* This is more than hacky but probably way better than doing nothing
* Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir
* and check against those as well
*/
private static boolean fileIsInFilesDir(Context context, Uri uri) {
try {
final String haystack = context.getFilesDir().getParentFile().getCanonicalPath();
final String needle = new File(uri.getPath()).getCanonicalPath();
return needle.startsWith(haystack);
} catch (IOException e) {
return false;
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static boolean weOwnFileLollipop(Uri uri) {
try {
File file = new File(uri.getPath());
@ -1944,11 +2033,7 @@ public class FileBackend {
}
public static String getGlobalDocumentsPath() {
if (Compatibility.runsNineTeen()) {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/monocles chat/";
} else {
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Documents";
}
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/monocles chat/";
}
public static String getGlobalAudiosPath() {
@ -1956,14 +2041,21 @@ public class FileBackend {
}
public void saveFile(final Message message, final Activity activity) {
final DownloadableFile source = getFile(message);
final File destination = new File(getDestinationToSaveFile(message));
try {
copyFile(source, destination);
ToastCompat.makeText(activity, activity.getString(R.string.file_copied_to, destination), ToastCompat.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
}
new Thread(() -> {
final DownloadableFile source = getFile(message);
final File destination = new File(getDestinationToSaveFile(message));
try {
activity.runOnUiThread(() -> {
ToastCompat.makeText(activity, activity.getString(R.string.copy_file_to, destination), ToastCompat.LENGTH_SHORT).show();
});
copyFile(source, destination);
activity.runOnUiThread(() -> {
ToastCompat.makeText(activity, activity.getString(R.string.file_copied_to, destination), ToastCompat.LENGTH_SHORT).show();
});
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
public String getDestinationToSaveFile(Message message) {

View file

@ -1,6 +1,7 @@
package eu.siacs.conversations.services;
import android.content.Context;
import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
@ -35,8 +36,6 @@ import okio.BufferedSink;
import okio.Okio;
import okio.Source;
import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
public class AbstractConnectionManager {
private static final int UI_REFRESH_THRESHOLD = Config.REFRESH_UI_INTERVAL;
private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0);
@ -119,9 +118,9 @@ public class AbstractConnectionManager {
}
public long getAutoAcceptFileSize() {
long defaultValue_wifi = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_wifi);
long defaultValue_mobile = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_mobile);
long defaultValue_roaming = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_roaming);
final long defaultValue_wifi = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_wifi);
final long defaultValue_mobile = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_mobile);
final long defaultValue_roaming = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_roaming);
String config = "0";
if (mXmppConnectionService.isWIFI()) {
@ -135,7 +134,7 @@ public class AbstractConnectionManager {
"auto_accept_file_size_roaming", String.valueOf(defaultValue_roaming));
}
try {
return Long.parseLong(config);
return Long.parseLong(config) <= 0 ? -1 : Long.parseLong(config);
} catch (NumberFormatException e) {
return defaultValue_mobile;
}
@ -154,7 +153,6 @@ public class AbstractConnectionManager {
}
}
public PowerManager.WakeLock createWakeLock(final String name) {
final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class);
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);

View file

@ -4,15 +4,17 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.util.Log;
import androidx.annotation.RequiresApi;
import androidx.annotation.NonNull;
import net.ypresto.androidtranscoder.MediaTranscoder;
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets;
import com.otaliastudios.transcoder.Transcoder;
import com.otaliastudios.transcoder.TranscoderListener;
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.FileDescriptor;
@ -20,6 +22,7 @@ import java.io.FileNotFoundException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@ -32,8 +35,9 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.TranscoderStrategies;
public class AttachFileToConversationRunnable implements Runnable, MediaTranscoder.Listener {
public class AttachFileToConversationRunnable implements Runnable, TranscoderListener {
private final XmppConnectionService mXmppConnectionService;
private final Message message;
@ -56,16 +60,15 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
this.callback = callback;
this.maxUploadSize = maxUploadSize;
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
final int autoAcceptFileSize = Config.FILE_SIZE;
this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri);
this.isVideoMessage = !getFileBackend().useFileAsIs(uri)
&& (mimeType != null && mimeType.startsWith("video/")
&& (mXmppConnectionService.getCompressVideoBitratePreference() != 0 && mXmppConnectionService.getCompressVideoResolutionPreference() != 0))
&& originalFileSize > autoAcceptFileSize;
&& (mimeType != null && mimeType.startsWith("video/"))
&& originalFileSize > Config.VIDEO_FAST_UPLOAD_SIZE
&& !"uncompressed".equals(getVideoCompression());
}
boolean isVideoMessage() {
return this.isVideoMessage && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
return this.isVideoMessage;
}
private void processAsFile() {
@ -100,21 +103,22 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
}
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
private void processAsVideo() throws Exception {
Log.d(Config.LOGTAG, "processing file as video");
mXmppConnectionService.startForcingForegroundNotification();
isCompressingVideo = conversation.getUuid();
SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
message.setRelativeFilePath("Sent/" + fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4) + "_komp.mp4");
final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
file.getParentFile().mkdirs();
if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
Log.d(Config.LOGTAG, "created parent directory for video file");
}
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 = getVideoCompressor(fileDescriptor, file, maxUploadSize);
final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
final Future<Void> future = getVideoCompression(fileDescriptor, file, maxUploadSize, originalFileSize);
try {
future.get();
} catch (InterruptedException e) {
@ -127,13 +131,13 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
isCompressingVideo = null;
processAsFile();
} else {
Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFailed() instead", e);
}
}
}
private Future<Void> getVideoCompressor(final FileDescriptor fileDescriptor, final File file, final long maxUploadSize) throws Exception {
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
private Future<Void> getVideoCompression(final FileDescriptor fileDescriptor, final File file, final long maxUploadSize, final long originalFileSize) throws Exception {
final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
try {
mediaMetadataRetriever.setDataSource(fileDescriptor);
} catch (Exception e) {
@ -141,31 +145,52 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
throw new Exception(e);
}
long videoDuration;
long estimatedFileSize = maxUploadSize / 2; // keep estimated filesize half as big as maxUploadSize
final long estimatedFileSize = maxUploadSize / 2; // keep estimated filesize half as big as maxUploadSize
try {
videoDuration = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) / 1000; //in seconds
} catch (NumberFormatException e) {
videoDuration = -1;
}
int bitrateAfterCompression = safeLongToInt(mXmppConnectionService.getCompressVideoBitratePreference() / 8); //in bytes
long size = videoDuration * bitrateAfterCompression;
if (estimatedFileSize >= size) {
return MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), MediaFormatStrategyPresets.createAndroidStandardStrategy(mXmppConnectionService.getCompressVideoBitratePreference(), mXmppConnectionService.getCompressVideoResolutionPreference()), this);
final int bitrateAfterCompression = safeLongToInt(mXmppConnectionService.getCompressVideoBitratePreference() / 8); //in bytes
final long sizeAfterCompression = videoDuration * bitrateAfterCompression;
if (sizeAfterCompression >= originalFileSize && originalFileSize <= Config.VIDEO_FAST_UPLOAD_SIZE) {
processAsFile();
onTranscodeCanceled();
return null;
} else if (estimatedFileSize >= sizeAfterCompression && sizeAfterCompression <= originalFileSize) {
return Transcoder.into(file.getAbsolutePath()).
addDataSource(mXmppConnectionService, uri)
.setVideoTrackStrategy(TranscoderStrategies.VIDEO(mXmppConnectionService.getCompressVideoBitratePreference(), mXmppConnectionService.getCompressVideoResolutionPreference()))
.setAudioTrackStrategy(mXmppConnectionService.getCompressAudioPreference())
.setListener(this)
.transcode();
} else {
DefaultAudioStrategy audioStrategy;
int newBitrate = safeLongToInt((estimatedFileSize / videoDuration) * 8); // in bits/sec
int newResoloution = 0;
if (newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_bitrate)) {
newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_res);
audioStrategy = TranscoderStrategies.AUDIO_LQ;
} else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.low_video_bitrate)) {
newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.low_video_res);
audioStrategy = TranscoderStrategies.AUDIO_LQ;
} else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.low_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.mid_video_bitrate)) {
newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.mid_video_res);
audioStrategy = TranscoderStrategies.AUDIO_MQ;
} else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.mid_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.high_video_bitrate)) {
newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.high_video_res);
audioStrategy = TranscoderStrategies.AUDIO_HQ;
} else {
newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.high_video_res);
audioStrategy = TranscoderStrategies.AUDIO_HQ;
}
return MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), MediaFormatStrategyPresets.createAndroidStandardStrategy(newBitrate, newResoloution), this);
return Transcoder.into(file.getAbsolutePath()).
addDataSource(mXmppConnectionService, uri)
.setVideoTrackStrategy(TranscoderStrategies.VIDEO(newBitrate, newResoloution))
.setAudioTrackStrategy(audioStrategy)
.setListener(this)
.transcode();
}
}
@ -187,7 +212,7 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
}
@Override
public void onTranscodeCompleted() {
public void onTranscodeCompleted(int successCode) {
mXmppConnectionService.stopForcingForegroundNotification();
isCompressingVideo = null;
final File file = mXmppConnectionService.getFileBackend().getFile(message);
@ -219,10 +244,10 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
}
@Override
public void onTranscodeFailed(Exception e) {
public void onTranscodeFailed(@NonNull @NotNull Throwable exception) {
mXmppConnectionService.stopForcingForegroundNotification();
isCompressingVideo = null;
Log.d(Config.LOGTAG, "video transcoding failed", e);
Log.d(Config.LOGTAG, "video transcoding failed", exception);
processAsFile();
}
@ -240,6 +265,10 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
}
}
private String getVideoCompression() {
return getVideoCompression(mXmppConnectionService);
}
public static String getVideoCompression(final Context context) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));

View file

@ -101,17 +101,10 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
private boolean init(ViewHolder viewHolder, Message message) {
messageAdapter.getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC);
if (viewHolder.darkBackground) {
viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
} else {
viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption);
}
viewHolder.progress.setOnSeekBarChangeListener(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ColorStateList color = ThemeHelper.AudioPlayerColor(messageAdapter.getContext());
viewHolder.progress.setThumbTintList(color);
viewHolder.progress.setProgressTintList(color);
}
ColorStateList color = ThemeHelper.AudioPlayerColor(messageAdapter.getContext());
viewHolder.progress.setThumbTintList(color);
viewHolder.progress.setProgressTintList(color);
viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
viewHolder.playPause.setOnClickListener(this);
if (message == currentlyPlayingMessage) {
@ -439,6 +432,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
Log.i(Config.LOGTAG, "Audio focus failed.");
}
}
public static class ViewHolder {
private TextView runtime;
private SeekBar progress;

View file

@ -50,7 +50,7 @@ public class ChannelDiscoveryService {
}
void initializeMuclumbusService() {
final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
final OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (service.useTorToConnect()) {
builder.proxy(HttpConnectionManager.getProxy());
}

View file

@ -1,6 +1,8 @@
package eu.siacs.conversations.services;
import static eu.siacs.conversations.services.NotificationService.EXPORT_BACKUP_NOTIFICATION_ID;
import static eu.siacs.conversations.utils.Compatibility.runsTwentySix;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@ -17,8 +19,6 @@ import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.ObjectOutputStream;
import androidx.core.app.NotificationCompat;
@ -81,7 +81,6 @@ public class ExportBackupService extends Service {
private static final String DIRECTORY_STRING_FORMAT = FileBackend.getAppLogsDirectory() + "%s";
private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n";
private static final int NOTIFICATION_ID = 19;
private static final int PAGE_SIZE = 20;
private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
private DatabaseBackend mDatabaseBackend;
@ -271,6 +270,7 @@ public class ExportBackupService extends Service {
RUNNING.set(false);
if (success) {
notifySuccess(files, notify);
FileBackend.deleteOldBackups(new File(FileBackend.getBackupDirectory(null)), this.mAccounts);
} else {
notifyError();
}
@ -313,7 +313,7 @@ public class ExportBackupService extends Service {
final int percentage = i * 100 / size;
if (p < percentage) {
p = percentage;
notificationManager.notify(NOTIFICATION_ID, progress.build(p));
notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, progress.build(p));
}
}
if (cursor != null) {
@ -327,7 +327,7 @@ public class ExportBackupService extends Service {
mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
.setSmallIcon(R.drawable.ic_archive_white_24dp)
.setProgress(1, 0, false);
startForeground(NOTIFICATION_ID, mBuilder.build());
startForeground(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build());
int count = 0;
final int max = this.mAccounts.size();
final SecureRandom secureRandom = new SecureRandom();
@ -344,55 +344,58 @@ public class ExportBackupService extends Service {
final List<File> files = new ArrayList<>();
Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
for (final Account account : this.mAccounts) {
final String password = account.getPassword();
if (Strings.nullToEmpty(password).trim().isEmpty()) {
Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid()));
continue;
}
Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid()));
final byte[] IV = new byte[12];
final byte[] salt = new byte[16];
secureRandom.nextBytes(IV);
secureRandom.nextBytes(salt);
final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt);
final Progress progress = new Progress(mBuilder, max, count);
final File file = new File(FileBackend.getBackupDirectory(null) + account.getJid().asBareJid().toEscapedString() + ".ceb");
files.add(file);
final File directory = file.getParentFile();
if (directory != null && directory.mkdirs()) {
Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath());
}
final FileOutputStream fileOutputStream = new FileOutputStream(file);
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
try {
final String password = account.getPassword();
if (Strings.nullToEmpty(password).trim().isEmpty()) {
Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid()));
continue;
}
Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid()));
final byte[] IV = new byte[12];
final byte[] salt = new byte[16];
secureRandom.nextBytes(IV);
secureRandom.nextBytes(salt);
final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt);
final Progress progress = new Progress(mBuilder, max, count);
final File file = new File(FileBackend.getBackupDirectory(null) + account.getJid().asBareJid().toEscapedString() + "_" + ((new SimpleDateFormat("yyyy-MM-dd")).format(new Date())) + ".ceb");
files.add(file);
final File directory = file.getParentFile();
if (directory != null && directory.mkdirs()) {
Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath());
}
final FileOutputStream fileOutputStream = new FileOutputStream(file);
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
final Cipher cipher = Compatibility.runsTwentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
final byte[] key = getKey(password, salt);
Log.d(Config.LOGTAG, backupFileHeader.toString());
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
PrintWriter writer = new PrintWriter(gzipOutputStream);
SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
final String uuid = account.getUuid();
accountExport(db, uuid, writer);
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
messageExport(db, uuid, writer, progress);
for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
final Cipher cipher = Compatibility.runsTwentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
final byte[] key = getKey(password, salt);
Log.d(Config.LOGTAG, backupFileHeader.toString());
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
PrintWriter writer = new PrintWriter(gzipOutputStream);
SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
final String uuid = account.getUuid();
accountExport(db, uuid, writer);
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
messageExport(db, uuid, writer, progress);
for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
}
writer.flush();
writer.close();
mediaScannerScanFile(file);
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
} catch (Exception e) {
Log.d(Config.LOGTAG, "backup for " + account.getJid() + " failed with " + e);
}
writer.flush();
writer.close();
mediaScannerScanFile(file);
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
count++;
}
stopForeground(true);
notificationManager.cancel(NOTIFICATION_ID);
notificationManager.cancel(EXPORT_BACKUP_NOTIFICATION_ID);
return files;
}
@ -465,7 +468,7 @@ public class ExportBackupService extends Service {
if (shareFilesIntent != null) {
mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent);
}
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build());
}
private void notifyError() {
@ -477,7 +480,7 @@ public class ExportBackupService extends Service {
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_failed_subtitle, FileBackend.getBackupDirectory(null))))
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_warning_white_24dp);
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build());
}
private void writeToFile(Conversation conversation) {

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.services;
import static eu.siacs.conversations.services.NotificationService.IMPORT_BACKUP_NOTIFICATION_ID;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@ -60,7 +62,6 @@ import eu.siacs.conversations.xmpp.Jid;
public class ImportBackupService extends Service {
private static final int NOTIFICATION_ID = 21;
private static final AtomicBoolean running = new AtomicBoolean(false);
private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
@ -162,7 +163,7 @@ public class ImportBackupService extends Service {
}
private void startForegroundService() {
startForeground(NOTIFICATION_ID, createImportBackupNotification(1, 0));
startForeground(IMPORT_BACKUP_NOTIFICATION_ID, createImportBackupNotification(1, 0));
}
private void updateImportBackupNotification(final long total, final long current) {
@ -177,7 +178,7 @@ public class ImportBackupService extends Service {
}
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
try {
notificationManager.notify(NOTIFICATION_ID, createImportBackupNotification(max, progress));
notificationManager.notify(IMPORT_BACKUP_NOTIFICATION_ID, createImportBackupNotification(max, progress));
} catch (final RuntimeException e) {
Log.d(Config.LOGTAG, "unable to make notification", e);
}
@ -303,7 +304,7 @@ public class ImportBackupService extends Service {
.setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(R.drawable.ic_unarchive_white_24dp);
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
notificationManager.notify(IMPORT_BACKUP_NOTIFICATION_ID, mBuilder.build());
}
private void stopBackgroundService() {

View file

@ -67,8 +67,8 @@ import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Locale;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
@ -95,6 +95,7 @@ import eu.siacs.conversations.ui.MemorizingActivity;
* opening sockets!
*/
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";
@ -358,6 +359,7 @@ public class MemorizingTrustManager {
}
}
private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
throws CertificateException {
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
@ -525,6 +527,7 @@ public class MemorizingTrustManager {
}
private void certDetails(final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
si.append("\n");
if (showValidFor) {
try {
@ -574,6 +577,7 @@ public class MemorizingTrustManager {
}
return si.toString();
}
/**
* Returns the top-most entry of the activity stack.
*
@ -646,6 +650,7 @@ public class MemorizingTrustManager {
public X509TrustManager getInteractive() {
return new InteractiveMemorizingTrustManager(null);
}
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;

View file

@ -229,31 +229,31 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
private void execute(final Query query) {
final Account account = query.getAccount();
if (account.getStatus() == Account.State.ONLINE) {
final Conversation conversation = query.getConversation();
if (conversation != null && conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
throw new IllegalStateException("Attempted to run MAM query for archived conversation");
}
final Conversation conversation = query.getConversation();
if (conversation != null && conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
throw new IllegalStateException("Attempted to run MAM query for archived conversation");
}
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString());
final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
this.mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> {
final Element fin = p.findChild("fin", query.version.namespace);
final Element fin = p.findChild("fin", query.version.namespace);
if (p.getType() == IqPacket.TYPE.TIMEOUT) {
synchronized (this.queries) {
this.queries.remove(query);
synchronized (this.queries) {
this.queries.remove(query);
if (query.hasCallback()) {
query.callback(false);
}
}
} else if (p.getType() == IqPacket.TYPE.RESULT && fin != null) {
final boolean running;
synchronized (this.queries) {
running = this.queries.contains(query);
}
if (running) {
processFin(query, fin);
} else {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring MAM iq result because query had been killed");
}
final boolean running;
synchronized (this.queries) {
running = this.queries.contains(query);
}
if (running) {
processFin(query, fin);
} else {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring MAM iq result because query had been killed");
}
} else if (p.getType() == IqPacket.TYPE.RESULT && query.isLegacy()) {
//do nothing
} else {
@ -268,11 +268,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
private void finalizeQuery(final Query query, boolean done) {
private void finalizeQuery(final Query query, boolean done) {
synchronized (this.queries) {
if (!this.queries.remove(query)) {
throw new IllegalStateException("Unable to remove query from queries");
}
if (!this.queries.remove(query)) {
throw new IllegalStateException("Unable to remove query from queries");
}
}
final Conversation conversation = query.getConversation();
if (conversation != null) {
@ -403,7 +403,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
synchronized (this.queries) {
for (final Query q : queries) {
for (final Query q : queries) {
if (q.conversation == conversation) {
toBeKilled.add(q);
}
@ -627,7 +627,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
@NotNull
@NotNull
@Override
public String toString() {
StringBuilder builder = new StringBuilder();

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.services;
import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
@ -34,6 +36,8 @@ import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.IconCompat;
import com.google.common.collect.Iterables;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@ -72,8 +76,6 @@ import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube;
public class NotificationService {
public static final Object CATCHUP_LOCK = new Object();
@ -106,6 +108,8 @@ public class NotificationService {
public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10;
private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 14;
public static final int IMPORT_BACKUP_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 16;
public static final int EXPORT_BACKUP_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 18;
private final XmppConnectionService mXmppConnectionService;
private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
private final LinkedHashMap<Conversational, MissedCallsInfo> mMissedCalls = new LinkedHashMap<>();
@ -730,14 +734,23 @@ public class NotificationService {
notify(INCOMING_CALL_NOTIFICATION_ID, notification);
}
public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) {
final AbstractJingleConnection.Id id = ongoingCall.id;
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, ONGOING_CALLS_CHANNEL_ID);
if (media.contains(Media.VIDEO)) {
if (ongoingCall.media.contains(Media.VIDEO)) {
builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
if (ongoingCall.reconnecting) {
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call));
} else {
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
}
} else {
builder.setSmallIcon(R.drawable.ic_call_white_24dp);
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
if (ongoingCall.reconnecting) {
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call));
} else {
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
}
}
builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
@ -1214,17 +1227,18 @@ public class NotificationService {
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
.setShowsUserInterface(false)
.build();
String replyLabel = mXmppConnectionService.getString(R.string.reply);
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
final String replyLabel = mXmppConnectionService.getString(R.string.reply);
final String lastMessageUuid = Iterables.getLast(messages).getUuid();
final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
R.drawable.ic_reply_white_24dp,
replyLabel,
createReplyIntent(conversation, false))
createReplyIntent(conversation, lastMessageUuid, false))
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
.setShowsUserInterface(false)
.addRemoteInput(remoteInput).build();
NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
replyLabel,
createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build();
mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
int addedActionsCount = 1;
mBuilder.addAction(markReadAction);
@ -1505,13 +1519,14 @@ public class NotificationService {
return PendingIntent.getService(mXmppConnectionService, 1, intent, 0);
}
private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
private PendingIntent createReplyIntent(final Conversation conversation, final String lastMessageUuid, final boolean dismissAfterReply) {
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
intent.putExtra("uuid", conversation.getUuid());
intent.putExtra("dismiss_notification", dismissAfterReply);
intent.putExtra("last_message_uuid", lastMessageUuid);
final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14);
return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
return PendingIntent.getService(mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent createReadPendingIntent(Conversation conversation) {

View file

@ -0,0 +1,117 @@
package eu.siacs.conversations.services;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.http.HttpConnectionManager;
public class ProviderService extends AsyncTask<String, Object, Boolean> {
public static List<String> providers = new ArrayList<>();
// in accordance with cat B (https://invent.kde.org/melvo/xmpp-providers/)
public static boolean REGISTRATION = true;
public static boolean FREE = true;
public static int COMPLIANCE = 90;
public static String RATING = "A";
public ProviderService() {
}
public static List<String> getProviders() {
final HashSet<String> provider = new HashSet<>(Config.DOMAIN.DOMAINS);
if (!providers.isEmpty()) {
provider.addAll(providers);
}
return new ArrayList<>(provider);
}
@Override
protected Boolean doInBackground(String... params) {
StringBuilder jsonString = new StringBuilder();
boolean isError = false;
try {
Log.d(Config.LOGTAG, "ProviderService: Updating provider list from " + Config.PROVIDER_URL);
final InputStream is = HttpConnectionManager.open(Config.PROVIDER_URL, false);
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = reader.readLine()) != null) {
jsonString.append(line);
}
is.close();
reader.close();
} catch (Exception e) {
e.printStackTrace();
isError = true;
}
try {
parseJson(new JSONObject(jsonString.toString()));
} catch (JSONException e) {
e.printStackTrace();
isError = true;
}
if (isError) {
Log.d(Config.LOGTAG, "ProviderService: Updating provider list failed");
}
return !isError;
}
private void parseJson(JSONObject jsonObject) {
if (jsonObject != null) {
try {
for (int i = 0; i < jsonObject.length(); i++) {
boolean inBandRegistration = false;
boolean freeOfCharge = false;
String ratingC2S = null;
String ratingS2S = null;
int ratingXmppComplianceTester = 0;
final String provider = jsonObject.names().getString(i);
if (provider.length() > 0) {
for (int ii = 0; ii < jsonObject.length(); ii++) {
final JSONObject json = new JSONObject(jsonObject.get(provider).toString());
String featureName = json.names().getString(ii);
final JSONObject subjson = new JSONObject(json.get(json.names().getString(ii)).toString());
if (featureName.equals("inBandRegistration")) {
inBandRegistration = subjson.getBoolean("content");
}
if (featureName.equals("ratingXmppComplianceTester")) {
ratingXmppComplianceTester = subjson.getInt("content");
}
if (featureName.equals("freeOfCharge")) {
freeOfCharge = subjson.getBoolean("content");
}
if (featureName.equals("ratingImObservatoryClientToServer")) {
ratingC2S = subjson.getString("content");
}
if (featureName.equals("ratingImObservatoryServerToServer")) {
ratingS2S = subjson.getString("content");
}
if (!Config.DOMAIN.BLACKLISTED_DOMAINS.contains(provider)
&& inBandRegistration == REGISTRATION
&& ratingXmppComplianceTester >= COMPLIANCE
&& freeOfCharge == FREE
&& (ratingC2S != null && ratingC2S.equalsIgnoreCase(RATING))
&& (ratingS2S != null && ratingS2S.equalsIgnoreCase(RATING))) {
//Log.d(Config.LOGTAG, "ProviderService: Updating provider list. Adding " + provider + " (Registration: " + inBandRegistration + " Compliance: " + ratingXmppComplianceTester + " Free: " + freeOfCharge + " Rating C2S/S2S: " + ratingC2S + "/" + ratingS2S + ")");
providers.add(provider);
}
}
}
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
}

View file

@ -0,0 +1,227 @@
package eu.siacs.conversations.services;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.http.NoSSLv3SocketFactory;
import eu.siacs.conversations.ui.UpdaterActivity;
import me.drakeet.support.toast.ToastCompat;
import static eu.siacs.conversations.http.HttpConnectionManager.getProxy;
public class UpdateService extends AsyncTask<String, Object, UpdateService.Wrapper> {
private boolean mUseTor;
private Context context;
private String store;
private NotificationService getNotificationService;
public UpdateService() {
}
public UpdateService(Context context, String Store, XmppConnectionService mXmppConnectionService) {
this.context = context;
this.store = Store;
this.mUseTor = mXmppConnectionService.useTorToConnect();
this.getNotificationService = mXmppConnectionService.getNotificationService();
}
@Override
protected Wrapper doInBackground(String... params) {
StringBuilder jsonString = new StringBuilder();
boolean UpdateAvailable = false;
boolean showNoUpdateToast = false;
boolean isError = false;
if (params[0].equals("true")) {
showNoUpdateToast = true;
}
SSLContext sslcontext = null;
SSLSocketFactory NoSSLv3Factory = null;
try {
sslcontext = SSLContext.getInstance("TLSv1");
if (sslcontext != null) {
sslcontext.init(null, null, null);
NoSSLv3Factory = new NoSSLv3SocketFactory(sslcontext.getSocketFactory());
}
} catch (NoSuchAlgorithmException | KeyManagementException e) {
e.printStackTrace();
}
HttpsURLConnection.setDefaultSSLSocketFactory(NoSSLv3Factory);
HttpsURLConnection connection = null;
try {
URL url = new URL(Config.UPDATE_URL);
if (mUseTor) {
connection = (HttpsURLConnection) url.openConnection(getProxy());
} else {
connection = (HttpsURLConnection) url.openConnection();
}
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setRequestProperty("User-agent", System.getProperty("http.agent"));
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
jsonString.append(line);
}
} catch (Exception e) {
e.printStackTrace();
isError = true;
} finally {
if (connection != null) {
connection.disconnect();
}
}
try {
JSONObject json = new JSONObject(jsonString.toString());
if (json.getBoolean("success") && json.has("latestVersion") && json.has("appURI") && json.has("filesize")) {
String version = json.getString("latestVersion");
String ownVersion = BuildConfig.VERSION_NAME;
String url = json.getString("appURI");
String filesize = json.getString("filesize");
String changelog = "";
if (json.has("changelog")) {
changelog = json.getString("changelog");
}
if (checkVersion(version, ownVersion) >= 1) {
Log.d(Config.LOGTAG, "AppUpdater: Version " + ownVersion + " should be updated to " + version);
UpdateAvailable = true;
showNotification(url, changelog, version, filesize, store);
} else {
Log.d(Config.LOGTAG, "AppUpdater: Version " + ownVersion + " is up to date");
UpdateAvailable = false;
}
}
} catch (JSONException e) {
e.printStackTrace();
}
Wrapper w = new Wrapper();
w.isError = isError;
w.UpdateAvailable = UpdateAvailable;
w.NoUpdate = showNoUpdateToast;
return w;
}
@Override
protected void onPostExecute(Wrapper w) {
super.onPostExecute(w);
if (w.isError) {
showToastMessage(true, true);
return;
}
if (!w.UpdateAvailable) {
showToastMessage(w.NoUpdate, false);
}
}
private void showToastMessage(boolean show, final boolean error) {
if (!show) {
return;
}
Handler handler = new Handler(Looper.getMainLooper());
handler.post(() -> {
String ToastMessage = "";
if (error) {
ToastMessage = context.getString(R.string.failed);
} else {
ToastMessage = context.getString(R.string.no_update_available);
}
ToastCompat.makeText(context, ToastMessage, ToastCompat.LENGTH_LONG).show();
});
}
private void showNotification(String url, String changelog, String version, String filesize, String store) {
Intent intent = new Intent(context, UpdaterActivity.class);
intent.putExtra("update", "MonoclesMessenger_UpdateService");
intent.putExtra("url", url);
intent.putExtra("changelog", changelog);
intent.putExtra("store", store);
PendingIntent pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
getNotificationService.AppUpdateServiceNotification(getNotificationService.AppUpdateNotification(pi, version, filesize));
}
private int checkVersion(String remoteVersion, String installedVersion) {
// Use this instead of String.compareTo() for a non-lexicographical
// comparison that works for version strings. e.g. "1.10".compareTo("1.6").
//
// @param str1 a string of ordinal numbers separated by decimal points.
// @param str2 a string of ordinal numbers separated by decimal points.
// @return The result is a negative integer if str1 is _numerically_ less than str2.
// The result is a positive integer if str1 is _numerically_ greater than str2.
// The result is zero if the strings are _numerically_ equal.
// @note It does not work if "1.10" is supposed to be equal to "1.10.0".
String[] remote = null;
String[] installed = null;
String[] remoteV = null;
String[] installedV = null;
try {
installedV = installedVersion.split("[ |\\-]");
Log.d(Config.LOGTAG, "AppUpdater: Version installed: " + installedV[0]);
installed = installedV[0].split("\\.");
} catch (Exception e) {
e.printStackTrace();
}
try {
remoteV = remoteVersion.split(" ");
if (installedV != null && installedV.length > 1) {
if (installedV[1] != null && installedV[1].toLowerCase().contains("beta")) {
remoteV[0] = remoteV[0] + ".1";
}
}
Log.d(Config.LOGTAG, "AppUpdater: Version on server: " + remoteV[0]);
remote = remoteV[0].split("\\.");
} catch (Exception e) {
e.printStackTrace();
}
int i = 0;
// set index to first non-equal ordinal or length of shortest localVersion string
try {
if (remote != null && installed != null) {
while (i < remote.length && i < installed.length && remote[i].equals(installed[i])) {
i++;
}
// compare first non-equal ordinal number
if (i < remote.length && i < installed.length) {
int diff = Integer.valueOf(remote[i]).compareTo(Integer.valueOf(installed[i]));
return Integer.signum(diff);
}
// the strings are equal or one string is a substring of the other
// e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4"
return Integer.signum(remote.length - installed.length);
}
} catch (Exception e) {
showToastMessage(true, true);
e.printStackTrace();
return 0;
}
return 0;
}
class Wrapper {
boolean UpdateAvailable = false;
boolean NoUpdate = false;
boolean isError = false;
}
}

View file

@ -1,5 +1,16 @@
package eu.siacs.conversations.services;
import static eu.siacs.conversations.ui.SettingsActivity.ALLOW_MESSAGE_CORRECTION;
import static eu.siacs.conversations.ui.SettingsActivity.ALLOW_MESSAGE_RETRACTION;
import static eu.siacs.conversations.ui.SettingsActivity.AUTOMATIC_ATTACHMENT_DELETION;
import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES;
import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES;
import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_MULTI_ACCOUNTS;
import static eu.siacs.conversations.ui.SettingsActivity.INDICATE_RECEIVED;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_OWN_ACCOUNTS;
import static eu.siacs.conversations.ui.SettingsActivity.USE_INNER_STORAGE;
import static eu.siacs.conversations.utils.RichPreview.RICH_LINK_METADATA;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
@ -43,15 +54,6 @@ import android.util.DisplayMetrics;
import android.util.Log;
import android.util.LruCache;
import android.util.Pair;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
import androidx.annotation.BoolRes;
import androidx.annotation.IntegerRes;
@ -60,6 +62,7 @@ import androidx.core.content.ContextCompat;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
import org.conscrypt.Conscrypt;
import org.openintents.openpgp.IOpenPgpService2;
@ -90,6 +93,8 @@ import java.util.TimeZone;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
@ -151,6 +156,7 @@ import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.TorServiceUtils;
import eu.siacs.conversations.utils.TranscoderStrategies;
import eu.siacs.conversations.utils.WakeLockHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
@ -182,15 +188,6 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
import me.leolin.shortcutbadger.ShortcutBadger;
import static eu.siacs.conversations.ui.SettingsActivity.ALLOW_MESSAGE_CORRECTION;
import static eu.siacs.conversations.ui.SettingsActivity.AUTOMATIC_ATTACHMENT_DELETION;
import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES;
import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES;
import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_MULTI_ACCOUNTS;
import static eu.siacs.conversations.ui.SettingsActivity.INDICATE_RECEIVED;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_OWN_ACCOUNTS;
import static eu.siacs.conversations.utils.RichPreview.RICH_LINK_METADATA;
public class XmppConnectionService extends Service {
public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
@ -256,18 +253,9 @@ public class XmppConnectionService extends Service {
Conversation conversation = find(getConversations(), contact);
if (conversation != null) {
if (online) {
conversation.endOtrIfNeeded();
if (contact.getPresences().size() == 1) {
sendUnsentMessages(conversation);
}
} else {
//check if the resource we are haveing a conversation with is still online
if (conversation.hasValidOtrSession()) {
String otrResource = conversation.getOtrSession().getSessionID().getUserID();
if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) {
conversation.endOtrIfNeeded();
}
}
}
}
};
@ -448,9 +436,6 @@ public class XmppConnectionService extends Service {
if (conversation.getAccount() == account
&& !pendingJoin
&& !inProgressJoin) {
if (!conversation.startOtrIfNeeded()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": couldn't start OTR with " + conversation.getContact().getJid() + " when needed");
}
sendUnsentMessages(conversation);
}
}
@ -505,7 +490,6 @@ public class XmppConnectionService extends Service {
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
private WakeLock wakeLock;
private PowerManager pm;
private LruCache<String, Bitmap> mBitmapCache;
private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
@ -609,8 +593,8 @@ public class XmppConnectionService extends Service {
return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
}
public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
final boolean compressPictures = getCompressImageResolutionPreference() != 0;
if (!compressPictures
|| getFileBackend().useImageAsIs(uri)
@ -775,6 +759,7 @@ public class XmppConnectionService extends Service {
}
final CharSequence body = remoteInput.getCharSequence("text_reply");
final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
if (body == null || body.length() <= 0) {
break;
}
@ -783,15 +768,7 @@ public class XmppConnectionService extends Service {
restoredFromDatabaseLatch.await();
final Conversation c = findConversationByUuid(uuid);
if (c != null) {
boolean pn = false;
if (c.getMode() == Conversational.MODE_MULTI) {
final Message latestMessage = c.getLatestMessage();
if (latestMessage.isPrivateMessage()) {
pn = true;
c.setNextCounterpart(latestMessage.getCounterpart());
}
}
directReply(c, body.toString(), dismissNotification, pn);
directReply(c, body.toString(), lastMessageUuid, dismissNotification);
}
} catch (InterruptedException e) {
Log.d(Config.LOGTAG, "unable to process direct reply");
@ -860,9 +837,6 @@ public class XmppConnectionService extends Service {
}
}
synchronized (this) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M && Build.MANUFACTURER.equals("Huawei")) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "LocationManagerService");
}
WakeLockHelper.acquire(wakeLock);
boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
final HashSet<Account> pingCandidates = new HashSet<>();
@ -1040,10 +1014,11 @@ public class XmppConnectionService extends Service {
}
}
private void directReply(Conversation conversation, String body, final boolean dismissAfterReply, final boolean pn) {
Message message = new Message(conversation, body, conversation.getNextEncryption());
if (pn) {
Message.configurePrivateMessage(message);
private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
final Message message = new Message(conversation, body, conversation.getNextEncryption());
if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
}
message.markUnread();
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
@ -1173,6 +1148,23 @@ public class XmppConnectionService extends Service {
}
}
public DefaultAudioStrategy getCompressAudioPreference() {
switch (getPreferences().getString("video_compression", getResources().getString(R.string.video_compression))) {
case "verylow":
return TranscoderStrategies.AUDIO_LQ;
case "low":
return TranscoderStrategies.AUDIO_LQ;
case "mid":
return TranscoderStrategies.AUDIO_MQ;
case "high":
return TranscoderStrategies.AUDIO_HQ;
case "uncompressed":
return TranscoderStrategies.AUDIO_HQ;
default:
return TranscoderStrategies.AUDIO_MQ;
}
}
public boolean getAttachmentChoicePreference() {
return getBooleanPreference(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, R.bool.quick_share_attachment_choice);
}
@ -1412,6 +1404,7 @@ public class XmppConnectionService extends Service {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
startContactObserver();
}
FileBackend.switchStorage(usingInnerStorage());
FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
if (Compatibility.hasStoragePermission(this)) {
Log.d(Config.LOGTAG, "starting file observer");
@ -1437,7 +1430,7 @@ public class XmppConnectionService extends Service {
this.pgpServiceConnection.bindToService();
}
this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class);
this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Config.LOGTAG + ":Service");
toggleForegroundService();
updateUnreadCountBadge();
@ -1574,8 +1567,8 @@ public class XmppConnectionService extends Service {
toggleForegroundService(false);
}
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
ongoingCall.set(new OngoingCall(id, media));
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
ongoingCall.set(new OngoingCall(id, media, reconnecting));
toggleForegroundService(false);
}
@ -1591,7 +1584,7 @@ public class XmppConnectionService extends Service {
final Notification notification;
final int id;
if (ongoing != null) {
notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media);
notification = this.mNotificationService.getOngoingCallNotification(ongoing);
id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
startForeground(id, notification);
mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
@ -1776,11 +1769,6 @@ public class XmppConnectionService extends Service {
databaseBackend.updateConversation(conversation);
}
}
if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
conversation.endOtrIfNeeded();
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
message1 -> markMessage(message1, Message.STATUS_SEND_FAILED));
}
final boolean inProgressJoin = isJoinInProgress(conversation);
@ -1813,30 +1801,6 @@ public class XmppConnectionService extends Service {
packet = mMessageGenerator.generatePgpChat(message);
}
break;
case Message.ENCRYPTION_OTR:
SessionImpl otrSession = conversation.getOtrSession();
if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
try {
message.setCounterpart(OtrJidHelper.fromSessionID(otrSession.getSessionID()));
} catch (IllegalArgumentException e) {
break;
}
if (message.needsUploading()) {
mJingleConnectionManager.startJingleFileTransfer(message);
} else {
packet = mMessageGenerator.generateOtrChat(message);
}
} else if (otrSession == null) {
if (message.fixCounterpart()) {
conversation.startOtrSession(message.getCounterpart().getResource(), true);
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fix counterpart for OTR message to contact " + message.getCounterpart());
break;
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " OTR session with " + message.getContact() + " is in wrong state: " + otrSession.getSessionStatus().toString());
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
if (message.needsUploading()) {
@ -1889,12 +1853,6 @@ public class XmppConnectionService extends Service {
}
}
break;
case Message.ENCRYPTION_OTR:
if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": create otr session without starting for " + message.getContact().getJid());
conversation.startOtrSession(message.getCounterpart().getResource(), false);
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
break;
@ -2148,6 +2106,14 @@ public class XmppConnectionService extends Service {
}
}
public void pushBookmarks(Account account) {
if (account.getXmppConnection().getFeatures().bookmarksConversion()) {
pushBookmarksPep(account);
} else {
pushBookmarksPrivateXml(account);
}
}
private void pushBookmarksPrivateXml(Account account) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
@ -2166,7 +2132,6 @@ public class XmppConnectionService extends Service {
storage.addChild(bookmark);
}
pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, "current", PublishOptions.persistentWhitelistAccess());
}
private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options) {
@ -2219,7 +2184,10 @@ public class XmppConnectionService extends Service {
long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
Runnable runnable = () -> {
long deletionDate = getAutomaticMessageDeletionDate();
if (DatabaseBackend.requiresMessageIndexRebuild()) {
DatabaseBackend.getInstance(this).rebuildMessagesIndex();
}
final long deletionDate = getAutomaticMessageDeletionDate();
mLastExpiryRun.set(SystemClock.elapsedRealtime());
if (deletionDate > 0) {
Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
@ -3728,35 +3696,26 @@ public class XmppConnectionService extends Service {
public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
final Jid jid = user.asBareJid();
IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
conference.getMucOptions().changeAffiliation(jid, affiliation);
getAvatarService().clear(conference);
final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
sendIqPacket(conference.getAccount(), request, (account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
conference.getMucOptions().changeAffiliation(jid, affiliation);
getAvatarService().clear(conference);
if (callback != null) {
callback.onAffiliationChangedSuccessful(jid);
} else {
callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
Log.d(Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation);
}
} else if (callback != null) {
callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
} else {
Log.d(Config.LOGTAG, "unable to change affiliation");
}
});
}
public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
List<Jid> jids = new ArrayList<>();
for (MucOptions.User user : conference.getMucOptions().getUsers()) {
if (user.getAffiliation() == before && user.getRealJid() != null) {
jids.add(user.getRealJid());
}
}
IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
}
public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) {
IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
Log.d(Config.LOGTAG, request.toString());
sendIqPacket(conference.getAccount(), request, (account, packet) -> {
if (packet.getType() != IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick);
@ -3798,12 +3757,6 @@ public class XmppConnectionService extends Service {
if (conversation.getAccount() == account) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
leaveMuc(conversation, true);
} else {
if (conversation.endOtrIfNeeded()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid()
+ ": ended otr session with "
+ conversation.getJid());
}
}
}
}
@ -3865,67 +3818,6 @@ public class XmppConnectionService extends Service {
pushContactToServer(contact, null);
}
public void onOtrSessionEstablished(Conversation conversation) {
final Account account = conversation.getAccount();
final Session otrSession = conversation.getOtrSession();
Log.d(Config.LOGTAG,
account.getJid().asBareJid() + " otr session established with "
+ conversation.getJid() + "/"
+ otrSession.getSessionID().getUserID());
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
SessionID id = otrSession.getSessionID();
try {
message.setCounterpart(Jid.of(id.getAccountID() + "/" + id.getUserID()));
} catch (IllegalArgumentException e) {
return;
}
if (message.needsUploading()) {
mJingleConnectionManager.startJingleFileTransfer(message);
} else {
MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
if (outPacket != null) {
mMessageGenerator.addDelay(outPacket, message.getTimeSent());
message.setStatus(Message.STATUS_SEND);
databaseBackend.updateMessage(message, false);
sendMessagePacket(account, outPacket);
}
}
updateConversationUi();
}
});
}
public boolean renewSymmetricKey(Conversation conversation) {
Account account = conversation.getAccount();
byte[] symmetricKey = new byte[32];
this.mRandom.nextBytes(symmetricKey);
Session otrSession = conversation.getOtrSession();
if (otrSession != null) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT);
packet.setFrom(account.getJid());
MessageGenerator.addMessageHints(packet);
packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
+ otrSession.getSessionID().getUserID());
try {
packet.setBody(otrSession
.transformSending(CryptoHelper.FILETRANSFER
+ CryptoHelper.bytesToHex(symmetricKey))[0]);
sendMessagePacket(account, packet);
conversation.setSymmetricKey(symmetricKey);
return true;
} catch (OtrException e) {
return false;
}
}
return false;
}
private void pushContactToServer(final Contact contact, final String preAuth) {
contact.resetOption(Contact.Options.DIRTY_DELETE);
contact.setOption(Contact.Options.DIRTY_PUSH);
@ -3939,10 +3831,10 @@ public class XmppConnectionService extends Service {
iq.query(Namespace.ROSTER).addChild(contact.asElement());
account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
if (sendUpdates) {
sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
}
if (ask) {
sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact));
sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
}
} else {
syncRoster(contact.getAccount());
@ -4403,9 +4295,13 @@ public class XmppConnectionService extends Service {
new Thread(() -> reconnectAccount(account, false, true)).start();
}
public void invite(Conversation conversation, Jid contact) {
public void invite(final Conversation conversation, final Jid contact) {
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid());
MessagePacket packet = mMessageGenerator.invite(conversation, contact);
final MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
}
final MessagePacket packet = mMessageGenerator.invite(conversation, contact);
sendMessagePacket(conversation.getAccount(), packet);
}
@ -4537,10 +4433,18 @@ public class XmppConnectionService extends Service {
return getBooleanPreference(CONFIRM_MESSAGES, R.bool.confirm_messages);
}
public boolean usingInnerStorage() {
return getBooleanPreference(USE_INNER_STORAGE, R.bool.use_inner_storage);
}
public boolean allowMessageCorrection() {
return getBooleanPreference(ALLOW_MESSAGE_CORRECTION, R.bool.allow_message_correction);
}
public boolean allowMessageRetraction() {
return getBooleanPreference(ALLOW_MESSAGE_RETRACTION, R.bool.allow_message_retraction);
}
public boolean sendChatStates() {
return getBooleanPreference(CHAT_STATES, R.bool.chat_states);
}
@ -4565,7 +4469,6 @@ public class XmppConnectionService extends Service {
return getBooleanPreference(SettingsActivity.WARN_UNENCRYPTED_CHAT, R.bool.warn_unencrypted_chat);
}
public boolean hideYouAreNotParticipating() {
return getBooleanPreference(SettingsActivity.HIDE_YOU_ARE_NOT_PARTICIPATING, R.bool.hide_you_are_not_participating);
}
@ -4586,6 +4489,14 @@ public class XmppConnectionService extends Service {
return getBooleanPreference(SHOW_OWN_ACCOUNTS, R.bool.show_own_accounts);
}
public boolean allowMergeMessages() {
return getBooleanPreference("allowmergemessages", R.bool.allowmergemessages);
}
public boolean showTextFormatting() {
return getBooleanPreference("showtextformatting", R.bool.showtextformatting);
}
public int unreadCount() {
int count = 0;
for (Conversation conversation : getConversations()) {
@ -5252,10 +5163,7 @@ public class XmppConnectionService extends Service {
boolean performedVerification = false;
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
for (XmppUri.Fingerprint fp : fingerprints) {
if (fp.type == XmppUri.FingerprintType.OTR) {
performedVerification |= contact.addOtrFingerprint(fp.fingerprint);
needsRosterWrite |= performedVerification;
} else if (fp.type == XmppUri.FingerprintType.OMEMO) {
if (fp.type == XmppUri.FingerprintType.OMEMO) {
String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
if (fingerprintStatus != null) {
@ -5491,12 +5399,14 @@ public class XmppConnectionService extends Service {
}
public static class OngoingCall {
private final AbstractJingleConnection.Id id;
private final Set<Media> media;
public final AbstractJingleConnection.Id id;
public final Set<Media> media;
public final boolean reconnecting;
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
this.id = id;
this.media = media;
this.reconnecting = reconnecting;
}
@Override
@ -5504,12 +5414,12 @@ public class XmppConnectionService extends Service {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OngoingCall that = (OngoingCall) o;
return Objects.equal(id, that.id);
return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media);
}
@Override
public int hashCode() {
return Objects.hashCode(id);
return Objects.hashCode(id, media, reconnecting);
}
}

View file

@ -37,7 +37,6 @@ public class AboutActivity extends XmppActivity {
setContentView(R.layout.activity_about);
setSupportActionBar(findViewById(R.id.toolbar));
configureActionBar(getSupportActionBar());
setTitle(getString(R.string.title_activity_about_x, getString(R.string.app_name)));
aboutmessage = findViewById(R.id.aboutmessage);
libraries = findViewById(R.id.libraries);
Button privacyButton = findViewById(R.id.show_privacy_policy);

View file

@ -5,23 +5,18 @@ import android.content.Intent;
import android.preference.Preference;
import android.util.AttributeSet;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.R;
import eu.siacs.conversations.utils.PhoneHelper;
public class AboutPreference extends Preference {
public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
setSummaryAndTitle(context);
setSummary();
}
public AboutPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
setSummaryAndTitle(context);
}
private void setSummaryAndTitle(final Context context) {
setSummary(String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
setTitle(context.getString(R.string.title_activity_about_x, BuildConfig.APP_NAME));
setSummary();
}
@Override
@ -30,5 +25,9 @@ public class AboutPreference extends Preference {
final Intent intent = new Intent(getContext(), AboutActivity.class);
getContext().startActivity(intent);
}
private void setSummary() {
setSummary(getContext().getString(R.string.app_name) + ' ' + PhoneHelper.getVersionName(getContext()));
}
}

View file

@ -4,10 +4,10 @@ import static eu.siacs.conversations.entities.Bookmark.printableValue;
import static eu.siacs.conversations.ui.util.IntroHelper.showIntro;
import static eu.siacs.conversations.utils.StringUtils.changed;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Editable;
@ -114,6 +114,14 @@ 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);
activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
}
private OnClickListener mNotifyStatusClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
@ -269,8 +277,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
if (mConversation != null) {
final Bookmark bookmark = mConversation.getBookmark();
if (bookmark != null) {
this.binding.autojoinCheckbox.setEnabled(!getBooleanPreference("autojoin", R.bool.autojoin));
bookmark.setAutojoin(this.binding.autojoinCheckbox.isChecked());
xmppConnectionService.createBookmark(mConversation.getAccount(), bookmark);
xmppConnectionService.pushBookmarks(mConversation.getAccount());
updateView();
}
}
@ -330,8 +339,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.mAdvancedMode = !menuItem.isChecked();
menuItem.setChecked(this.mAdvancedMode);
getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply();
final boolean online = mConversation != null && mConversation.getMucOptions().online();
this.binding.mucInfoMore.setVisibility(this.mAdvancedMode && online ? View.VISIBLE : View.GONE);
invalidateOptionsMenu();
updateView();
break;
@ -483,7 +490,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
final MenuItem share = menu.findItem(R.id.action_share);
share.setVisible(!groupChat);
final MenuItem menuMessageNotification = menu.findItem(R.id.action_message_notifications);
if (Compatibility.runsTwentySix()) {
if (Compatibility.runsTwentySix() && xmppConnectionServiceBound) {
menuMessageNotification.setVisible(xmppConnectionService.hasIndividualNotification(mConversation));
} else {
menuMessageNotification.setVisible(false);
@ -598,7 +605,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
if (printableValue(subject)) {
SpannableStringBuilder spannable = new SpannableStringBuilder(subject);
StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor(), true);
MyLinkify.addLinks(spannable, false);
this.binding.mucSubject.setText(EmojiWrapper.transform(spannable));
this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead);
@ -630,18 +637,15 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.mucInfoMam.setText(R.string.server_info_unavailable);
}
if (bookmark != null) {
this.binding.autojoinCheckbox.setEnabled(!getBooleanPreference("autojoin", R.bool.autojoin));
this.binding.autojoinCheckbox.setVisibility(View.VISIBLE);
if (bookmark.autojoin()) {
this.binding.autojoinCheckbox.setChecked(true);
} else {
this.binding.autojoinCheckbox.setChecked(false);
}
this.binding.autojoinCheckbox.setChecked(bookmark.autojoin());
} else {
this.binding.autojoinCheckbox.setVisibility(View.GONE);
}
if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
if (mAdvancedMode) {
this.binding.destroy.getBackground().setColorFilter(getWarningButtonColor(), PorterDuff.Mode.MULTIPLY);
this.binding.destroy.getBackground().setTint(getWarningButtonColor());
this.binding.destroy.setTextColor(getWarningTextColor());
this.binding.destroy.setVisibility(View.VISIBLE);
} else {
@ -667,12 +671,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
});
LeaveMucDialog.create().show();
});
this.binding.leaveMuc.getBackground().setColorFilter(getWarningButtonColor(), PorterDuff.Mode.MULTIPLY);
this.binding.leaveMuc.getBackground().setTint(getWarningButtonColor());
this.binding.leaveMuc.setTextColor(getWarningTextColor());
this.binding.addContactButton.setVisibility(View.VISIBLE);
if (mConversation.getBookmark() != null) {
this.binding.addContactButton.setText(R.string.delete_bookmark);
this.binding.addContactButton.getBackground().setColorFilter(getWarningButtonColor(), PorterDuff.Mode.MULTIPLY);
this.binding.addContactButton.getBackground().setTint(getWarningButtonColor());
this.binding.addContactButton.setTextColor(getWarningTextColor());
this.binding.addContactButton.setOnClickListener(v2 -> {
final AlertDialog.Builder deleteFromRosterDialog = new AlertDialog.Builder(ConferenceDetailsActivity.this);
@ -682,6 +686,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
deleteFromRosterDialog.setPositiveButton(getString(R.string.delete),
(dialog, which) -> {
deleteBookmark();
recreate();
});
deleteFromRosterDialog.create().show();
});
@ -691,6 +696,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.addContactButton.setTextColor(getDefaultButtonTextColor());
this.binding.addContactButton.setOnClickListener(v2 -> {
saveAsBookmark();
recreate();
});
}
} else {
@ -814,4 +820,4 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
}
}
}
}

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.ui;
import static eu.siacs.conversations.ui.util.IntroHelper.showIntro;
import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.Context;
@ -7,7 +9,6 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -28,8 +29,6 @@ import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ImageButton;
import android.widget.TextView;
import eu.siacs.conversations.utils.CryptoHelper;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
@ -84,8 +83,6 @@ import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import me.drakeet.support.toast.ToastCompat;
import static eu.siacs.conversations.ui.util.IntroHelper.showIntro;
public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded {
public static final String ACTION_VIEW_CONTACT = "view_contact";
private final int REQUEST_SYNC_CONTACTS = 0x28cf;
@ -102,6 +99,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override
public void onClick(DialogInterface dialog, int which) {
xmppConnectionService.deleteContactOnServer(contact);
recreate();
}
};
private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
@ -161,6 +159,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
}
private void showAddToPhoneBookDialog() {
//TODO check if isQuicksy and contact is on quicksy.im domain
// store in final boolean. show different message. use phone number for add
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.action_add_phone_book));
builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toEscapedString()));
@ -170,6 +170,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
intent.setType(Contacts.CONTENT_ITEM_TYPE);
intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toEscapedString());
intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
//TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP'
// however we dont have such a field and thus have to use the legacy PROTOCOL_JABBER
intent.putExtra("finishActivityOnSaveCompleted", true);
try {
startActivityForResult(intent, 0);
@ -319,6 +321,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
@ -553,7 +556,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
binding.detailsReceivePresence.setOnCheckedChangeListener(null);
binding.addContactButton.setVisibility(View.VISIBLE);
binding.addContactButton.setText(getString(R.string.action_delete_contact));
binding.addContactButton.getBackground().setColorFilter(getWarningButtonColor(), PorterDuff.Mode.MULTIPLY);
binding.addContactButton.getBackground().setTint(getWarningButtonColor());
binding.addContactButton.setTextColor(getWarningTextColor());
binding.addContactButton.setOnClickListener(view -> {
final AlertDialog.Builder deleteFromRosterDialog = new AlertDialog.Builder(ContactDetailsActivity.this);
@ -677,26 +680,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
binding.detailsContactKeys.removeAllViews();
boolean hasKeys = false;
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (Config.supportOtr()) {
for (final String otrFingerprint : contact.getOtrFingerprints()) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, binding.detailsContactKeys, false);
TextView key = view.findViewById(R.id.key);
TextView keyType = view.findViewById(R.id.key_type);
ImageButton removeButton = view
.findViewById(R.id.button_remove);
removeButton.setVisibility(View.VISIBLE);
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
if (otrFingerprint != null && otrFingerprint.equalsIgnoreCase(messageFingerprint)) {
keyType.setText(R.string.otr_fingerprint_selected_message);
keyType.setTextColor(ContextCompat.getColor(this, R.color.accent));
} else {
keyType.setText(R.string.otr_fingerprint);
}
binding.detailsContactKeys.addView(view);
removeButton.setOnClickListener(v -> confirmToDeleteFingerprint(otrFingerprint));
}
}
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
if (Config.supportOmemo() && axolotlService != null) {
final Collection<XmppAxolotlSession> sessions = axolotlService.findSessionsForContact(contact);
@ -785,20 +768,6 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
}
}
}
protected void confirmToDeleteFingerprint(final String fingerprint) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.delete_fingerprint);
builder.setMessage(R.string.sure_delete_fingerprint);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.delete,
(dialog, which) -> {
if (contact.deleteOtrFingerprint(fingerprint)) {
populateView();
xmppConnectionService.syncRosterToDisk(contact.getAccount());
}
});
builder.create().show();
}
public void onBackendConnected() {
if (accountJid != null && contactJid != null) {

View file

@ -29,7 +29,10 @@
package eu.siacs.conversations.ui;
import net.java.otr4j.session.SessionStatus;
import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
import static eu.siacs.conversations.ui.SettingsActivity.HIDE_MEMORY_WARNING;
import static eu.siacs.conversations.ui.SettingsActivity.MIN_ANDROID_SDK21_SHOWN;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
@ -53,9 +56,7 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
@ -77,6 +78,7 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.databinding.ActivityConversationsBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.MucOptions;
@ -87,13 +89,13 @@ import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
import eu.siacs.conversations.ui.interfaces.OnConversationRead;
import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
import eu.siacs.conversations.ui.util.ActionBarUtil;
import eu.siacs.conversations.ui.util.ActivityResult;
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
import eu.siacs.conversations.ui.util.IntroHelper;
import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.ui.util.UpdateHelper;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.EmojiWrapper;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.MenuDoubleTabUtil;
@ -106,10 +108,6 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import me.drakeet.support.toast.ToastCompat;
import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
import static eu.siacs.conversations.ui.SettingsActivity.HIDE_MEMORY_WARNING;
import static eu.siacs.conversations.ui.SettingsActivity.MIN_ANDROID_SDK21_SHOWN;
public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoomDestroy {
public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.VIEW";
@ -125,7 +123,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
public static final String ACTION_DESTROY_MUC = "eu.siacs.conversations.DESTROY_MUC";
public static final int REQUEST_OPEN_MESSAGE = 0x9876;
public static final int REQUEST_PLAY_PAUSE = 0x5432;
private static List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
public static final String EXTRA_TYPE = "type";
private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
ACTION_VIEW_CONVERSATION,
Intent.ACTION_SEND,
Intent.ACTION_SEND_MULTIPLE
@ -215,6 +215,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
System.exit(0);
}
if (useInternalUpdater()) {
if (xmppConnectionService.getAccounts().size() != 0) {
if (xmppConnectionService.hasInternetConnection()) {
if (xmppConnectionService.isWIFI() || (xmppConnectionService.isMobile() && !xmppConnectionService.isMobileRoaming())) {
AppUpdate(xmppConnectionService.installedFrom());
}
}
}
}
for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
notifyFragmentOfBackendConnected(id);
}
@ -276,7 +286,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
private void showOutdatedVersionWarning() {
if (Compatibility.runsTwentyOne() || getPreferences().getBoolean(MIN_ANDROID_SDK21_SHOWN, false)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP || getPreferences().getBoolean(MIN_ANDROID_SDK21_SHOWN, false)) {
Log.d(Config.LOGTAG, "Device is running Android >= SDK 21");
return;
}
@ -302,6 +312,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply();
}
public boolean getAttachmentChoicePreference() {
return getBooleanPreference(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, R.bool.quick_share_attachment_choice);
}
public boolean warnUnecryptedChat() {
return getBooleanPreference(SettingsActivity.WARN_UNENCRYPTED_CHAT, R.bool.warn_unencrypted_chat);
}
private void openBatteryOptimizationDialogIfNeeded() {
if (hasAccountWithoutPush()
&& isOptimizingBattery()
@ -320,9 +338,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
ToastCompat.makeText(this, R.string.device_does_not_support_battery_op, ToastCompat.LENGTH_SHORT).show();
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
}
builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
final AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
dialog.show();
@ -349,12 +365,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
@Override
protected Void doInBackground(Void... params) {
totalMemory = FileBackend.getDiskSize();
mediaUsage = FileBackend.getDirectorySize(new File(FileBackend.getAppMediaDirectory()));
try {
totalMemory = FileBackend.getDiskSize();
mediaUsage = FileBackend.getDirectorySize(new File(FileBackend.getAppMediaDirectory()));
relativeUsage = ((double) mediaUsage / (double) totalMemory);
try {
percentUsage = String.format("%.2f", relativeUsage * 100) + " %";
percentUsage = String.format(Locale.getDefault(),"%.2f", relativeUsage * 100) + " %";
} catch (Exception e) {
e.printStackTrace();
percentUsage = String.format(Locale.ENGLISH,"%.2f", relativeUsage * 100) + " %";
@ -611,8 +627,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
private void openConversation(Conversation conversation, Bundle extras) {
final FragmentManager fragmentManager = getFragmentManager();
executePendingTransactions(fragmentManager);
ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
xmppConnectionService.updateNotificationChannels();
ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment);
final boolean mainNeedsRefresh;
if (conversationFragment == null) {
mainNeedsRefresh = false;
@ -648,6 +666,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
IntroHelper.showIntro(this, conversation.getMode() == Conversational.MODE_MULTI);
}
private static void executePendingTransactions(final FragmentManager fragmentManager) {
try {
fragmentManager.executePendingTransactions();
} catch (final Exception e) {
Log.e(Config.LOGTAG,"unable to execute pending fragment transactions");
}
}
public boolean onXmppUriClicked(Uri uri) {
XmppUri xmppUri = new XmppUri(uri);
if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
@ -660,6 +686,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
return false;
}
@SuppressLint("NonConstantResourceId")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (MenuDoubleTabUtil.shouldIgnoreTap()) {
@ -692,13 +719,13 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid());
startActivity(intent);
return true;
//case R.id.action_check_updates:
// if (xmppConnectionService.hasInternetConnection()) {
// openInstallFromUnknownSourcesDialogIfNeeded(true);
// } else {
// ToastCompat.makeText(this, R.string.account_status_no_internet, ToastCompat.LENGTH_LONG).show();
// }
// break;
case R.id.action_check_updates:
if (xmppConnectionService.hasInternetConnection()) {
openInstallFromUnknownSourcesDialogIfNeeded(true);
} else {
ToastCompat.makeText(this, R.string.account_status_no_internet, ToastCompat.LENGTH_LONG).show();
}
break;
case R.id.action_invite_user:
inviteUser();
break;
@ -726,6 +753,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
@Override
protected void onStart() {
super.onStart();
final int theme = findTheme();
if (this.mTheme != theme) {
this.mSkipBackgroundBinding = true;
@ -835,164 +863,113 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
private void invalidateActionBarTitle() {
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
if (mainFragment instanceof ConversationFragment) {
final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
if (conversation != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
final View view = getLayoutInflater().inflate(R.layout.ab_title, null);
getSupportActionBar().setCustomView(view);
actionBar.setIcon(null);
actionBar.setBackgroundDrawable(new ColorDrawable(StyledAttributes.getColor(this, R.attr.colorPrimary)));
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowCustomEnabled(true);
TextView abtitle = findViewById(android.R.id.text1);
TextView absubtitle = findViewById(android.R.id.text2);
abtitle.setText(EmojiWrapper.transform(conversation.getName()));
abtitle.setOnClickListener(view1 -> {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
switchToContactDetails(conversation.getContact());
} else if (conversation.getMode() == Conversation.MODE_MULTI) {
Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
startActivity(intent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
}
});
abtitle.setSelected(true);
if (conversation.getMode() == Conversation.MODE_SINGLE && !conversation.withSelf()) {
if (actionBar == null) {
return;
}
final FragmentManager fragmentManager = getFragmentManager();
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
if (mainFragment instanceof ConversationFragment) {
final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
if (conversation != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
final View view = getLayoutInflater().inflate(R.layout.ab_title, null);
getSupportActionBar().setCustomView(view);
actionBar.setIcon(null);
actionBar.setBackgroundDrawable(new ColorDrawable(StyledAttributes.getColor(this, R.attr.colorPrimary)));
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowCustomEnabled(true);
TextView abtitle = findViewById(android.R.id.text1);
TextView absubtitle = findViewById(android.R.id.text2);
abtitle.setText(EmojiWrapper.transform(conversation.getName()));
abtitle.setSelected(true);
if (conversation.getMode() == Conversation.MODE_SINGLE) {
if (!conversation.withSelf()) {
ChatState state = conversation.getIncomingChatState();
if (state == ChatState.COMPOSING) {
absubtitle.setText(getString(R.string.is_typing));
absubtitle.setVisibility(View.VISIBLE);
absubtitle.setTypeface(null, Typeface.BOLD_ITALIC);
absubtitle.setSelected(true);
absubtitle.setOnClickListener(view13 -> {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
switchToContactDetails(conversation.getContact());
} else if (conversation.getMode() == Conversation.MODE_MULTI) {
Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
startActivity(intent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
}
});
} else {
if (showLastSeen && conversation.getContact().getLastseen() > 0 && conversation.getContact().getPresences().allOrNonSupport(Namespace.IDLE)) {
absubtitle.setText(UIHelper.lastseen(getApplicationContext(), conversation.getContact().isActive(), conversation.getContact().getLastseen()));
absubtitle.setVisibility(View.VISIBLE);
} else {
absubtitle.setText(null);
absubtitle.setVisibility(View.GONE);
}
absubtitle.setSelected(true);
absubtitle.setOnClickListener(view14 -> {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
switchToContactDetails(conversation.getContact());
} else if (conversation.getMode() == Conversation.MODE_MULTI) {
Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
startActivity(intent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
}
});
}
} else {
ChatState state = ChatState.COMPOSING;
List<MucOptions.User> userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5);
if (userWithChatStates.size() == 0) {
state = ChatState.PAUSED;
userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5);
}
List<MucOptions.User> users = conversation.getMucOptions().getUsers(true);
if (state == ChatState.COMPOSING) {
if (userWithChatStates.size() > 0) {
if (userWithChatStates.size() == 1) {
MucOptions.User user = userWithChatStates.get(0);
absubtitle.setText(EmojiWrapper.transform(getString(R.string.contact_is_typing, UIHelper.getDisplayName(user))));
absubtitle.setVisibility(View.VISIBLE);
} else {
StringBuilder builder = new StringBuilder();
for (MucOptions.User user : userWithChatStates) {
if (builder.length() != 0) {
builder.append(", ");
}
builder.append(UIHelper.getDisplayName(user));
}
absubtitle.setText(EmojiWrapper.transform(getString(R.string.contacts_are_typing, builder.toString())));
absubtitle.setVisibility(View.VISIBLE);
}
}
absubtitle.setText(getString(R.string.is_typing));
absubtitle.setVisibility(View.VISIBLE);
absubtitle.setTypeface(null, Typeface.BOLD_ITALIC);
absubtitle.setSelected(true);
} else {
if (users.size() == 0) {
absubtitle.setText(getString(R.string.one_participant));
if (showLastSeen && conversation.getContact().getLastseen() > 0 && conversation.getContact().getPresences().allOrNonSupport(Namespace.IDLE)) {
absubtitle.setText(UIHelper.lastseen(getApplicationContext(), conversation.getContact().isActive(), conversation.getContact().getLastseen()));
absubtitle.setVisibility(View.VISIBLE);
} else {
int size = users.size() + 1;
absubtitle.setText(getString(R.string.more_participants, size));
absubtitle.setText(null);
absubtitle.setVisibility(View.GONE);
}
absubtitle.setSelected(true);
}
} else {
absubtitle.setText(null);
absubtitle.setVisibility(View.GONE);
}
} else {
ChatState state = ChatState.COMPOSING;
List<MucOptions.User> userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5);
if (userWithChatStates.size() == 0) {
state = ChatState.PAUSED;
userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5);
}
List<MucOptions.User> users = conversation.getMucOptions().getUsers(true);
if (state == ChatState.COMPOSING) {
if (userWithChatStates.size() > 0) {
if (userWithChatStates.size() == 1) {
MucOptions.User user = userWithChatStates.get(0);
absubtitle.setText(EmojiWrapper.transform(getString(R.string.contact_is_typing, UIHelper.getDisplayName(user))));
absubtitle.setVisibility(View.VISIBLE);
} else {
StringBuilder builder = new StringBuilder();
for (MucOptions.User user : userWithChatStates) {
if (builder.length() != 0) {
builder.append(", ");
}
builder.append(UIHelper.getDisplayName(user));
}
absubtitle.setText(EmojiWrapper.transform(getString(R.string.contacts_are_typing, builder.toString())));
absubtitle.setVisibility(View.VISIBLE);
}
}
absubtitle.setSelected(true);
absubtitle.setOnClickListener(view15 -> {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
switchToContactDetails(conversation.getContact());
} else if (conversation.getMode() == Conversation.MODE_MULTI) {
Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
startActivity(intent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
}
});
} else {
if (users.size() == 0) {
absubtitle.setText(getString(R.string.one_participant));
absubtitle.setVisibility(View.VISIBLE);
} else {
int size = users.size() + 1;
absubtitle.setText(getString(R.string.more_participants, size));
absubtitle.setVisibility(View.VISIBLE);
}
}
return;
absubtitle.setSelected(true);
}
ActionBarUtil.setCustomActionBarOnClickListener(
binding.toolbar,
(v) -> openConversationDetails(conversation)
);
return;
}
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayShowCustomEnabled(false);
actionBar.setTitle(null);
actionBar.setIcon(R.drawable.logo_actionbar);
actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.header_background)));
actionBar.setSubtitle(null);
actionBar.setDisplayHomeAsUpEnabled(false);
}
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayShowCustomEnabled(false);
actionBar.setTitle(null);
actionBar.setIcon(R.drawable.logo_actionbar);
actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.header_background)));
actionBar.setSubtitle(null);
actionBar.setDisplayHomeAsUpEnabled(false);
ActionBarUtil.resetCustomActionBarOnClickListeners(binding.toolbar);
}
public void verifyOtrSessionDialog(final Conversation conversation, View view) {
if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
ToastCompat.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show();
return;
}
if (view == null) {
return;
}
PopupMenu popup = new PopupMenu(this, view);
popup.inflate(R.menu.verification_choices);
popup.setOnMenuItemClickListener(menuItem -> {
Intent intent = new Intent(ConversationsActivity.this, VerifyOTRActivity.class);
intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
intent.putExtra("contact", conversation.getContact().getJid().asBareJid().toString());
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
switch (menuItem.getItemId()) {
case R.id.scan_fingerprint:
intent.putExtra("mode", VerifyOTRActivity.MODE_SCAN_FINGERPRINT);
break;
case R.id.ask_question:
intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION);
break;
case R.id.manual_verification:
intent.putExtra("mode", VerifyOTRActivity.MODE_MANUAL_VERIFICATION);
break;
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);
}
startActivity(intent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
return true;
});
popup.show();
}
}
@Override
@ -1000,17 +977,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
if (performRedirectIfNecessary(conversation, false)) {
return;
}
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment);
final FragmentManager fragmentManager = getFragmentManager();
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
if (mainFragment instanceof ConversationFragment) {
try {
getFragmentManager().popBackStack();
} catch (IllegalStateException e) {
fragmentManager.popBackStack();
} catch (final IllegalStateException e) {
Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e);
//this usually means activity is no longer active; meaning on the next open we will run through this again
}
return;
}
Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment);
final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
if (secondaryFragment instanceof ConversationFragment) {
if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);
@ -1072,6 +1050,25 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
runOnUiThread(() -> ToastCompat.makeText(this, resId, ToastCompat.LENGTH_SHORT).show());
}
protected void AppUpdate(String Store) {
String PREFS_NAME = "UpdateTimeStamp";
SharedPreferences UpdateTimeStamp = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
long lastUpdateTime = UpdateTimeStamp.getLong("lastUpdateTime", 0);
Log.d(Config.LOGTAG, "AppUpdater: LastUpdateTime: " + lastUpdateTime);
if ((lastUpdateTime + (Config.UPDATE_CHECK_TIMER * 1000)) < System.currentTimeMillis()) {
lastUpdateTime = System.currentTimeMillis();
SharedPreferences.Editor editor = UpdateTimeStamp.edit();
editor.putLong("lastUpdateTime", lastUpdateTime);
editor.apply();
Log.d(Config.LOGTAG, "AppUpdater: CurrentTime: " + lastUpdateTime);
if (Store == null) {
Log.d(Config.LOGTAG, "AppUpdater started");
openInstallFromUnknownSourcesDialogIfNeeded(false);
}
} else {
Log.d(Config.LOGTAG, "AppUpdater stopped");
}
}
@Override
public void onRoomDestroySucceeded() {

View file

@ -121,20 +121,20 @@ public class ConversationsOverviewFragment extends XmppFragment {
}
}
@Override
public void onDestroyView() {
Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onDestroyView()");
super.onDestroyView();
this.binding = null;
this.conversationsAdapter = null;
}
@Override
public void onDestroyView() {
Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onDestroyView()");
super.onDestroyView();
this.binding = null;
this.conversationsAdapter = null;
}
@Override
public void onDestroy() {
Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onDestroy()");
super.onDestroy();
@Override
public void onDestroy() {
Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onDestroy()");
super.onDestroy();
}
}
@Override
public void onPause() {
@ -236,7 +236,7 @@ public class ConversationsOverviewFragment extends XmppFragment {
return true;
}
return super.onOptionsItemSelected(item);
}
}
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {

View file

@ -37,7 +37,6 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_easy_invite);
setSupportActionBar((Toolbar) binding.toolbar);
configureActionBar(getSupportActionBar(), true);
this.binding.shareButton.setOnClickListener(v -> share());
if (bundle != null && bundle.containsKey("invite")) {
this.easyOnboardingInvite = bundle.getParcelable("invite");
if (this.easyOnboardingInvite != null) {
@ -100,7 +99,7 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn
final Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
final int width = Math.min(size.x, size.y);
final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableLink(), width);
final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableUri(), width);
binding.qrCode.setImageBitmap(bitmap);
}

View file

@ -1,5 +1,8 @@
package eu.siacs.conversations.ui;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.readGranted;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
@ -33,7 +36,6 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.textfield.TextInputLayout;
@ -86,9 +88,6 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
import me.drakeet.support.toast.ToastCompat;
import okhttp3.HttpUrl;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.readGranted;
public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
@ -428,6 +427,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
if (view.getId() == R.id.hostname) {
resId = mUseTor ? R.string.hostname_or_onion : R.string.hostname_example;
}
if (view.getId() == R.id.port) {
resId = R.string.port_example;
}
final int res = resId;
new Handler().postDelayed(() -> et.setHint(res), 500);
} else {
@ -648,6 +650,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener);
this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog());
this.binding.port.setText(String.valueOf(Resolver.DEFAULT_PORT_XMPP));
this.binding.port.setOnFocusChangeListener(mEditTextFocusListener);
this.binding.port.addTextChangedListener(mTextWatcher);
this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener);
this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener);
@ -1142,7 +1145,11 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
binding.accountPassword.getEditableText().append(this.mAccount.getPassword());
binding.accountPassword.setText(this.mAccount.getPassword());
this.binding.hostname.setText("");
this.binding.hostname.getEditableText().append(this.mAccount.getHostname());
if (this.mAccount.getHostname().length() > 0) {
this.binding.hostname.getEditableText().append(this.mAccount.getHostname());
} else {
this.binding.hostname.getEditableText().append(this.mAccount.getDomain());
}
this.binding.port.setText("");
this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort()));
this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
@ -1298,25 +1305,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} else {
this.binding.pgpFingerprintBox.setVisibility(View.GONE);
}
final String otrFingerprint = this.mAccount.getOtrFingerprint();
if (otrFingerprint != null && Config.supportOtr()) {
if ("otr".equals(messageFingerprint)) {
this.binding.otrFingerprintDesc.setTextColor(ContextCompat.getColor(this, R.color.accent));
}
this.binding.otrFingerprintBox.setVisibility(View.VISIBLE);
this.binding.otrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
this.binding.actionCopyToClipboard.setVisibility(View.VISIBLE);
this.binding.actionCopyToClipboard.setOnClickListener(v -> {
if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(otrFingerprint), R.string.otr_fingerprint)) {
ToastCompat.makeText(
EditAccountActivity.this,
R.string.toast_message_otr_fingerprint,
Toast.LENGTH_SHORT).show();
}
});
} else {
this.binding.otrFingerprintBox.setVisibility(View.GONE);
}
final String ownAxolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint();
if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);

View file

@ -138,7 +138,9 @@ public class ImportBackupActivity extends XmppActivity implements ServiceConnect
try {
final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
showEnterPasswordDialog(backupFile, finishOnCancel);
} catch (final IOException | IllegalArgumentException e) {
} catch (final IOException e) {
Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
} catch (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();
}

View file

@ -1,74 +1,289 @@
package eu.siacs.conversations.ui;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import android.view.MenuItem;
import androidx.core.app.ActivityCompat;
import androidx.annotation.BoolRes;
import androidx.annotation.NonNull;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration;
import org.osmdroid.config.IConfigurationProvider;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.CustomZoomButtonsController;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.CopyrightOverlay;
import org.osmdroid.views.overlay.Overlay;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.ui.util.LocationHelper;
import eu.siacs.conversations.ui.widget.Marker;
import eu.siacs.conversations.ui.widget.MyLocation;
import eu.siacs.conversations.utils.ThemeHelper;
public abstract class LocationActivity extends XmppActivity implements LocationListener {
private LocationManager locationManager;
public static final int REQUEST_LOCATION_PERMISSION = 0x682f41;
protected LocationManager locationManager;
protected boolean hasLocationFeature;
public static final int REQUEST_CODE_CREATE = 0;
public static final int REQUEST_CODE_FAB_PRESSED = 1;
public static final int REQUEST_CODE_SNACKBAR_PRESSED = 2;
protected static final String KEY_LOCATION = "loc";
protected static final String KEY_ZOOM_LEVEL = "zoom";
protected Location myLoc = null;
private MapView map = null;
protected IMapController mapController = null;
protected Bitmap marker_icon;
protected void clearMarkers() {
synchronized (this.map.getOverlays()) {
for (final Overlay overlay : this.map.getOverlays()) {
if (overlay instanceof Marker || overlay instanceof MyLocation) {
this.map.getOverlays().remove(overlay);
}
}
}
}
protected void updateLocationMarkers() {
clearMarkers();
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context ctx = getApplicationContext();
setTheme(ThemeHelper.find(this));
final PackageManager packageManager = ctx.getPackageManager();
hasLocationFeature = packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION) ||
packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS) ||
packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_NETWORK);
this.locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
this.marker_icon = BitmapFactory.decodeResource(ctx.getResources(), R.drawable.marker);
// Ask for location permissions if location services are enabled and we're
// just starting the activity (we don't want to keep pestering them on every
// screen rotation or if there's no point because it's disabled anyways).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && savedInstanceState == null) {
requestPermissions(REQUEST_CODE_CREATE);
}
final IConfigurationProvider config = Configuration.getInstance();
config.load(ctx, getPreferences());
//config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
config.setHttpProxy(HttpConnectionManager.getProxy());
}
}
protected abstract void gotoLoc() throws UnsupportedOperationException;
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
protected abstract void setmLastLocation(final Location location);
final IGeoPoint center = map.getMapCenter();
outState.putParcelable(KEY_LOCATION, new GeoPoint(
center.getLatitude(),
center.getLongitude()
));
outState.putDouble(KEY_ZOOM_LEVEL, map.getZoomLevelDouble());
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState.containsKey(KEY_LOCATION)) {
mapController.setCenter(savedInstanceState.getParcelable(KEY_LOCATION));
}
if (savedInstanceState.containsKey(KEY_ZOOM_LEVEL)) {
mapController.setZoom(savedInstanceState.getDouble(KEY_ZOOM_LEVEL));
}
}
protected void setupMapView(MapView mapView, final GeoPoint pos) {
map = mapView;
map.getOverlays().add(new CopyrightOverlay(this));
map.setTileSource(TileSourceFactory.MAPNIK);
map.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
map.setMultiTouchControls(true);
map.setTilesScaledToDpi(false);
mapController = map.getController();
mapController.setZoom(Config.Map.INITIAL_ZOOM_LEVEL);
mapController.setCenter(pos);
}
protected void gotoLoc() {
gotoLoc(map.getZoomLevelDouble() == Config.Map.INITIAL_ZOOM_LEVEL);
}
protected abstract void gotoLoc(final boolean setZoomLevel);
protected abstract void setMyLoc(final Location location);
protected void requestLocationUpdates() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
if (!hasLocationFeature || locationManager == null) {
return;
}
final Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (lastKnownLocation != null) {
setmLastLocation(lastKnownLocation);
try {
gotoLoc();
} catch (final UnsupportedOperationException ignored) {
Log.d(Config.LOGTAG, "Requesting location updates...");
final Location lastKnownLocationGps;
final Location lastKnownLocationNetwork;
try {
if (locationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER)) {
lastKnownLocationGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (lastKnownLocationGps != null) {
setMyLoc(lastKnownLocationGps);
}
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, Config.Map.LOCATION_FIX_TIME_DELTA,
Config.Map.LOCATION_FIX_SPACE_DELTA, this);
} else {
lastKnownLocationGps = null;
}
}
if (locationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER)
&& locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, Config.LOCATION_FIX_TIME_DELTA, Config.LOCATION_FIX_SPACE_DELTA, this);
}
if (locationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER)
&& locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, Config.LOCATION_FIX_TIME_DELTA, Config.LOCATION_FIX_SPACE_DELTA, this);
}
// If something else is also querying for location more frequently than we are, the battery is already being
// drained. Go ahead and use the existing locations as often as we can get them.
if (locationManager.getAllProviders().contains(LocationManager.PASSIVE_PROVIDER)) {
locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this);
if (locationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER)) {
lastKnownLocationNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
if (lastKnownLocationNetwork != null && LocationHelper.isBetterLocation(lastKnownLocationNetwork,
lastKnownLocationGps)) {
setMyLoc(lastKnownLocationNetwork);
}
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, Config.Map.LOCATION_FIX_TIME_DELTA,
Config.Map.LOCATION_FIX_SPACE_DELTA, this);
}
// If something else is also querying for location more frequently than we are, the battery is already being
// drained. Go ahead and use the existing locations as often as we can get them.
if (locationManager.getAllProviders().contains(LocationManager.PASSIVE_PROVIDER)) {
locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this);
}
} catch (final SecurityException ignored) {
// Do nothing if the users device has no location providers.
}
}
protected void pauseLocationUpdates() {
locationManager.removeUpdates(this);
protected void pauseLocationUpdates() throws SecurityException {
if (locationManager != null) {
locationManager.removeUpdates(this);
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onPause() {
super.onPause();
pauseLocationUpdates();
Configuration.getInstance().save(this, getPreferences());
map.onPause();
try {
pauseLocationUpdates();
} catch (final SecurityException ignored) {
}
}
protected abstract void updateUi();
protected boolean mapAtInitialLoc() {
return map.getZoomLevelDouble() == Config.Map.INITIAL_ZOOM_LEVEL;
}
@Override
public void onResume() {
super.onResume();
this.setmLastLocation(null);
if (hasLocationPermission(REQUEST_LOCATION_PERMISSION)) {
requestLocationUpdates();
Configuration.getInstance().load(this, getPreferences());
map.onResume();
this.setMyLoc(null);
requestLocationUpdates();
updateLocationMarkers();
updateUi();
map.setTileSource(TileSourceFactory.MAPNIK);
map.setTilesScaledToDpi(true);
if (mapAtInitialLoc()) {
gotoLoc();
}
}
}
@TargetApi(Build.VERSION_CODES.M)
protected boolean hasLocationPermissions() {
return (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED);
}
@TargetApi(Build.VERSION_CODES.M)
protected void requestPermissions(final int request_code) {
if (!hasLocationPermissions()) {
requestPermissions(
new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
},
request_code
);
}
}
@Override
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
for (int i = 0; i < grantResults.length; i++) {
if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[i]) ||
Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[i])) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
requestLocationUpdates();
}
}
}
}
public SharedPreferences getPreferences() {
return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
}
protected boolean getBooleanPreference(String name, @BoolRes int res) {
return getPreferences().getBoolean(name, getResources().getBoolean(res));
}
protected boolean isLocationEnabled() {
try {
final int locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE);
return locationMode != Settings.Secure.LOCATION_MODE_OFF;
} catch (final Settings.SettingNotFoundException e) {
return false;
}
}
}

View file

@ -5,37 +5,31 @@ import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.databinding.DataBindingUtil;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityMagicCreateBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.ProviderService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.InstallReferrerUtils;
import eu.siacs.conversations.xmpp.Jid;
public class MagicCreateActivity extends XmppActivity implements TextWatcher, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener {
private void setupHyperlink() {
TextView linkTextView = findViewById(R.id.activity_main_link);
linkTextView.setMovementMethod(LinkMovementMethod.getInstance());
}
private boolean useOwnProvider = false;
private boolean registerFromUri = false;
public static final String EXTRA_DOMAIN = "domain";
@ -86,10 +80,17 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher, Ad
}
super.onCreate(savedInstanceState);
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_magic_create);
final List<String> domains = Arrays.asList(getResources().getStringArray(R.array.domains));
final List<String> domains = ProviderService.getProviders();
Collections.sort(domains, String::compareToIgnoreCase);
final ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_selectable_list_item, domains);
int defaultServer = adapter.getPosition("monocles.de");
try {
if (new ProviderService().execute().get()) {
adapter.notifyDataSetChanged();
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
int defaultServer = adapter.getPosition(Config.DOMAIN.getRandomServer());
if (registerFromUri && !useOwnProvider && (this.preAuth != null || domain != null)) {
binding.server.setEnabled(false);
binding.server.setVisibility(View.GONE);
@ -188,7 +189,6 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher, Ad
}
});
binding.username.addTextChangedListener(this);
setupHyperlink();
}
private String updateDomain() {
@ -230,7 +230,7 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher, Ad
private void updateFullJidInformation(String username) {
if (useOwnProvider && !registerFromUri) {
this.domain = updateDomain();
} else if (!registerFromUri){
} else if (!registerFromUri) {
this.domain = binding.server.getSelectedItem().toString();
}
if (username.trim().isEmpty()) {

View file

@ -1,5 +1,8 @@
package eu.siacs.conversations.ui;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.readGranted;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
@ -35,9 +38,6 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import me.drakeet.support.toast.ToastCompat;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.readGranted;
public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState {
private final String STATE_SELECTED_ACCOUNT = "selected_account";
@ -212,6 +212,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0) {
if (allGranted(grantResults)) {
switch (requestCode) {

View file

@ -1,5 +1,10 @@
package eu.siacs.conversations.ui;
import static eu.siacs.conversations.persistance.FileBackend.AUDIOS;
import static eu.siacs.conversations.persistance.FileBackend.FILES;
import static eu.siacs.conversations.persistance.FileBackend.IMAGES;
import static eu.siacs.conversations.persistance.FileBackend.VIDEOS;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.ImageButton;
@ -14,31 +19,26 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.utils.UIHelper;
import static eu.siacs.conversations.persistance.FileBackend.AUDIOS;
import static eu.siacs.conversations.persistance.FileBackend.FILES;
import static eu.siacs.conversations.persistance.FileBackend.IMAGES;
import static eu.siacs.conversations.persistance.FileBackend.VIDEOS;
public class MemoryManagementActivity extends XmppActivity {
private TextView disk_storage;
private TextView media_usage;
private static TextView disk_storage;
private static TextView media_usage;
private ImageButton delete_media;
private TextView pictures_usage;
private static TextView pictures_usage;
private ImageButton delete_pictures;
private TextView videos_usage;
private static TextView videos_usage;
private ImageButton delete_videos;
private TextView files_usage;
private static TextView files_usage;
private ImageButton delete_files;
private TextView audios_usage;
private static TextView audios_usage;
private ImageButton delete_audios;
String totalMemory = "...";
String mediaUsage = "...";
String picturesUsage = "...";
String videosUsage = "...";
String filesUsage = "...";
String audiosUsage = "...";
static String totalMemory = "...";
static String mediaUsage = "...";
static String picturesUsage = "...";
static String videosUsage = "...";
static String filesUsage = "...";
static String audiosUsage = "...";
@Override
protected void refreshUiReal() {
@ -117,7 +117,7 @@ public class MemoryManagementActivity extends XmppActivity {
builder.create().show();
}
class getMemoryUsages extends AsyncTask<Void, Void, Void> {
static class getMemoryUsages extends AsyncTask<Void, Void, Void> {
@Override
protected void onPreExecute() {

View file

@ -114,7 +114,7 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ
@Override
public void onAffiliationChangeFailed(Jid jid, int resId) {
displayToast(getString(resId, jid.asBareJid().toEscapedString()));
displayToast(getString(resId, jid.asBareJid().toString()));
}
@Override

View file

@ -252,4 +252,4 @@ public class RecordingActivity extends AppCompatActivity implements View.OnClick
});
builder.create().show();
}
}
}

View file

@ -2,7 +2,6 @@ package eu.siacs.conversations.ui;
import static java.util.Arrays.asList;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
import eu.siacs.conversations.ui.util.Rationals;
import android.Manifest;
import android.annotation.SuppressLint;
@ -37,6 +36,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import org.webrtc.RendererCommon;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoTrack;
@ -56,6 +56,7 @@ import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.ui.util.Rationals;
import eu.siacs.conversations.utils.Namespace;
import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
@ -67,7 +68,6 @@ import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import me.drakeet.support.toast.ToastCompat;
public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
public static final String EXTRA_WITH = "with";
@ -80,8 +80,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
private static final int CALL_DURATION_UPDATE_INTERVAL = 333;
private boolean shouldAllowBack = false;
private static final List<RtpEndUserState> END_CARD = Arrays.asList(
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.SECURITY_ERROR,
@ -97,7 +95,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
);
private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
RtpEndUserState.CONNECTING,
RtpEndUserState.CONNECTED
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING
);
private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED = Arrays.asList(
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING
);
private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList(
RtpEndUserState.ACCEPTING_CALL,
RtpEndUserState.CONNECTING,
RtpEndUserState.RECONNECTING
);
private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
private static final int REQUEST_ACCEPT_CALL = 0x1111;
@ -484,10 +492,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
return;
}
} else {
if (shouldAllowBack) {
super.onBackPressed();
}
endCall();
}
super.onBackPressed();
}
@Override
@ -504,7 +511,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
private boolean isConnected() {
final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED;
return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
}
private boolean switchToPictureInPicture() {
@ -533,6 +540,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e);
}
}
@Override
public void onAspectRatioChanged(final Rational rational) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) {
@ -636,8 +644,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
surfaceViewRenderer.setVisibility(View.VISIBLE);
try {
surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
} catch (IllegalStateException e) {
Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
} catch (final IllegalStateException e) {
//Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
}
surfaceViewRenderer.setEnableHardwareScaler(true);
}
@ -649,7 +657,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media) {
switch (state) {
case INCOMING_CALL:
shouldAllowBack = false;
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
if (media.contains(Media.VIDEO)) {
setTitle(R.string.rtp_state_incoming_video_call);
@ -658,53 +665,45 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
}
break;
case CONNECTING:
shouldAllowBack = false;
setTitle(R.string.rtp_state_connecting);
break;
case CONNECTED:
shouldAllowBack = false;
setTitle(R.string.rtp_state_connected);
break;
case RECONNECTING:
setTitle(R.string.rtp_state_reconnecting);
break;
case ACCEPTING_CALL:
shouldAllowBack = false;
setTitle(R.string.rtp_state_accepting_call);
break;
case ENDING_CALL:
shouldAllowBack = false;
setTitle(R.string.rtp_state_ending_call);
break;
case FINDING_DEVICE:
shouldAllowBack = false;
setTitle(R.string.rtp_state_finding_device);
break;
case RINGING:
shouldAllowBack = false;
setTitle(R.string.rtp_state_ringing);
break;
case DECLINED_OR_BUSY:
shouldAllowBack = true;
setTitle(R.string.rtp_state_declined_or_busy);
break;
case CONNECTIVITY_ERROR:
shouldAllowBack = true;
setTitle(R.string.rtp_state_connectivity_error);
break;
case CONNECTIVITY_LOST_ERROR:
setTitle(R.string.rtp_state_connectivity_lost_error);
break;
case RETRACTED:
shouldAllowBack = false;
setTitle(R.string.rtp_state_retracted);
break;
case APPLICATION_ERROR:
shouldAllowBack = true;
setTitle(R.string.rtp_state_application_failure);
break;
case SECURITY_ERROR:
setTitle(R.string.rtp_state_security_error);
break;
case ENDED:
shouldAllowBack = true;
throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
default:
throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
@ -816,7 +815,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
@SuppressLint("RestrictedApi")
private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) {
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
if (media.contains(Media.VIDEO)) {
final JingleRtpConnection rtpConnection = requireRtpConnection();
@ -944,14 +943,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
this.binding.duration.setVisibility(View.GONE);
return;
}
final long rtpConnectionStarted = connection.getRtpConnectionStarted();
final long rtpConnectionEnded = connection.getRtpConnectionEnded();
if (rtpConnectionStarted != 0) {
final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded;
this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false));
this.binding.duration.setVisibility(View.VISIBLE);
} else {
if (connection.zeroDuration()) {
this.binding.duration.setVisibility(View.GONE);
} else {
this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
this.binding.duration.setVisibility(View.VISIBLE);
}
}
@ -959,17 +955,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
binding.localVideo.setVisibility(View.GONE);
binding.localVideo.release();
binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE);
binding.remoteVideo.release();
binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
if (isPictureInPicture()) {
binding.appBarLayout.setVisibility(View.GONE);
binding.pipPlaceholder.setVisibility(View.VISIBLE);
if (Arrays.asList(
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.SECURITY_ERROR)
.contains(state)) {
binding.appBarLayout.setVisibility(View.GONE);
binding.pipPlaceholder.setVisibility(View.VISIBLE);
binding.pipWarning.setVisibility(View.VISIBLE);
binding.pipWaiting.setVisibility(View.GONE);
} else {
@ -983,9 +979,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
return;
}
if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
binding.localVideo.setVisibility(View.GONE);
binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE);
binding.appBarLayout.setVisibility(View.GONE);
binding.pipPlaceholder.setVisibility(View.VISIBLE);
binding.pipWarning.setVisibility(View.GONE);
@ -1007,12 +1003,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
if (remoteVideoTrack.isPresent()) {
ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
addSink(remoteVideoTrack.get(), binding.remoteVideo);
binding.remoteVideo.setScalingType(
RendererCommon.ScalingType.SCALE_ASPECT_FILL,
RendererCommon.ScalingType.SCALE_ASPECT_FIT
);
if (state == RtpEndUserState.CONNECTED) {
binding.appBarLayout.setVisibility(View.GONE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
} else {
binding.appBarLayout.setVisibility(View.VISIBLE);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE);
}
if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
@ -1021,7 +1023,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
}
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE);
binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
}
}

View file

@ -76,11 +76,11 @@ public final class ScanActivity extends Activity implements SurfaceTextureListen
private static final long AUTO_FOCUS_INTERVAL_MS = 2500L;
private static boolean DISABLE_CONTINUOUS_AUTOFOCUS =
Build.MODEL.equals("GT-I9100") // Galaxy S2
|| Build.MODEL.equals("SGH-T989") // Galaxy S2
|| Build.MODEL.equals("SGH-T989D") // Galaxy S2 X
|| Build.MODEL.equals("SAMSUNG-SGH-I727") // Galaxy S2 Skyrocket
|| Build.MODEL.equals("GT-I9300") // Galaxy S3
|| Build.MODEL.equals("GT-N7000"); // Galaxy Note
|| Build.MODEL.equals("SGH-T989") // Galaxy S2
|| Build.MODEL.equals("SGH-T989D") // Galaxy S2 X
|| Build.MODEL.equals("SAMSUNG-SGH-I727") // Galaxy S2 Skyrocket
|| Build.MODEL.equals("GT-I9300") // Galaxy S3
|| Build.MODEL.equals("GT-N7000"); // Galaxy Note
private final CameraManager cameraManager = new CameraManager();
private ScannerView scannerView;
private TextureView previewView;

View file

@ -78,20 +78,20 @@ import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard;
public class SearchActivity extends XmppActivity implements TextWatcher, OnSearchResultsAvailable, MessageAdapter.OnContactPictureClicked {
private static final String EXTRA_SEARCH_TERM = "search-term";
public static final String EXTRA_CONVERSATION_UUID = "uuid";
public static final String EXTRA_CONVERSATION_UUID = "uuid";
private ActivitySearchBinding binding;
private MessageAdapter messageListAdapter;
private final List<Message> messages = new ArrayList<>();
private WeakReference<Message> selectedMessageReference = new WeakReference<>(null);
private String uuid;
private String uuid;
private final ChangeWatcher<List<String>> currentSearch = new ChangeWatcher<>();
private final PendingItem<String> pendingSearchTerm = new PendingItem<>();
private final PendingItem<List<String>> pendingSearch = new PendingItem<>();
@Override
public void onCreate(final Bundle bundle) {
final Intent intent = getIntent();
this.uuid = intent == null ? null : Strings.emptyToNull(intent.getStringExtra(EXTRA_CONVERSATION_UUID));
final Intent intent = getIntent();
this.uuid = intent == null ? null : Strings.emptyToNull(intent.getStringExtra(EXTRA_CONVERSATION_UUID));
final String searchTerm = bundle == null ? null : bundle.getString(EXTRA_SEARCH_TERM);
if (searchTerm != null) {
pendingSearchTerm.push(searchTerm);
@ -114,10 +114,10 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
final String term = pendingSearchTerm.pop();
if (term != null) {
searchField.append(term);
final List<String> searchTerm = FtsUtils.parse(term);
final List<String> searchTerm = FtsUtils.parse(term);
if (xmppConnectionService != null) {
if (currentSearch.watch(searchTerm)) {
xmppConnectionService.search(searchTerm, uuid, this);
xmppConnectionService.search(searchTerm, uuid, this);
}
} else {
pendingSearch.push(searchTerm);
@ -125,30 +125,30 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
}
searchField.addTextChangedListener(this);
searchField.setHint(R.string.search_messages);
searchField.setContentDescription(getString(R.string.search_messages));
searchField.setContentDescription(getString(R.string.search_messages));
searchField.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
showKeyboard(searchField);
return super.onCreateOptionsMenu(menu);
}
@Override
public void onCreateContextMenu(final ContextMenu menu, final View v, ContextMenu.ContextMenuInfo menuInfo) {
v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
final Message message = this.messages.get(acmi.position);
this.selectedMessageReference = new WeakReference<>(message);
getMenuInflater().inflate(R.menu.search_result_context, menu);
MenuItem copy = menu.findItem(R.id.copy_message);
MenuItem quote = menu.findItem(R.id.quote_message);
MenuItem copyUrl = menu.findItem(R.id.copy_url);
if (message.isGeoUri()) {
copy.setVisible(false);
quote.setVisible(false);
} else {
copyUrl.setVisible(false);
}
super.onCreateContextMenu(menu, v, menuInfo);
}
@Override
public void onCreateContextMenu(final ContextMenu menu, final View v, ContextMenu.ContextMenuInfo menuInfo) {
v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
final Message message = this.messages.get(acmi.position);
this.selectedMessageReference = new WeakReference<>(message);
getMenuInflater().inflate(R.menu.search_result_context, menu);
MenuItem copy = menu.findItem(R.id.copy_message);
MenuItem quote = menu.findItem(R.id.quote_message);
MenuItem copyUrl = menu.findItem(R.id.copy_url);
if (message.isGeoUri()) {
copy.setVisible(false);
quote.setVisible(false);
} else {
copyUrl.setVisible(false);
}
super.onCreateContextMenu(menu, v, menuInfo);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
@ -220,7 +220,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
void onBackendConnected() {
final List<String> searchTerm = pendingSearch.pop();
if (searchTerm != null && currentSearch.watch(searchTerm)) {
xmppConnectionService.search(searchTerm, uuid,this);
xmppConnectionService.search(searchTerm, uuid,this);
}
}
@ -253,7 +253,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
return;
}
if (term.size() > 0) {
xmppConnectionService.search(term, uuid,this);
xmppConnectionService.search(term, uuid,this);
} else {
MessageSearchTask.cancelRunningTasks();
this.messages.clear();

View file

@ -1,5 +1,15 @@
package eu.siacs.conversations.ui;
import static eu.siacs.conversations.ui.SettingsActivity.BROADCAST_LAST_ACTIVITY;
import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES;
import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES;
import static eu.siacs.conversations.ui.SettingsActivity.EASY_DOWNLOADER;
import static eu.siacs.conversations.ui.SettingsActivity.FORBID_SCREENSHOTS;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE;
import static eu.siacs.conversations.ui.SettingsActivity.USE_INNER_STORAGE;
import static eu.siacs.conversations.ui.SettingsActivity.USE_INVIDIOUS;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
@ -15,19 +25,12 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivitySetSettingsBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.FirstStartManager;
import eu.siacs.conversations.utils.ThemeHelper;
import static eu.siacs.conversations.ui.SettingsActivity.BROADCAST_LAST_ACTIVITY;
import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES;
import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES;
import static eu.siacs.conversations.ui.SettingsActivity.FORBID_SCREENSHOTS;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE;
import static eu.siacs.conversations.ui.SettingsActivity.USE_INVIDIOUS;
public class SetSettingsActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate {
ActivitySetSettingsBinding binding;
Account account;
@ -38,6 +41,8 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS
static final int CONFIRMMESSAGES = 5;
static final int LASTSEEN = 6;
static final int INVIDIOUS = 7;
static final int INNER_STORAGE = 8;
static final int EASY_DOWNLOADER_ATTACHMENTS = 9;
@Override
protected void refreshUiReal() {
@ -69,6 +74,8 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS
this.binding.actionInfoConfirmMessages.setOnClickListener(string -> showInfo(CONFIRMMESSAGES));
this.binding.actionInfoLastSeen.setOnClickListener(string -> showInfo(LASTSEEN));
this.binding.actionInfoInvidious.setOnClickListener(string -> showInfo(INVIDIOUS));
this.binding.actionInfoUsingInnerStorage.setOnClickListener(string -> showInfo(INNER_STORAGE));
this.binding.actionInfoEasyDownloader.setOnClickListener(string -> showInfo(EASY_DOWNLOADER_ATTACHMENTS));
}
private void getDefaults() {
@ -79,6 +86,8 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS
this.binding.confirmMessages.setChecked(getResources().getBoolean(R.bool.confirm_messages));
this.binding.lastSeen.setChecked(getResources().getBoolean(R.bool.last_activity));
this.binding.invidious.setChecked(getResources().getBoolean(R.bool.use_invidious));
this.binding.usingInnerStorage.setChecked(getResources().getBoolean(R.bool.use_inner_storage));
this.binding.easyDownloader.setChecked(getResources().getBoolean(R.bool.easy_downloader));
}
private void next(View view) {
@ -96,7 +105,6 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS
}
private void showInfo(int setting) {
Log.d(Config.LOGTAG, "STRING " + setting);
String title;
String message;
switch (setting) {
@ -128,6 +136,14 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS
title = getString(R.string.pref_use_invidious);
message = getString(R.string.pref_use_invidious_summary);
break;
case INNER_STORAGE:
title = getString(R.string.pref_use_inner_storage);
message = getString(R.string.pref_use_inner_storage_summary);
break;
case EASY_DOWNLOADER_ATTACHMENTS:
title = getString(R.string.pref_easy_downloader);
message = getString(R.string.pref_easy_downloader_summary);
break;
default:
title = getString(R.string.error);
message = getString(R.string.error);
@ -143,41 +159,16 @@ public class SetSettingsActivity extends XmppActivity implements XmppConnectionS
private void setSettings() {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
if (this.binding.forbidScreenshots.isChecked()) {
preferences.edit().putBoolean(FORBID_SCREENSHOTS, true).apply();
} else {
preferences.edit().putBoolean(FORBID_SCREENSHOTS, false).apply();
}
if (this.binding.showLinks.isChecked()) {
preferences.edit().putBoolean(SHOW_LINKS_INSIDE, true).apply();
} else {
preferences.edit().putBoolean(SHOW_LINKS_INSIDE, false).apply();
}
if (this.binding.showMappreview.isChecked()) {
preferences.edit().putBoolean(SHOW_MAPS_INSIDE, true).apply();
} else {
preferences.edit().putBoolean(SHOW_MAPS_INSIDE, false).apply();
}
if (this.binding.chatStates.isChecked()) {
preferences.edit().putBoolean(CHAT_STATES, true).apply();
} else {
preferences.edit().putBoolean(CHAT_STATES, false).apply();
}
if (this.binding.confirmMessages.isChecked()) {
preferences.edit().putBoolean(CONFIRM_MESSAGES, true).apply();
} else {
preferences.edit().putBoolean(CONFIRM_MESSAGES, false).apply();
}
if (this.binding.lastSeen.isChecked()) {
preferences.edit().putBoolean(BROADCAST_LAST_ACTIVITY, true).apply();
} else {
preferences.edit().putBoolean(BROADCAST_LAST_ACTIVITY, false).apply();
}
if (this.binding.invidious.isChecked()) {
preferences.edit().putBoolean(USE_INVIDIOUS, true).apply();
} else {
preferences.edit().putBoolean(USE_INVIDIOUS, false).apply();
}
preferences.edit().putBoolean(FORBID_SCREENSHOTS, this.binding.forbidScreenshots.isChecked()).apply();
preferences.edit().putBoolean(SHOW_LINKS_INSIDE, this.binding.showLinks.isChecked()).apply();
preferences.edit().putBoolean(SHOW_MAPS_INSIDE, this.binding.showMappreview.isChecked()).apply();
preferences.edit().putBoolean(CHAT_STATES, this.binding.chatStates.isChecked()).apply();
preferences.edit().putBoolean(CONFIRM_MESSAGES, this.binding.confirmMessages.isChecked()).apply();
preferences.edit().putBoolean(BROADCAST_LAST_ACTIVITY, this.binding.lastSeen.isChecked()).apply();
preferences.edit().putBoolean(USE_INVIDIOUS, this.binding.invidious.isChecked()).apply();
preferences.edit().putBoolean(USE_INNER_STORAGE, this.binding.usingInnerStorage.isChecked()).apply();
preferences.edit().putBoolean(EASY_DOWNLOADER, this.binding.easyDownloader.isChecked()).apply();
FileBackend.switchStorage(xmppConnectionService.usingInnerStorage());
}
@Override

View file

@ -16,11 +16,6 @@ import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.Log;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
@ -86,16 +81,16 @@ public class SettingsActivity extends XmppActivity implements
public static final String CONFIRM_MESSAGES = "confirm_messages";
public static final String INDICATE_RECEIVED = "indicate_received";
public static final String USE_INVIDIOUS = "use_invidious";
public static final String USE_INNER_STORAGE = "use_inner_storage";
public static final String INVIDIOUS_HOST = "invidious_host";
public static final String MAPPREVIEW_HOST = "mappreview_host";
public static final String ALLOW_MESSAGE_CORRECTION = "allow_message_correction";
public static final String ALLOW_MESSAGE_RETRACTION = "allow_message_retraction";
public static final String USE_UNICOLORED_CHATBG = "unicolored_chatbg";
public static final String EASY_DOWNLOADER = "easy_downloader";
public static final String MIN_ANDROID_SDK21_SHOWN = "min_android_sdk21_shown";
public static final String INDIVIDUAL_NOTIFICATION_PREFIX = "individual_notification_set_";
public static final String PAUSE_VOICE = "pause_voice_on_move_from_ear";
public static final String ENABLE_OTR_ENCRYPTION = "enable_otr_encryption";
public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
public static final int REQUEST_IMPORT_SETTINGS = 0xbf8702;
@ -194,7 +189,7 @@ public class SettingsActivity extends XmppActivity implements
if (choices[i] == 0) {
entries[i] = getString(R.string.never);
} else {
entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]);
entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]);
}
}
automaticMessageDeletionList.setEntries(entries);
@ -564,6 +559,7 @@ public class SettingsActivity extends XmppActivity implements
TREAT_VIBRATE_AS_SILENT,
MANUALLY_CHANGE_PRESENCE,
BROADCAST_LAST_ACTIVITY);
FileBackend.switchStorage(preferences.getBoolean(USE_INNER_STORAGE, true));
if (name.equals(OMEMO_SETTING)) {
OmemoSetting.load(this, preferences);
changeOmemoSettingSummary();
@ -594,7 +590,6 @@ public class SettingsActivity extends XmppActivity implements
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (requestCode == REQUEST_CREATE_BACKUP) {
@ -613,10 +608,10 @@ public class SettingsActivity extends XmppActivity implements
final Intent intent = new Intent(this, ExportBackupService.class);
intent.putExtra("NOTIFY_ON_BACKUP_COMPLETE", notify);
ContextCompat.startForegroundService(this, intent);
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.backup_started_message);
builder.setPositiveButton(R.string.ok, null);
builder.create().show();
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.backup_started_message);
builder.setPositiveButton(R.string.ok, null);
builder.create().show();
}
@SuppressWarnings({ "unchecked" })
@ -661,6 +656,11 @@ public class SettingsActivity extends XmppActivity implements
ex.printStackTrace();
}
}
if (success) {
ToastCompat.makeText(this, R.string.success_import_settings, ToastCompat.LENGTH_SHORT).show();
} else {
ToastCompat.makeText(this, R.string.error_import_settings, ToastCompat.LENGTH_SHORT).show();
}
return success;
}

View file

@ -1,67 +1,63 @@
package eu.siacs.conversations.ui;
import android.annotation.TargetApi;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.webkit.WebView;
import android.widget.Button;
import android.text.Html;
import android.view.View;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.snackbar.Snackbar;
import com.google.common.math.DoubleMath;
import org.jetbrains.annotations.Nullable;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.util.GeoPoint;
import java.lang.ref.WeakReference;
import java.math.RoundingMode;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.utils.LocationHelper;
import eu.siacs.conversations.databinding.ActivityShareLocactionBinding;
import eu.siacs.conversations.ui.util.LocationHelper;
import eu.siacs.conversations.ui.widget.Marker;
import eu.siacs.conversations.ui.widget.MyLocation;
import eu.siacs.conversations.utils.LocationProvider;
import eu.siacs.conversations.utils.ThemeHelper;
import me.drakeet.support.toast.ToastCompat;
public class ShareLocationActivity extends LocationActivity implements LocationListener {
LocationManager locationManager;
private Location mLastLocation;
private Button mCancelButton;
private Button mShareButton;
private String mLocationName;
private Snackbar snackBar;
private ActivityShareLocactionBinding binding;
private boolean marker_fixed_to_loc = false;
private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
private Boolean noAskAgain = false;
private static String getAddress(Context context, Location location) {
double longitude = location.getLongitude();
double latitude = location.getLatitude();
String address = "";
if (latitude != 0 && longitude != 0) {
try {
Geocoder geoCoder = new Geocoder(context, Locale.getDefault());
List<Address> addresses = geoCoder.getFromLocation(latitude, longitude, 1);
if (addresses != null && addresses.size() > 0) {
Address Address = addresses.get(0);
StringBuilder strAddress = new StringBuilder("");
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
}
if (Address.getAddressLine(0).length() > 0) {
strAddress.append(Address.getAddressLine(0));
}
address = strAddress.toString().replace(", ", "<br>");
}
} catch (Exception e) {
e.printStackTrace();
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
}
return address;
}
@Override
@ -77,62 +73,99 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_locaction);
setTitle(getString(R.string.share_location));
setSupportActionBar(findViewById(R.id.toolbar));
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_locaction);
setSupportActionBar((Toolbar) binding.toolbar);
configureActionBar(getSupportActionBar());
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
setupMapView(binding.map, LocationProvider.getGeoPoint(this));
mLocationName = getString(R.string.me);
mCancelButton = findViewById(R.id.cancel_button);
mCancelButton.setOnClickListener(view -> {
this.binding.cancelButton.setOnClickListener(view -> {
setResult(RESULT_CANCELED);
finish();
});
mShareButton = findViewById(R.id.share_button);
mShareButton.setOnClickListener(view -> {
if (mLastLocation != null) {
Intent result = new Intent();
result.putExtra("latitude", mLastLocation.getLatitude());
result.putExtra("longitude", mLastLocation.getLongitude());
result.putExtra("altitude", mLastLocation.getAltitude());
result.putExtra("accuracy", (int) mLastLocation.getAccuracy());
setResult(RESULT_OK, result);
finish();
this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_sharing_disabled, Snackbar.LENGTH_INDEFINITE);
this.snackBar.setAction(R.string.enable, view -> {
if (isLocationEnabledAndAllowed()) {
updateUi();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
} else if (!isLocationEnabled()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
});
ThemeHelper.fix(this.snackBar);
final CoordinatorLayout snackBarCoordinator = findViewById(R.id.snackbarCoordinator);
if (snackBarCoordinator != null) {
this.snackBar = Snackbar.make(snackBarCoordinator, R.string.location_sharing_disabled, Snackbar.LENGTH_INDEFINITE);
snackBar.setAction(R.string.enable, view -> {
showLocation(null, "");
if (isLocationEnabled()) {
if (hasLocationPermission(LocationActivity.REQUEST_LOCATION_PERMISSION)) {
requestLocationUpdates();
}
} else {
setShareButtonEnabled(false);
if (hasLocationPermission(LocationActivity.REQUEST_LOCATION_PERMISSION)) {
requestLocationUpdates();
}
this.binding.shareButton.setOnClickListener(this::shareLocation);
this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
this.binding.fab.setOnClickListener(view -> {
if (!marker_fixed_to_loc) {
if (!isLocationEnabled()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(REQUEST_CODE_FAB_PRESSED);
}
});
ThemeHelper.fix(this.snackBar);
}
toggleFixedLocation();
});
}
private void shareLocation(final View view) {
final Intent result = new Intent();
if (marker_fixed_to_loc && myLoc != null) {
result.putExtra("latitude", myLoc.getLatitude());
result.putExtra("longitude", myLoc.getLongitude());
result.putExtra("altitude", myLoc.getAltitude());
result.putExtra("accuracy", DoubleMath.roundToInt(myLoc.getAccuracy(), RoundingMode.HALF_UP));
} else {
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
result.putExtra("latitude", markerPoint.getLatitude());
result.putExtra("longitude", markerPoint.getLongitude());
}
setResult(RESULT_OK, result);
finish();
}
@Override
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0 &&
grantResults[0] != PackageManager.PERMISSION_GRANTED &&
Build.VERSION.SDK_INT >= 23 &&
permissions.length > 0 &&
(
Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) ||
Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) ||
Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
) &&
!shouldShowRequestPermissionRationale(permissions[0])) {
noAskAgain = true;
}
if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
updateUi();
}
@Override
protected void gotoLoc(final boolean setZoomLevel) {
if (this.myLoc != null && mapController != null) {
if (setZoomLevel) {
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
}
mapController.animateTo(new GeoPoint(this.myLoc));
}
}
@Override
protected void gotoLoc() throws UnsupportedOperationException {
new getAddressAsync(this).execute();
}
@Override
protected void setmLastLocation(Location location) {
this.mLastLocation = location;
protected void setMyLoc(final Location location) {
this.myLoc = location;
}
@Override
@ -141,23 +174,59 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
}
@Override
public void onResume() {
super.onResume();
if (isLocationEnabled()) {
this.snackBar.dismiss();
showLocation(null, "");
protected void updateLocationMarkers() {
super.updateLocationMarkers();
if (this.myLoc != null) {
this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
if (this.marker_fixed_to_loc) {
this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
new getAddressAsync(this, this.myLoc).execute();
} else {
this.binding.map.getOverlays().add(new Marker(marker_icon));
new getAddressAsync(this, getMarkerPosition()).execute();
}
} else {
this.snackBar.show();
this.binding.map.getOverlays().add(new Marker(marker_icon));
hideAddress();
}
setShareButtonEnabled(false);
}
private Location getMarkerPosition() {
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
final Location location = new Location("");
location.setLatitude(markerPoint.getLatitude());
location.setLongitude(markerPoint.getLongitude());
return location;
}
private void showAddress(final Location myLoc) {
this.binding.address.setText(Html.fromHtml(getAddress(this, myLoc)));
if (Html.fromHtml(getAddress(this, myLoc)).length() > 0) {
this.binding.address.setVisibility(View.VISIBLE);
} else {
hideAddress();
}
}
private void hideAddress() {
this.binding.address.setVisibility(View.GONE);
}
@Override
public void onLocationChanged(final Location location) {
if (LocationHelper.isBetterLocation(location, this.mLastLocation)) {
setShareButtonEnabled(true);
this.mLastLocation = 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;
// Don't jump back to the users location if they're not moving (more or less).
if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) {
gotoLoc();
}
updateLocationMarkers();
}
}
@ -176,91 +245,92 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private boolean isLocationEnabledKitkat() {
try {
final int locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE);
return locationMode != Settings.Secure.LOCATION_MODE_OFF;
} catch (final Settings.SettingNotFoundException e) {
return false;
private boolean isLocationEnabledAndAllowed() {
return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
}
private void toggleFixedLocation() {
this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
if (this.marker_fixed_to_loc) {
gotoLoc(false);
}
updateLocationMarkers();
updateUi();
}
@SuppressWarnings("deprecation")
private boolean isLocationEnabledLegacy() {
final String locationProviders = Settings.Secure.getString(getContentResolver(),
Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
return !TextUtils.isEmpty(locationProviders);
}
private boolean isLocationEnabled() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return isLocationEnabledKitkat();
@Override
protected void updateUi() {
if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
this.snackBar.dismiss();
} else {
return isLocationEnabledLegacy();
this.snackBar.show();
}
}
private void setShareButtonEnabled(final boolean enabled) {
if (enabled) {
this.mShareButton.setEnabled(true);
this.mShareButton.setText(R.string.share);
if (isLocationEnabledAndAllowed()) {
this.binding.fab.setVisibility(View.VISIBLE);
runOnUiThread(() -> {
this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
R.drawable.ic_gps_not_fixed_white_24dp);
this.binding.fab.setContentDescription(getResources().getString(
marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
));
this.binding.fab.invalidate();
});
} else {
this.mShareButton.setEnabled(false);
this.mShareButton.setText(R.string.locating);
this.binding.fab.setVisibility(View.GONE);
}
}
private void showLocation(@Nullable Location location, String address) {
try {
if (location == null && TextUtils.isEmpty(address)) { // no location and no address available
final WebView webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl("file:///android_asset/map.html");
} else if (location != null && TextUtils.isEmpty(address)) { // location but no address available
String LocationName = "<b>" + mLocationName + "</b>";
final WebView webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl("javascript:toCoordinates(" + mLastLocation.getLatitude() + "," + mLastLocation.getLongitude() + "," + "'" + LocationName + "'" + ");");
} else if (location != null && !TextUtils.isEmpty(address)) { // location and address available
String LocationName = "<b>" + mLocationName + "</b><br>" + address;
final WebView webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl("javascript:toCoordinates(" + mLastLocation.getLatitude() + "," + mLastLocation.getLongitude() + "," + "'" + LocationName + "'" + ");");
private static String getAddress(final Context context, final Location location) {
final double longitude = location.getLongitude();
final double latitude = location.getLatitude();
String address = "";
if (latitude != 0 && longitude != 0) {
try {
final Geocoder geoCoder = new Geocoder(context, Locale.getDefault());
final List<Address> addresses = geoCoder.getFromLocation(latitude, longitude, 1);
if (addresses != null && addresses.size() > 0) {
final Address Address = addresses.get(0);
StringBuilder strAddress = new StringBuilder("");
if (Address.getAddressLine(0).length() > 0) {
strAddress.append(Address.getAddressLine(0));
}
address = strAddress.toString().replace(", ", "<br>");
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
ToastCompat.makeText(this, R.string.error, ToastCompat.LENGTH_LONG);
}
return address;
}
private class getAddressAsync extends AsyncTask<Void, Void, Void> {
String address = null;
Location location;
private WeakReference<ShareLocationActivity> activityReference;
getAddressAsync(ShareLocationActivity context) {
getAddressAsync(final ShareLocationActivity context, final Location location) {
activityReference = new WeakReference<>(context);
this.location = location;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
showLocation(mLastLocation, "");
}
@Override
protected Void doInBackground(Void... params) {
if (mLastLocation != null) {
address = getAddress(ShareLocationActivity.this, mLastLocation);
}
address = getAddress(ShareLocationActivity.this, this.location);
return null;
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
showLocation(mLastLocation, address);
showAddress(this.location);
}
}
}
}

View file

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

View file

@ -1,24 +1,28 @@
package eu.siacs.conversations.ui;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.text.Html;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.webkit.WebView;
import android.view.View;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.databinding.DataBindingUtil;
import org.osmdroid.util.GeoPoint;
import java.lang.ref.WeakReference;
import java.util.List;
@ -26,79 +30,107 @@ import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.utils.MenuDoubleTabUtil;
import eu.siacs.conversations.databinding.ActivityShowLocationBinding;
import eu.siacs.conversations.ui.util.LocationHelper;
import eu.siacs.conversations.ui.widget.Marker;
import eu.siacs.conversations.ui.widget.MyLocation;
import eu.siacs.conversations.utils.LocationProvider;
import me.drakeet.support.toast.ToastCompat;
public class ShowLocationActivity extends XmppActivity {
FloatingActionButton fab;
private Location location;
private String mLocationName;
private static String getAddress(Context context, Location location) {
double longitude = location.getLongitude();
double latitude = location.getLatitude();
String address = "";
if (latitude != 0 && longitude != 0) {
try {
Geocoder geoCoder = new Geocoder(context, Locale.getDefault());
List<Address> addresses = geoCoder.getFromLocation(latitude, longitude, 1);
if (addresses != null && addresses.size() > 0) {
Address Address = addresses.get(0);
StringBuilder strAddress = new StringBuilder("");
public class ShowLocationActivity extends LocationActivity implements LocationListener {
if (Address.getAddressLine(0).length() > 0) {
strAddress.append(Address.getAddressLine(0));
}
address = strAddress.toString().replace(", ", "<br>");
}
} catch (Exception e) {
e.printStackTrace();
}
}
return address;
private GeoPoint loc = LocationProvider.FALLBACK;
private ActivityShowLocationBinding binding;
private String name;
private Uri createGeoUri() {
return Uri.parse("geo:" + this.loc.getLatitude() + "," + this.loc.getLongitude());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_show_locaction);
setTitle(getString(R.string.show_location));
setSupportActionBar(findViewById(R.id.toolbar));
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_show_location);
setSupportActionBar((Toolbar) binding.toolbar);
configureActionBar(getSupportActionBar());
showLocation(null, "");
Intent intent = getIntent();
setupMapView(this.binding.map, this.loc);
this.mLocationName = intent != null ? intent.getStringExtra("name") : null;
this.binding.fab.setOnClickListener(view -> startNavigation());
if (intent != null && intent.hasExtra("longitude") && intent.hasExtra("latitude")) {
double longitude = intent.getDoubleExtra("longitude", 0);
double latitude = intent.getDoubleExtra("latitude", 0);
this.location = new Location("");
this.location.setLatitude(latitude);
this.location.setLongitude(longitude);
Log.d(Config.LOGTAG, "Location: lat: " + latitude + " long: " + longitude);
markAndCenterOnLocation(this.location);
fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
navigate(this.location);
});
final Intent intent = getIntent();
if (intent != null) {
this.name = intent.hasExtra("name") ? intent.getStringExtra("name") : null;
if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) {
final double longitude = intent.getDoubleExtra("longitude", 0);
final double latitude = intent.getDoubleExtra("latitude", 0);
this.loc = new GeoPoint(latitude, longitude);
}
}
updateLocationMarkers();
}
@Override
protected void gotoLoc(final boolean setZoomLevel) {
if (this.loc != null && mapController != null) {
if (setZoomLevel) {
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
}
mapController.animateTo(new GeoPoint(this.loc));
}
}
private void markAndCenterOnLocation(final Location location) {
if (location == null) {
Log.d(Config.LOGTAG, "No location given");
return;
@Override
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
updateUi();
}
@Override
protected void setMyLoc(final Location location) {
this.myLoc = location;
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_show_location, menu);
updateUi();
return true;
}
@Override
protected void updateLocationMarkers() {
super.updateLocationMarkers();
if (this.myLoc != null) {
this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
}
double longitude = location.getLongitude();
double latitude = location.getLatitude();
if (latitude != 0 && longitude != 0) {
new getAddressAsync(this).execute();
this.binding.map.getOverlays().add(new Marker(this.marker_icon, this.loc));
new getAddressAsync(this, this.loc, this.name).execute();
}
private void showAddress(final GeoPoint loc, final String name) {
this.binding.address.setText(Html.fromHtml(getAddress(this, loc, name)));
if (Html.fromHtml(getAddress(this, loc, name)).length() > 0) {
this.binding.address.setVisibility(View.VISIBLE);
} else {
hideAddress();
}
}
public SharedPreferences getPreferences() {
return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
private void hideAddress() {
this.binding.address.setVisibility(View.GONE);
}
@Override
public void onPause() {
super.onPause();
}
@Override
@ -112,89 +144,140 @@ public class ShowLocationActivity extends XmppActivity {
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (MenuDoubleTabUtil.shouldIgnoreTap()) {
return false;
}
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
case R.id.action_copy_location:
final ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
if (clipboard != null) {
final ClipData clip = ClipData.newPlainText("location", createGeoUri().toString());
clipboard.setPrimaryClip(clip);
ToastCompat.makeText(this, R.string.url_copied_to_clipboard, ToastCompat.LENGTH_SHORT).show();
}
return true;
case R.id.action_share_location:
final Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString());
shareIntent.setType("text/plain");
try {
startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
} catch (final ActivityNotFoundException e) {
//This should happen only on faulty androids because normally chooser is always available
ToastCompat.makeText(this, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show();
}
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
return true;
private void startNavigation() {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(
"google.navigation:q=" +
this.loc.getLatitude() + "," + this.loc.getLongitude()
)));
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
protected void updateUi() {
final Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q=0,0"));
final ComponentName component = i.resolveActivity(getPackageManager());
this.binding.fab.setVisibility(component == null ? View.GONE : View.VISIBLE);
}
private void showLocation(Location location, String address) {
try {
if (location != null && TextUtils.isEmpty(address)) { // location but no address available
String LocationName = "<b>" + mLocationName + "</b>";
final WebView webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl("file:///android_asset/map.html?lat=" + location.getLatitude() + "&lon=" + location.getLongitude() + "&name=" + LocationName);
} else if (location != null && !TextUtils.isEmpty(address)) { // location and address available
String LocationName = "<b>" + mLocationName + "</b><br>" + address;
final WebView webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl("javascript:toCoordinates(" + location.getLatitude() + "," + location.getLongitude() + "," + "'" + LocationName + "'" + ");");
@Override
public void onLocationChanged(final Location location) {
if (LocationHelper.isBetterLocation(location, this.myLoc)) {
this.myLoc = location;
updateLocationMarkers();
}
}
@Override
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
}
@Override
public void onProviderEnabled(final String provider) {
}
@Override
public void onProviderDisabled(final String provider) {
}
private static String getAddress(final Context context, final GeoPoint location, final String name) {
final double longitude = location.getLongitude();
final double latitude = location.getLatitude();
String address = "";
if (latitude != 0 && longitude != 0) {
try {
final Geocoder geoCoder = new Geocoder(context, Locale.getDefault());
final List<Address> addresses = geoCoder.getFromLocation(latitude, longitude, 1);
if (addresses != null && addresses.size() > 0) {
final Address Address = addresses.get(0);
StringBuilder strAddress = new StringBuilder("");
if (name != null && name.length() > 0) {
strAddress.append("<b>");
strAddress.append(name);
strAddress.append(":</b><br>");
}
if (Address.getAddressLine(0).length() > 0) {
strAddress.append(Address.getAddressLine(0));
}
address = strAddress.toString().replace(", ", "<br>");
} else {
StringBuilder strAddress = new StringBuilder("");
if (name != null && name.length() > 0) {
strAddress.append("<b>");
strAddress.append(name);
strAddress.append("</b>");
}
address = strAddress.toString();
}
} catch (Exception e) {
e.printStackTrace();
StringBuilder strAddress = new StringBuilder("");
if (name != null && name.length() > 0) {
strAddress.append("<b>");
strAddress.append(name);
strAddress.append("</b>");
}
address = strAddress.toString();
}
} catch (Exception e) {
e.printStackTrace();
ToastCompat.makeText(this, R.string.error, ToastCompat.LENGTH_LONG);
}
}
private void navigate(Location location) {
if (location == null) {
Log.d(Config.LOGTAG, "No location given");
return;
}
double longitude = location.getLongitude();
double latitude = location.getLatitude();
try {
Intent intent = new Intent(android.content.Intent.ACTION_VIEW, Uri.parse("google.navigation:q=" + String.valueOf(latitude) + "," + String.valueOf(longitude)));
startActivity(intent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
} catch (ActivityNotFoundException e) {
ToastCompat.makeText(this, R.string.no_application_found_to_display_location, ToastCompat.LENGTH_SHORT).show();
}
return address;
}
private class getAddressAsync extends AsyncTask<Void, Void, Void> {
String address = null;
String name = null;
GeoPoint location;
private WeakReference<ShowLocationActivity> activityReference;
getAddressAsync(ShowLocationActivity context) {
getAddressAsync(final ShowLocationActivity context, final GeoPoint location, final String name) {
activityReference = new WeakReference<>(context);
this.location = location;
this.name = name;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
showLocation(location, "");
}
@Override
protected Void doInBackground(Void... params) {
address = getAddress(ShowLocationActivity.this, location);
address = getAddress(ShowLocationActivity.this, this.location, this.name);
return null;
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
showLocation(location, address);
showAddress(this.location, this.name);
}
}
}
}

View file

@ -9,6 +9,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -31,15 +32,18 @@ import android.widget.AutoCompleteTextView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -49,6 +53,8 @@ import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.textfield.TextInputLayout;
import com.leinardi.android.speeddial.SpeedDialActionItem;
import com.leinardi.android.speeddial.SpeedDialView;
import java.util.ArrayList;
import java.util.Collections;
@ -94,17 +100,17 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
public int conference_context_id;
public int contact_context_id;
private ListPagerAdapter mListPagerAdapter;
private final List<ListItem> contacts = new ArrayList<>();
private final List<ListItem> contacts = new ArrayList<>();
private ListItemAdapter mContactsAdapter;
private final List<ListItem> conferences = new ArrayList<>();
private final List<ListItem> conferences = new ArrayList<>();
private ListItemAdapter mConferenceAdapter;
private final List<String> mActivatedAccounts = new ArrayList<>();
private final List<String> mActivatedAccounts = new ArrayList<>();
private EditText mSearchEditText;
private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
private final AtomicBoolean mOpenedFab = new AtomicBoolean(false);
private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
private final AtomicBoolean mOpenedFab = new AtomicBoolean(false);
private boolean mHideOfflineContacts = false;
private boolean createdByViewIntent = false;
private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
@ -133,7 +139,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
return true;
}
};
private final TextWatcher mSearchTextWatcher = new TextWatcher() {
private final TextWatcher mSearchTextWatcher = new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
@ -149,7 +155,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
};
private MenuItem mMenuSearchView;
private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
@Override
public void onTagClicked(String tag) {
if (mMenuSearchView != null) {
@ -162,7 +168,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
};
private Pair<Integer, Intent> mPostponedActivityResult;
private Toast mToast;
private final UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
private final UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
@Override
public void success(final Conversation conversation) {
runOnUiThread(() -> {
@ -182,7 +188,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
};
private ActivityStartConversationBinding binding;
private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
int pos = binding.startConversationViewPager.getCurrentItem();
@ -263,7 +269,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
Toolbar toolbar = (Toolbar) binding.toolbar;
setSupportActionBar(toolbar);
configureActionBar(getSupportActionBar());
binding.speedDial.inflate(R.menu.start_conversation_fab_submenu);
inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu);
binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
@ -330,6 +336,22 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
return false;
});
binding.speedDial.getMainFab().setSupportImageTintList(ColorStateList.valueOf(getResources().getColor(R.color.realwhite)));
}
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) {
@ -623,10 +645,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
if (binding.startConversationViewPager.getCurrentItem() == 0) {
mSearchEditText.setHint(R.string.search_contacts);
mSearchEditText.setContentDescription(getString(R.string.search_contacts));
mSearchEditText.setContentDescription(getString(R.string.search_contacts));
} else {
mSearchEditText.setHint(R.string.search_bookmarks);
mSearchEditText.setContentDescription(getString(R.string.search_bookmarks));
mSearchEditText.setContentDescription(getString(R.string.search_bookmarks));
}
}

View file

@ -0,0 +1,443 @@
package eu.siacs.conversations.ui;
import android.Manifest;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.util.Log;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.http.NoSSLv3SocketFactory;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.CustomTab;
import eu.siacs.conversations.utils.WakeLockHelper;
import me.drakeet.support.toast.ToastCompat;
import static eu.siacs.conversations.Config.monocles;
import static eu.siacs.conversations.http.HttpConnectionManager.getProxy;
import static eu.siacs.conversations.services.XmppConnectionService.FDroid;
import static eu.siacs.conversations.services.XmppConnectionService.PlayStore;
public class UpdaterActivity extends XmppActivity {
static final private String FileName = "update.apk";
String appURI = "";
String changelog = "";
Integer filesize = 0;
String store;
ProgressDialog mProgressDialog;
DownloadTask downloadTask;
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//set activity
setContentView(R.layout.activity_updater);
this.mTheme = findTheme();
setTheme(this.mTheme);
textView = findViewById(R.id.updater);
mProgressDialog = new ProgressDialog(UpdaterActivity.this) {
//show warning on back pressed
@Override
public void onBackPressed() {
showCancelDialog();
}
};
mProgressDialog.setMessage(getString(R.string.download_started));
mProgressDialog.setProgressNumberFormat(null);
mProgressDialog.setIndeterminate(true);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setCancelable(false);
mProgressDialog.setCanceledOnTouchOutside(false);
}
@Override
protected void refreshUiReal() {
//ignored
}
@Override
protected void onStart() {
super.onStart();
this.mTheme = findTheme();
setTheme(this.mTheme);
setTitle(getString(R.string.update_service));
textView.setText(R.string.update_info);
setSupportActionBar(findViewById(R.id.toolbar));
configureActionBar(getSupportActionBar());
if (getIntent() != null && getIntent().getStringExtra("update").equals("PixArtMessenger_UpdateService")) {
try {
appURI = getIntent().getStringExtra("url");
} catch (Exception e) {
ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show();
UpdaterActivity.this.finish();
}
try {
changelog = getIntent().getStringExtra("changelog");
} catch (Exception e) {
ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show();
UpdaterActivity.this.finish();
}
try {
store = getIntent().getStringExtra("store");
} catch (Exception e) {
store = null;
}
//delete old downloaded localVersion files
File dir = new File(FileBackend.getAppUpdateDirectory());
if (dir.isDirectory()) {
String[] children = dir.list();
for (String aChildren : children) {
Log.d(Config.LOGTAG, "AppUpdater: delete old update files " + aChildren + " in " + dir);
new File(dir, aChildren).delete();
}
}
//oh yeah we do need an upgrade, let the user know send an alert message
final AlertDialog.Builder builder = new AlertDialog.Builder(UpdaterActivity.this);
builder.setCancelable(false);
//open link to changelog
//if the user agrees to upgrade
builder.setMessage(getString(R.string.install_update))
.setPositiveButton(R.string.update, (dialog, id) -> {
Log.d(Config.LOGTAG, "AppUpdater: downloading " + FileName + " from " + appURI);
//ask for permissions on devices >= SDK 23
if (isStoragePermissionGranted() && isNetworkAvailable(getApplicationContext())) {
//start downloading the file using the download manager
if (store != null && store.equalsIgnoreCase(PlayStore)) {
Uri uri = Uri.parse("market://details?id=" + getString(R.string.applicationId));
Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri);
PackageManager manager = getApplicationContext().getPackageManager();
List<ResolveInfo> infos = manager.queryIntentActivities(marketIntent, 0);
if (infos.size() > 0) {
startActivity(marketIntent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
} else {
try {
uri = Uri.parse("https://" + monocles());
CustomTab.openTab(this, uri, isDarkTheme());
} catch (Exception e) {
ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show();
}
}
} else if (store != null && store.equalsIgnoreCase(FDroid)) {
Uri uri = Uri.parse("https://f-droid.org/de/packages/" + getString(R.string.applicationId) + "/");
Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri);
PackageManager manager = getApplicationContext().getPackageManager();
List<ResolveInfo> infos = manager.queryIntentActivities(marketIntent, 0);
if (infos.size() > 0) {
startActivity(marketIntent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
} else {
uri = Uri.parse("https://" + monocles());
try {
CustomTab.openTab(this, uri, isDarkTheme());
} catch (Exception e) {
ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show();
}
}
} else {
ToastCompat.makeText(getApplicationContext(), getText(R.string.download_started), ToastCompat.LENGTH_LONG).show();
downloadTask = new DownloadTask(UpdaterActivity.this);
downloadTask.execute(appURI);
}
} else {
Log.d(Config.LOGTAG, "AppUpdater: failed - has storage permissions " + isStoragePermissionGranted() + " and internet " + isNetworkAvailable(getApplicationContext()));
}
})
.setNeutralButton(R.string.changelog, (dialog, id) -> {
Uri uri = Uri.parse(Config.CHANGELOG_URL); // missing 'http://' will cause crash
try {
CustomTab.openTab(this, uri, isDarkTheme());
} catch (Exception e) {
ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show();
} finally {
//restart updater to show dialog again after coming back after opening changelog
recreate();
}
})
.setNegativeButton(R.string.remind_later, (dialog, id) -> {
// User cancelled the dialog
UpdaterActivity.this.finish();
});
//show the alert message
builder.create().show();
} else {
ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show();
UpdaterActivity.this.finish();
}
}
@Override
void onBackendConnected() {
//ignored
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
}
@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
//check for internet connection
private boolean isNetworkAvailable(Context context) {
ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity != null) {
NetworkInfo[] info = connectivity.getAllNetworkInfo();
if (info != null) {
for (NetworkInfo anInfo : info) {
if (anInfo.getState() == NetworkInfo.State.CONNECTED) {
return true;
}
}
}
}
return false;
}
public boolean isStoragePermissionGranted() {
if (Build.VERSION.SDK_INT >= 23) {
if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
return true;
} else {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
return false;
}
} else { //permission is automatically granted on sdk<23 upon installation
return true;
}
}
//show warning on back pressed
@Override
public void onBackPressed() {
showCancelDialog();
}
private void showCancelDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.cancel_update)
.setCancelable(false)
.setPositiveButton(R.string.yes, (dialog, id) -> {
if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) {
downloadTask.cancel(true);
}
if (mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
UpdaterActivity.this.finish();
})
.setNegativeButton(R.string.no, (dialog, id) -> dialog.cancel());
final AlertDialog alert = builder.create();
alert.show();
}
@Override
public void onPause() {
super.onPause();
if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) {
downloadTask.cancel(true);
}
UpdaterActivity.this.finish();
}
@Override
protected void onStop() {
super.onStop();
if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) {
downloadTask.cancel(true);
}
UpdaterActivity.this.finish();
}
private class DownloadTask extends AsyncTask<String, Integer, String> {
XmppActivity activity;
File dir = new File(FileBackend.getAppUpdateDirectory());
File file = new File(dir, FileName);
XmppConnectionService xmppConnectionService;
private Context context;
private PowerManager.WakeLock mWakeLock;
private long startTime = 0;
private boolean mUseTor;
DownloadTask(Context context) {
this.context = context;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
startTime = System.currentTimeMillis();
// take CPU lock to prevent CPU from going off if the user
// presses the power button during download
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getName());
mWakeLock.acquire();
mUseTor = xmppConnectionService != null && xmppConnectionService.useTorToConnect();
}
mProgressDialog.show();
}
@Override
protected void onProgressUpdate(Integer... progress) {
super.onProgressUpdate(progress);
// if we get here, length is known, now set indeterminate to false
mProgressDialog.setIndeterminate(false);
mProgressDialog.setMax(100);
mProgressDialog.setProgress(progress[0]);
}
@Override
protected String doInBackground(String... sUrl) {
InputStream is = null;
OutputStream os = null;
SSLContext sslcontext = null;
SSLSocketFactory NoSSLv3Factory = null;
try {
sslcontext = SSLContext.getInstance("TLSv1");
if (sslcontext != null) {
sslcontext.init(null, null, null);
NoSSLv3Factory = new NoSSLv3SocketFactory(sslcontext.getSocketFactory());
}
} catch (NoSuchAlgorithmException | KeyManagementException e) {
e.printStackTrace();
}
HttpsURLConnection.setDefaultSSLSocketFactory(NoSSLv3Factory);
HttpsURLConnection connection = null;
try {
Log.d(Config.LOGTAG, "AppUpdater: save file to " + file.toString());
Log.d(Config.LOGTAG, "AppUpdater: download update from url: " + sUrl[0] + " to file name: " + file.toString());
URL url = new URL(sUrl[0]);
if (mUseTor) {
connection = (HttpsURLConnection) url.openConnection(getProxy());
} else {
connection = (HttpsURLConnection) url.openConnection();
}
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setRequestProperty("User-agent", System.getProperty("http.agent"));
connection.connect();
// expect HTTP 200 OK, so we don't mistakenly save error report
// instead of the file
if (connection.getResponseCode() != HttpsURLConnection.HTTP_OK) {
ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show();
return connection.getResponseCode() + ": " + connection.getResponseMessage();
}
// this will be useful to display download percentage
// might be -1: server did not report the length
int fileLength = connection.getContentLength();
// create folders
File parentDirectory = file.getParentFile();
if (parentDirectory.mkdirs()) {
Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
}
// download the file
is = connection.getInputStream();
os = new FileOutputStream(file);
byte[] data = new byte[4096];
long total = 0;
int count;
while ((count = is.read(data)) != -1) {
// allow canceling with back button
if (isCancelled()) {
is.close();
return "canceled";
}
total += count;
// publishing the progress....
if (fileLength > 0) // only if total length is known
publishProgress((int) (total * 100 / fileLength));
os.write(data, 0, count);
}
} catch (Exception e) {
e.printStackTrace();
return e.toString();
} finally {
try {
if (os != null)
os.close();
if (is != null)
is.close();
} catch (IOException ignored) {
}
if (connection != null)
connection.disconnect();
}
return null;
}
@Override
protected void onPostExecute(String result) {
WakeLockHelper.release(mWakeLock);
mProgressDialog.dismiss();
if (result != null) {
ToastCompat.makeText(getApplicationContext(), getString(R.string.failed), ToastCompat.LENGTH_LONG).show();
Log.d(Config.LOGTAG, "AppUpdater: failed with " + result);
UpdaterActivity.this.finish();
} else {
Log.d(Config.LOGTAG, "AppUpdater: download ready in " + ((System.currentTimeMillis() - startTime) / 1000) + " sec");
//start the installation of the latest localVersion
Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
installIntent.setDataAndType(FileBackend.getUriForFile(UpdaterActivity.this, file), "application/vnd.android.package-archive");
installIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
installIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(installIntent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
UpdaterActivity.this.finish();
}
}
}
}

View file

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

View file

@ -1,450 +0,0 @@
package eu.siacs.conversations.ui;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import me.drakeet.support.toast.ToastCompat;
public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate {
public static final String ACTION_VERIFY_CONTACT = "verify_contact";
public static final int MODE_SCAN_FINGERPRINT = -0x0502;
public static final int MODE_ASK_QUESTION = 0x0503;
public static final int MODE_ANSWER_QUESTION = 0x0504;
public static final int MODE_MANUAL_VERIFICATION = 0x0505;
private LinearLayout mManualVerificationArea;
private LinearLayout mSmpVerificationArea;
private TextView mRemoteFingerprint;
private TextView mYourFingerprint;
private TextView mVerificationExplain;
private TextView mStatusMessage;
private TextView mSharedSecretHint;
private EditText mSharedSecretHintEditable;
private EditText mSharedSecretSecret;
private Button mLeftButton;
private Button mRightButton;
private Account mAccount;
private Conversation mConversation;
private int mode = MODE_MANUAL_VERIFICATION;
private XmppUri mPendingUri = null;
private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int click) {
mConversation.verifyOtrFingerprint();
xmppConnectionService.syncRosterToDisk(mConversation.getAccount());
ToastCompat.makeText(VerifyOTRActivity.this, R.string.verified, Toast.LENGTH_SHORT).show();
finish();
}
};
private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() {
@Override
public void onClick(final View view) {
if (isAccountOnline()) {
final String question = mSharedSecretHintEditable.getText().toString();
final String secret = mSharedSecretSecret.getText().toString();
if (question.trim().isEmpty()) {
mSharedSecretHintEditable.requestFocus();
mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty));
} else if (secret.trim().isEmpty()) {
mSharedSecretSecret.requestFocus();
mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty));
} else {
mSharedSecretSecret.setError(null);
mSharedSecretHintEditable.setError(null);
initSmp(question, secret);
updateView();
}
}
}
};
private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
if (isAccountOnline()) {
abortSmp();
updateView();
}
}
};
private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
if (isAccountOnline()) {
final String question = mSharedSecretHintEditable.getText().toString();
final String secret = mSharedSecretSecret.getText().toString();
respondSmp(question, secret);
updateView();
}
}
};
private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
mConversation.smp().status = Conversation.Smp.STATUS_NONE;
mConversation.smp().hint = null;
mConversation.smp().secret = null;
updateView();
}
};
private View.OnClickListener mFinishListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
mConversation.smp().status = Conversation.Smp.STATUS_NONE;
finish();
}
};
protected boolean initSmp(final String question, final String secret) {
final Session session = mConversation.getOtrSession();
if (session != null) {
try {
session.initSmp(question, secret);
mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED;
mConversation.smp().secret = secret;
mConversation.smp().hint = question;
return true;
} catch (OtrException e) {
return false;
}
} else {
return false;
}
}
protected boolean abortSmp() {
final Session session = mConversation.getOtrSession();
if (session != null) {
try {
session.abortSmp();
mConversation.smp().status = Conversation.Smp.STATUS_NONE;
mConversation.smp().hint = null;
mConversation.smp().secret = null;
return true;
} catch (OtrException e) {
return false;
}
} else {
return false;
}
}
protected boolean respondSmp(final String question, final String secret) {
final Session session = mConversation.getOtrSession();
if (session != null) {
try {
session.respondSmp(question, secret);
return true;
} catch (OtrException e) {
return false;
}
} else {
return false;
}
}
protected boolean verifyWithUri(XmppUri uri) {
Contact contact = mConversation.getContact();
if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) {
xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints());
ToastCompat.makeText(this, R.string.verified, Toast.LENGTH_SHORT).show();
updateView();
return true;
} else {
ToastCompat.makeText(this, R.string.could_not_verify_fingerprint, Toast.LENGTH_SHORT).show();
return false;
}
}
protected boolean isAccountOnline() {
if (this.mAccount.getStatus() != Account.State.ONLINE) {
ToastCompat.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
return false;
} else {
return true;
}
}
protected boolean handleIntent(Intent intent) {
if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) {
this.mAccount = extractAccount(intent);
if (this.mAccount == null) {
return false;
}
try {
this.mConversation = this.xmppConnectionService.find(this.mAccount, Jid.of(intent.getExtras().getString("contact")));
if (this.mConversation == null) {
return false;
}
} catch (final IllegalArgumentException ignored) {
ignored.printStackTrace();
return false;
} catch (Exception e) {
e.printStackTrace();
return false;
}
this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION);
// todo scan OTR fingerprint
if (this.mode == MODE_SCAN_FINGERPRINT) {
Log.d(Config.LOGTAG, "Scan OTR fingerprint is not implemented in this version");
//new IntentIntegrator(this).initiateScan();
return false;
}
return true;
} else {
return false;
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
// todo onActivityResult for OTR scan
Log.d(Config.LOGTAG, "Scan OTR fingerprint result is not implemented in this version");
/*if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) {
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
if (scanResult != null && scanResult.getFormatName() != null) {
String data = scanResult.getContents();
XmppUri uri = new XmppUri(data);
if (xmppConnectionServiceBound) {
verifyWithUri(uri);
finish();
} else {
this.mPendingUri = uri;
}
} else {
finish();
}
}*/
super.onActivityResult(requestCode, requestCode, intent);
}
@Override
protected void onBackendConnected() {
if (handleIntent(getIntent())) {
updateView();
} else if (mPendingUri != null) {
verifyWithUri(mPendingUri);
finish();
mPendingUri = null;
}
setIntent(null);
}
protected void updateView() {
if (this.mConversation != null && this.mConversation.hasValidOtrSession()) {
final ActionBar actionBar = getSupportActionBar();
this.mVerificationExplain.setText(R.string.no_otr_session_found);
invalidateOptionsMenu();
switch (this.mode) {
case MODE_ASK_QUESTION:
if (actionBar != null) {
actionBar.setTitle(R.string.ask_question);
}
this.updateViewAskQuestion();
break;
case MODE_ANSWER_QUESTION:
if (actionBar != null) {
actionBar.setTitle(R.string.smp_requested);
}
this.updateViewAnswerQuestion();
break;
case MODE_MANUAL_VERIFICATION:
default:
if (actionBar != null) {
actionBar.setTitle(R.string.manually_verify);
}
this.updateViewManualVerification();
break;
}
} else {
this.mManualVerificationArea.setVisibility(View.GONE);
this.mSmpVerificationArea.setVisibility(View.GONE);
}
}
protected void updateViewManualVerification() {
this.mVerificationExplain.setText(R.string.manual_verification_explanation);
this.mManualVerificationArea.setVisibility(View.VISIBLE);
this.mSmpVerificationArea.setVisibility(View.GONE);
this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint()));
this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint()));
if (this.mConversation.isOtrFingerprintVerified()) {
deactivateButton(this.mRightButton, R.string.verified);
activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
} else {
activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
activateButton(this.mRightButton, R.string.verify, new View.OnClickListener() {
@Override
public void onClick(View view) {
showManuallyVerifyDialog();
}
});
}
}
protected void updateViewAskQuestion() {
this.mManualVerificationArea.setVisibility(View.GONE);
this.mSmpVerificationArea.setVisibility(View.VISIBLE);
this.mVerificationExplain.setText(R.string.smp_explain_question);
final int smpStatus = this.mConversation.smp().status;
switch (smpStatus) {
case Conversation.Smp.STATUS_WE_REQUESTED:
this.mStatusMessage.setVisibility(View.GONE);
this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
this.mSharedSecretSecret.setVisibility(View.VISIBLE);
this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint);
this.mSharedSecretSecret.setText(this.mConversation.smp().secret);
this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener);
this.deactivateButton(this.mRightButton, R.string.in_progress);
break;
case Conversation.Smp.STATUS_FAILED:
this.mStatusMessage.setVisibility(View.GONE);
this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
this.mSharedSecretSecret.setVisibility(View.VISIBLE);
this.mSharedSecretSecret.requestFocus();
this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match));
this.deactivateButton(this.mLeftButton, R.string.cancel);
this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener);
break;
case Conversation.Smp.STATUS_VERIFIED:
this.mSharedSecretHintEditable.setText("");
this.mSharedSecretHintEditable.setVisibility(View.GONE);
this.mSharedSecretSecret.setText("");
this.mSharedSecretSecret.setVisibility(View.GONE);
this.mStatusMessage.setVisibility(View.VISIBLE);
this.deactivateButton(this.mLeftButton, R.string.cancel);
this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
break;
default:
this.mStatusMessage.setVisibility(View.GONE);
this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
this.mSharedSecretSecret.setVisibility(View.VISIBLE);
this.activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener);
break;
}
}
protected void updateViewAnswerQuestion() {
this.mManualVerificationArea.setVisibility(View.GONE);
this.mSmpVerificationArea.setVisibility(View.VISIBLE);
this.mVerificationExplain.setText(R.string.smp_explain_answer);
this.mSharedSecretHintEditable.setVisibility(View.GONE);
this.mSharedSecretHint.setVisibility(View.VISIBLE);
this.deactivateButton(this.mLeftButton, R.string.cancel);
final int smpStatus = this.mConversation.smp().status;
switch (smpStatus) {
case Conversation.Smp.STATUS_CONTACT_REQUESTED:
this.mStatusMessage.setVisibility(View.GONE);
this.mSharedSecretHint.setText(this.mConversation.smp().hint);
this.activateButton(this.mRightButton, R.string.respond, this.mRespondSharedSecretListener);
break;
case Conversation.Smp.STATUS_VERIFIED:
this.mSharedSecretHintEditable.setText("");
this.mSharedSecretHintEditable.setVisibility(View.GONE);
this.mSharedSecretHint.setVisibility(View.GONE);
this.mSharedSecretSecret.setText("");
this.mSharedSecretSecret.setVisibility(View.GONE);
this.mStatusMessage.setVisibility(View.VISIBLE);
this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
break;
case Conversation.Smp.STATUS_FAILED:
default:
this.mSharedSecretSecret.requestFocus();
this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match));
this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
break;
}
}
protected void activateButton(Button button, int text, View.OnClickListener listener) {
button.setEnabled(true);
button.setText(text);
button.setOnClickListener(listener);
}
protected void deactivateButton(Button button, int text) {
button.setEnabled(false);
button.setText(text);
button.setOnClickListener(null);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_verify_otr);
this.mRemoteFingerprint = findViewById(R.id.remote_fingerprint);
this.mYourFingerprint = findViewById(R.id.your_fingerprint);
this.mLeftButton = findViewById(R.id.left_button);
this.mRightButton = findViewById(R.id.right_button);
this.mVerificationExplain = findViewById(R.id.verification_explanation);
this.mStatusMessage = findViewById(R.id.status_message);
this.mSharedSecretSecret = findViewById(R.id.shared_secret_secret);
this.mSharedSecretHintEditable = findViewById(R.id.shared_secret_hint_editable);
this.mSharedSecretHint = findViewById(R.id.shared_secret_hint);
this.mManualVerificationArea = findViewById(R.id.manual_verification_area);
this.mSmpVerificationArea = findViewById(R.id.smp_verification_area);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.verify_otr, menu);
return true;
}
private void showManuallyVerifyDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.manually_verify);
builder.setMessage(R.string.are_you_sure_verify_fingerprint);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener);
builder.create().show();
}
@Override
protected String getShareableUri() {
if (mAccount != null) {
return mAccount.getShareableUri();
} else {
return "";
}
}
public void onConversationUpdate() {
refreshUi();
}
@Override
protected void refreshUiReal() {
updateView();
}
}

View file

@ -1,5 +1,9 @@
package eu.siacs.conversations.ui;
import static eu.siacs.conversations.Config.DISALLOW_REGISTRATION_IN_UI;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.readGranted;
import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.Intent;
@ -8,12 +12,10 @@ import android.net.Uri;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
@ -37,13 +39,8 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import me.drakeet.support.toast.ToastCompat;
import static eu.siacs.conversations.Config.DISALLOW_REGISTRATION_IN_UI;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.readGranted;
public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback {
private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
private static final int REQUEST_READ_EXTERNAL_STORAGE = 0XD737;
@ -106,7 +103,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
}
@Override
public void onNewIntent(Intent intent) {
public void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
if (intent != null) {
setIntent(intent);
@ -232,6 +229,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
if (grantResults.length > 0) {
if (allGranted(grantResults)) {

View file

@ -1,13 +1,9 @@
package eu.siacs.conversations.ui;
import android.Manifest;
import android.util.Pair;
import net.java.otr4j.session.SessionID;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.utils.CryptoHelper;
import static eu.siacs.conversations.ui.SettingsActivity.USE_BUNDLED_EMOJIS;
import static eu.siacs.conversations.ui.SettingsActivity.USE_INTERNAL_UPDATER;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
@ -91,6 +87,7 @@ import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.EmojiService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.UpdateService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
@ -109,10 +106,6 @@ import eu.siacs.conversations.xmpp.XmppConnection;
import me.drakeet.support.toast.ToastCompat;
import pl.droidsonroids.gif.GifDrawable;
import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_OTR_ENCRYPTION;
import static eu.siacs.conversations.ui.SettingsActivity.USE_BUNDLED_EMOJIS;
import static eu.siacs.conversations.ui.SettingsActivity.USE_INTERNAL_UPDATER;
public abstract class XmppActivity extends ActionBarActivity {
protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
@ -426,17 +419,7 @@ public abstract class XmppActivity extends ActionBarActivity {
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
final Contact contact = conversation.getContact();
if (conversation.hasValidOtrSession()) {
SessionID id = conversation.getOtrSession().getSessionID();
Jid jid;
try {
jid = Jid.of(id.getAccountID() + "/" + id.getUserID());
} catch (IllegalArgumentException e) {
jid = null;
}
conversation.setNextCounterpart(jid);
listener.onPresenceSelected();
} else if (contact.showInRoster() || contact.isSelf()) {
if (contact.showInRoster() || contact.isSelf()) {
final Presences presences = contact.getPresences();
if (presences.size() == 0) {
if (contact.isSelf()) {
@ -507,8 +490,8 @@ public abstract class XmppActivity extends ActionBarActivity {
return getBooleanPreference("unicolored_chatbg", R.bool.use_unicolored_chatbg) || getPreferences().getString(SettingsActivity.THEME, getString(R.string.theme)).equals("black");
}
public boolean enableOTR() {
return getBooleanPreference(ENABLE_OTR_ENCRYPTION, R.bool.enable_otr);
public boolean showDateInQuotes() {
return getBooleanPreference("show_date_in_quotes", R.bool.show_date_in_quotes);
}
public void setBubbleColor(final View v, final int backgroundColor, final int borderColor) {
@ -788,7 +771,10 @@ public abstract class XmppActivity extends ActionBarActivity {
builder.setTitle(contact.getJid().toString());
builder.setMessage(getString(R.string.not_in_roster));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> xmppConnectionService.createContact(contact, true));
builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> {
xmppConnectionService.createContact(contact, true);
recreate();
});
builder.create().show();
}
@ -923,56 +909,6 @@ public abstract class XmppActivity extends ActionBarActivity {
}
}
private void showPresenceSelectionDialog(Presences presences, final Conversation conversation, final OnPresenceSelected listener) {
final Contact contact = conversation.getContact();
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.choose_presence));
final String[] resourceArray = presences.toResourceArray();
Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
final Map<String, String> resourceTypeMap = typeAndName.first;
final Map<String, String> resourceNameMap = typeAndName.second;
final String[] readableIdentities = new String[resourceArray.length];
final AtomicInteger selectedResource = new AtomicInteger(0);
for (int i = 0; i < resourceArray.length; ++i) {
String resource = resourceArray[i];
if (resource.equals(contact.getLastResource())) {
selectedResource.set(i);
}
String type = resourceTypeMap.get(resource);
String name = resourceNameMap.get(resource);
if (type != null) {
if (Collections.frequency(resourceTypeMap.values(), type) == 1) {
readableIdentities[i] = PresenceSelector.translateType(this, type);
} else if (name != null) {
if (Collections.frequency(resourceNameMap.values(), name) == 1
|| CryptoHelper.UUID_PATTERN.matcher(resource).matches()) {
readableIdentities[i] = PresenceSelector.translateType(this, type) + " (" + name + ")";
} else {
readableIdentities[i] = PresenceSelector.translateType(this, type) + " (" + name + " / " + resource + ")";
}
} else {
readableIdentities[i] = PresenceSelector.translateType(this, type) + " (" + resource + ")";
}
} else {
readableIdentities[i] = resource;
}
}
builder.setSingleChoiceItems(readableIdentities,
selectedResource.get(),
(dialog, which) -> selectedResource.set(which));
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.ok, (dialog, which) -> {
try {
Jid next = Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()]);
conversation.setNextCounterpart(next);
} catch (IllegalArgumentException e) {
conversation.setNextCounterpart(null);
}
listener.onPresenceSelected();
});
builder.create().show();
}
protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
@ -1036,14 +972,11 @@ public abstract class XmppActivity extends ActionBarActivity {
ToastCompat.makeText(this, R.string.no_accounts, ToastCompat.LENGTH_SHORT).show();
return;
}
if (!xmppConnectionService.multipleAccounts()) {
Account mAccount = xmppConnectionService.getAccounts().get(0);
if (EasyOnboardingInvite.hasAccountSupport(mAccount)) {
selectAccountToStartEasyInvite();
} else {
String user = Jid.ofEscaped(mAccount.getJid()).getLocal();
String domain = Jid.ofEscaped(mAccount.getJid()).getDomain().toEscapedString();
if (!selectAccountToStartEasyInvite()) {
if (!xmppConnectionService.multipleAccounts()) {
final Account mAccount = xmppConnectionService.getAccounts().get(0);
final String user = Jid.ofEscaped(mAccount.getJid()).getLocal();
final String domain = Jid.ofEscaped(mAccount.getJid()).getDomain().toEscapedString();
String inviteURL;
try {
inviteURL = new getAdHocInviteUri(mAccount.getXmppConnection(), mAccount).execute().get();
@ -1058,40 +991,36 @@ public abstract class XmppActivity extends ActionBarActivity {
inviteURL = Config.inviteUserURL + user + "/" + domain;
}
Log.d(Config.LOGTAG, "Invite uri = " + inviteURL);
String inviteText = getString(R.string.InviteText, user);
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
final String inviteText = getString(R.string.InviteText, user);
final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_SUBJECT, user + " " + getString(R.string.inviteUser_Subject) + " " + getString(R.string.app_name));
intent.putExtra(Intent.EXTRA_TEXT, inviteText + "\n\n" + inviteURL);
startActivity(Intent.createChooser(intent, getString(R.string.invite_contact)));
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
}
} else {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.chooce_account);
final View dialogView = this.getLayoutInflater().inflate(R.layout.choose_account_dialog, null);
final Spinner spinner = dialogView.findViewById(R.id.account);
builder.setView(dialogView);
List<String> mActivatedAccounts = new ArrayList<>();
for (Account account : xmppConnectionService.getAccounts()) {
if (account.getStatus() != Account.State.DISABLED) {
if (Config.DOMAIN_LOCK != null) {
mActivatedAccounts.add(account.getJid().getLocal());
} else {
mActivatedAccounts.add(account.getJid().asBareJid().toString());
} else {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.chooce_account);
final View dialogView = this.getLayoutInflater().inflate(R.layout.choose_account_dialog, null);
final Spinner spinner = dialogView.findViewById(R.id.account);
builder.setView(dialogView);
List<String> mActivatedAccounts = new ArrayList<>();
for (Account account : xmppConnectionService.getAccounts()) {
if (account.getStatus() != Account.State.DISABLED) {
if (Config.DOMAIN_LOCK != null) {
mActivatedAccounts.add(account.getJid().getLocal());
} else {
mActivatedAccounts.add(account.getJid().asBareJid().toString());
}
}
}
}
StartConversationActivity.populateAccountSpinner(this, mActivatedAccounts, spinner);
builder.setPositiveButton(R.string.ok,
(dialog, id) -> {
String selection = spinner.getSelectedItem().toString();
Account mAccount = xmppConnectionService.findAccountByJid(Jid.of(selection).asBareJid());
if (EasyOnboardingInvite.hasAccountSupport(mAccount)) {
selectAccountToStartEasyInvite();
} else {
String user = Jid.of(mAccount.getJid()).getLocal();
String domain = Jid.of(mAccount.getJid()).getDomain().toEscapedString();
StartConversationActivity.populateAccountSpinner(this, mActivatedAccounts, spinner);
builder.setPositiveButton(R.string.ok,
(dialog, id) -> {
final String selection = spinner.getSelectedItem().toString();
final Account mAccount = xmppConnectionService.findAccountByJid(Jid.of(selection).asBareJid());
final String user = Jid.of(mAccount.getJid()).getLocal();
final String domain = Jid.of(mAccount.getJid()).getDomain().toEscapedString();
String inviteURL;
try {
inviteURL = new getAdHocInviteUri(mAccount.getXmppConnection(), mAccount).execute().get();
@ -1106,30 +1035,31 @@ public abstract class XmppActivity extends ActionBarActivity {
inviteURL = Config.inviteUserURL + user + "/" + domain;
}
Log.d(Config.LOGTAG, "Invite uri = " + inviteURL);
String inviteText = getString(R.string.InviteText, user);
Intent intent = new Intent(Intent.ACTION_SEND);
final String inviteText = getString(R.string.InviteText, user);
final Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_SUBJECT, user + " " + getString(R.string.inviteUser_Subject) + " " + getString(R.string.app_name));
intent.putExtra(Intent.EXTRA_TEXT, inviteText + "\n\n" + inviteURL);
startActivity(Intent.createChooser(intent, getString(R.string.invite_contact)));
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
}
});
builder.setNegativeButton(R.string.cancel, null);
builder.create().show();
});
builder.setNegativeButton(R.string.cancel, null);
builder.create().show();
}
}
}
private void selectAccountToStartEasyInvite() {
private boolean selectAccountToStartEasyInvite() {
final List<Account> accounts = EasyOnboardingInvite.getSupportingAccounts(this.xmppConnectionService);
if (accounts.size() == 0) {
//This can technically happen if opening the menu item races with accounts reconnecting or something
ToastCompat.makeText(this, R.string.no_active_accounts_support_this, ToastCompat.LENGTH_LONG).show();
return false;
} else if (accounts.size() == 1) {
openEasyInviteScreen(accounts.get(0));
} else {
final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
final android.app.AlertDialog.Builder alertDialogBuilder = new android.app.AlertDialog.Builder(this);
final androidx.appcompat.app.AlertDialog.Builder alertDialogBuilder = new androidx.appcompat.app.AlertDialog.Builder(this);
alertDialogBuilder.setTitle(R.string.choose_account);
final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toEscapedString()).toArray(new String[0]);
alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which)));
@ -1137,6 +1067,7 @@ public abstract class XmppActivity extends ActionBarActivity {
alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> openEasyInviteScreen(selectedAccount.get()));
alertDialogBuilder.create().show();
}
return true;
}
private void openEasyInviteScreen(final Account account) {
@ -1457,7 +1388,44 @@ public abstract class XmppActivity extends ActionBarActivity {
return installFromUnknownSource;
}
protected void openInstallFromUnknownSourcesDialogIfNeeded(boolean showToast) {
String ShowToast;
if (showToast == true) {
ShowToast = "true";
} else {
ShowToast = "false";
}
if (!installFromUnknownSourceAllowed() && xmppConnectionService.installedFrom() == null) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.install_from_unknown_sources_disabled);
builder.setMessage(R.string.install_from_unknown_sources_disabled_dialog);
builder.setPositiveButton(R.string.next, (dialog, which) -> {
Intent intent;
if (android.os.Build.VERSION.SDK_INT >= 26) {
intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
Uri uri = Uri.parse("package:" + getPackageName());
intent.setData(uri);
} else {
intent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
}
Log.d(Config.LOGTAG, "Allow install from unknown sources for Android SDK " + Build.VERSION.SDK_INT + " intent " + intent.toString());
try {
startActivityForResult(intent, REQUEST_UNKNOWN_SOURCE_OP);
} catch (ActivityNotFoundException e) {
ToastCompat.makeText(XmppActivity.this, R.string.device_does_not_support_unknown_source_op, ToastCompat.LENGTH_SHORT).show();
} finally {
UpdateService task = new UpdateService(this, xmppConnectionService.installedFrom(), xmppConnectionService);
task.executeOnExecutor(UpdateService.THREAD_POOL_EXECUTOR, ShowToast);
Log.d(Config.LOGTAG, "AppUpdater started");
}
});
builder.create().show();
} else {
UpdateService task = new UpdateService(this, xmppConnectionService.installedFrom(), xmppConnectionService);
task.executeOnExecutor(UpdateService.THREAD_POOL_EXECUTOR, ShowToast);
Log.d(Config.LOGTAG, "AppUpdater started");
}
}
public void ShowAvatarPopup(final Activity activity, final AvatarService.Avatarable user) {
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);

View file

@ -1,10 +1,13 @@
package eu.siacs.conversations.ui.adapter;
import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.preference.PreferenceManager;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.StyleSpan;
import android.util.Pair;
import android.view.LayoutInflater;
@ -32,15 +35,12 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.EmojiWrapper;
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD;
import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube;
public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapter.ConversationViewHolder> {
private static final float INACTIVE_ALPHA = 0.4684f;
@ -162,12 +162,12 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
}
final Pair<CharSequence, Boolean> preview = UIHelper.getMessagePreview(activity, message, viewHolder.binding.conversationLastmsg.getCurrentTextColor());
if (showPreviewText) {
if (message.getBody().equals(DELETED_MESSAGE_BODY)) {
viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(activity.getString(R.string.message_deleted))));
} else if (message.getBody().equals(DELETED_MESSAGE_BODY_OLD)) {
if (message.hasDeletedBody()) {
viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(activity.getString(R.string.message_deleted))));
} else {
viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(replaceYoutube(activity.getApplicationContext(), preview.first.toString()))));
SpannableStringBuilder body = new SpannableStringBuilder(replaceYoutube(activity.getApplicationContext(), preview.first.toString()));
StylingHelper.format(body, viewHolder.binding.conversationLastmsg.getCurrentTextColor(), true);
viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(body)));
}
} else {
viewHolder.binding.conversationLastmsgImg.setContentDescription(preview.first);
@ -220,8 +220,8 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
if (ongoingCall.isPresent()) {
viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
final int ic_ongoing_call = activity.getThemeResource(R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp);
viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call);
final int ic_ongoing_call = activity.getThemeResource(R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp);
viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call);
} else {
final long muted_till = conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
if (muted_till == Long.MAX_VALUE) {
@ -298,18 +298,18 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
if (activity.xmppConnectionService.indicateReceived()) {
switch (message.getMergedStatus()) {
case Message.STATUS_SEND_RECEIVED:
if (viewHolder.binding.indicatorReceived != null) {
viewHolder.binding.indicatorReceived.setVisibility(View.VISIBLE);
viewHolder.binding.indicatorReceived.setImageResource(activity.isDarkTheme() ? R.drawable.ic_check_white_18dp : R.drawable.ic_check_black_18dp);
viewHolder.binding.indicatorReceived.setAlpha(activity.isDarkTheme() ? 0.7f : 0.57f);
}
if (viewHolder.binding.indicatorReceived != null) {
viewHolder.binding.indicatorReceived.setVisibility(View.VISIBLE);
viewHolder.binding.indicatorReceived.setImageResource(activity.isDarkTheme() ? R.drawable.ic_check_white_18dp : R.drawable.ic_check_black_18dp);
viewHolder.binding.indicatorReceived.setAlpha(activity.isDarkTheme() ? 0.7f : 0.57f);
}
break;
case Message.STATUS_SEND_DISPLAYED:
if (viewHolder.binding.indicatorReceived != null) {
viewHolder.binding.indicatorReceived.setVisibility(View.VISIBLE);
viewHolder.binding.indicatorReceived.setImageResource(activity.isDarkTheme() ? R.drawable.ic_check_all_white_18dp : R.drawable.ic_check_all_black_18dp);
viewHolder.binding.indicatorReceived.setAlpha(activity.isDarkTheme() ? 0.7f : 0.57f);
}
if (viewHolder.binding.indicatorReceived != null) {
viewHolder.binding.indicatorReceived.setVisibility(View.VISIBLE);
viewHolder.binding.indicatorReceived.setImageResource(activity.isDarkTheme() ? R.drawable.ic_check_all_white_18dp : R.drawable.ic_check_all_black_18dp);
viewHolder.binding.indicatorReceived.setAlpha(activity.isDarkTheme() ? 0.7f : 0.57f);
}
break;
default:
viewHolder.binding.indicatorReceived.setVisibility(View.GONE);

View file

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

View file

@ -1,14 +1,18 @@
package eu.siacs.conversations.ui.adapter;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
@ -21,6 +25,7 @@ import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.MediaPreviewBinding;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.ConversationFragment;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.Attachment;
@ -89,6 +94,21 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
notifyItemRemoved(pos);
conversationFragment.toggleInputMethod();
});
holder.binding.mediaPreview.setOnClickListener(v -> view(context, attachment));
}
private static void view(final Context context, Attachment attachment) {
final Intent view = new Intent(Intent.ACTION_VIEW);
final Uri uri = FileBackend.getUriForUri(context, attachment.getUri());
view.setDataAndType(uri, attachment.getMime());
view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
context.startActivity(view);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
} catch (final SecurityException e) {
Toast.makeText(context, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show();
}
}
public void addMediaPreviews(List<Attachment> attachments) {

View file

@ -1,11 +1,24 @@
package eu.siacs.conversations.ui.adapter;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD;
import static eu.siacs.conversations.persistance.FileBackend.formatTime;
import static eu.siacs.conversations.persistance.FileBackend.safeLongToInt;
import static eu.siacs.conversations.ui.SettingsActivity.PLAY_GIF_INSIDE;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE;
import static eu.siacs.conversations.ui.util.MyLinkify.removeTrackingParameter;
import static eu.siacs.conversations.ui.util.MyLinkify.removeTrailingBracket;
import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
@ -36,7 +49,6 @@ import android.widget.TextView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.drawable.DrawableCompat;
import com.google.common.base.Strings;
@ -71,6 +83,7 @@ import eu.siacs.conversations.ui.text.DividerSpan;
import eu.siacs.conversations.ui.text.QuoteSpan;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.ui.util.ViewUtil;
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
@ -90,18 +103,6 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
import me.drakeet.support.toast.ToastCompat;
import pl.droidsonroids.gif.GifImageView;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD;
import static eu.siacs.conversations.persistance.FileBackend.formatTime;
import static eu.siacs.conversations.persistance.FileBackend.safeLongToInt;
import static eu.siacs.conversations.ui.SettingsActivity.PLAY_GIF_INSIDE;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE;
import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE;
import static eu.siacs.conversations.ui.util.MyLinkify.removeTrackingParameter;
import static eu.siacs.conversations.ui.util.MyLinkify.removeTrailingBracket;
import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube;
public class MessageAdapter extends ArrayAdapter<Message> {
public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
@ -209,7 +210,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.indicatorReceived.setVisibility(View.GONE);
}
if (viewHolder.edit_indicator != null) {
if (message.edited()) {
if (message.edited() && message.getRetractId() == null) {
viewHolder.edit_indicator.setVisibility(View.VISIBLE);
viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp);
viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f);
@ -217,6 +218,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.edit_indicator.setVisibility(View.GONE);
}
}
if (viewHolder.retract_indicator != null) {
if (message.getRetractId() != null) {
viewHolder.retract_indicator.setVisibility(View.VISIBLE);
viewHolder.retract_indicator.setImageResource(darkBackground ? R.drawable.ic_delete_white_18dp : R.drawable.ic_delete_black_18dp);
viewHolder.retract_indicator.setAlpha(darkBackground ? 0.7f : 0.57f);
} else {
viewHolder.retract_indicator.setVisibility(View.GONE);
}
}
final Transferable transferable = message.getTransferable();
boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
@ -236,7 +246,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
case Message.STATUS_UNSEND:
if (transferable != null) {
info = getContext().getString(R.string.sending);
showProgress(viewHolder, transferable);
showProgress(viewHolder, transferable, message);
} else {
info = getContext().getString(R.string.sending);
}
@ -405,10 +415,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.audioPlayer.setVisibility(View.GONE);
showImages(false, viewHolder);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.VISIBLE);
viewHolder.messageBody.setText(text);
showProgress(viewHolder, message.getTransferable());
showProgress(viewHolder, message.getTransferable(), message);
if (darkBackground) {
viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark);
} else {
@ -417,16 +427,35 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setTextIsSelectable(false);
}
private void showProgress(final ViewHolder viewHolder, final Transferable transferable) {
private void showProgress(final ViewHolder viewHolder, final Transferable transferable, final Message message) {
if (transferable != null) {
if (transferable.getStatus() == Transferable.STATUS_DOWNLOADING || transferable.getStatus() == Transferable.STATUS_UPLOADING || transferable.getStatus() == Transferable.STATUS_WAITING) {
viewHolder.progressBar.setVisibility(View.VISIBLE);
if (message.fileIsTransferring()) {
viewHolder.transfer.setVisibility(View.VISIBLE);
viewHolder.progressBar.setProgress(transferable.getProgress());
Drawable icon = activity.getResources().getDrawable(R.drawable.ic_cancel_black_24dp);
Drawable drawable = DrawableCompat.wrap(icon);
DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent));
viewHolder.cancel_transfer.setImageDrawable(drawable);
viewHolder.cancel_transfer.setEnabled(true);
viewHolder.cancel_transfer.setOnClickListener(v -> {
try {
if (activity instanceof ConversationsActivity) {
ConversationFragment conversationFragment = ConversationFragment.get(activity);
if (conversationFragment != null) {
activity.invalidateOptionsMenu();
conversationFragment.cancelTransmission(message);
}
}
} catch (Exception e) {
viewHolder.cancel_transfer.setEnabled(false);
e.printStackTrace();
}
});
} else {
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
}
} else {
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
}
}
@ -435,7 +464,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.audioPlayer.setVisibility(View.GONE);
showImages(false, viewHolder);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.VISIBLE);
if (darkBackground) {
viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark);
@ -486,7 +515,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
});
showImages(false, viewHolder);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.GONE);
}
@ -511,48 +540,51 @@ public class MessageAdapter extends ArrayAdapter<Message> {
*/
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
boolean startsWithQuote = false;
char previous = '\n';
int lineStart = -1;
int lineTextStart = -1;
int quoteStart = -1;
for (int i = 0; i <= body.length(); i++) {
char current = body.length() > i ? body.charAt(i) : '\n';
if (lineStart == -1) {
if (previous == '\n') {
if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i))
|| current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) {
// Line start with quote
lineStart = i;
if (quoteStart == -1) quoteStart = i;
if (i == 0) startsWithQuote = true;
} else if (quoteStart >= 0) {
// Line start without quote, apply spans there
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
quoteStart = -1;
int quoteDepth = 1;
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
char previous = '\n';
int lineStart = -1;
int lineTextStart = -1;
int quoteStart = -1;
for (int i = 0; i <= body.length(); i++) {
char current = body.length() > i ? body.charAt(i) : '\n';
if (lineStart == -1) {
if (previous == '\n') {
if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
// Line start with quote
lineStart = i;
if (quoteStart == -1) quoteStart = i;
if (i == 0) startsWithQuote = true;
} else if (quoteStart >= 0) {
// Line start without quote, apply spans there
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
quoteStart = -1;
}
}
} else {
// Remove extra spaces between > and first character in the line
// > character will be removed too
if (current != ' ' && lineTextStart == -1) {
lineTextStart = i;
}
if (current == '\n') {
body.delete(lineStart, lineTextStart);
i -= lineTextStart - lineStart;
if (i == lineStart) {
// Avoid empty lines because span over empty line can be hidden
body.insert(i++, " ");
}
lineStart = -1;
lineTextStart = -1;
}
}
} else {
// Remove extra spaces between > and first character in the line
// > character will be removed too
if (current != ' ' && lineTextStart == -1) {
lineTextStart = i;
}
if (current == '\n') {
body.delete(lineStart, lineTextStart);
i -= lineTextStart - lineStart;
if (i == lineStart) {
// Avoid empty lines because span over empty line can be hidden
body.insert(i++, " ");
}
lineStart = -1;
lineTextStart = -1;
}
previous = current;
}
previous = current;
}
if (quoteStart >= 0) {
// Apply spans to finishing open quote
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
if (quoteStart >= 0) {
// Apply spans to finishing open quote
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
}
quoteDepth++;
}
return startsWithQuote;
}
@ -561,7 +593,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.download_button.setVisibility(View.GONE);
showImages(false, viewHolder);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
viewHolder.audioPlayer.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.VISIBLE);
if (darkBackground) {
@ -587,7 +619,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
body.append("\u2026");
}
final Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class);
final Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class);
for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
int start = body.getSpanStart(mergeSeparator);
int end = body.getSpanEnd(mergeSeparator);
@ -636,7 +668,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
body.setSpan(new RelativeSizeSpan(1.5f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor(), true);
if (highlightedTerm != null) {
StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody));
}
@ -659,7 +691,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.audioPlayer.setVisibility(View.GONE);
showImages(false, viewHolder);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
viewHolder.download_button.setVisibility(View.VISIBLE);
viewHolder.download_button.setText(text);
final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_download_grey600_48dp);
@ -675,7 +707,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.audioPlayer.setVisibility(View.GONE);
showImages(false, viewHolder);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
final String mimeType = message.getMimeType();
if (mimeType != null && message.getMimeType().contains("vcard")) {
try {
@ -764,7 +796,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.audioPlayer.setVisibility(View.GONE);
showImages(false, viewHolder);
viewHolder.download_button.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
String url;
if (message.isWebUri()) {
url = removeTrackingParameter(Uri.parse(message.getBody().trim())).toString();
@ -786,9 +818,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else {
scaledH = (int) (100 / ((double) 100 / target));
}
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(WRAP_CONTENT, scaledH);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4));
viewHolder.richlinkview.setLayoutParams(layoutParams);
viewHolder.richlinkview.setMinimumHeight(scaledH);
final String weburl;
if (link.startsWith("http://") || link.startsWith("https://")) {
weburl = removeTrailingBracket(link);
@ -817,7 +850,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
final String url = GeoHelper.MapPreviewUri(message, activity);
showImages(false, viewHolder);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
if (mShowMapsInside) {
showImages(mShowMapsInside, 0, false, viewHolder);
final double target = activity.getResources().getDimension(R.dimen.image_preview_width);
@ -858,10 +891,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground) {
final Resources res = activity.getResources();
viewHolder.messageBody.setWidth((int) res.getDimension(R.dimen.audio_player_width));
toggleWhisperInfo(viewHolder, message, showTitle(message), darkBackground);
showImages(false, viewHolder);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
viewHolder.download_button.setVisibility(View.GONE);
final RelativeLayout audioPlayer = viewHolder.audioPlayer;
audioPlayer.setVisibility(View.VISIBLE);
@ -899,9 +934,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.download_button.setVisibility(View.GONE);
viewHolder.audioPlayer.setVisibility(View.GONE);
viewHolder.richlinkview.setVisibility(View.GONE);
viewHolder.progressBar.setVisibility(View.GONE);
viewHolder.transfer.setVisibility(View.GONE);
final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
if (!file.exists()) {
if (!file.exists() && !message.isFileDeleted()) {
markFileDeleted(message);
displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted), darkBackground, message);
ToastCompat.makeText(activity, R.string.file_deleted, ToastCompat.LENGTH_SHORT).show();
return;
}
@ -1092,6 +1129,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.resend_button = view.findViewById(R.id.resend_button);
viewHolder.indicator = view.findViewById(R.id.security_indicator);
viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
viewHolder.retract_indicator = view.findViewById(R.id.retract_indicator);
viewHolder.images = view.findViewById(R.id.images);
viewHolder.mediaduration = view.findViewById(R.id.media_duration);
viewHolder.image = view.findViewById(R.id.message_image);
@ -1101,7 +1139,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.user = view.findViewById(R.id.message_user);
viewHolder.time = view.findViewById(R.id.message_time);
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
viewHolder.transfer = view.findViewById(R.id.transfer);
viewHolder.progressBar = view.findViewById(R.id.progressBar);
viewHolder.cancel_transfer = view.findViewById(R.id.cancel_transfer);
break;
case RECEIVED:
view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false);
@ -1113,6 +1153,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.answer_button = view.findViewById(R.id.answer);
viewHolder.indicator = view.findViewById(R.id.security_indicator);
viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
viewHolder.retract_indicator = view.findViewById(R.id.retract_indicator);
viewHolder.images = view.findViewById(R.id.images);
viewHolder.mediaduration = view.findViewById(R.id.media_duration);
viewHolder.image = view.findViewById(R.id.message_image);
@ -1123,7 +1164,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.time = view.findViewById(R.id.message_time);
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
viewHolder.encryption = view.findViewById(R.id.message_encryption);
viewHolder.transfer = view.findViewById(R.id.transfer);
viewHolder.progressBar = view.findViewById(R.id.progressBar);
viewHolder.cancel_transfer = view.findViewById(R.id.cancel_transfer);
break;
case STATUS:
view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false);
@ -1153,9 +1196,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
}
viewHolder.message_box.setBackgroundResource(darkBackground ? R.drawable.date_bubble_dark : R.drawable.date_bubble);
int date_bubble_color = ColorUtils.setAlphaComponent(StyledAttributes.getColor(activity, R.attr.colorAccent), 80); //set alpha to date_bubble
activity.setBubbleColor(viewHolder.message_box, (date_bubble_color), -1); // themed color
return view;
} else if (type == RTP_SESSION) {
final boolean isDarkTheme = activity.isDarkTheme();
@ -1179,9 +1219,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful, isDarkTheme));
viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f);
viewHolder.message_box.setBackgroundResource(darkBackground ? R.drawable.date_bubble_dark : R.drawable.date_bubble);
int date_bubble_color = ColorUtils.setAlphaComponent(StyledAttributes.getColor(activity, R.attr.colorAccent), 80); //set alpha to date bubble
activity.setBubbleColor(viewHolder.message_box, (date_bubble_color), -1); //themed color
return view;
} else if (type == STATUS) {
if ("LOAD_MORE".equals(message.getBody())) {
@ -1240,6 +1277,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground);
} else {
if (!activity.xmppConnectionService.getFileBackend().getFile(message).exists() && !message.isFileDeleted()) {
markFileDeleted(message);
displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted), darkBackground, message);
}
if (checkFileExistence(message, view, viewHolder)) {
markFileExisting(message);
} else {
@ -1282,12 +1323,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (message.treatAsDownloadable()) {
try {
final URI uri = new URI(message.getBody());
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize_on_host,
UIHelper.getFileDescriptionString(activity, message),
uri.getHost()),
darkBackground);
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize_on_host,
UIHelper.getFileDescriptionString(activity, message),
uri.getHost()),
darkBackground);
} catch (Exception e) {
displayDownloadableMessage(viewHolder,
message,
@ -1348,10 +1389,21 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
private void markFileExisting(Message message) {
Log.d(Config.LOGTAG, "Found and restored orphaned file " + message.getRelativeFilePath());
message.setFileDeleted(false);
activity.xmppConnectionService.updateMessage(message, false);
activity.xmppConnectionService.updateConversation((Conversation) message.getConversation());
new Thread(() -> {
Log.d(Config.LOGTAG, "Found and restored orphaned file " + message.getRelativeFilePath());
message.setFileDeleted(false);
activity.xmppConnectionService.updateMessage(message, false);
activity.xmppConnectionService.updateConversation((Conversation) message.getConversation());
}).start();
}
private void markFileDeleted(Message message) {
new Thread(() -> {
Log.d(Config.LOGTAG, "Mark file deleted " + message.getRelativeFilePath());
message.setFileDeleted(true);
activity.xmppConnectionService.updateMessage(message, false);
activity.xmppConnectionService.updateConversation((Conversation) message.getConversation());
}).start();
}
private boolean checkFileExistence(Message message, View view, ViewHolder viewHolder) {
@ -1436,6 +1488,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
public Button load_more_messages;
public ImageView edit_indicator;
public ImageView retract_indicator;
public RelativeLayout audioPlayer;
public RelativeLayout images;
protected LinearLayout message_box;
@ -1455,7 +1508,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
protected ImageView contact_picture;
protected TextView status_message;
protected TextView encryption;
protected RelativeLayout transfer;
protected ProgressBar progressBar;
protected ImageButton cancel_transfer;
}
public void setBubbleBackgroundColor(final View viewHolder, final int type,
@ -1490,4 +1545,4 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
}
}
}
}

View file

@ -0,0 +1,97 @@
package eu.siacs.conversations.ui.adapter;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY;
import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Date;
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.adapter.model.MessageLogModel;
public class MessageLogAdapter extends ArrayAdapter<MessageLogModel> implements View.OnClickListener {
private final ArrayList<MessageLogModel> dataSet;
Context mContext;
// View lookup cache
private static class ViewHolder {
TextView txtLineNr;
TextView txtBody;
TextView txtTimeSent;
}
public MessageLogAdapter(ArrayList<MessageLogModel> data, Context context) {
super(context, R.layout.message_log_item, data);
this.dataSet = data;
this.mContext = context;
}
@Override
public void onClick(View v) {
}
private int lastPosition = -1;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Get the data item for this position
MessageLogModel dataModel = getItem(position);
// Check if an existing view is being reused, otherwise inflate the view
ViewHolder viewHolder; // view lookup cache stored in tag
final View result;
if (convertView == null) {
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.message_log_item, parent, false);
viewHolder.txtLineNr = convertView.findViewById(R.id.nr);
viewHolder.txtBody = convertView.findViewById(R.id.body);
viewHolder.txtTimeSent = convertView.findViewById(R.id.timeSent);
result = convertView;
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
result = convertView;
}
Animation animation = AnimationUtils.loadAnimation(mContext, (position > lastPosition) ? R.anim.ufb : R.anim.dft);
result.startAnimation(animation);
lastPosition = position;
viewHolder.txtLineNr.setText(String.valueOf(position + 1));
viewHolder.txtBody.setText(preview(dataModel));
viewHolder.txtTimeSent.setText(getTimeSentFormated(dataModel.getTimeSent()));
// Return the completed view to render on screen
return convertView;
}
private String getTimeSentFormated(long timeSent) {
return android.text.format.DateFormat.getDateFormat(getContext()).format(new Date(timeSent)) + " " + android.text.format.DateFormat.getTimeFormat(getContext()).format(new Date(timeSent));
}
private String preview(MessageLogModel dataModel) {
if (hasDeletedBody(dataModel.getBody())) {
return getContext().getString(R.string.message_deleted);
} else {
return dataModel.getBody();
}
}
public boolean hasDeletedBody(String message) {
return message.trim().equals(DELETED_MESSAGE_BODY) || message.trim().equals(DELETED_MESSAGE_BODY_OLD);
}
}

View file

@ -67,7 +67,7 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
final MucOptions.User user = getItem(position);
AvatarWorkerTask.loadAvatar(user, viewHolder.binding.contactPhoto, R.dimen.avatar);
AvatarWorkerTask.loadAvatar(user, viewHolder.binding.contactPhoto, R.dimen.avatar_on_details_screen_size);
viewHolder.binding.getRoot().setOnClickListener(v -> {
final XmppActivity activity = XmppActivity.find(v);
if (activity != null) {
@ -115,8 +115,6 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
} else {
viewHolder.binding.key.setVisibility(View.GONE);
}
}
public MucOptions.User getSelectedUser() {

View file

@ -58,7 +58,6 @@ public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreview
return selectedUser;
}
class ViewHolder extends RecyclerView.ViewHolder {
private final UserPreviewBinding binding;

View file

@ -0,0 +1,21 @@
package eu.siacs.conversations.ui.adapter.model;
public class MessageLogModel {
String body;
long timeSent;
public MessageLogModel(String body, long timeSent) {
this.body = body;
this.timeSent = timeSent;
}
public String getBody() {
return body;
}
public long getTimeSent() {
return timeSent;
}
}

View file

@ -0,0 +1,114 @@
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 resetCustomActionBarOnClickListeners(@NonNull View view) {
final View title = view.findViewById(android.R.id.text1);
final View subtitle = view.findViewById(android.R.id.text2);
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);
}
}
public static void setCustomActionBarOnClickListener(@NonNull View view,
@NonNull final View.OnClickListener onClickListener) {
final View title = view.findViewById(android.R.id.text1);
final View subtitle = view.findViewById(android.R.id.text2);
if (title != null) {
title.setOnClickListener(onClickListener);
}
if (subtitle != null) {
subtitle.setOnClickListener(onClickListener);
}
}
private static @Nullable
View findActionBarTitle(@NonNull View root) {
return findActionBarItem(root, "action_bar_title", "mTitleTextView");
}
private static @Nullable
View findActionBarSubTitle(@NonNull View root) {
return findActionBarItem(root, "action_bar_subtitle", "mSubtitleTextView");
}
private static @Nullable
View findActionBarItem(@NonNull View root,
@NonNull String resourceName,
@NonNull String toolbarFieldName) {
View result = findViewSupportOrAndroid(root, resourceName);
if (result == null) {
View actionBar = findViewSupportOrAndroid(root, "action_bar");
if (actionBar != null) {
result = reflectiveRead(actionBar, toolbarFieldName);
}
}
if (result == null && root.getClass().getName().endsWith("widget.Toolbar")) {
result = reflectiveRead(root, toolbarFieldName);
}
return result;
}
@SuppressWarnings("ConstantConditions")
private static @Nullable
View findViewSupportOrAndroid(@NonNull View root,
@NonNull String resourceName) {
Context context = root.getContext();
View result = null;
if (result == null) {
int supportID = context.getResources().getIdentifier(resourceName, "id", context.getPackageName());
result = root.findViewById(supportID);
}
if (result == null) {
int androidID = context.getResources().getIdentifier(resourceName, "id", "android");
result = root.findViewById(androidID);
}
return result;
}
@SuppressWarnings("unchecked")
private static <T> T reflectiveRead(@NonNull Object object, @NonNull String fieldName) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (T) field.get(object);
} catch (final Exception ex) {
return null;
}
}
}

View file

@ -46,7 +46,6 @@ import java.util.List;
import java.util.UUID;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.MimeUtils;
public class Attachment implements Parcelable {
@ -138,10 +137,10 @@ public class Attachment implements Parcelable {
return Collections.singletonList(new Attachment(uri, type, mime));
}
public static List<Attachment> of(final Context context, List<Uri> uris) {
List<Attachment> attachments = new ArrayList<>();
for (Uri uri : uris) {
final String mime = MimeUtils.guessMimeTypeFromUri(context, uri);
public static List<Attachment> of(final Context context, List<Uri> uris, final String type) {
final List<Attachment> attachments = new ArrayList<>();
for (final Uri uri : uris) {
final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type);
attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
}
return attachments;
@ -182,7 +181,7 @@ public class Attachment implements Parcelable {
private static boolean renderFileThumbnail(final String mime) {
return mime.startsWith("video/")
|| isImage(mime)
|| (Compatibility.runsTwentyOne() && "application/pdf".equals(mime));
|| "application/pdf".equals(mime);
}
public Uri getUri() {

View file

@ -132,7 +132,6 @@ public class ConversationMenuConfigurator {
if (conversation.getNextEncryption() != Message.ENCRYPTION_NONE) {
menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
}
otr.setVisible(Config.supportOtr() && activity.enableOTR());
if (conversation.getMode() == Conversation.MODE_MULTI) {
otr.setVisible(false);
}

View file

@ -11,8 +11,6 @@ import androidx.browser.customtabs.CustomTabsIntent;
import eu.siacs.conversations.R;
import static eu.siacs.conversations.utils.Compatibility.runsTwentyOne;
public class CustomTab {
public static void openTab(Context context, Uri uri, boolean dark) throws ActivityNotFoundException {
CustomTabsIntent.Builder tabBuilder = new CustomTabsIntent.Builder();
@ -25,9 +23,7 @@ public class CustomTab {
tabBuilder.setColorScheme(dark ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT);
tabBuilder.setShareState(CustomTabsIntent.SHARE_STATE_ON);
CustomTabsIntent customTabsIntent = tabBuilder.build();
if (runsTwentyOne()) {
customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
}
customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
customTabsIntent.intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
customTabsIntent.launchUrl(context, uri);
}

View file

@ -50,10 +50,7 @@ public class GridManager {
private static ColumnInfo calculateColumnCount(Context context, int availableWidth, @DimenRes int desiredSize) {
final float desiredWidth = context.getResources().getDimension(desiredSize);
int columns = Math.round(availableWidth / desiredWidth);
if (columns < 1) {
columns = 1;
}
final int columns = Math.round(availableWidth / desiredWidth);
final int realWidth = availableWidth / columns;
Log.d(Config.LOGTAG, "desired=" + desiredWidth + " real=" + realWidth);
return new ColumnInfo(columns, realWidth);

View file

@ -0,0 +1,121 @@
package eu.siacs.conversations.ui.util;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager;
import java.util.HashMap;
/**
* Based on the following Stackoverflow answer:
* http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
* https://github.com/ravindu1024/android-keyboardlistener/tree/master/keyboard-listener/src/main/java/com/rw/keyboardlistener
*/
@SuppressWarnings("WeakerAccess")
public class KeyboardUtils implements ViewTreeObserver.OnGlobalLayoutListener {
private final static int MAGIC_NUMBER = 200;
private SoftKeyboardToggleListener mCallback;
private View mRootView;
private Boolean prevValue = null;
private float mScreenDensity;
private static HashMap<SoftKeyboardToggleListener, KeyboardUtils> sListenerMap = new HashMap<>();
public interface SoftKeyboardToggleListener {
void onToggleSoftKeyboard(boolean isVisible);
}
@Override
public void onGlobalLayout() {
Rect r = new Rect();
mRootView.getWindowVisibleDisplayFrame(r);
int heightDiff = mRootView.getRootView().getHeight() - (r.bottom - r.top);
float dp = heightDiff / mScreenDensity;
boolean isVisible = dp > MAGIC_NUMBER;
if (mCallback != null && (prevValue == null || isVisible != prevValue)) {
prevValue = isVisible;
mCallback.onToggleSoftKeyboard(isVisible);
}
}
/**
* Add a new keyboard listener
*
* @param act calling activity
* @param listener callback
*/
public static void addKeyboardToggleListener(Activity act, SoftKeyboardToggleListener listener) {
removeKeyboardToggleListener(listener);
sListenerMap.put(listener, new KeyboardUtils(act, listener));
}
/**
* Remove a registered listener
*
* @param listener {@link SoftKeyboardToggleListener}
*/
public static void removeKeyboardToggleListener(SoftKeyboardToggleListener listener) {
if (sListenerMap.containsKey(listener)) {
KeyboardUtils k = sListenerMap.get(listener);
k.removeListener();
sListenerMap.remove(listener);
}
}
/**
* Remove all registered keyboard listeners
*/
public static void removeAllKeyboardToggleListeners() {
for (SoftKeyboardToggleListener l : sListenerMap.keySet())
sListenerMap.get(l).removeListener();
sListenerMap.clear();
}
/**
* Manually toggle soft keyboard visibility
*
* @param context calling context
*/
public static void toggleKeyboardVisibility(Context context) {
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null)
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
/**
* Force closes the soft keyboard
*
* @param activeView the view with the keyboard focus
*/
public static void forceCloseKeyboard(View activeView) {
InputMethodManager inputMethodManager = (InputMethodManager) activeView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null)
inputMethodManager.hideSoftInputFromWindow(activeView.getWindowToken(), 0);
}
private void removeListener() {
mCallback = null;
mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
private KeyboardUtils(Activity act, SoftKeyboardToggleListener listener) {
mCallback = listener;
mRootView = ((ViewGroup) act.findViewById(android.R.id.content)).getChildAt(0);
mRootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
mScreenDensity = act.getResources().getDisplayMetrics().density;
}
}

View file

@ -0,0 +1,69 @@
package eu.siacs.conversations.ui.util;
import android.location.Location;
import org.osmdroid.util.GeoPoint;
import eu.siacs.conversations.Config;
public final class LocationHelper {
/**
* Parses a lat long string in the form "lat,long".
*
* @param latlong A string in the form "lat,long"
* @return A GeoPoint representing the lat,long string.
* @throws NumberFormatException If an invalid lat or long is specified.
*/
public static GeoPoint parseLatLong(final String latlong) throws NumberFormatException {
if (latlong == null || latlong.isEmpty()) {
return null;
}
final String[] parts = latlong.split(",");
if (parts[1].contains("?")) {
parts[1] = parts[1].substring(0, parts[1].indexOf("?"));
}
return new GeoPoint(Double.valueOf(parts[0]), Double.valueOf(parts[1]));
}
private static boolean isSameProvider(final String provider1, final String provider2) {
if (provider1 == null) {
return provider2 == null;
}
return provider1.equals(provider2);
}
public static boolean isBetterLocation(final Location location, final Location prevLoc) {
if (prevLoc == null) {
return true;
}
// Check whether the new location fix is newer or older
final long timeDelta = location.getTime() - prevLoc.getTime();
final boolean isSignificantlyNewer = timeDelta > Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA;
final boolean isSignificantlyOlder = timeDelta < -Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA;
final boolean isNewer = timeDelta > 0;
if (isSignificantlyNewer) {
return true;
} else if (isSignificantlyOlder) {
return false;
}
// Check whether the new location fix is more or less accurate
final int accuracyDelta = (int) (location.getAccuracy() - prevLoc.getAccuracy());
final boolean isLessAccurate = accuracyDelta > 0;
final boolean isMoreAccurate = accuracyDelta < 0;
final boolean isSignificantlyLessAccurate = accuracyDelta > 200;
// Check if the old and new location are from the same provider
final boolean isFromSameProvider = isSameProvider(location.getProvider(), prevLoc.getProvider());
// Determine location quality using a combination of timeliness and accuracy
if (isMoreAccurate) {
return true;
} else if (isNewer && !isLessAccurate) {
return true;
} else return isNewer && !isSignificantlyLessAccurate && isFromSameProvider;
}
}

View file

@ -32,18 +32,22 @@ package eu.siacs.conversations.ui.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.util.Linkify;
import android.util.Base64;
import android.util.Log;
import android.webkit.URLUtil;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -53,12 +57,32 @@ import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.text.FixedURLSpan;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.TrackingHelper;
import eu.siacs.conversations.utils.XmppUri;
public class MyLinkify {
private static final Pattern youtubePattern = Pattern.compile("(www\\.|m\\.)?(youtube\\.com|youtu\\.be|youtube-nocookie\\.com)\\/(((?!(\"|'|<)).)*)");
private final static Pattern youtubePattern = Pattern.compile("(www\\.|m\\.)?(youtube\\.com|youtu\\.be|youtube-nocookie\\.com)/(((?!([\"'<])).)*)");
private final static String youtubeURLPattern = "(?:youtube(?:-nocookie)?\\.com\\/(?:[^\\/\\n\\s]+\\/\\S+\\/|(?:v|e(?:mbed)?)\\/|\\S*?[?&]v=)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})";
public static boolean isYoutubeUrl(String url) {
return !url.isEmpty() && url.matches("(?i:http|https):\\/\\/" + youtubePattern);
}
public static String getYoutubeVideoId(String url) {
if (url == null || url.trim().length() <= 0) {
return null;
}
final Pattern pattern = Pattern.compile(youtubeURLPattern, Pattern.CASE_INSENSITIVE);
final Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
public static String getYoutubeImageUrl(String url) {
return "https://img.youtube.com/vi/" + getYoutubeVideoId(url) + "/0.jpg";
}
public static String replaceYoutube(Context context, String content) {
return replaceYoutube(context, new SpannableStringBuilder(content)).toString();
@ -69,30 +93,53 @@ public class MyLinkify {
if (useInvidious(context)) {
while (matcher.find()) {
final String youtubeId = matcher.group(3);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (matcher.group(2) != null && Objects.equals(matcher.group(2), "youtu.be")) {
content = new SpannableStringBuilder(content.toString().replaceAll("(?i)https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true")));
content = new SpannableStringBuilder(content.toString().replaceAll(">" + Pattern.quote(matcher.group()), Matcher.quoteReplacement(">" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true")));
} else {
content = new SpannableStringBuilder(content.toString().replaceAll("(?i)https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/" + youtubeId + "&local=true")));
content = new SpannableStringBuilder(content.toString().replaceAll(">" + Pattern.quote(matcher.group()), Matcher.quoteReplacement(">" + invidiousHost(context) + "/" + youtubeId + "&local=true")));
}
if (matcher.group(2) != null && matcher.group(2).equals("youtu.be")) {
content = new SpannableStringBuilder(content.toString().replaceAll("https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true")));
} else {
if (matcher.group(2) != null && matcher.group(2) == "youtu.be") {
content = new SpannableStringBuilder(content.toString().replaceAll("(?i)https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true")));
content = new SpannableStringBuilder(content.toString().replaceAll(">" + Pattern.quote(matcher.group()), Matcher.quoteReplacement(">" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true")));
} else {
content = new SpannableStringBuilder(content.toString().replaceAll("(?i)https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/" + youtubeId + "&local=true")));
content = new SpannableStringBuilder(content.toString().replaceAll(">" + Pattern.quote(matcher.group()), Matcher.quoteReplacement(">" + invidiousHost(context) + "/" + youtubeId + "&local=true")));
}
content = new SpannableStringBuilder(content.toString().replaceAll("https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/" + youtubeId + "&local=true")));
}
}
}
return content;
}
// https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/AdapterMessage.java
// https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
// https://github.com/newhouse/url-tracking-stripper
private static final List<String> PARANOID_QUERY = Collections.unmodifiableList(Arrays.asList(
// https://en.wikipedia.org/wiki/UTM_parameters
"awt_a", // AWeber
"awt_l", // AWeber
"awt_m", // AWeber
"icid", // Adobe
"gclid", // Google
"gclsrc", // Google ads
"dclid", // DoubleClick (Google)
"fbclid", // Facebook
"igshid", // Instagram
"mc_cid", // MailChimp
"mc_eid", // MailChimp
"zanpid", // Zanox (Awin)
"kclickid" // https://support.freespee.com/hc/en-us/articles/202577831-Kenshoo-integration
));
// https://github.com/snarfed/granary/blob/master/granary/facebook.py#L1789
private static final List<String> FACEBOOK_WHITELIST_PATH = Collections.unmodifiableList(Arrays.asList(
"/nd/", "/n/", "/story.php"
));
private static final List<String> FACEBOOK_WHITELIST_QUERY = Collections.unmodifiableList(Arrays.asList(
"story_fbid", "fbid", "id", "comment_id"
));
public static SpannableString removeTrackingParameter(Uri uri) {
if (uri.isOpaque())
return new SpannableString(uri.toString());
boolean changed = false;
Uri url;
Uri.Builder builder;
@ -101,6 +148,12 @@ public class MyLinkify {
!TextUtils.isEmpty(uri.getQueryParameter("url"))) {
changed = true;
url = Uri.parse(uri.getQueryParameter("url"));
} else if ("https".equals(uri.getScheme()) &&
"smex-ctp.trendmicro.com".equals(uri.getHost()) &&
"/wis/clicktime/v1/query".equals(uri.getPath()) &&
!TextUtils.isEmpty(uri.getQueryParameter("url"))) {
changed = true;
url = Uri.parse(uri.getQueryParameter("url"));
} else if ("https".equals(uri.getScheme()) &&
"www.google.com".equals(uri.getHost()) &&
uri.getPath() != null &&
@ -121,26 +174,53 @@ public class MyLinkify {
}
changed = (result != null);
url = (result == null ? uri : result);
} else {
} else if (uri.getQueryParameterNames().size() == 1) {
// Sophos Email Appliance
Uri result = null;
String key = uri.getQueryParameterNames().iterator().next();
if (TextUtils.isEmpty(uri.getQueryParameter(key)))
try {
String data = new String(Base64.decode(key, Base64.DEFAULT));
int v = data.indexOf("ver=");
int u = data.indexOf("&&url=");
if (v == 0 && u > 0)
result = Uri.parse(URLDecoder.decode(data.substring(u + 6), StandardCharsets.UTF_8.name()));
} catch (Throwable ex) {
ex.printStackTrace();
}
changed = (result != null);
url = (result == null ? uri : result);
} else
url = uri;
}
if (url.isOpaque()) {
return new SpannableString(uri.toString());
//return uri;
}
builder = url.buildUpon();
builder.clearQuery();
for (String key : url.getQueryParameterNames())
String host = uri.getHost();
String path = uri.getPath();
if (host != null)
host = host.toLowerCase(Locale.ROOT);
if (path != null)
path = path.toLowerCase(Locale.ROOT);
boolean first = "www.facebook.com".equals(host);
for (String key : url.getQueryParameterNames()) {
// https://en.wikipedia.org/wiki/UTM_parameters
// https://docs.oracle.com/en/cloud/saas/marketing/eloqua-user/Help/EloquaAsynchronousTrackingScripts/EloquaTrackingParameters.htm
if (key.toLowerCase(Locale.ROOT).startsWith("utm_") ||
key.toLowerCase(Locale.ROOT).startsWith("elq") ||
TrackingHelper.TRACKING_PARAMETER.contains(key.toLowerCase(Locale.ROOT)) ||
("snr".equals(key) && "store.steampowered.com".equals(uri.getHost())))
String lkey = key.toLowerCase(Locale.ROOT);
if (PARANOID_QUERY.contains(lkey) ||
lkey.startsWith("utm_") ||
lkey.startsWith("elq") ||
((host != null && host.endsWith("facebook.com")) &&
!first &&
FACEBOOK_WHITELIST_PATH.contains(path) &&
!FACEBOOK_WHITELIST_QUERY.contains(lkey)) ||
("store.steampowered.com".equals(host) &&
"snr".equals(lkey)))
changed = true;
else if (!TextUtils.isEmpty(key))
for (String value : url.getQueryParameters(key)) {
Log.i(Config.LOGTAG, "Query " + key + "=" + value);
Log.d(Config.LOGTAG, "Query " + key + "=" + value);
Uri suri = Uri.parse(value);
if ("http".equals(suri.getScheme()) || "https".equals(suri.getScheme())) {
Uri s = Uri.parse(removeTrackingParameter(suri).toString());
@ -151,6 +231,8 @@ public class MyLinkify {
}
builder.appendQueryParameter(key, value);
}
first = false;
}
return (changed ? new SpannableString(builder.build().toString()) : new SpannableString(uri.toString()));
}
@ -202,16 +284,13 @@ public class MyLinkify {
return false;
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (end < cs.length()) {
// Reject strings that were probably matched only because they contain a dot followed by
// by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java)
if (isAlphabetic(cs.charAt(end - 1)) && isAlphabetic(cs.charAt(end))) {
return false;
}
if (end < cs.length()) {
// Reject strings that were probably matched only because they contain a dot followed by
// by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java)
if (isAlphabetic(cs.charAt(end - 1)) && isAlphabetic(cs.charAt(end))) {
return false;
}
}
return true;
};
@ -221,20 +300,7 @@ public class MyLinkify {
};
private static boolean isAlphabetic(final int code) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return Character.isAlphabetic(code);
}
switch (Character.getType(code)) {
case Character.UPPERCASE_LETTER:
case Character.LOWERCASE_LETTER:
case Character.TITLECASE_LETTER:
case Character.MODIFIER_LETTER:
case Character.OTHER_LETTER:
case Character.LETTER_NUMBER:
return true;
default:
return false;
}
return Character.isAlphabetic(code);
}
private static String invidiousHost(Context context) {
@ -248,8 +314,7 @@ public class MyLinkify {
private static boolean useInvidious(Context context) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean invidious = sharedPreferences.getBoolean(SettingsActivity.USE_INVIDIOUS, context.getResources().getBoolean(R.bool.use_invidious));
return invidious;
return sharedPreferences.getBoolean(SettingsActivity.USE_INVIDIOUS, context.getResources().getBoolean(R.bool.use_invidious));
}
public static void addLinks(Editable body, boolean includeGeo) {

View file

@ -1,51 +1,114 @@
package eu.siacs.conversations.ui.util;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.UIHelper;
public class QuoteHelper {
public static boolean isPositionQuoteCharacter(CharSequence body, int pos){
return body.charAt(pos) == '>';
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 );
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){
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.isPositionFollowedByWhitespace(body, pos)
|| isPositionFollowedByQuoteChar(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)){
public static boolean bodyContainsQuoteStart(CharSequence body) {
for (int i = 0; i < body.length(); i++) {
if (isPositionQuoteStart(body, i)) {
return true;
}
}
return false;
}
/*public static int getQuoteColors(XmppActivity activity, boolean darkBackground, int quoteDepth){
int[] colorsLight = R.style.ConversationsTheme_Dark;
int[] colorsDark = Config.QUOTE_COLOR_ARRAY_DARK;
Collections.rotate(Collections.singletonList(colorsLight), quoteDepth);
Collections.rotate(Collections.singletonList(colorsDark), quoteDepth);
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;
}
Arrays.stream(colorsLight).toArray();
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;
}
int quoteColors = darkBackground ? ContextCompat.getColor(activity, colorsLight[quoteDepth-1])
: ContextCompat.getColor(activity, colorsDark[quoteDepth-1]);
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;
}
Collections.rotate
return quoteColors;
};*/
}
public static boolean isMessageQuoteable(Message m){
if (m.isTypeText() && MessageUtils.prepareQuote(m).length() > 0) {
return true;
}
if (m.isFileOrImage()){
return true;
}
return false;
}
}

View file

@ -23,4 +23,4 @@ public final class Rationals {
return input;
}
}
}

View file

@ -97,10 +97,10 @@ public class ShareUtil {
url = message.getBody();
} else if (message.hasFileOnRemoteHost()) {
resId = R.string.file_url;
url = message.getFileParams().url;
url = message.getFileParams().url;
} else {
final Message.FileParams fileParams = message.getFileParams();
url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim();
final Message.FileParams fileParams = message.getFileParams();
url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim();
resId = R.string.file_url;
}
if (activity.copyTextToClipboard(url, resId)) {

View file

@ -28,12 +28,12 @@ import me.drakeet.support.toast.ToastCompat;
public class UpdateHelper {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
private static final String INSTALL_DATE = "2020-11-01";
private static final String monocles_message = "MONOCLES_CHAT_UPDATE_MESSAGE";
private static final String monocles_message = "MONOCLES.IM_UPDATE_MESSAGE";
private static boolean moveData = true;
private static boolean dataMoved = false;
private static final File PAM_MainDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/");
private static final File Monocles_MainDirectory = new File(Environment.getExternalStorageDirectory() + "/monocles chat/");
private static final File monocles_MainDirectory = new File(Environment.getExternalStorageDirectory() + "/monocles chat/");
private static final File PAM_PicturesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Images/");
private static final File PAM_FilesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Files/");
private static final File PAM_AudiosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Audios/");
@ -115,7 +115,7 @@ public class UpdateHelper {
private static void checkOldData() {
if (PAM_MainDirectory.exists() && PAM_MainDirectory.isDirectory()) {
if (Monocles_MainDirectory.exists() && Monocles_MainDirectory.isDirectory()) {
if (monocles_MainDirectory.exists() && monocles_MainDirectory.isDirectory()) {
moveData = false;
} else {
moveData = true;
@ -180,16 +180,16 @@ public class UpdateHelper {
}
}
if (PAM_MainDirectory.exists() && PAM_MainDirectory.isDirectory()) {
Monocles_MainDirectory.mkdirs();
monocles_MainDirectory.mkdirs();
final File[] files = PAM_MainDirectory.listFiles();
if (files == null) {
return;
}
if (PAM_MainDirectory.renameTo(Monocles_MainDirectory)) {
if (PAM_MainDirectory.renameTo(monocles_MainDirectory)) {
dataMoved = true;
Log.d(Config.LOGTAG, "moved " + PAM_MainDirectory.getAbsolutePath() + " to " + Monocles_MainDirectory.getAbsolutePath());
Log.d(Config.LOGTAG, "moved " + PAM_MainDirectory.getAbsolutePath() + " to " + monocles_MainDirectory.getAbsolutePath());
} else {
Log.d(Config.LOGTAG, "could not move " + PAM_MainDirectory.getAbsolutePath() + " to " + Monocles_MainDirectory.getAbsolutePath());
Log.d(Config.LOGTAG, "could not move " + PAM_MainDirectory.getAbsolutePath() + " to " + monocles_MainDirectory.getAbsolutePath());
}
}
}
@ -265,7 +265,7 @@ public class UpdateHelper {
private static boolean PAMInstalled(Activity activity) {
PackageManager pm = activity.getPackageManager();
try {
return pm.getApplicationLabel(pm.getApplicationInfo("de.monocles.chat", 0)).equals("Monocles Messenger");
return pm.getApplicationLabel(pm.getApplicationInfo("de.pixart.messenger", 0)).equals("Monocles Messenger");
} catch (PackageManager.NameNotFoundException e) {
return false;
}

View file

@ -0,0 +1,30 @@
package eu.siacs.conversations.ui.util;
import java.util.HashMap;
/**
* Helper methods for parsing URI's.
*/
public final class UriHelper {
/**
* Parses a query string into a hashmap.
*
* @param q The query string to split.
* @return A hashmap containing the key-value pairs from the query string.
*/
public static HashMap<String, String> parseQueryString(final String q) {
if (q == null || q.isEmpty()) {
return null;
}
final String[] query = q.split("&");
// TODO: Look up the HashMap implementation and figure out what the load factor is and make sure we're not reallocating here.
final HashMap<String, String> queryMap = new HashMap<>(query.length);
for (final String param : query) {
final String[] pair = param.split("=");
queryMap.put(pair[0], pair.length == 2 && !pair[1].isEmpty() ? pair[1] : null);
}
return queryMap;
}
}

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