Merge remote-tracking branch 'upstream/master'

* upstream/master: (27 commits)
  show 'using account …' in incoming call screen
  show contact jid in call screen
  bump copyright year
  Add handling of status code 333
  increase default pw length
  do not build emoji flavors
  pulled translations from transifex
  add changelog
  fix ice candidate sending when different credentials are used
  remove security check that ensures rtp connection was properly finished
  code clean up
  bump agp
  store encrypted pgp files in private cache dir
  do not restart wakelock if activity is finishing
  delete pre lolipop weOwnFile()
  use try with resources. remove unused methods
  rename version suffix to playstore/free
  bump appcompat, migrate to emoji2 and get rid of emoji flavor
  fix rare npe
  store recordings and documents in their respective folders
  ...
for-singpolyma
Stephen Paul Weber 1 year ago
commit 5e149cfcd1
No known key found for this signature in database
GPG Key ID: D11C2911CE519CDE

@ -28,6 +28,6 @@ tasks:
sed -ie 's/\/\/ INSERT/implementation "io.sentry:sentry-android:5.6.1"/' build.gradle
- build: |
cd cheogram-android
./gradlew assembleCheogramFreeCompatDebug
./gradlew assembleCheogramFreeDebug
- assets: |
mv cheogram-android/build/outputs/apk/cheogramFreeCompat/debug/*.apk cheogram.apk
mv cheogram-android/build/outputs/apk/cheogramFree/debug/*.apk cheogram.apk

@ -22,14 +22,10 @@ jobs:
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
- name: Build Quicksy
run: ./gradlew assembleQuicksyFreeDebug
- name: Build Conversations
run: ./gradlew assembleConversationsFreeDebug
- uses: actions/upload-artifact@v2
with:
name: Conversations all-flavors (debug)

@ -1,5 +1,10 @@
# Changelog
### Version 2.10.3
* Store files in location appropriate for Android 11
* Attempt to reconnect call after network switch
### Version 2.10.2
* Fix crash when rendering some quotes

@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.1'
classpath 'com.android.tools.build:gradle:7.1.2'
}
}
@ -38,14 +38,13 @@ def urlFile = { url, name ->
configurations {
playstoreImplementation
compatImplementation
conversationsFreeCompatImplementation
cheogramFreeCompatImplementation
conversationsPlaystoreCompatImplementation
conversationsPlaystoreSystemImplementation
quicksyPlaystoreCompatImplementation
quicksyPlaystoreSystemImplementation
quicksyFreeCompatImplementation
freeImplementation
conversationsFreeImplementation
conversationsPlaystorImplementation
conversationsPlaystoreImplementation
quicksyPlaystoreImplementation
quicksyPlaystoreImplementation
quicksyFreeImplementation
quicksyImplementation
}
@ -57,22 +56,19 @@ dependencies {
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2")
conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2")
quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2")
quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
implementation 'org.sufficientlysecure:openpgp-api:10.0'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.exifinterface:exifinterface:1.3.3'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.emoji:emoji:1.1.0'
implementation 'com.google.android.material:material:1.4.0'
compatImplementation 'androidx.emoji:emoji-appcompat:1.1.0'
conversationsFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
cheogramFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
quicksyFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0'
implementation "androidx.emoji2:emoji2:1.1.0-rc01"
freeImplementation "androidx.emoji2:emoji2-bundled:1.1.0-rc01"
implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
//zxing stopped supporting Java 7 so we have to stick with 3.3.3
//https://github.com/zxing/zxing/issues/1170
@ -138,7 +134,7 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
flavorDimensions("mode", "distribution", "emoji")
flavorDimensions("mode", "distribution")
productFlavors {
@ -169,45 +165,21 @@ android {
playstore {
dimension "distribution"
versionNameSuffix "+p"
versionNameSuffix "+playstore"
}
free {
dimension "distribution"
versionNameSuffix "+f"
}
system {
dimension "emoji"
versionNameSuffix "s"
}
compat {
dimension "emoji"
versionNameSuffix "c"
versionNameSuffix "+free"
}
}
sourceSets {
quicksyFreeSystem {
java {
srcDir 'src/quicksyFree/java'
}
}
quicksyFreeCompat {
quicksyFree {
java {
srcDir 'src/freeCompat/java'
srcDir 'src/quicksyFree/java'
}
}
quicksyPlaystoreCompat {
java {
srcDir 'src/playstoreCompat/java'
srcDir 'src/quicksyPlaystore/java'
}
res {
srcDir 'src/playstoreCompat/res'
srcDir 'src/quicksyPlaystore/res'
}
}
quicksyPlaystoreSystem {
quicksyPlaystore {
java {
srcDir 'src/quicksyPlaystore/java'
}
@ -215,39 +187,17 @@ android {
srcDir 'src/quicksyPlaystore/res'
}
}
conversationsFreeCompat {
conversationsFree {
java {
srcDir 'src/freeCompat/java'
srcDir 'src/conversationsFree/java'
}
}
conversationsFreeSystem {
cheogramFree {
java {
srcDir 'src/conversationsFree/java'
}
}
cheogramFreeCompat {
java {
srcDir 'src/freeCompat/java'
srcDir 'src/conversationsFree/java'
}
}
cheogramFreeSystem {
java {
srcDir 'src/conversationsFree/java'
}
}
conversationsPlaystoreCompat {
java {
srcDir 'src/playstoreCompat/java'
srcDir 'src/conversationsPlaystore/java'
}
res {
srcDir 'src/playstoreCompat/res'
srcDir 'src/conversationsPlaystore/res'
}
}
conversationsPlaystoreSystem {
conversationsPlaystore {
java {
srcDir 'src/conversationsPlaystore/java'
}
@ -262,13 +212,11 @@ android {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
versionNameSuffix "r"
}
debug {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
versionNameSuffix "d"
}
}

@ -128,16 +128,19 @@ public class ImportBackupService extends Service {
final List<Jid> accounts = mDatabaseBackend.getAccountJids(false);
final ArrayList<BackupFile> backupFiles = new ArrayList<>();
final Set<String> apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name)));
for (String app : apps) {
final File directory = new File(FileBackend.getBackupDirectory(app));
final List<File> directories = new ArrayList<>();
for (final String app : apps) {
directories.add(FileBackend.getLegacyBackupDirectory(app));
}
directories.add(FileBackend.getBackupDirectory(this));
for (final File directory : directories) {
if (!directory.exists() || !directory.isDirectory()) {
Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
continue;
}
final File[] files = directory.listFiles();
if (files == null) {
onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
return;
continue;
}
for (final File file : files) {
if (file.isFile() && file.getName().endsWith(".ceb")) {

@ -1,18 +0,0 @@
package eu.siacs.conversations.ui.widget;
import android.content.Context;
import android.util.AttributeSet;
import androidx.emoji.widget.EmojiAppCompatEditText;
public class EmojiWrapperEditText extends EmojiAppCompatEditText {
public EmojiWrapperEditText(Context context) {
super(context);
}
public EmojiWrapperEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
}

@ -1,47 +0,0 @@
/*
* Copyright (c) 2017, Daniel Gultsch All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package eu.siacs.conversations.utils;
import androidx.emoji.text.EmojiCompat;
public class EmojiWrapper {
public static CharSequence transform(CharSequence input) {
try {
if (EmojiCompat.get().getLoadState() == EmojiCompat.LOAD_STATE_SUCCEEDED) {
return EmojiCompat.get().process(input);
} else {
return input;
}
} catch (IllegalStateException e) {
return input;
}
}
}

@ -128,16 +128,19 @@ public class ImportBackupService extends Service {
final List<Jid> accounts = mDatabaseBackend.getAccountJids(false);
final ArrayList<BackupFile> backupFiles = new ArrayList<>();
final Set<String> apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name)));
for (String app : apps) {
final File directory = new File(FileBackend.getBackupDirectory(app));
final List<File> directories = new ArrayList<>();
for (final String app : apps) {
directories.add(FileBackend.getLegacyBackupDirectory(app));
}
directories.add(FileBackend.getBackupDirectory(this));
for (final File directory : directories) {
if (!directory.exists() || !directory.isDirectory()) {
Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
continue;
}
final File[] files = directory.listFiles();
if (files == null) {
onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
return;
continue;
}
for (final File file : files) {
if (file.isFile() && file.getName().endsWith(".ceb")) {

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Escolle o teu provedor XMPP</string>
<string name="pick_a_server">Elixe o teu provedor XMPP</string>
<string name="use_conversations.im">Utilizar conversations.im</string>
<string name="create_new_account">Crear nova conta</string>
<string name="do_you_have_an_account">Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP.</string>
<string name="server_select_text">XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im¹; un provedor especialmente axeitado para utilizar con Conversations.</string>
<string name="magic_create_text_on_x">Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo escoller %1$s como provedor poderás comunicarte con usuarias de outros provedores cando lles deas o teu enderezo XMPP completo.</string>
<string name="magic_create_text_fixed">Convidáronte a %1$s. Escollemos un nome de usuaria por ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias de outros provedores cando lles digas o teu enderezo XMPP completo.</string>
<string name="magic_create_text_on_x">Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo.</string>
<string name="magic_create_text_fixed">Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo.</string>
<string name="your_server_invitation">O convite do teu servidor</string>
<string name="improperly_formatted_provisioning">Código de aprovisionamento con formato non válido</string>
<string name="tap_share_button_send_invite">Toca no botón compartir para convidar ó teu contacto a %1$s.</string>
<string name="tap_share_button_send_invite">Toca no botón compartir para convidar ao teu contacto a %1$s.</string>
<string name="if_contact_is_nearby_use_qr">Se o contacto está preto de ti, pode escanear o código inferior para aceptar o teu convite.</string>
<string name="easy_invite_share_text">Únete a %1$s e conversa conmigo: %2$s</string>
<string name="share_invite_with">Enviar convite a...</string>

@ -0,0 +1,14 @@
package eu.siacs.conversations.services;
import android.content.Context;
import androidx.emoji2.bundled.BundledEmojiCompatConfig;
import androidx.emoji2.text.EmojiCompat;
public class EmojiInitializationService {
public static void execute(final Context context) {
EmojiCompat.init(new BundledEmojiCompatConfig(context).setReplaceAll(true));
}
}

@ -1,27 +0,0 @@
package eu.siacs.conversations.ui.service;
import android.content.Context;
import android.os.Build;
import androidx.emoji.text.EmojiCompat;
import androidx.emoji.text.FontRequestEmojiCompatConfig;
import androidx.emoji.bundled.BundledEmojiCompatConfig;
public class EmojiService {
private final Context context;
public EmojiService(Context context) {
this.context = context;
}
public void init() {
BundledEmojiCompatConfig config = new BundledEmojiCompatConfig(context);
//On recent Androids we assume to have the latest emojis
//there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible
// a) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible)
// b) can trigger a hardware rendering bug https://issuetracker.google.com/issues/67102093
config.setReplaceAll(Build.VERSION.SDK_INT < Build.VERSION_CODES.O);
EmojiCompat.init(config);
}
}

@ -50,6 +50,10 @@
android:name="android.hardware.microphone"
android:required="false" />
<queries>
<package android:name="org.sufficientlysecure.keychain"/>
</queries>
<application
android:allowBackup="true"
@ -61,6 +65,7 @@
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_configuration"
android:requestLegacyExternalStorage="true"
android:preserveLegacyExternalStorage="true"
android:theme="@style/ConversationsTheme"
tools:replace="android:label"
tools:targetApi="q">

@ -9,6 +9,7 @@ import org.openintents.openpgp.util.OpenPgpApi;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@ -147,9 +148,6 @@ public class PgpDecryptionService {
try {
os.flush();
final String body = os.toString();
if (body == null) {
throw new IOException("body was null");
}
message.setBody(body);
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
@ -194,9 +192,9 @@ public class PgpDecryptionService {
String originalExtension = originalFilename == null ? null : MimeUtils.extractRelevantExtension(originalFilename);
if (originalExtension != null && MimeUtils.extractRelevantExtension(outputFile.getName()) == null) {
Log.d(Config.LOGTAG,"detected original filename during pgp decryption");
String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension);
String path = outputFile.getName()+"."+originalExtension;
DownloadableFile fixedFile = mXmppConnectionService.getFileBackend().getFileForPath(path,mime);
final String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension);
final String filename = outputFile.getName()+"."+originalExtension;
final File fixedFile = mXmppConnectionService.getFileBackend().getStorageLocation(filename,mime);
if (fixedFile.getParentFile().mkdirs()) {
Log.d(Config.LOGTAG,"created parent directories for "+fixedFile.getAbsolutePath());
}
@ -205,7 +203,7 @@ public class PgpDecryptionService {
}
if (outputFile.renameTo(fixedFile)) {
Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath());
message.setRelativeFilePath(path);
message.setRelativeFilePath(fixedFile.getAbsolutePath());
}
}
final String url = message.getFileParams().url;

@ -16,6 +16,10 @@ public class DownloadableFile extends File {
private byte[] aeskey;
private byte[] iv;
public DownloadableFile(final File parent, final String file) {
super(parent, file);
}
public DownloadableFile(String path) {
super(path);
}

@ -96,11 +96,8 @@ public class HttpDownloadConnection implements Transferable {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
final String ext = extension.getExtension();
if (ext != null) {
message.setRelativeFilePath(String.format("%s.%s", message.getUuid(), ext));
} else if (Strings.isNullOrEmpty(message.getRelativeFilePath())) {
message.setRelativeFilePath(message.getUuid());
}
final String filename = Strings.isNullOrEmpty(ext) ? message.getUuid() : String.format("%s.%s", message.getUuid(), ext);
mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, filename);
setupFile();
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
@ -122,7 +119,7 @@ public class HttpDownloadConnection implements Transferable {
private void setupFile() {
final String reference = mUrl.fragment();
if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir(), message.getUuid());
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
} else {
@ -326,7 +323,7 @@ public class HttpDownloadConnection implements Transferable {
if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
if (fileExtension != null) {
message.setRelativeFilePath(String.format("%s.%s", message.getUuid(), fileExtension));
mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), fileExtension), contentType);
Log.d(Config.LOGTAG, "rewriting name after not finding extension in url but in content type");
setupFile();
}
@ -419,8 +416,9 @@ public class HttpDownloadConnection implements Transferable {
Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
}
file.getParentFile().mkdirs();
Log.d(Config.LOGTAG,"creating file: "+file.getAbsolutePath());
if (!file.exists() && !file.createNewFile()) {
throw new FileWriterException();
throw new FileWriterException(file);
}
outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
}
@ -431,7 +429,7 @@ public class HttpDownloadConnection implements Transferable {
try {
outputStream.write(buffer, 0, count);
} catch (IOException e) {
throw new FileWriterException();
throw new FileWriterException(file);
}
updateProgress(Math.round(((double) transmitted / expected) * 100));
}

@ -91,7 +91,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
private void processAsVideo() throws FileNotFoundException {
Log.d(Config.LOGTAG, "processing file as video");
mXmppConnectionService.startForcingForegroundNotification();
message.setRelativeFilePath(message.getUuid() + ".mp4");
mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4"));
final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
Log.d(Config.LOGTAG, "created parent directory for video file");

@ -291,7 +291,7 @@ public class ExportBackupService extends Service {
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(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
final File file = new File(FileBackend.getBackupDirectory(this), account.getJid().asBareJid().toEscapedString() + ".ceb");
files.add(file);
final File directory = file.getParentFile();
if (directory != null && directory.mkdirs()) {
@ -335,7 +335,7 @@ public class ExportBackupService extends Service {
}
private void notifySuccess(final List<File> files) {
final String path = FileBackend.getBackupDirectory(this);
final String path = FileBackend.getBackupDirectory(this).getAbsolutePath();
PendingIntent openFolderIntent = null;
@ -363,7 +363,7 @@ public class ExportBackupService extends Service {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_backup_created_title))
.setContentText(getString(R.string.notification_backup_created_subtitle, path))
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this))))
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this).getAbsolutePath())))
.setAutoCancel(true)
.setContentIntent(openFolderIntent)
.setSmallIcon(R.drawable.ic_archive_white_24dp);

@ -46,7 +46,6 @@ import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.EmojiWrapper;
import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.XmppUri;
@ -471,11 +470,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
String subject = mucOptions.getSubject();
final boolean hasTitle;
if (printableValue(roomName)) {
this.binding.mucTitle.setText(EmojiWrapper.transform(roomName));
this.binding.mucTitle.setText(roomName);
this.binding.mucTitle.setVisibility(View.VISIBLE);
hasTitle = true;
} else if (!printableValue(subject)) {
this.binding.mucTitle.setText(EmojiWrapper.transform(mConversation.getName()));
this.binding.mucTitle.setText(mConversation.getName());
hasTitle = true;
this.binding.mucTitle.setVisibility(View.VISIBLE);
} else {
@ -486,7 +485,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
SpannableStringBuilder spannable = new SpannableStringBuilder(subject);
StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
MyLinkify.addLinks(spannable, false);
this.binding.mucSubject.setText(EmojiWrapper.transform(spannable));
this.binding.mucSubject.setText(spannable);
this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead);
this.binding.mucSubject.setAutoLinkMask(0);
this.binding.mucSubject.setVisibility(View.VISIBLE);

@ -6,6 +6,7 @@ import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@ -1183,8 +1184,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
cancelTransmission.setVisible(true);
}
if (m.isFileOrImage() && !deleted && !cancelable) {
String path = m.getRelativeFilePath();
if (path == null || !path.startsWith("/") || FileBackend.isInDirectoryThatShouldNotBeScanned(getActivity(), path)) {
final String path = m.getRelativeFilePath();
if (path == null || !path.startsWith("/")) {
deleteFile.setVisible(true);
deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m)));
}
@ -1744,7 +1745,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (context == null) {
return;
}
if (intent.resolveActivity(context.getPackageManager()) != null) {
try {
if (chooser) {
startActivityForResult(
Intent.createChooser(intent, getString(R.string.perform_action_with)),
@ -1752,7 +1753,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} else {
startActivityForResult(intent, attachmentChoice);
}
} else {
} catch (final ActivityNotFoundException e) {
Toast.makeText(context, R.string.no_application_found, Toast.LENGTH_LONG).show();
}
}
@ -2254,10 +2255,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
private List<Uri> cleanUris(final List<Uri> uris) {
Iterator<Uri> iterator = uris.iterator();
final Iterator<Uri> iterator = uris.iterator();
while (iterator.hasNext()) {
final Uri uri = iterator.next();
if (FileBackend.weOwnFile(getActivity(), uri)) {
if (FileBackend.weOwnFile(uri)) {
iterator.remove();
Toast.makeText(getActivity(), R.string.security_violation_not_attaching_file, Toast.LENGTH_SHORT).show();
}

@ -81,7 +81,6 @@ import eu.siacs.conversations.ui.util.ActivityResult;
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.utils.EmojiWrapper;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.SignupUtils;
import eu.siacs.conversations.utils.XmppUri;
@ -615,7 +614,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
if (mainFragment instanceof ConversationFragment) {
final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
if (conversation != null) {
actionBar.setTitle(EmojiWrapper.transform(conversation.getName()));
actionBar.setTitle(conversation.getName());
actionBar.setDisplayHomeAsUpEnabled(true);
ActionBarUtil.setActionBarOnClickListener(
binding.toolbar,

@ -22,6 +22,7 @@ import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.EnterJidDialogBinding;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
import eu.siacs.conversations.ui.util.DelayedHintHelper;
@ -29,234 +30,250 @@ import eu.siacs.conversations.xmpp.Jid;
public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher {
private static final List<String> SUSPICIOUS_DOMAINS = Arrays.asList("conference","muc","room","rooms","chat");
private OnEnterJidDialogPositiveListener mListener = null;
private static final String TITLE_KEY = "title";
private static final String POSITIVE_BUTTON_KEY = "positive_button";
private static final String PREFILLED_JID_KEY = "prefilled_jid";
private static final String ACCOUNT_KEY = "account";
private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid";
private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
private static final String SANITY_CHECK_JID = "sanity_check_jid";
private KnownHostsAdapter knownHostsAdapter;
private Collection<String> whitelistedDomains = Collections.emptyList();
private EnterJidDialogBinding binding;
private AlertDialog dialog;
private boolean sanityCheckJid = false;
private boolean issuedWarning = false;
public static EnterJidDialog newInstance(final List<String> activatedAccounts,
final String title, final String positiveButton,
final String prefilledJid, final String account,
boolean allowEditJid, final boolean sanity_check_jid) {
EnterJidDialog dialog = new EnterJidDialog();
Bundle bundle = new Bundle();
bundle.putString(TITLE_KEY, title);
bundle.putString(POSITIVE_BUTTON_KEY, positiveButton);
bundle.putString(PREFILLED_JID_KEY, prefilledJid);
bundle.putString(ACCOUNT_KEY, account);
bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid);
bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) activatedAccounts);
bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid);
dialog.setArguments(bundle);
return dialog;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onStart() {
super.onStart();
final Activity activity = getActivity();
if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) {
refreshKnownHosts();
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getArguments().getString(TITLE_KEY));
binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.enter_jid_dialog, null, false);
this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item);
binding.jid.setAdapter(this.knownHostsAdapter);
binding.jid.addTextChangedListener(this);
String prefilledJid = getArguments().getString(PREFILLED_JID_KEY);
if (prefilledJid != null) {
binding.jid.append(prefilledJid);
if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) {
binding.jid.setFocusable(false);
binding.jid.setFocusableInTouchMode(false);
binding.jid.setClickable(false);
binding.jid.setCursorVisible(false);
}
}
sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false);
DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
String account = getArguments().getString(ACCOUNT_KEY);
if (account == null) {
StartConversationActivity.populateAccountSpinner(getActivity(), getArguments().getStringArrayList(ACCOUNTS_LIST_KEY), binding.account);
} else {
ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),
R.layout.simple_list_item,
new String[]{account});
binding.account.setEnabled(false);
adapter.setDropDownViewResource(R.layout.simple_list_item);
binding.account.setAdapter(adapter);
}
builder.setView(binding.getRoot());
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null);
this.dialog = builder.create();
View.OnClickListener dialogOnClick = v -> {
handleEnter(binding, account);
};
binding.jid.setOnEditorActionListener((v, actionId, event) -> {
handleEnter(binding, account);
return true;
});
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick);
return dialog;
}
private void handleEnter(EnterJidDialogBinding binding, String account) {
final Jid accountJid;
if (!binding.account.isEnabled() && account == null) {
return;
}
try {
if (Config.DOMAIN_LOCK != null) {
accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null);
} else {
accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem());
}
} catch (final IllegalArgumentException e) {
return;
}
final Jid contactJid;
try {
contactJid = Jid.ofEscaped(binding.jid.getText().toString());
} catch (final IllegalArgumentException e) {
binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
return;
}
if (!issuedWarning && sanityCheckJid) {
if (contactJid.isDomainJid()) {
binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_a_domain));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
issuedWarning = true;
return;
}
if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) {
binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_channel));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
issuedWarning = true;
return;
}
}
if (mListener != null) {
try {
if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) {
dialog.dismiss();
}
} catch (JidError error) {
binding.jidLayout.setError(error.toString());
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
issuedWarning = false;
}
}
}
public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) {
this.mListener = listener;
}
@Override
public void onBackendConnected() {
refreshKnownHosts();
}
private void refreshKnownHosts() {
Activity activity = getActivity();
if (activity instanceof XmppActivity) {
Collection<String> hosts = ((XmppActivity) activity).xmppConnectionService.getKnownHosts();
this.knownHostsAdapter.refresh(hosts);
this.whitelistedDomains = hosts;
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (issuedWarning) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
binding.jidLayout.setError(null);
issuedWarning = false;
}
}
public interface OnEnterJidDialogPositiveListener {
boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError;
}
public static class JidError extends Exception {
final String msg;
public JidError(final String msg) {
this.msg = msg;
}
public String toString() {
return msg;
}
}
@Override
public void onDestroyView() {
Dialog dialog = getDialog();
if (dialog != null && getRetainInstance()) {
dialog.setDismissMessage(null);
}
super.onDestroyView();
}
private boolean suspiciousSubDomain(String domain) {
if (this.whitelistedDomains.contains(domain)) {
return false;
}
final String[] parts = domain.split("\\.");
return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]);
}
private static final List<String> SUSPICIOUS_DOMAINS =
Arrays.asList("conference", "muc", "room", "rooms", "chat");
private OnEnterJidDialogPositiveListener mListener = null;
private static final String TITLE_KEY = "title";
private static final String POSITIVE_BUTTON_KEY = "positive_button";
private static final String PREFILLED_JID_KEY = "prefilled_jid";
private static final String ACCOUNT_KEY = "account";
private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid";
private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
private static final String SANITY_CHECK_JID = "sanity_check_jid";
private KnownHostsAdapter knownHostsAdapter;
private Collection<String> whitelistedDomains = Collections.emptyList();
private EnterJidDialogBinding binding;
private AlertDialog dialog;
private boolean sanityCheckJid = false;
private boolean issuedWarning = false;
public static EnterJidDialog newInstance(
final List<String> activatedAccounts,
final String title,
final String positiveButton,
final String prefilledJid,
final String account,
boolean allowEditJid,
final boolean sanity_check_jid) {
EnterJidDialog dialog = new EnterJidDialog();
Bundle bundle = new Bundle();
bundle.putString(TITLE_KEY, title);
bundle.putString(POSITIVE_BUTTON_KEY, positiveButton);
bundle.putString(PREFILLED_JID_KEY, prefilledJid);
bundle.putString(ACCOUNT_KEY, account);
bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid);
bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) activatedAccounts);
bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid);
dialog.setArguments(bundle);
return dialog;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onStart() {
super.onStart();
final Activity activity = getActivity();
if (activity instanceof XmppActivity
&& ((XmppActivity) activity).xmppConnectionService != null) {
refreshKnownHosts();
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getArguments().getString(TITLE_KEY));
binding =
DataBindingUtil.inflate(
getActivity().getLayoutInflater(), R.layout.enter_jid_dialog, null, false);
this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item);
binding.jid.setAdapter(this.knownHostsAdapter);
binding.jid.addTextChangedListener(this);
String prefilledJid = getArguments().getString(PREFILLED_JID_KEY);
if (prefilledJid != null) {
binding.jid.append(prefilledJid);
if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) {
binding.jid.setFocusable(false);
binding.jid.setFocusableInTouchMode(false);
binding.jid.setClickable(false);
binding.jid.setCursorVisible(false);
}
}
sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false);
DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
String account = getArguments().getString(ACCOUNT_KEY);
if (account == null) {
StartConversationActivity.populateAccountSpinner(
getActivity(),
getArguments().getStringArrayList(ACCOUNTS_LIST_KEY),
binding.account);
} else {
ArrayAdapter<String> adapter =
new ArrayAdapter<>(
getActivity(), R.layout.simple_list_item, new String[] {account});
binding.account.setEnabled(false);
adapter.setDropDownViewResource(R.layout.simple_list_item);
binding.account.setAdapter(adapter);
}
builder.setView(binding.getRoot());
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null);
this.dialog = builder.create();
View.OnClickListener dialogOnClick =
v -> {
handleEnter(binding, account);
};
binding.jid.setOnEditorActionListener(
(v, actionId, event) -> {
handleEnter(binding, account);
return true;
});
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick);
return dialog;
}
private void handleEnter(EnterJidDialogBinding binding, String account) {
final Jid accountJid;
if (!binding.account.isEnabled() && account == null) {
return;
}
try {
if (Config.DOMAIN_LOCK != null) {
accountJid =
Jid.ofEscaped(
(String) binding.account.getSelectedItem(),
Config.DOMAIN_LOCK,
null);
} else {
accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem());
}
} catch (final IllegalArgumentException e) {
return;
}
final Jid contactJid;
try {
contactJid = Jid.ofEscaped(binding.jid.getText().toString());
} catch (final IllegalArgumentException e) {
binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
return;
}
if (!issuedWarning && sanityCheckJid) {
if (contactJid.isDomainJid()) {
binding.jidLayout.setError(
getActivity().getString(R.string.this_looks_like_a_domain));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
issuedWarning = true;
return;
}
if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) {
binding.jidLayout.setError(
getActivity().getString(R.string.this_looks_like_channel));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
issuedWarning = true;
return;
}
}
if (mListener != null) {
try {
if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) {
dialog.dismiss();
}
} catch (JidError error) {
binding.jidLayout.setError(error.toString());
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
issuedWarning = false;
}
}
}
public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) {
this.mListener = listener;
}
@Override
public void onBackendConnected() {
refreshKnownHosts();
}
private void refreshKnownHosts() {
final Activity activity = getActivity();
if (activity instanceof XmppActivity) {
final XmppConnectionService service = ((XmppActivity) activity).xmppConnectionService;
if (service == null) {
return;
}
final Collection<String> hosts = service.getKnownHosts();
this.knownHostsAdapter.refresh(hosts);
this.whitelistedDomains = hosts;
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (issuedWarning) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
binding.jidLayout.setError(null);
issuedWarning = false;
}
}
public interface OnEnterJidDialogPositiveListener {
boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError;
}
public static class JidError extends Exception {
final String msg;
public JidError(final String msg) {
this.msg = msg;
}
@NonNull
public String toString() {
return msg;
}
}
@Override
public void onDestroyView() {
Dialog dialog = getDialog();
if (dialog != null && getRetainInstance()) {
dialog.setDismissMessage(null);
}
super.onDestroyView();
}
private boolean suspiciousSubDomain(String domain) {
if (this.whitelistedDomains.contains(domain)) {
return false;
}
final String[] parts = domain.split("\\.");
return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]);
}
}

@ -1,11 +1,12 @@
package eu.siacs.conversations.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileObserver;
import android.os.Handler;
import android.os.SystemClock;
@ -17,25 +18,22 @@ import android.widget.Toast;
import androidx.databinding.DataBindingUtil;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityRecordingBinding;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
public class RecordingActivity extends Activity implements View.OnClickListener {
public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings";
private ActivityRecordingBinding binding;
private MediaRecorder mRecorder;
@ -44,13 +42,14 @@ public class RecordingActivity extends Activity implements View.OnClickListener
private final CountDownLatch outputFileWrittenLatch = new CountDownLatch(1);
private final Handler mHandler = new Handler();
private final Runnable mTickExecutor = new Runnable() {
@Override
public void run() {
tick();
mHandler.postDelayed(mTickExecutor, 100);
}
};
private final Runnable mTickExecutor =
new Runnable() {
@Override
public void run() {
tick();
mHandler.postDelayed(mTickExecutor, 100);
}
};
private File mOutputFile;
@ -68,7 +67,7 @@ public class RecordingActivity extends Activity implements View.OnClickListener
}
@Override
protected void onResume(){
protected void onResume() {
super.onResume();
SettingsUtils.applyScreenshotPreventionSetting(this);
}
@ -137,56 +136,69 @@ public class RecordingActivity extends Activity implements View.OnClickListener
}
}
if (saveFile) {
new Thread(() -> {
try {
if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) {
Log.d(Config.LOGTAG, "time out waiting for output file to be written");
}
} catch (InterruptedException e) {
Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e);
}
runOnUiThread(() -> {
setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile)));
finish();
});
}).start();
new Thread(
() -> {
try {
if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) {
Log.d(
Config.LOGTAG,
"time out waiting for output file to be written");
}
} catch (InterruptedException e) {
Log.d(
Config.LOGTAG,
"interrupted while waiting for output file to be written",
e);
}
runOnUiThread(
() -> {