diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..8ade90d85
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+liberapay: inputmice
+custom: https://paypal.me/ConversationsIM
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index d26e15c2c..b5f52feda 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,12 +1,12 @@
#### General information
-* **Version:** eg 1.21.0
-* **Device:** eg Google Nexus 5
-* **Android Version:** eg Android 6.0 Stock or Android 5.1 Cyanogenmod
-* **Server name:** eg conversations.im, jabber.at or self hosted
+* **Version:** 2.5.3
+* **Device:** Xiaomi Mi A1
+* **Android Version:** Android 9 (stock)
+* **Server name:** conversations.im, jabber.at or self hosted
* **Server software:** ejabberd 16.04 or prosody 0.10 (if known)
-* **Installed server modules:** eg Stream Managment, CSI, MAM
-* **Conversations source:** eg PlayStore, PlayStore Beta Channel, F-Droid, self build (latest HEAD)
+* **Installed server modules:** Stream Managment, CSI, MAM
+* **Conversations source:** PlayStore, PlayStore Beta Channel, F-Droid, self build (latest HEAD)
#### Steps to reproduce
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42fe078ee..c57da8926 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+### Version 2.5.5
+* allow backups to be restored from anywhere
+* bug fixes
+
### Version 2.5.4
* stability improvements for group chats and channels
diff --git a/build.gradle b/build.gradle
index 3abe985b5..c976b3b13 100644
--- a/build.gradle
+++ b/build.gradle
@@ -81,8 +81,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 25
- versionCode 333
- versionName "2.5.4"
+ versionCode 334
+ versionName "2.5.5"
archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId
diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml
index 0a01c6c0f..a91f4c129 100644
--- a/src/conversations/AndroidManifest.xml
+++ b/src/conversations/AndroidManifest.xml
@@ -23,7 +23,20 @@
+ android:launchMode="singleTask">
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java
index 9892e6492..d8246a6af 100644
--- a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java
+++ b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java
@@ -7,6 +7,7 @@ import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
@@ -16,18 +17,20 @@ import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPInputStream;
+import java.util.zip.ZipException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -81,14 +84,22 @@ public class ImportBackupService extends Service {
return START_NOT_STICKY;
}
final String password = intent.getStringExtra("password");
- final String file = intent.getStringExtra("file");
- if (password == null || file == null) {
+ final Uri data = intent.getData();
+ final Uri uri;
+ if (data == null) {
+ final String file = intent.getStringExtra("file");
+ uri = file == null ? null : Uri.fromFile(new File(file));
+ } else {
+ uri = data;
+ }
+
+ if (password == null || password.isEmpty() || uri == null) {
return START_NOT_STICKY;
}
if (running.compareAndSet(false, true)) {
executor.execute(() -> {
startForegroundService();
- final boolean success = importBackup(new File(file), password);
+ final boolean success = importBackup(uri, password);
stopForeground(true);
running.set(false);
if (success) {
@@ -122,7 +133,7 @@ public class ImportBackupService extends Service {
try {
final BackupFile backupFile = BackupFile.read(file);
if (accounts.contains(backupFile.getHeader().getJid())) {
- Log.d(Config.LOGTAG,"skipping backup for "+backupFile.getHeader().getJid());
+ Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid());
} else {
backupFiles.add(backupFile);
}
@@ -145,21 +156,35 @@ public class ImportBackupService extends Service {
startForeground(NOTIFICATION_ID, mBuilder.build());
}
- private boolean importBackup(File file, String password) {
- Log.d(Config.LOGTAG, "importing backup from file " + file.getAbsolutePath());
+ private boolean importBackup(Uri uri, String password) {
+ Log.d(Config.LOGTAG, "importing backup from " + uri);
try {
SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
- final FileInputStream fileInputStream = new FileInputStream(file);
- final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
- BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ final InputStream inputStream;
+ if ("file".equals(uri.getScheme())) {
+ inputStream = new FileInputStream(new File(uri.getPath()));
+ } else {
+ inputStream = getContentResolver().openInputStream(uri);
+ }
+ final DataInputStream dataInputStream = new DataInputStream(inputStream);
+ final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
Log.d(Config.LOGTAG, backupFileHeader.toString());
+ if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) {
+ synchronized (mOnBackupProcessedListeners) {
+ for (OnBackupProcessed l : mOnBackupProcessedListeners) {
+ l.onAccountAlreadySetup();
+ }
+ }
+ return false;
+ }
+
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(backupFileHeader.getIv());
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
- CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, cipher);
+ CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
@@ -197,12 +222,7 @@ public class ImportBackupService extends Service {
return true;
} catch (Exception e) {
Throwable throwable = e.getCause();
- final boolean reasonWasCrypto;
- if (throwable instanceof BadPaddingException) {
- reasonWasCrypto = true;
- } else {
- reasonWasCrypto = false;
- }
+ final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException;
synchronized (mOnBackupProcessedListeners) {
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
if (reasonWasCrypto) {
@@ -212,7 +232,7 @@ public class ImportBackupService extends Service {
}
}
}
- Log.d(Config.LOGTAG, "error restoring backup " + file.getAbsolutePath(), e);
+ Log.d(Config.LOGTAG, "error restoring backup " + uri, e);
return false;
}
}
@@ -259,14 +279,16 @@ public class ImportBackupService extends Service {
void onBackupDecryptionFailed();
void onBackupRestoreFailed();
+
+ void onAccountAlreadySetup();
}
public static class BackupFile {
- private final File file;
+ private final Uri uri;
private final BackupFileHeader header;
- private BackupFile(File file, BackupFileHeader header) {
- this.file = file;
+ private BackupFile(Uri uri, BackupFileHeader header) {
+ this.uri = uri;
this.header = header;
}
@@ -275,15 +297,26 @@ public class ImportBackupService extends Service {
final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
fileInputStream.close();
- return new BackupFile(file, backupFileHeader);
+ return new BackupFile(Uri.fromFile(file), backupFileHeader);
+ }
+
+ public static BackupFile read(final Context context, final Uri uri) throws IOException {
+ final InputStream inputStream = context.getContentResolver().openInputStream(uri);
+ if (inputStream == null) {
+ throw new FileNotFoundException();
+ }
+ final DataInputStream dataInputStream = new DataInputStream(inputStream);
+ BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ inputStream.close();
+ return new BackupFile(uri, backupFileHeader);
}
public BackupFileHeader getHeader() {
return header;
}
- public File getFile() {
- return file;
+ public Uri getUri() {
+ return uri;
}
}
diff --git a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java
index d5de333da..d6dbfd222 100644
--- a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java
+++ b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java
@@ -2,9 +2,12 @@ package eu.siacs.conversations.ui;
import android.content.ComponentName;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.databinding.DataBindingUtil;
+import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.support.design.widget.Snackbar;
@@ -13,8 +16,11 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
+import java.io.IOException;
import java.util.List;
import eu.siacs.conversations.Config;
@@ -32,6 +38,8 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
private BackupFileAdapter backupFileAdapter;
private ImportBackupService service;
+ private boolean mLoadingState = false;
+
private int mTheme;
@Override
@@ -41,12 +49,26 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
setSupportActionBar((Toolbar) binding.toolbar);
- configureActionBar(getSupportActionBar());
+ setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false));
this.backupFileAdapter = new BackupFileAdapter();
this.binding.list.setAdapter(this.backupFileAdapter);
this.backupFileAdapter.setOnItemClickedListener(this);
}
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.import_backup, menu);
+ final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file);
+ openBackup.setVisible(!this.mLoadingState);
+ return true;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ bundle.putBoolean("loading_state", this.mLoadingState);
+ super.onSaveInstanceState(bundle);
+ }
+
@Override
public void onStart() {
super.onStart();
@@ -56,6 +78,13 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
} else {
bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
}
+ final Intent intent = getIntent();
+ if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) {
+ Uri uri = intent.getData();
+ if (uri != null) {
+ openBackupFileFromUri(uri, true);
+ }
+ }
}
@Override
@@ -87,31 +116,83 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
}
@Override
- public void onClick(ImportBackupService.BackupFile backupFile) {
+ public void onClick(final ImportBackupService.BackupFile backupFile) {
+ showEnterPasswordDialog(backupFile, false);
+ }
+
+ private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) {
+ try {
+ final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
+ showEnterPasswordDialog(backupFile, finishOnCancel);
+ } catch (IOException e) {
+ Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
+ }
+ }
+
+ private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) {
final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
- Log.d(Config.LOGTAG, "attempting to import " + backupFile.getFile().getAbsolutePath());
+ Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri());
enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString()));
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(enterPasswordBinding.getRoot());
builder.setTitle(R.string.enter_password);
- builder.setNegativeButton(R.string.cancel, null);
- builder.setPositiveButton(R.string.restore, (dialog, which) -> {
- final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
- Intent intent = new Intent(this, ImportBackupService.class);
- intent.putExtra("password", password);
- intent.putExtra("file", backupFile.getFile().getAbsolutePath());
- setLoadingState(true);
- ContextCompat.startForegroundService(this, intent);
+ builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
+ if (finishOnCancel) {
+ finish();
+ }
});
+ builder.setPositiveButton(R.string.restore, null);
builder.setCancelable(false);
- builder.create().show();
+ final AlertDialog dialog = builder.create();
+ dialog.setOnShowListener((d) -> {
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
+ final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
+ if (password.isEmpty()) {
+ enterPasswordBinding.accountPasswordLayout.setError(getString(R.string.please_enter_password));
+ return;
+ }
+ final Uri uri = backupFile.getUri();
+ Intent intent = new Intent(this, ImportBackupService.class);
+ intent.setAction(Intent.ACTION_SEND);
+ intent.putExtra("password", password);
+ if ("file".equals(uri.getScheme())) {
+ intent.putExtra("file", uri.getPath());
+ } else {
+ intent.setData(uri);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ setLoadingState(true);
+ ContextCompat.startForegroundService(this, intent);
+ d.dismiss();
+ });
+ });
+ dialog.show();
}
private void setLoadingState(final boolean loadingState) {
- binding.coordinator.setVisibility(loadingState ? View.GONE :View.VISIBLE);
+ binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE);
binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE);
setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup);
- configureActionBar(getSupportActionBar(),!loadingState);
+ configureActionBar(getSupportActionBar(), !loadingState);
+ this.mLoadingState = loadingState;
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (resultCode == RESULT_OK) {
+ if (requestCode == 0xbac) {
+ openBackupFileFromUri(intent.getData(), false);
+ }
+ }
+ }
+
+ @Override
+ public void onAccountAlreadySetup() {
+ runOnUiThread(() -> {
+ setLoadingState(false);
+ Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show();
+ });
}
@Override
@@ -126,17 +207,32 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
@Override
public void onBackupDecryptionFailed() {
- runOnUiThread(()-> {
+ runOnUiThread(() -> {
setLoadingState(false);
- Snackbar.make(binding.coordinator,R.string.unable_to_decrypt_backup,Snackbar.LENGTH_LONG).show();
+ Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show();
});
}
@Override
public void onBackupRestoreFailed() {
- runOnUiThread(()-> {
+ runOnUiThread(() -> {
setLoadingState(false);
- Snackbar.make(binding.coordinator,R.string.unable_to_restore_backup,Snackbar.LENGTH_LONG).show();
+ Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show();
});
}
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_open_backup_file) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("*/*");
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
+ }
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
}
diff --git a/src/conversations/java/eu/siacs/conversations/utils/SignupUtils.java b/src/conversations/java/eu/siacs/conversations/utils/SignupUtils.java
index 6e8ed2eff..fc5d874d3 100644
--- a/src/conversations/java/eu/siacs/conversations/utils/SignupUtils.java
+++ b/src/conversations/java/eu/siacs/conversations/utils/SignupUtils.java
@@ -20,13 +20,12 @@ public class SignupUtils {
}
public static Intent getSignUpIntent(final Activity activity, final boolean toServerChooser) {
- Intent intent;
+ final Intent intent;
if (toServerChooser) {
intent = new Intent(activity, PickServerActivity.class);
} else {
intent = new Intent(activity, WelcomeActivity.class);
}
- StartConversationActivity.addInviteUri(intent, activity.getIntent());
return intent;
}
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 6b43106f9..31f33e5fd 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -92,7 +92,6 @@
android:theme="@style/SplashTheme">
-
diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java
index f4ca658d4..1b618c5ad 100644
--- a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java
+++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java
@@ -22,6 +22,7 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Locale;
import javax.net.ssl.SSLSession;
@@ -80,14 +81,14 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
break;
}
Log.d(LOGTAG, "comparing " + needle.substring(i) + " and " + entry.substring(1));
- if (needle.substring(i).equals(entry.substring(1))) {
+ if (needle.substring(i).equalsIgnoreCase(entry.substring(1))) {
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
return true;
}
offset = i + 1;
}
} else {
- if (entry.equals(needle)) {
+ if (entry.equalsIgnoreCase(needle)) {
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
return true;
}
@@ -117,25 +118,25 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
List domains = new ArrayList<>();
if (alternativeNames != null) {
for (List> san : alternativeNames) {
- Integer type = (Integer) san.get(0);
+ final Integer type = (Integer) san.get(0);
if (type == 0) {
- Pair otherName = parseOtherName((byte[]) san.get(1));
- if (otherName != null) {
+ final Pair otherName = parseOtherName((byte[]) san.get(1));
+ if (otherName != null && otherName.first != null && otherName.second != null) {
switch (otherName.first) {
case SRV_NAME:
- srvNames.add(otherName.second);
+ srvNames.add(otherName.second.toLowerCase(Locale.US));
break;
case XMPP_ADDR:
- xmppAddrs.add(otherName.second);
+ xmppAddrs.add(otherName.second.toLowerCase(Locale.US));
break;
default:
Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
}
}
} else if (type == 2) {
- Object value = san.get(1);
+ final Object value = san.get(1);
if (value instanceof String) {
- domains.add((String) value);
+ domains.add(((String) value).toLowerCase(Locale.US));
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index c9ec5637a..2004953ee 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -285,7 +285,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
final ArrayList results = new ArrayList<>();
synchronized (this.messages) {
for (Message message : this.messages) {
- if (message.getType() != Message.TYPE_IMAGE && message.getStatus() == Message.STATUS_UNSEND) {
+ if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
results.add(message);
}
}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index 5adaffe34..33179b851 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -543,7 +543,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
|| replacedMessage.getFingerprint().equals(message.getFingerprint());
final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
- && replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
+ && message.getTrueCounterpart() != null
+ && replacedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid());
final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
final boolean duplicate = conversation.hasDuplicateMessage(message);
if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java
index d715ea539..7b034a255 100644
--- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java
+++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java
@@ -21,7 +21,9 @@ import java.io.PrintWriter;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPOutputStream;
@@ -50,6 +52,8 @@ public class ExportBackupService extends Service {
public static final String CIPHERMODE = "AES/GCM/NoPadding";
public static final String PROVIDER = "BC";
+ public static final String MIME_TYPE = "application/vnd.conversations.backup";
+
private static final int NOTIFICATION_ID = 19;
private static final int PAGE_SIZE = 20;
private static AtomicBoolean running = new AtomicBoolean(false);
@@ -213,11 +217,19 @@ public class ExportBackupService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) {
if (running.compareAndSet(false, true)) {
new Thread(() -> {
- final boolean success = export();
+ boolean success;
+ List files;
+ try {
+ files = export();
+ success = true;
+ } catch (Exception e) {
+ success = false;
+ files = Collections.emptyList();
+ }
stopForeground(true);
running.set(false);
if (success) {
- notifySuccess();
+ notifySuccess(files);
}
stopSelf();
}).start();
@@ -250,81 +262,97 @@ public class ExportBackupService extends Service {
}
}
- private boolean export() {
+ private List export() throws Exception {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
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());
- try {
- int count = 0;
- final int max = this.mAccounts.size();
- final SecureRandom secureRandom = new SecureRandom();
- for (Account account : this.mAccounts) {
- 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(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
- if (file.getParentFile().mkdirs()) {
- Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
- }
- final FileOutputStream fileOutputStream = new FileOutputStream(file);
- final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
- backupFileHeader.write(dataOutputStream);
- dataOutputStream.flush();
-
- final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
- byte[] key = getKey(account.getPassword(), 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();
- Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
- count++;
+ int count = 0;
+ final int max = this.mAccounts.size();
+ final SecureRandom secureRandom = new SecureRandom();
+ final List files = new ArrayList<>();
+ for (Account account : this.mAccounts) {
+ 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(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
+ files.add(file);
+ if (file.getParentFile().mkdirs()) {
+ Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
}
- return true;
- } catch (Exception e) {
- Log.d(Config.LOGTAG, "unable to create backup ", e);
- return false;
+ final FileOutputStream fileOutputStream = new FileOutputStream(file);
+ final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
+ backupFileHeader.write(dataOutputStream);
+ dataOutputStream.flush();
+
+ final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
+ byte[] key = getKey(account.getPassword(), 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();
+ Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
+ count++;
}
+ return files;
}
- private void notifySuccess() {
+ private void notifySuccess(List files) {
final String path = FileBackend.getBackupDirectory(this);
- PendingIntent pendingIntent = null;
+ PendingIntent openFolderIntent = null;
for (Intent intent : getPossibleFileOpenIntents(this, path)) {
if (intent.resolveActivityInfo(getPackageManager(), 0) != null) {
- pendingIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ openFolderIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT);
break;
}
}
+ PendingIntent shareFilesIntent = null;
+ if (files.size() > 0) {
+ final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+ ArrayList uris = new ArrayList<>();
+ for(File file : files) {
+ uris.add(FileBackend.getUriForFile(this, file));
+ }
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setType(MIME_TYPE);
+ final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files));
+ shareFilesIntent = PendingIntent.getActivity(this,190, chooser, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
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))))
.setAutoCancel(true)
- .setContentIntent(pendingIntent)
+ .setContentIntent(openFolderIntent)
.setSmallIcon(R.drawable.ic_archive_white_24dp);
+
+ if (shareFilesIntent != null) {
+ mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent);
+ }
+
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
index 9d6385db8..ce64af856 100644
--- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
@@ -131,10 +131,11 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
if (Intent.ACTION_SEND.equals(action)) {
final String text = intent.getStringExtra(Intent.EXTRA_TEXT);
final Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+
if (data != null && "geo".equals(data.getScheme())) {
this.share.uris.clear();
this.share.uris.add(data);
- } else if (type != null && uri != null && (text == null || !type.equals("text/plain"))) {
+ } else if (type != null && uri != null) {
this.share.uris.clear();
this.share.uris.add(uri);
} else {
diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
index 95f179ceb..d1c0fa7d4 100644
--- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
@@ -90,6 +90,7 @@ public class UriHandlerActivity extends AppCompatActivity {
if (accounts.size() == 0) {
if (xmppUri.isJidValid()) {
intent = SignupUtils.getSignUpIntent(this);
+ intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
startActivity(intent);
} else {
Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
index ad5ddce89..3ea451653 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -94,6 +94,7 @@ public class ConversationAdapter extends RecyclerView.Adapter LOCALPART_BLACKLIST = Arrays.asList("xmpp","jabber","me");
+ private static List LOCAL_PART_BLACKLIST = Arrays.asList("xmpp", "jabber", "me");
- public static String localPartOrFallback(Jid jid) {
- if (LOCALPART_BLACKLIST.contains(jid.getLocal().toLowerCase(Locale.ENGLISH))) {
- final String domain = jid.getDomain();
- final int index = domain.lastIndexOf('.');
- return index > 1 ? domain.substring(0,index) : domain;
- } else {
- return jid.getLocal();
- }
- }
+ public static String localPartOrFallback(Jid jid) {
+ if (LOCAL_PART_BLACKLIST.contains(jid.getLocal().toLowerCase(Locale.ENGLISH))) {
+ final String domain = jid.getDomain();
+ final int index = domain.indexOf('.');
+ return index > 1 ? domain.substring(0, index) : domain;
+ } else {
+ return jid.getLocal();
+ }
+ }
- public static Jid parseOrFallbackToInvalid(String jid) {
- try {
- return Jid.of(jid);
- } catch (IllegalArgumentException e) {
- return InvalidJid.of(jid, true);
- }
- }
+ public static Jid parseOrFallbackToInvalid(String jid) {
+ try {
+ return Jid.of(jid);
+ } catch (IllegalArgumentException e) {
+ return InvalidJid.of(jid, true);
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
index 59ad512c5..aaaef8e0d 100644
--- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
@@ -29,6 +29,7 @@ import java.util.Properties;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.services.ExportBackupService;
/**
* Utilities for dealing with MIME types.
@@ -72,6 +73,7 @@ public final class MimeUtils {
add("application/vnd.amazon.mobi8-ebook","kfx");
add("application/vnd.android.package-archive", "apk");
add("application/vnd.cinderella", "cdy");
+ add(ExportBackupService.MIME_TYPE, "ceb");
add("application/vnd.ms-pki.stl", "stl");
add("application/vnd.oasis.opendocument.database", "odb");
add("application/vnd.oasis.opendocument.formula", "odf");
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index e9360226f..84df63dc2 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -32,6 +32,7 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.services.ExportBackupService;
import rocks.xmpp.addr.Jid;
public class UIHelper {
@@ -483,8 +484,12 @@ public class UIHelper {
return context.getString(R.string.pdf_document);
} else if (mime.equals("application/vnd.android.package-archive")) {
return context.getString(R.string.apk);
+ } else if (mime.equals(ExportBackupService.MIME_TYPE)) {
+ return context.getString(R.string.conversations_backup);
} else if (mime.contains("vcard")) {
return context.getString(R.string.vcard);
+ } else if (mime.equals("text/x-vcalendar") || mime.equals("text/calendar")) {
+ return context.getString(R.string.event);
} else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
return context.getString(R.string.ebook);
} else {
diff --git a/src/main/res/drawable-hdpi/ic_backup_black_48dp.png b/src/main/res/drawable-hdpi/ic_backup_black_48dp.png
new file mode 100644
index 000000000..6506c7236
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_backup_black_48dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_backup_white_48dp.png b/src/main/res/drawable-hdpi/ic_backup_white_48dp.png
new file mode 100644
index 000000000..3ff57ad3e
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_backup_white_48dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png
new file mode 100644
index 000000000..4c5d2d049
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_backup_black_48dp.png b/src/main/res/drawable-mdpi/ic_backup_black_48dp.png
new file mode 100644
index 000000000..81155da52
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_backup_black_48dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_backup_white_48dp.png b/src/main/res/drawable-mdpi/ic_backup_white_48dp.png
new file mode 100644
index 000000000..a9602d11b
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_backup_white_48dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png
new file mode 100644
index 000000000..0c6978c1f
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_backup_black_48dp.png b/src/main/res/drawable-xhdpi/ic_backup_black_48dp.png
new file mode 100644
index 000000000..248289e97
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_backup_black_48dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_backup_white_48dp.png b/src/main/res/drawable-xhdpi/ic_backup_white_48dp.png
new file mode 100644
index 000000000..2180f73e8
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_backup_white_48dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png
new file mode 100644
index 000000000..d1a0573af
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_backup_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_backup_black_48dp.png
new file mode 100644
index 000000000..932af18ce
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_backup_black_48dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_backup_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_backup_white_48dp.png
new file mode 100644
index 000000000..e3a373a9b
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_backup_white_48dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png
new file mode 100644
index 000000000..3392d972f
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_backup_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_backup_black_48dp.png
new file mode 100644
index 000000000..33d75230c
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_backup_black_48dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_backup_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_backup_white_48dp.png
new file mode 100644
index 000000000..19e40517d
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_backup_white_48dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png
new file mode 100644
index 000000000..b24e573ed
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png differ
diff --git a/src/main/res/menu/import_backup.xml b/src/main/res/menu/import_backup.xml
new file mode 100644
index 000000000..0d8a14497
--- /dev/null
+++ b/src/main/res/menu/import_backup.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml
index 69312b862..7d74e4c44 100644
--- a/src/main/res/values-es/strings.xml
+++ b/src/main/res/values-es/strings.xml
@@ -17,6 +17,8 @@
Desbloquear contacto
Bloquear dominio
Desbloquear dominio
+ Bloquear participante
+ Desbloquear participante
Gestionar Cuentas
Ajustes
Compartir con Conversación
diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml
index 3c14e2f5a..7bc92e1d9 100644
--- a/src/main/res/values-pl/strings.xml
+++ b/src/main/res/values-pl/strings.xml
@@ -17,6 +17,8 @@
Odblokuj kontakt
Zablokuj domenę
Odblokuj domenę
+ Zablokuj użytkownika
+ Odblokuj użytkownika
Zarządzaj kontami
Ustawienia
Udostępnij w konwersacji
@@ -205,8 +207,8 @@
ID klucza OpenPGP
Odcisk OMEMO
Odcisk v\\OMEMO
- Odcisk OMEMO wiadomości
- Odcisk v\\OMEMO wiadomości
+ Odcisk OMEMO tej wiadomości
+ Odcisk v\\OMEMO tej wiadomości
Pozostałe urządzenia
Zaufane odciski OMEMO
Pobieranie kluczy...
diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml
index 2c2acba97..1f78b19a2 100644
--- a/src/main/res/values/attrs.xml
+++ b/src/main/res/values/attrs.xml
@@ -43,6 +43,9 @@
+
+
+
@@ -63,6 +66,7 @@
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index b095751f6..3159c119d 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -866,4 +866,11 @@
This looks like a domain address
Add anyway
This looks like a channel address
+ Share backup files
+ Conversations backup
+ Event
+ Open backup
+ The file you selected is not a Conversations backup file
+ This account has already been setup
+ Please enter the password for this account
diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml
index d4a6cc4c2..9ea201428 100644
--- a/src/main/res/values/themes.xml
+++ b/src/main/res/values/themes.xml
@@ -74,6 +74,7 @@
- @drawable/ic_event_black_48dp
- @drawable/ic_archive_black_48dp
- @drawable/ic_book_black_48dp
+ - @drawable/ic_backup_black_48dp
- @drawable/ic_help_black_48dp
- @drawable/ic_group_add_white_24dp
@@ -97,6 +98,7 @@
- @drawable/ic_lock_open_white_24dp
- @drawable/ic_settings_black_24dp
- @drawable/ic_share_white_24dp
+ - @drawable/ic_cloud_download_white_24dp
- @drawable/ic_qr_code_scan_white_24dp
- @drawable/ic_scroll_to_end_black
@@ -187,6 +189,7 @@
- @drawable/ic_event_white_48dp
- @drawable/ic_archive_white_48dp
- @drawable/ic_book_white_48dp
+ - @drawable/ic_backup_white_48dp
- @drawable/ic_help_white_48dp
- @drawable/ic_group_add_white_24dp
@@ -210,6 +213,7 @@
- @drawable/ic_lock_open_white_24dp
- @drawable/ic_settings_white_24dp
- @drawable/ic_share_white_24dp
+ - @drawable/ic_cloud_download_white_24dp
- @drawable/ic_qr_code_scan_white_24dp
- @drawable/ic_scroll_to_end_white
diff --git a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java
index 2a3d1a648..1fc8e58fe 100644
--- a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java
+++ b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java
@@ -151,7 +151,13 @@ public class PushManagementService {
if (!task.isSuccessful()) {
Log.d(Config.LOGTAG, "unable to get Firebase instance token", task.getException());
}
- final InstanceIdResult result = task.getResult();
+ final InstanceIdResult result;
+ try {
+ result = task.getResult();
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG, "unable to get Firebase instance token due to bug in library ", e);
+ return;
+ }
if (result != null) {
instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result.getToken());
}
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/util/PinEntryWrapper.java b/src/quicksy/java/eu/siacs/conversations/ui/util/PinEntryWrapper.java
index 17da72798..8e91f659d 100644
--- a/src/quicksy/java/eu/siacs/conversations/ui/util/PinEntryWrapper.java
+++ b/src/quicksy/java/eu/siacs/conversations/ui/util/PinEntryWrapper.java
@@ -119,13 +119,19 @@ public class PinEntryWrapper {
}
}
- public void setEnabled(boolean enabled) {
- for(EditText digit : digits) {
+ public void setEnabled(final boolean enabled) {
+ for (EditText digit : digits) {
digit.setEnabled(enabled);
digit.setCursorVisible(enabled);
digit.setFocusable(enabled);
digit.setFocusableInTouchMode(enabled);
}
+ if (enabled) {
+ final EditText last = digits.get(digits.size() - 1);
+ if (last.getEditableText().length() > 0) {
+ last.requestFocus();
+ }
+ }
}
public boolean isEmpty() {
diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml
index bb9c8d206..2c4a1b0b7 100644
--- a/src/quicksy/res/values-es/strings.xml
+++ b/src/quicksy/res/values-es/strings.xml
@@ -19,4 +19,5 @@
Quicksy necesita acceder al micrófono
Esta categoría de notificación se usa para mostrar una notificación permantente indicando que Quicksy está ejecutándose.
Foto de perfil en Quicksy
-
+ Quicksy no está disponible en tu país.
+
diff --git a/src/quicksy/res/values-pl/strings.xml b/src/quicksy/res/values-pl/strings.xml
index b08a73a90..9b9961c6e 100644
--- a/src/quicksy/res/values-pl/strings.xml
+++ b/src/quicksy/res/values-pl/strings.xml
@@ -19,4 +19,5 @@
Quicksy potrzebuje dostępu do mikrofonu.
Ta kategoria powiadomień jest używana do wyświetlania ciągłego powiadomienia o tym, że Quicksy działa.
Obrazek profilowy Quicksy
-
+ Quicksy nie jest dostępne w twoim kraju
+