aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/AndroidManifest.xml7
-rw-r--r--src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java4
-rw-r--r--src/main/java/de/pixart/messenger/services/AlarmReceiver.java2
-rw-r--r--src/main/java/de/pixart/messenger/services/AvatarService.java84
-rw-r--r--src/main/java/de/pixart/messenger/services/ExportBackupService.java287
-rw-r--r--src/main/java/de/pixart/messenger/services/ExportLogsService.java248
-rw-r--r--src/main/java/de/pixart/messenger/services/ImportBackupService.java290
-rw-r--r--src/main/java/de/pixart/messenger/services/NotificationService.java1
-rw-r--r--src/main/java/de/pixart/messenger/ui/ConversationFragment.java30
-rw-r--r--src/main/java/de/pixart/messenger/ui/EditAccountActivity.java31
-rw-r--r--src/main/java/de/pixart/messenger/ui/ImportBackupActivity.java124
-rw-r--r--src/main/java/de/pixart/messenger/ui/ManageAccountActivity.java36
-rw-r--r--src/main/java/de/pixart/messenger/ui/SettingsActivity.java27
-rw-r--r--src/main/java/de/pixart/messenger/ui/UriHandlerActivity.java2
-rw-r--r--src/main/java/de/pixart/messenger/ui/WelcomeActivity.java311
-rw-r--r--src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java148
-rw-r--r--src/main/java/de/pixart/messenger/ui/adapter/BackupFileAdapter.java167
-rw-r--r--src/main/java/de/pixart/messenger/utils/BackupFileHeader.java85
-rw-r--r--src/main/java/de/pixart/messenger/utils/PermissionUtils.java34
-rw-r--r--src/main/res/drawable-hdpi/ic_archive_white_24dp.pngbin0 -> 310 bytes
-rw-r--r--src/main/res/drawable-hdpi/ic_unarchive_white_24dp.pngbin0 -> 312 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_archive_white_24dp.pngbin0 -> 278 bytes
-rw-r--r--src/main/res/drawable-mdpi/ic_unarchive_white_24dp.pngbin0 -> 279 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_archive_white_24dp.pngbin0 -> 509 bytes
-rw-r--r--src/main/res/drawable-xhdpi/ic_unarchive_white_24dp.pngbin0 -> 505 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_archive_white_24dp.pngbin0 -> 622 bytes
-rw-r--r--src/main/res/drawable-xxhdpi/ic_unarchive_white_24dp.pngbin0 -> 619 bytes
-rw-r--r--src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.pngbin0 -> 1405 bytes
-rw-r--r--src/main/res/drawable-xxxhdpi/ic_unarchive_white_24dp.pngbin0 -> 1425 bytes
-rw-r--r--src/main/res/layout/account_row.xml99
-rw-r--r--src/main/res/layout/activity_import_backup.xml32
-rw-r--r--src/main/res/layout/dialog_enter_password.xml47
-rw-r--r--src/main/res/layout/dialog_quickedit.xml6
-rw-r--r--src/main/res/menu/editaccount.xml5
-rw-r--r--src/main/res/menu/manageaccounts.xml16
-rw-r--r--src/main/res/values/attrs.xml1
-rw-r--r--src/main/res/values/strings.xml19
-rw-r--r--src/main/res/values/themes.xml4
-rw-r--r--src/main/res/xml/preferences.xml8
39 files changed, 1384 insertions, 771 deletions
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 7787a652a..2edfdbf67 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -163,6 +163,10 @@
android:label="@string/create_account"
android:launchMode="singleTask" />
<activity
+ android:name=".ui.ImportBackupActivity"
+ android:label="@string/restore_backup"
+ android:launchMode="singleTask" />
+ <activity
android:name=".ui.SettingsActivity"
android:label="@string/title_activity_settings">
<intent-filter>
@@ -302,7 +306,8 @@
android:label="@string/enter_your_name"
android:launchMode="singleTask" />
- <service android:name=".services.ExportLogsService" />
+ <service android:name=".services.ExportBackupService" />
+ <service android:name=".services.ImportBackupService"/>
<service
android:name=".services.ContactChooserTargetService"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
diff --git a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java
index d4033eef1..d3220a875 100644
--- a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java
+++ b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java
@@ -926,11 +926,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return getAccounts(db);
}
- public List<Jid> getAccountJids() {
+ public List<Jid> getAccountJids(final boolean enabledOnly) {
SQLiteDatabase db = this.getReadableDatabase();
final List<Jid> jids = new ArrayList<>();
final String[] columns = new String[]{Account.USERNAME, Account.SERVER};
- String where = "not options & (1 <<1)";
+ String where = enabledOnly ? "not options & (1 <<1)" : null;
Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null);
try {
while (cursor.moveToNext()) {
diff --git a/src/main/java/de/pixart/messenger/services/AlarmReceiver.java b/src/main/java/de/pixart/messenger/services/AlarmReceiver.java
index eb38da8b3..887a59684 100644
--- a/src/main/java/de/pixart/messenger/services/AlarmReceiver.java
+++ b/src/main/java/de/pixart/messenger/services/AlarmReceiver.java
@@ -15,7 +15,7 @@ public class AlarmReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if (intent.getAction().contains("exportlogs")) {
Log.d(Config.LOGTAG, "Received alarm broadcast to export logs");
- Compatibility.startService(context, new Intent(context, ExportLogsService.class));
+ Compatibility.startService(context, new Intent(context, ExportBackupService.class));
}
}
}
diff --git a/src/main/java/de/pixart/messenger/services/AvatarService.java b/src/main/java/de/pixart/messenger/services/AvatarService.java
index 93d24a63f..fc6235a55 100644
--- a/src/main/java/de/pixart/messenger/services/AvatarService.java
+++ b/src/main/java/de/pixart/messenger/services/AvatarService.java
@@ -51,6 +51,8 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
private static final int TRANSPARENT = 0x00000000;
private static final int PLACEHOLDER_COLOR = 0xFF202020;
+ public static final int SYSTEM_UI_AVATAR_SIZE = 48;
+
private static final String PREFIX_CONTACT = "contact";
private static final String PREFIX_CONVERSATION = "conversation";
private static final String PREFIX_ACCOUNT = "account";
@@ -65,19 +67,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
this.mXmppConnectionService = service;
}
- private static String getFirstLetter(String name) {
- for (Character c : name.toCharArray()) {
- if (Character.isLetterOrDigit(c)) {
- return c.toString();
- }
- }
- return "X";
- }
-
- private static String emptyOnNull(@Nullable Jid value) {
- return value == null ? "" : value.toString();
- }
-
public static int getSystemUiAvatarSize(final Context context) {
return (int) (SYSTEM_UI_AVATAR_SIZE * context.getResources().getDisplayMetrics().density);
}
@@ -91,15 +80,14 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (avatar != null || cachedOnly) {
return avatar;
}
- if (contact.getAvatarFilename() != null) {
+ if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) {
avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size);
}
if (avatar == null && contact.getProfilePhoto() != null) {
- try {
- avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
- } catch (Exception e) {
- e.printStackTrace();
- }
+ avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
+ }
+ if (avatar == null && contact.getAvatarFilename() != null) {
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size);
}
if (avatar == null) {
avatar = get(contact.getDisplayName(), contact.getJid().asBareJid().toString(), size, false);
@@ -123,6 +111,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
final Paint paint = new Paint();
+
drawAvatar(bitmap, canvas, paint);
if (withIcon) {
drawIcon(canvas, paint);
@@ -318,7 +307,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (bitmap != null || cachedOnly) {
return bitmap;
}
+
bitmap = mXmppConnectionService.getFileBackend().getAvatar(mucOptions.getAvatar(), size);
+
if (bitmap == null) {
final List<MucOptions.User> users = mucOptions.getUsersRelevantForNameAndAvatar();
if (users.size() == 0) {
@@ -328,7 +319,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
bitmap = getImpl(users, size);
}
}
+
this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+
return bitmap;
}
@@ -488,10 +481,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
}
}
- /*public Bitmap get(String name, int size) {
- return get(name,null, size,false);
- }*/
-
public void clear(MucOptions.User user) {
synchronized (this.sizes) {
for (Integer size : sizes) {
@@ -510,6 +499,10 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
+ String.valueOf(size);
}
+ /*public Bitmap get(String name, int size) {
+ return get(name,null, size,false);
+ }*/
+
public Bitmap get(final String name, String seed, final int size, boolean cachedOnly) {
final String KEY = key(seed == null ? name : name + "\0" + seed, size);
Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
@@ -521,7 +514,11 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return bitmap;
}
- private Bitmap getImpl(final String name, final String seed, final int size) {
+ public static Bitmap get(final Jid jid, final int size) {
+ return getImpl(jid.asBareJid().toEscapedString(), null, size);
+ }
+
+ private static Bitmap getImpl(final String name, final String seed, final int size) {
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
final String trimmedName = name == null ? "" : name.trim();
@@ -538,7 +535,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size);
}
- private boolean drawTile(Canvas canvas, String letter, int tileColor, int left, int top, int right, int bottom) {
+ private static boolean drawTile(Canvas canvas, String letter, int tileColor, int left, int top, int right, int bottom) {
letter = letter.toUpperCase(Locale.getDefault());
Paint tilePaint = new Paint(), textPaint = new Paint();
tilePaint.setColor(tileColor);
@@ -561,14 +558,12 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
Contact contact = user.getContact();
if (contact != null) {
Uri uri = null;
- if (contact.getAvatarFilename() != null) {
- try {
- uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
- } catch (Exception e) {
- e.printStackTrace();
- }
+ if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
} else if (contact.getProfilePhoto() != null) {
uri = Uri.parse(contact.getProfilePhoto());
+ } else if (contact.getAvatarFilename() != null) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
}
if (drawTile(canvas, uri, left, top, right, bottom)) {
return true;
@@ -578,16 +573,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (drawTile(canvas, uri, left, top, right, bottom)) {
return true;
}
- } else if (user.getAvatar() != null) {
- Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar());
- if (drawTile(canvas, uri, left, top, right, bottom)) {
- return true;
- }
- } else if (user.getAvatar() != null) {
- Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar());
- if (drawTile(canvas, uri, left, top, right, bottom)) {
- return true;
- }
}
if (contact != null) {
String seed = contact.getJid().asBareJid().toString();
@@ -613,7 +598,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return drawTile(canvas, name, name, left, top, right, bottom);
}
- private boolean drawTile(Canvas canvas, String name, String seed, int left, int top, int right, int bottom) {
+ private static boolean drawTile(Canvas canvas, String name, String seed, int left, int top, int right, int bottom) {
if (name != null) {
final String letter = getFirstLetter(name);
final int color = UIHelper.getColorForName(seed == null ? name : seed);
@@ -623,6 +608,15 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return false;
}
+ private static String getFirstLetter(String name) {
+ for (Character c : name.toCharArray()) {
+ if (Character.isLetterOrDigit(c)) {
+ return c.toString();
+ }
+ }
+ return "X";
+ }
+
private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) {
if (uri != null) {
Bitmap bitmap = mXmppConnectionService.getFileBackend()
@@ -651,4 +645,8 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
}
}
}
-}
+
+ private static String emptyOnNull(@Nullable Jid value) {
+ return value == null ? "" : value.toString();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/services/ExportBackupService.java b/src/main/java/de/pixart/messenger/services/ExportBackupService.java
new file mode 100644
index 000000000..36b926f6a
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/services/ExportBackupService.java
@@ -0,0 +1,287 @@
+package de.pixart.messenger.services;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.GZIPOutputStream;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import de.pixart.messenger.Config;
+import de.pixart.messenger.R;
+import de.pixart.messenger.crypto.axolotl.SQLiteAxolotlStore;
+import de.pixart.messenger.entities.Account;
+import de.pixart.messenger.entities.Conversation;
+import de.pixart.messenger.entities.Message;
+import de.pixart.messenger.persistance.DatabaseBackend;
+import de.pixart.messenger.persistance.FileBackend;
+import de.pixart.messenger.utils.BackupFileHeader;
+import de.pixart.messenger.utils.Compatibility;
+
+public class ExportBackupService extends Service {
+
+ public static final String KEYTYPE = "AES";
+ public static final String CIPHERMODE = "AES/GCM/NoPadding";
+ public static final String PROVIDER = "BC";
+
+ private static final int NOTIFICATION_ID = 19;
+ private static final int PAGE_SIZE = 20;
+ private static AtomicBoolean running = new AtomicBoolean(false);
+ private DatabaseBackend mDatabaseBackend;
+ private List<Account> mAccounts;
+ private NotificationManager notificationManager;
+
+ private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
+ final StringBuilder builder = new StringBuilder();
+ final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
+ while (accountCursor != null && accountCursor.moveToNext()) {
+ builder.append("INSERT INTO ").append(Account.TABLENAME).append("(");
+ for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ builder.append(accountCursor.getColumnName(i));
+ }
+ builder.append(") VALUES(");
+ for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ final String value = accountCursor.getString(i);
+ if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) {
+ builder.append("NULL");
+ } else if (value.matches("\\d+")) {
+ int intValue = Integer.parseInt(value);
+ if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) {
+ intValue |= 1 << Account.OPTION_DISABLED;
+ }
+ builder.append(intValue);
+ } else {
+ DatabaseUtils.appendEscapedSQLString(builder, value);
+ }
+ }
+ builder.append(")");
+ builder.append(';');
+ builder.append('\n');
+ }
+ if (accountCursor != null) {
+ accountCursor.close();
+ }
+ writer.append(builder.toString());
+ }
+
+ private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
+ final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
+ while (cursor != null && cursor.moveToNext()) {
+ writer.write(cursorToString(table, cursor, PAGE_SIZE));
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ public static byte[] getKey(String password, byte[] salt) {
+ try {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static String cursorToString(String tablename, Cursor cursor, int max) {
+ return cursorToString(tablename, cursor, max, false);
+ }
+
+ private static String cursorToString(String tablename, Cursor cursor, int max, boolean ignore) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("INSERT ");
+ if (ignore) {
+ builder.append("OR IGNORE ");
+ }
+ builder.append("INTO ").append(tablename).append("(");
+ for (int i = 0; i < cursor.getColumnCount(); ++i) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ builder.append(cursor.getColumnName(i));
+ }
+ builder.append(") VALUES");
+ for (int i = 0; i < max; ++i) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ appendValues(cursor, builder);
+ if (i < max - 1 && !cursor.moveToNext()) {
+ break;
+ }
+ }
+ builder.append(';');
+ builder.append('\n');
+ return builder.toString();
+ }
+
+ private static void appendValues(Cursor cursor, StringBuilder builder) {
+ builder.append("(");
+ for (int i = 0; i < cursor.getColumnCount(); ++i) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ final String value = cursor.getString(i);
+ if (value == null) {
+ builder.append("NULL");
+ } else if (value.matches("\\d+")) {
+ builder.append(value);
+ } else {
+ DatabaseUtils.appendEscapedSQLString(builder, value);
+ }
+ }
+ builder.append(")");
+
+ }
+
+ @Override
+ public void onCreate() {
+ mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+ mAccounts = mDatabaseBackend.getAccounts();
+ notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (running.compareAndSet(false, true)) {
+ new Thread(() -> {
+ export();
+ stopForeground(true);
+ running.set(false);
+ stopSelf();
+ }).start();
+ return START_STICKY;
+ }
+ return START_NOT_STICKY;
+ }
+
+ private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
+ Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
+ int size = cursor != null ? cursor.getCount() : 0;
+ Log.d(Config.LOGTAG, "exporting " + size + " messages");
+ int i = 0;
+ int p = 0;
+ while (cursor != null && cursor.moveToNext()) {
+ writer.write(cursorToString(Message.TABLENAME, cursor, PAGE_SIZE, false));
+ if (i + PAGE_SIZE > size) {
+ i = size;
+ } else {
+ i += PAGE_SIZE;
+ }
+ final int percentage = i * 100 / size;
+ if (p < percentage) {
+ p = percentage;
+ notificationManager.notify(NOTIFICATION_ID, progress.build(p));
+ }
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ private void export() {
+ 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() + 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++;
+ }
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG, "unable to create backup ", e);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private class Progress {
+ private final NotificationCompat.Builder builder;
+ private final int max;
+ private final int count;
+
+ private Progress(NotificationCompat.Builder builder, int max, int count) {
+ this.builder = builder;
+ this.max = max;
+ this.count = count;
+ }
+
+ private Notification build(int percentage) {
+ builder.setProgress(max * 100, count * 100 + percentage, false);
+ return builder.build();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/services/ExportLogsService.java b/src/main/java/de/pixart/messenger/services/ExportLogsService.java
deleted file mode 100644
index a607fcb66..000000000
--- a/src/main/java/de/pixart/messenger/services/ExportLogsService.java
+++ /dev/null
@@ -1,248 +0,0 @@
-package de.pixart.messenger.services;
-
-import android.app.NotificationManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.IBinder;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.preference.PreferenceManager;
-import android.support.annotation.BoolRes;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import javax.crypto.NoSuchPaddingException;
-
-import de.pixart.messenger.Config;
-import de.pixart.messenger.R;
-import de.pixart.messenger.entities.Account;
-import de.pixart.messenger.entities.Conversation;
-import de.pixart.messenger.entities.Message;
-import de.pixart.messenger.persistance.DatabaseBackend;
-import de.pixart.messenger.persistance.FileBackend;
-import de.pixart.messenger.utils.EncryptDecryptFile;
-import de.pixart.messenger.utils.WakeLockHelper;
-import rocks.xmpp.addr.Jid;
-
-import static de.pixart.messenger.services.NotificationService.BACKUP_CHANNEL_ID;
-import static de.pixart.messenger.services.NotificationService.NOTIFICATION_ID;
-import static de.pixart.messenger.ui.SettingsActivity.USE_MULTI_ACCOUNTS;
-
-public class ExportLogsService extends XmppConnectionService {
-
- private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
- private static final String DIRECTORY_STRING_FORMAT = FileBackend.getAppLogsDirectory() + "%s";
- private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n";
- private static AtomicBoolean running = new AtomicBoolean(false);
- boolean ReadableLogsEnabled = false;
- private DatabaseBackend mDatabaseBackend;
- private List<Account> mAccounts;
- private WakeLock wakeLock;
- private PowerManager pm;
-
- @Override
- public void onCreate() {
- mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
- mAccounts = mDatabaseBackend.getAccounts();
- final SharedPreferences ReadableLogs = PreferenceManager.getDefaultSharedPreferences(this);
- ReadableLogsEnabled = ReadableLogs.getBoolean("export_plain_text_logs", getResources().getBoolean(R.bool.plain_text_logs));
- pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Config.LOGTAG + ": ExportLogsService");
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- if (running.compareAndSet(false, true)) {
- new Thread(() -> {
- export();
- WakeLockHelper.release(wakeLock);
- stopForeground(true);
- running.set(false);
- stopSelf();
- }).start();
- }
- return START_NOT_STICKY;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- stopForeground(true);
- }
-
- private void export() {
- wakeLock.acquire();
- NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), BACKUP_CHANNEL_ID);
- mBuilder.setContentTitle(getString(R.string.notification_export_logs_title));
- mBuilder.setSmallIcon(R.drawable.ic_import_export_white_24dp);
- mBuilder.setProgress(0, 0, true);
- startForeground(NOTIFICATION_ID, mBuilder.build());
- List<Conversation> conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE);
- conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED));
- if (mAccounts.size() >= 1) {
- if (ReadableLogsEnabled) {
- for (Conversation conversation : conversations) {
- writeToFile(conversation);
- }
- }
- try {
- ExportDatabase();
- } catch (IOException e) {
- e.printStackTrace();
- }
- } else {
- Log.d(Config.LOGTAG, "ExportLogsService: no accounts, aborting export");
- }
- }
-
- private void writeToFile(Conversation conversation) {
- Jid accountJid = resolveAccountUuid(conversation.getAccountUuid());
- Jid contactJid = conversation.getJid();
-
- File dir = new File(String.format(DIRECTORY_STRING_FORMAT, accountJid.asBareJid().toString()));
- dir.mkdirs();
-
- BufferedWriter bw = null;
- try {
- for (Message message : mDatabaseBackend.getMessagesIterable(conversation)) {
- if (message == null)
- continue;
- if (message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) {
- String date = simpleDateFormat.format(new Date(message.getTimeSent()));
- if (bw == null) {
- bw = new BufferedWriter(new FileWriter(
- new File(dir, contactJid.asBareJid().toString() + ".txt")));
- }
- String jid = null;
- switch (message.getStatus()) {
- case Message.STATUS_RECEIVED:
- jid = getMessageCounterpart(message);
- break;
- case Message.STATUS_SEND:
- case Message.STATUS_SEND_RECEIVED:
- case Message.STATUS_SEND_DISPLAYED:
- jid = accountJid.asBareJid().toString();
- break;
- }
- if (jid != null) {
- String body = message.hasFileOnRemoteHost() ? message.getFileParams().url.toString() : message.getBody();
- bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid,
- body.replace("\\\n", "\\ \n").replace("\n", "\\ \n")));
- }
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (bw != null) {
- bw.close();
- }
- } catch (IOException e1) {
- e1.printStackTrace();
- }
- }
- }
-
- private Jid resolveAccountUuid(String accountUuid) {
- for (Account account : mAccounts) {
- if (account.getUuid().equals(accountUuid)) {
- return account.getJid();
- }
- }
- return null;
- }
-
- private String getMessageCounterpart(Message message) {
- String trueCounterpart = (String) message.getContentValues().get(Message.TRUE_COUNTERPART);
- if (trueCounterpart != null) {
- return trueCounterpart;
- } else {
- return message.getCounterpart().toString();
- }
- }
-
- public void ExportDatabase() throws IOException {
- Log.d(Config.LOGTAG, "ExportLogsService: start creating backup");
- Account mAccount = mAccounts.get(0);
- String EncryptionKey = null;
- // Get hold of the db:
- FileInputStream InputFile = new FileInputStream(this.getDatabasePath(DatabaseBackend.DATABASE_NAME));
- // Set the output folder on the SDcard
- File directory = new File(FileBackend.getBackupDirectory());
- // Create the folder if it doesn't exist:
- if (!directory.exists()) {
- boolean directory_created = directory.mkdirs();
- Log.d(Config.LOGTAG, "ExportLogsService: backup directory created " + directory_created);
- }
- //Delete old database export file
- File temp_db_file = new File(directory + "/database.bak");
- if (temp_db_file.exists()) {
- Log.d(Config.LOGTAG, "ExportLogsService: Delete temp database backup file from " + temp_db_file.toString());
- boolean temp_db_file_deleted = temp_db_file.delete();
- Log.d(Config.LOGTAG, "ExportLogsService: old backup file deleted " + temp_db_file_deleted);
- }
- // Set the output file stream up:
- FileOutputStream OutputFile = new FileOutputStream(directory.getPath() + "/database.db.crypt");
-
- if (mAccounts.size() == 1 && !multipleAccounts()) {
- EncryptionKey = mAccount.getPassword(); //get account password
- } else {
- SharedPreferences multiaccount_prefs = getApplicationContext().getSharedPreferences(USE_MULTI_ACCOUNTS, Context.MODE_PRIVATE);
- EncryptionKey = multiaccount_prefs.getString("BackupPW", null);
- }
- if (EncryptionKey == null) {
- Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: failed to write encryted backup to sdcard because of missing password");
- return;
- }
-
- // encrypt database from the input file to the output file
- try {
- EncryptDecryptFile.encrypt(InputFile, OutputFile, EncryptionKey);
- Log.d(Config.LOGTAG, "ExportLogsService: starting encrypted output to " + OutputFile.toString());
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed with " + e);
- e.printStackTrace();
- } catch (InvalidKeyException e) {
- Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed (invalid key) with " + e);
- e.printStackTrace();
- } catch (IOException e) {
- Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed (IO) with " + e);
- e.printStackTrace();
- } finally {
- Log.d(Config.LOGTAG, "ExportLogsService: backup job finished");
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- public SharedPreferences getPreferences() {
- return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- }
-
- public boolean getBooleanPreference(String name, @BoolRes int res) {
- return getPreferences().getBoolean(name, getResources().getBoolean(res));
- }
-
- public boolean multipleAccounts() {
- return getBooleanPreference("enable_multi_accounts", R.bool.enable_multi_accounts);
- }
-}
diff --git a/src/main/java/de/pixart/messenger/services/ImportBackupService.java b/src/main/java/de/pixart/messenger/services/ImportBackupService.java
new file mode 100644
index 000000000..e73b1972f
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/services/ImportBackupService.java
@@ -0,0 +1,290 @@
+package de.pixart.messenger.services;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Binder;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+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 javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+
+import de.pixart.messenger.Config;
+import de.pixart.messenger.R;
+import de.pixart.messenger.persistance.DatabaseBackend;
+import de.pixart.messenger.persistance.FileBackend;
+import de.pixart.messenger.ui.ManageAccountActivity;
+import de.pixart.messenger.utils.BackupFileHeader;
+import de.pixart.messenger.utils.Compatibility;
+import de.pixart.messenger.utils.SerialSingleThreadExecutor;
+import rocks.xmpp.addr.Jid;
+
+import static de.pixart.messenger.services.ExportBackupService.CIPHERMODE;
+import static de.pixart.messenger.services.ExportBackupService.KEYTYPE;
+import static de.pixart.messenger.services.ExportBackupService.PROVIDER;
+
+public class ImportBackupService extends Service {
+
+ private static final int NOTIFICATION_ID = 21;
+ private static AtomicBoolean running = new AtomicBoolean(false);
+ private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
+ private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
+ private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
+ private DatabaseBackend mDatabaseBackend;
+ private NotificationManager notificationManager;
+
+ private static int count(String input, char c) {
+ int count = 0;
+ for (char aChar : input.toCharArray()) {
+ if (aChar == c) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public void onCreate() {
+ mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+ notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+ final String password = intent.getStringExtra("password");
+ final String file = intent.getStringExtra("file");
+ if (password == null || file == null) {
+ return START_NOT_STICKY;
+ }
+ if (running.compareAndSet(false, true)) {
+ executor.execute(() -> {
+ startForegroundService();
+ final boolean success = importBackup(new File(file), password);
+ stopForeground(true);
+ running.set(false);
+ if (success) {
+ notifySuccess();
+ }
+ stopSelf();
+ });
+ } else {
+ Log.d(Config.LOGTAG, "backup already running");
+ }
+ return START_NOT_STICKY;
+ }
+
+ public void loadBackupFiles(OnBackupFilesLoaded onBackupFilesLoaded) {
+ executor.execute(() -> {
+ 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());
+ if (!directory.exists() || !directory.isDirectory()) {
+ Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
+ continue;
+ }
+ for (File file : directory.listFiles()) {
+ if (file.isFile() && file.getName().endsWith(".ceb")) {
+ try {
+ final BackupFile backupFile = BackupFile.read(file);
+ if (accounts.contains(backupFile.getHeader().getJid())) {
+ Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid());
+ } else {
+ backupFiles.add(backupFile);
+ }
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG, "unable to read backup file ", e);
+ }
+ }
+ }
+ }
+ onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
+ });
+ }
+
+ private void startForegroundService() {
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ mBuilder.setContentTitle(getString(R.string.notification_restore_backup_title))
+ .setSmallIcon(R.drawable.ic_unarchive_white_24dp)
+ .setProgress(1, 0, true);
+ startForeground(NOTIFICATION_ID, mBuilder.build());
+ }
+
+ private boolean importBackup(File file, String password) {
+ Log.d(Config.LOGTAG, "importing backup from file " + file.getAbsolutePath());
+ try {
+ SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
+ final FileInputStream fileInputStream = new FileInputStream(file);
+ final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
+ BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ Log.d(Config.LOGTAG, backupFileHeader.toString());
+
+ 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);
+
+ GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
+ String line;
+ StringBuilder multiLineQuery = null;
+ while ((line = reader.readLine()) != null) {
+ int count = count(line, '\'');
+ if (multiLineQuery != null) {
+ multiLineQuery.append('\n');
+ multiLineQuery.append(line);
+ if (count % 2 == 1) {
+ db.execSQL(multiLineQuery.toString());
+ multiLineQuery = null;
+ }
+ } else {
+ if (count % 2 == 0) {
+ db.execSQL(line);
+ } else {
+ multiLineQuery = new StringBuilder(line);
+ }
+ }
+ }
+ final Jid jid = backupFileHeader.getJid();
+ Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain()});
+ countCursor.moveToFirst();
+ int count = countCursor.getInt(0);
+ Log.d(Config.LOGTAG, "restored " + count + " messages");
+ countCursor.close();
+ stopBackgroundService();
+ synchronized (mOnBackupProcessedListeners) {
+ for (OnBackupProcessed l : mOnBackupProcessedListeners) {
+ l.onBackupRestored();
+ }
+ }
+ return true;
+ } catch (Exception e) {
+ Throwable throwable = e.getCause();
+ final boolean reasonWasCrypto;
+ if (throwable instanceof BadPaddingException) {
+ reasonWasCrypto = true;
+ } else {
+ reasonWasCrypto = false;
+ }
+ synchronized (mOnBackupProcessedListeners) {
+ for (OnBackupProcessed l : mOnBackupProcessedListeners) {
+ if (reasonWasCrypto) {
+ l.onBackupDecryptionFailed();
+ } else {
+ l.onBackupRestoreFailed();
+ }
+ }
+ }
+ Log.d(Config.LOGTAG, "error restoring backup " + file.getAbsolutePath(), e);
+ return false;
+ }
+ }
+
+ private void notifySuccess() {
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title))
+ .setContentText(getString(R.string.notification_restored_backup_subtitle))
+ .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());
+ }
+
+ private void stopBackgroundService() {
+ Intent intent = new Intent(this, XmppConnectionService.class);
+ stopService(intent);
+ }
+
+ public void removeOnBackupProcessedListener(OnBackupProcessed listener) {
+ synchronized (mOnBackupProcessedListeners) {
+ mOnBackupProcessedListeners.remove(listener);
+ }
+ }
+
+ public void addOnBackupProcessedListener(OnBackupProcessed listener) {
+ synchronized (mOnBackupProcessedListeners) {
+ mOnBackupProcessedListeners.add(listener);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return this.binder;
+ }
+
+ public interface OnBackupFilesLoaded {
+ void onBackupFilesLoaded(List<BackupFile> files);
+ }
+
+ public interface OnBackupProcessed {
+ void onBackupRestored();
+
+ void onBackupDecryptionFailed();
+
+ void onBackupRestoreFailed();
+ }
+
+ public static class BackupFile {
+ private final File file;
+ private final BackupFileHeader header;
+
+ private BackupFile(File file, BackupFileHeader header) {
+ this.file = file;
+ this.header = header;
+ }
+
+ private static BackupFile read(File file) throws IOException {
+ final FileInputStream fileInputStream = new FileInputStream(file);
+ final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
+ BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ fileInputStream.close();
+ return new BackupFile(file, backupFileHeader);
+ }
+
+ public BackupFileHeader getHeader() {
+ return header;
+ }
+
+ public File getFile() {
+ return file;
+ }
+ }
+
+ public class ImportBackupServiceBinder extends Binder {
+ public ImportBackupService getService() {
+ return ImportBackupService.this;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/services/NotificationService.java b/src/main/java/de/pixart/messenger/services/NotificationService.java
index 8ded39d50..c3f97a7fe 100644
--- a/src/main/java/de/pixart/messenger/services/NotificationService.java
+++ b/src/main/java/de/pixart/messenger/services/NotificationService.java
@@ -1047,7 +1047,6 @@ public class NotificationService {
Notification AppUpdateNotification(PendingIntent intent, String version, String filesize) {
Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
- mBuilder.setContentText(mXmppConnectionService.getString(R.string.notification_export_logs_title));
mBuilder.setContentText(String.format(mXmppConnectionService.getString(R.string.update_available), version, filesize));
mBuilder.setSmallIcon(R.drawable.ic_update_notification);
mBuilder.setContentIntent(intent);
diff --git a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java
index d7205506d..4f0486cf7 100644
--- a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java
+++ b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java
@@ -126,6 +126,9 @@ import rocks.xmpp.addr.Jid;
import static de.pixart.messenger.ui.XmppActivity.EXTRA_ACCOUNT;
import static de.pixart.messenger.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
import static de.pixart.messenger.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
+import static de.pixart.messenger.utils.PermissionUtils.allGranted;
+import static de.pixart.messenger.utils.PermissionUtils.getFirstDenied;
+import static de.pixart.messenger.utils.PermissionUtils.writeGranted;
import static de.pixart.messenger.xmpp.Patches.ENCRYPTION_EXCEPTIONS;
public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {
@@ -596,33 +599,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return getConversation(activity, R.id.main_fragment);
}
- private static boolean allGranted(int[] grantResults) {
- for (int grantResult : grantResults) {
- if (grantResult != PackageManager.PERMISSION_GRANTED) {
- return false;
- }
- }
- return true;
- }
-
- private static boolean writeGranted(int[] grantResults, String[] permission) {
- for (int i = 0; i < grantResults.length; ++i) {
- if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) {
- return grantResults[i] == PackageManager.PERMISSION_GRANTED;
- }
- }
- return false;
- }
-
- private static String getFirstDenied(int[] grantResults, String[] permissions) {
- for (int i = 0; i < grantResults.length; ++i) {
- if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
- return permissions[i];
- }
- }
- return null;
- }
-
private static boolean scrolledToBottom(AbsListView listView) {
final int count = listView.getCount();
if (count == 0) {
diff --git a/src/main/java/de/pixart/messenger/ui/EditAccountActivity.java b/src/main/java/de/pixart/messenger/ui/EditAccountActivity.java
index d04289d22..beddb195d 100644
--- a/src/main/java/de/pixart/messenger/ui/EditAccountActivity.java
+++ b/src/main/java/de/pixart/messenger/ui/EditAccountActivity.java
@@ -16,6 +16,7 @@ import android.preference.PreferenceManager;
import android.provider.Settings;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
+import android.support.annotation.NonNull;
import android.support.design.widget.TextInputLayout;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
@@ -82,6 +83,9 @@ import de.pixart.messenger.xmpp.forms.Data;
import de.pixart.messenger.xmpp.pep.Avatar;
import rocks.xmpp.addr.Jid;
+import static de.pixart.messenger.utils.PermissionUtils.allGranted;
+import static de.pixart.messenger.utils.PermissionUtils.writeGranted;
+
public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
@@ -90,6 +94,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
private static final int REQUEST_DATA_SAVER = 0xf244;
private static final int REQUEST_CHANGE_STATUS = 0xee11;
private static final int REQUEST_ORBOT = 0xff22;
+ private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
private AlertDialog mCaptchaDialog = null;
private final AtomicBoolean mPendingReconnect = new AtomicBoolean(false);
@@ -825,6 +830,12 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
return false;
}
switch (item.getItemId()) {
+ case R.id.action_import_backup:
+ if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ }
+ overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
+ break;
case R.id.mgmt_account_reconnect:
XmppConnection connection = mAccount.getXmppConnection();
if (connection != null) {
@@ -1444,6 +1455,26 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
}
@Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
+ if (grantResults.length > 0) {
+ if (allGranted(grantResults)) {
+ switch (requestCode) {
+ case REQUEST_IMPORT_BACKUP:
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ break;
+ }
+ } else {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+ }
+ }
+ if (writeGranted(grantResults, permissions)) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.restartFileObserver();
+ }
+ }
+ }
+
+ @Override
public void OnUpdateBlocklist(Status status) {
if (isFinishing()) {
return;
diff --git a/src/main/java/de/pixart/messenger/ui/ImportBackupActivity.java b/src/main/java/de/pixart/messenger/ui/ImportBackupActivity.java
new file mode 100644
index 000000000..f1c9eea25
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/ui/ImportBackupActivity.java
@@ -0,0 +1,124 @@
+package de.pixart.messenger.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.os.Bundle;
+import android.os.IBinder;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.widget.Toast;
+
+import java.util.List;
+
+import de.pixart.messenger.Config;
+import de.pixart.messenger.R;
+import de.pixart.messenger.databinding.ActivityImportBackupBinding;
+import de.pixart.messenger.databinding.DialogEnterPasswordBinding;
+import de.pixart.messenger.services.ImportBackupService;
+import de.pixart.messenger.ui.adapter.BackupFileAdapter;
+
+public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
+
+ private ActivityImportBackupBinding binding;
+
+ private BackupFileAdapter backupFileAdapter;
+ private ImportBackupService service;
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
+ setSupportActionBar((Toolbar) binding.toolbar);
+ configureActionBar(getSupportActionBar());
+ this.backupFileAdapter = new BackupFileAdapter();
+ this.binding.list.setAdapter(this.backupFileAdapter);
+ this.backupFileAdapter.setOnItemClickedListener(this);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (this.service != null) {
+ this.service.removeOnBackupProcessedListener(this);
+ }
+ unbindService(this);
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ ImportBackupService.ImportBackupServiceBinder binder = (ImportBackupService.ImportBackupServiceBinder) service;
+ this.service = binder.getService();
+ this.service.addOnBackupProcessedListener(this);
+ this.service.loadBackupFiles(this);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ this.service = null;
+ }
+
+ @Override
+ public void onBackupFilesLoaded(final List<ImportBackupService.BackupFile> files) {
+ runOnUiThread(() -> {
+ backupFileAdapter.setFiles(files);
+ });
+ }
+
+ @Override
+ public void onClick(ImportBackupService.BackupFile backupFile) {
+ 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());
+ 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());
+ ContextCompat.startForegroundService(this, intent);
+ });
+ builder.setCancelable(false);
+ builder.create().show();
+ }
+
+ @Override
+ public void onBackupRestored() {
+ runOnUiThread(() -> {
+ Intent intent = new Intent(this, StartUI.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ finish();
+ });
+ }
+
+ @Override
+ public void onBackupDecryptionFailed() {
+ runOnUiThread(() -> {
+ Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show();
+ });
+ }
+
+ @Override
+ public void onBackupRestoreFailed() {
+ runOnUiThread(() -> {
+ Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show();
+ });
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/ui/ManageAccountActivity.java b/src/main/java/de/pixart/messenger/ui/ManageAccountActivity.java
index 607e2626d..a09a285f2 100644
--- a/src/main/java/de/pixart/messenger/ui/ManageAccountActivity.java
+++ b/src/main/java/de/pixart/messenger/ui/ManageAccountActivity.java
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
+import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.util.Pair;
@@ -35,10 +36,15 @@ import de.pixart.messenger.utils.MenuDoubleTabUtil;
import de.pixart.messenger.xmpp.XmppConnection;
import rocks.xmpp.addr.Jid;
+import static de.pixart.messenger.utils.PermissionUtils.allGranted;
+import static de.pixart.messenger.utils.PermissionUtils.writeGranted;
+
public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState {
private final String STATE_SELECTED_ACCOUNT = "selected_account";
+ private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
+
protected Account selectedAccount = null;
protected Jid selectedAccountJid = null;
@@ -75,7 +81,6 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_manage_accounts);
-
setSupportActionBar(findViewById(R.id.toolbar));
configureActionBar(getSupportActionBar());
if (savedInstanceState != null) {
@@ -156,6 +161,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
addAccount.setVisible(false);
addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
+
return true;
}
@@ -187,7 +193,13 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
}
switch (item.getItemId()) {
case R.id.action_add_account:
- startActivity(new Intent(getApplicationContext(), EditAccountActivity.class));
+ startActivity(new Intent(this, EditAccountActivity.class));
+ overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
+ break;
+ case R.id.action_import_backup:
+ if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ }
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
break;
case R.id.action_add_account_with_cert:
@@ -200,6 +212,26 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
}
@Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
+ if (grantResults.length > 0) {
+ if (allGranted(grantResults)) {
+ switch (requestCode) {
+ case REQUEST_IMPORT_BACKUP:
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ break;
+ }
+ } else {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+ }
+ }
+ if (writeGranted(grantResults, permissions)) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.restartFileObserver();
+ }
+ }
+ }
+
+ @Override
public boolean onNavigateUp() {
if (xmppConnectionService.getConversations().size() == 0) {
Intent contactsIntent = new Intent(this,
diff --git a/src/main/java/de/pixart/messenger/ui/SettingsActivity.java b/src/main/java/de/pixart/messenger/ui/SettingsActivity.java
index 9c45d01b8..3eb01eb83 100644
--- a/src/main/java/de/pixart/messenger/ui/SettingsActivity.java
+++ b/src/main/java/de/pixart/messenger/ui/SettingsActivity.java
@@ -36,7 +36,8 @@ import de.pixart.messenger.Config;
import de.pixart.messenger.R;
import de.pixart.messenger.crypto.OmemoSetting;
import de.pixart.messenger.entities.Account;
-import de.pixart.messenger.services.ExportLogsService;
+import de.pixart.messenger.persistance.FileBackend;
+import de.pixart.messenger.services.ExportBackupService;
import de.pixart.messenger.services.MemorizingTrustManager;
import de.pixart.messenger.ui.util.StyledAttributes;
import de.pixart.messenger.utils.Compatibility;
@@ -64,7 +65,7 @@ public class SettingsActivity extends XmppActivity implements
public static final String NUMBER_OF_ACCOUNTS = "number_of_accounts";
public static final String PLAY_GIF_INSIDE = "play_gif_inside";
- public static final int REQUEST_WRITE_LOGS = 0xbf8701;
+ public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
Preference multiAccountPreference;
Preference BundledEmojiPreference;
Preference QuickShareAttachmentChoicePreference;
@@ -249,11 +250,12 @@ public class SettingsActivity extends XmppActivity implements
});
}
- final Preference exportLogsPreference = mSettingsFragment.findPreference("export_logs");
- if (exportLogsPreference != null) {
- exportLogsPreference.setOnPreferenceClickListener(preference -> {
- if (hasStoragePermission(REQUEST_WRITE_LOGS)) {
- startExport();
+ final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup");
+ if (createBackupPreference != null) {
+ createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, FileBackend.getBackupDirectory()));
+ createBackupPreference.setOnPreferenceClickListener(preference -> {
+ if (hasStoragePermission(REQUEST_CREATE_BACKUP)) {
+ createBackup();
}
return true;
});
@@ -538,18 +540,19 @@ public class SettingsActivity extends XmppActivity implements
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- if (grantResults.length > 0)
+ if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- if (requestCode == REQUEST_WRITE_LOGS) {
- startExport();
+ if (requestCode == REQUEST_CREATE_BACKUP) {
+ createBackup();
}
} else {
Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
}
+ }
}
- private void startExport() {
- Compatibility.startService(this, new Intent(this, ExportLogsService.class));
+ private void createBackup() {
+ ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class));
}
private void displayToast(final String msg) {
diff --git a/src/main/java/de/pixart/messenger/ui/UriHandlerActivity.java b/src/main/java/de/pixart/messenger/ui/UriHandlerActivity.java
index 559e20648..f5a5fb14f 100644
--- a/src/main/java/de/pixart/messenger/ui/UriHandlerActivity.java
+++ b/src/main/java/de/pixart/messenger/ui/UriHandlerActivity.java
@@ -86,7 +86,7 @@ public class UriHandlerActivity extends AppCompatActivity {
private void handleUri(Uri uri, final boolean scanned) {
final Intent intent;
final XmppUri xmppUri = new XmppUri(uri);
- final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(); //TODO only look at enabled accounts
+ final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
if (accounts.size() == 0) {
if (xmppUri.isJidValid()) {
intent = SignupUtils.getSignUpIntent(this);
diff --git a/src/main/java/de/pixart/messenger/ui/WelcomeActivity.java b/src/main/java/de/pixart/messenger/ui/WelcomeActivity.java
index 75493d6c8..0ce07dda4 100644
--- a/src/main/java/de/pixart/messenger/ui/WelcomeActivity.java
+++ b/src/main/java/de/pixart/messenger/ui/WelcomeActivity.java
@@ -10,6 +10,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
@@ -41,9 +42,12 @@ import de.pixart.messenger.persistance.FileBackend;
import de.pixart.messenger.utils.EncryptDecryptFile;
import de.pixart.messenger.utils.XmppUri;
+import static de.pixart.messenger.utils.PermissionUtils.allGranted;
+import static de.pixart.messenger.utils.PermissionUtils.writeGranted;
+
public class WelcomeActivity extends XmppActivity {
- boolean importSuccessful = false;
+ private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
@Override
protected void refreshUiReal() {
@@ -85,21 +89,14 @@ public class WelcomeActivity extends XmppActivity {
ab.setDisplayHomeAsUpEnabled(false);
}
- //check if there is a backed up database --
- if (hasStoragePermission(REQUEST_READ_EXTERNAL_STORAGE)) {
- BackupAvailable();
- }
-
-
final Button ImportDatabase = findViewById(R.id.import_database);
final TextView ImportText = findViewById(R.id.import_text);
-
- if (BackupAvailable() != 0) {
+ if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
ImportDatabase.setVisibility(View.VISIBLE);
ImportText.setVisibility(View.VISIBLE);
}
+ ImportDatabase.setOnClickListener(v -> startActivity(new Intent(this, ImportBackupActivity.class)));
- ImportDatabase.setOnClickListener(v -> enterPasswordDialog(BackupAvailable()));
final Button createAccount = findViewById(R.id.create_account);
createAccount.setOnClickListener(v -> {
@@ -127,290 +124,34 @@ public class WelcomeActivity extends XmppActivity {
}
- public void enterPasswordDialog(final int backup_type) {
- if (backup_type == 1) {
- LayoutInflater li = LayoutInflater.from(WelcomeActivity.this);
- View promptsView = li.inflate(R.layout.password, null);
- final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(WelcomeActivity.this);
- alertDialogBuilder.setView(promptsView);
- final EditText userInput = promptsView
- .findViewById(R.id.password);
- alertDialogBuilder.setTitle(R.string.enter_password);
- alertDialogBuilder.setMessage(R.string.enter_account_password);
- alertDialogBuilder
- .setCancelable(false)
- .setPositiveButton(R.string.ok,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- final String password = userInput.getText().toString();
- final ProgressDialog pd = ProgressDialog.show(WelcomeActivity.this, getString(R.string.please_wait), getString(R.string.databaseimport_started), true);
- if (!password.isEmpty()) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- checkDatabase(password);
- } catch (IOException e) {
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- }
- pd.dismiss();
- }
- }).start();
- } else {
- AlertDialog.Builder builder = new AlertDialog.Builder(WelcomeActivity.this);
- builder.setTitle(R.string.error);
- builder.setMessage(R.string.password_should_not_be_empty);
- builder.setNegativeButton(R.string.cancel, null);
- builder.setPositiveButton(R.string.try_again, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int id) {
- enterPasswordDialog(backup_type);
- }
- });
- builder.create().show();
- }
- }
- })
- .setNegativeButton(R.string.cancel,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- Toast.makeText(WelcomeActivity.this, R.string.import_canceled, Toast.LENGTH_LONG).show();
- dialog.dismiss();
- }
- }
- );
- WelcomeActivity.this.runOnUiThread(new Runnable() {
- public void run() {
- // create alert dialog
- AlertDialog alertDialog = alertDialogBuilder.create();
- // show it
- alertDialog.show();
- }
- });
- } else {
- try {
- checkDatabase(null);
- } catch (IOException e) {
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
+ public void addInviteUri(Intent intent) {
+ StartConversationActivity.addInviteUri(intent, getIntent());
}
- private int BackupAvailable() {
- // Set the folder on the SDcard
- File filePath_enc = new File(FileBackend.getBackupDirectory() + "/database.db.crypt");
- File filePath_dec = new File(FileBackend.getBackupDirectory() + "/database.db");
- if (filePath_enc.exists()) {
- Log.d(Config.LOGTAG, "DB Path existing (encrypted)");
- return 1;
- } else if (filePath_dec.exists()) {
- Log.d(Config.LOGTAG, "DB Path existing (decrypted)");
- return 2;
- } else {
- Log.d(Config.LOGTAG, "DB Path not existing");
- return 0;
- }
+ public static void launch(AppCompatActivity activity) {
+ Intent intent = new Intent(activity, WelcomeActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ activity.startActivity(intent);
+ activity.overridePendingTransition(0, 0);
}
- private void checkDatabase(String DecryptionKey) throws IOException {
- if (DecryptionKey != null) {
- // Set the folder on the SDcard
- File directory = new File(FileBackend.getBackupDirectory());
- // Set the input file stream up:
- FileInputStream InputFile = new FileInputStream(directory.getPath() + "/database.db.crypt");
- // Temp output for DB checks
- File TempFile = new File(directory.getPath() + "database.bak");
- FileOutputStream OutputTemp = new FileOutputStream(TempFile);
-
- try {
- EncryptDecryptFile.decrypt(InputFile, OutputTemp, DecryptionKey);
- } catch (NoSuchAlgorithmException e) {
- Log.d(Config.LOGTAG, "Database importer: decryption failed with " + e);
- e.printStackTrace();
- } catch (NoSuchPaddingException e) {
- Log.d(Config.LOGTAG, "Database importer: decryption failed with " + e);
- e.printStackTrace();
- } catch (InvalidKeyException e) {
- Log.d(Config.LOGTAG, "Database importer: decryption failed (invalid key) with " + e);
- e.printStackTrace();
- } catch (IOException e) {
- Log.d(Config.LOGTAG, "Database importer: decryption failed (IO) with " + e);
- e.printStackTrace();
- } catch (Exception e) {
- Log.d(Config.LOGTAG, "Database importer: Error " + e);
- e.printStackTrace();
- }
-
- SQLiteDatabase checkDB = null;
- int DB_Version = DatabaseBackend.DATABASE_VERSION;
- int Backup_DB_Version = 0;
-
- try {
- String dbPath = TempFile.toString();
- checkDB = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY);
- Backup_DB_Version = checkDB.getVersion();
- Log.d(Config.LOGTAG, "Backup found: " + checkDB + " Version: " + checkDB.getVersion());
- } catch (SQLiteException e) {
- //database does't exist yet.
- Log.d(Config.LOGTAG, "No backup found: " + checkDB);
- } catch (Exception e) {
- Log.d(Config.LOGTAG, "Error importing backup: " + e);
- }
-
- if (checkDB != null) {
- checkDB.close();
- }
- if (checkDB != null) {
- Log.d(Config.LOGTAG, "checkDB = " + checkDB.toString() + ", Backup DB = " + Backup_DB_Version + ", DB = " + DB_Version);
- }
- if (checkDB != null && Backup_DB_Version != 0 && Backup_DB_Version <= DB_Version) {
- try {
- ImportDatabase();
- importSuccessful = true;
- } catch (Exception e) {
- importSuccessful = false;
- e.printStackTrace();
- } finally {
- if (importSuccessful) {
- restart();
- }
- }
- } else if (checkDB != null && Backup_DB_Version == 0) {
- WelcomeActivity.this.runOnUiThread(new Runnable() {
- public void run() {
- Toast.makeText(WelcomeActivity.this, R.string.Password_wrong, Toast.LENGTH_LONG).show();
- enterPasswordDialog(1);
- }
- });
- } else {
- WelcomeActivity.this.runOnUiThread(new Runnable() {
- public void run() {
- Toast.makeText(WelcomeActivity.this, R.string.Import_failed, Toast.LENGTH_LONG).show();
- }
- });
- }
- } else {
- // Set the folder on the SDcard
- File directory = new File(FileBackend.getBackupDirectory());
- // Set the input file stream up:
- FileInputStream InputFile = new FileInputStream(directory.getPath() + "/database.db");
- // Temp output for DB checks
- File TempFile = new File(directory.getPath() + "database.bak");
- FileOutputStream OutputTemp = new FileOutputStream(TempFile);
-
- try {
- // Transfer bytes from in to out
- byte[] buf = new byte[1024];
- int len;
- while ((len = InputFile.read(buf)) > 0) {
- OutputTemp.write(buf, 0, len);
- }
- } finally {
- OutputTemp.close();
- }
-
- SQLiteDatabase checkDB = null;
- int DB_Version = DatabaseBackend.DATABASE_VERSION;
- int Backup_DB_Version = 0;
-
- try {
- String dbPath = TempFile.toString();
- checkDB = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY);
- Backup_DB_Version = checkDB.getVersion();
- Log.d(Config.LOGTAG, "Backup found: " + checkDB + " Version: " + checkDB.getVersion());
- } catch (SQLiteException e) {
- //database does't exist yet.
- Log.d(Config.LOGTAG, "No backup found: " + checkDB);
- } catch (Exception e) {
- Log.d(Config.LOGTAG, "Error importing backup: " + e);
- }
-
- if (checkDB != null) {
- checkDB.close();
- }
- if (checkDB != null) {
- Log.d(Config.LOGTAG, "checkDB = " + checkDB.toString() + ", Backup DB = " + Backup_DB_Version + ", DB = " + DB_Version);
- }
- if (checkDB != null && Backup_DB_Version != 0 && Backup_DB_Version <= DB_Version) {
- try {
- ImportDatabase();
- importSuccessful = true;
- } catch (Exception e) {
- importSuccessful = false;
- e.printStackTrace();
- } finally {
- if (importSuccessful) {
- restart();
- }
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
+ if (grantResults.length > 0) {
+ if (allGranted(grantResults)) {
+ switch (requestCode) {
+ case REQUEST_IMPORT_BACKUP:
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ break;
}
} else {
- WelcomeActivity.this.runOnUiThread(new Runnable() {
- public void run() {
- Toast.makeText(WelcomeActivity.this, R.string.Import_failed, Toast.LENGTH_LONG).show();
- }
- });
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
}
}
- }
-
- private void ImportDatabase() throws Exception {
- // Set location for the db:
- final OutputStream OutputFile = new FileOutputStream(this.getDatabasePath(DatabaseBackend.DATABASE_NAME));
- // Set the folder on the SDcard
- File directory = new File(FileBackend.getBackupDirectory());
- // Set the input file stream up:
- final InputStream InputFile = new FileInputStream(directory.getPath() + "database.bak");
- //set temp file
- File TempFile = new File(directory.getPath() + "database.bak");
-
- // Transfer bytes from the input file to the output file
- byte[] buffer = new byte[1024];
- int length;
- while ((length = InputFile.read(buffer)) > 0) {
- OutputFile.write(buffer, 0, length);
- }
- if (TempFile.exists()) {
- Log.d(Config.LOGTAG, "Delete temp file from " + TempFile.toString());
- TempFile.delete();
- }
- }
-
- private void restart() {
- //restart app
- Log.d(Config.LOGTAG, "Restarting " + getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName()));
- Intent intent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName());
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
- System.exit(0);
- }
-
- public boolean hasStoragePermission(int requestCode) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
- requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, requestCode);
- return false;
- } else {
- return true;
+ if (writeGranted(grantResults, permissions)) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.restartFileObserver();
}
- } else {
- return true;
}
}
-
- public void addInviteUri(Intent intent) {
- StartConversationActivity.addInviteUri(intent, getIntent());
- }
-
- public static void launch(AppCompatActivity activity) {
- Intent intent = new Intent(activity, WelcomeActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- activity.startActivity(intent);
- activity.overridePendingTransition(0, 0);
- }
} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java
index 391fee0b9..e060d8828 100644
--- a/src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java
+++ b/src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java
@@ -1,19 +1,17 @@
package de.pixart.messenger.ui.adapter;
-import android.content.Context;
import android.content.res.Resources;
+import android.databinding.DataBindingUtil;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
-import android.support.v7.widget.SwitchCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
-import android.widget.TextView;
import java.lang.ref.WeakReference;
import java.util.List;
@@ -21,8 +19,8 @@ import java.util.concurrent.RejectedExecutionException;
import de.pixart.messenger.Config;
import de.pixart.messenger.R;
+import de.pixart.messenger.databinding.AccountRowBinding;
import de.pixart.messenger.entities.Account;
-import de.pixart.messenger.ui.ManageAccountActivity;
import de.pixart.messenger.ui.XmppActivity;
import de.pixart.messenger.ui.util.StyledAttributes;
import de.pixart.messenger.utils.UIHelper;
@@ -44,47 +42,46 @@ public class AccountAdapter extends ArrayAdapter<Account> {
this.showStateButton = true;
}
- @NonNull
@Override
public View getView(int position, View view, @NonNull ViewGroup parent) {
final Account account = getItem(position);
+ final ViewHolder viewHolder;
if (view == null) {
- LayoutInflater inflater = (LayoutInflater) getContext()
- .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- view = inflater.inflate(R.layout.account_row, parent, false);
+ AccountRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.account_row, parent, false);
+ view = binding.getRoot();
+ viewHolder = new ViewHolder(binding);
+ view.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
}
- TextView jid = view.findViewById(R.id.account_jid);
if (Config.DOMAIN_LOCK != null) {
- jid.setText(account.getJid().getLocal());
+ viewHolder.binding.accountJid.setText(account.getJid().getLocal());
} else {
- jid.setText(account.getJid().asBareJid().toString());
+ viewHolder.binding.accountJid.setText(account.getJid().asBareJid().toString());
}
- TextView statusView = view.findViewById(R.id.account_status);
- ImageView imageView = view.findViewById(R.id.account_image);
- loadAvatar(account,imageView);
- statusView.setText(getContext().getString(account.getStatus().getReadableId()));
+ loadAvatar(account, viewHolder.binding.accountImage);
+ viewHolder.binding.accountStatus.setText(getContext().getString(account.getStatus().getReadableId()));
switch (account.getStatus()) {
case ONLINE:
- statusView.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
+ viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
break;
case DISABLED:
case CONNECTING:
- statusView.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
+ viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
break;
default:
- statusView.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
+ viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
break;
}
- final SwitchCompat tglAccountState = view.findViewById(R.id.tgl_account_status);
final boolean isDisabled = (account.getStatus() == Account.State.DISABLED);
- tglAccountState.setOnCheckedChangeListener(null);
- tglAccountState.setChecked(!isDisabled);
+ viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener(null);
+ viewHolder.binding.tglAccountStatus.setChecked(!isDisabled);
if (this.showStateButton) {
- tglAccountState.setVisibility(View.VISIBLE);
+ viewHolder.binding.tglAccountStatus.setVisibility(View.VISIBLE);
} else {
- tglAccountState.setVisibility(View.GONE);
+ viewHolder.binding.tglAccountStatus.setVisibility(View.GONE);
}
- tglAccountState.setOnCheckedChangeListener((compoundButton, b) -> {
+ viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener((compoundButton, b) -> {
if (b == isDisabled && activity instanceof OnTglAccountState) {
((OnTglAccountState) activity).onClickTglAccountState(account, b);
}
@@ -92,6 +89,62 @@ public class AccountAdapter extends ArrayAdapter<Account> {
return view;
}
+ private static class ViewHolder {
+ private final AccountRowBinding binding;
+
+ private ViewHolder(AccountRowBinding binding) {
+ this.binding = binding;
+ }
+ }
+
+ class BitmapWorkerTask extends AsyncTask<Account, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private Account account = null;
+
+ public BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<>(imageView);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Account... params) {
+ this.account = params[0];
+ return activity.avatarService().get(this.account, activity.getPixel(48), isCancelled());
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null && !isCancelled()) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ }
+ }
+ }
+ }
+
+ public void loadAvatar(Account account, ImageView imageView) {
+ if (cancelPotentialWork(account, imageView)) {
+ final Bitmap bm = activity.avatarService().get(account, activity.getPixel(48), true);
+ if (bm != null) {
+ cancelPotentialWork(account, imageView);
+ imageView.setImageBitmap(bm);
+ imageView.setBackgroundColor(0x00000000);
+ } else {
+ imageView.setBackgroundColor(UIHelper.getColorForName(account.getJid().asBareJid().toString()));
+ imageView.setImageDrawable(null);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(account);
+ } catch (final RejectedExecutionException ignored) {
+ }
+ }
+ }
+ }
+
+
public interface OnTglAccountState {
void onClickTglAccountState(Account account, boolean state);
}
@@ -121,27 +174,6 @@ public class AccountAdapter extends ArrayAdapter<Account> {
return null;
}
- public void loadAvatar(Account account, ImageView imageView) {
- if (cancelPotentialWork(account, imageView)) {
- final Bitmap bm = activity.avatarService().get(account, activity.getPixel(56), true);
- if (bm != null) {
- cancelPotentialWork(account, imageView);
- imageView.setImageBitmap(bm);
- imageView.setBackgroundColor(0x00000000);
- } else {
- imageView.setBackgroundColor(UIHelper.getColorForName(account.getJid().asBareJid().toString()));
- imageView.setImageDrawable(null);
- final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
- final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
- imageView.setImageDrawable(asyncDrawable);
- try {
- task.executeOnExecutor(BitmapWorkerTask.THREAD_POOL_EXECUTOR, account);
- } catch (final RejectedExecutionException ignored) {
- }
- }
- }
- }
-
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
@@ -154,30 +186,4 @@ public class AccountAdapter extends ArrayAdapter<Account> {
return bitmapWorkerTaskReference.get();
}
}
-
- class BitmapWorkerTask extends AsyncTask<Account, Void, Bitmap> {
- private final WeakReference<ImageView> imageViewReference;
- private Account account = null;
-
- public BitmapWorkerTask(ImageView imageView) {
- imageViewReference = new WeakReference<>(imageView);
- }
-
- @Override
- protected Bitmap doInBackground(Account... params) {
- this.account = params[0];
- return activity.avatarService().get(this.account, activity.getPixel(56), isCancelled());
- }
-
- @Override
- protected void onPostExecute(Bitmap bitmap) {
- if (bitmap != null && !isCancelled()) {
- final ImageView imageView = imageViewReference.get();
- if (imageView != null) {
- imageView.setImageBitmap(bitmap);
- imageView.setBackgroundColor(0x00000000);
- }
- }
- }
- }
}
diff --git a/src/main/java/de/pixart/messenger/ui/adapter/BackupFileAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/BackupFileAdapter.java
new file mode 100644
index 000000000..84ae61e11
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/ui/adapter/BackupFileAdapter.java
@@ -0,0 +1,167 @@
+package de.pixart.messenger.ui.adapter;
+
+import android.content.res.Resources;
+import android.databinding.DataBindingUtil;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.text.format.DateUtils;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import de.pixart.messenger.R;
+import de.pixart.messenger.databinding.AccountRowBinding;
+import de.pixart.messenger.services.AvatarService;
+import de.pixart.messenger.services.ImportBackupService;
+import de.pixart.messenger.utils.BackupFileHeader;
+import de.pixart.messenger.utils.UIHelper;
+import rocks.xmpp.addr.Jid;
+
+public class BackupFileAdapter extends RecyclerView.Adapter<BackupFileAdapter.BackupFileViewHolder> {
+
+ private OnItemClickedListener listener;
+
+ private final List<ImportBackupService.BackupFile> files = new ArrayList<>();
+
+
+ @NonNull
+ @Override
+ public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+ return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.account_row, viewGroup, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull BackupFileViewHolder backupFileViewHolder, int position) {
+ final ImportBackupService.BackupFile backupFile = files.get(position);
+ final BackupFileHeader header = backupFile.getHeader();
+ backupFileViewHolder.binding.accountJid.setText(header.getJid().asBareJid().toString());
+ backupFileViewHolder.binding.accountStatus.setText(String.format("%s · %s", header.getApp(), DateUtils.formatDateTime(backupFileViewHolder.binding.getRoot().getContext(), header.getTimestamp(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)));
+ backupFileViewHolder.binding.tglAccountStatus.setVisibility(View.GONE);
+ backupFileViewHolder.binding.getRoot().setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onClick(backupFile);
+ }
+ });
+ loadAvatar(header.getJid(), backupFileViewHolder.binding.accountImage);
+ }
+
+ @Override
+ public int getItemCount() {
+ return files.size();
+ }
+
+ public void setFiles(List<ImportBackupService.BackupFile> files) {
+ this.files.clear();
+ this.files.addAll(files);
+ notifyDataSetChanged();
+ }
+
+ public void setOnItemClickedListener(OnItemClickedListener listener) {
+ this.listener = listener;
+ }
+
+ static class BackupFileViewHolder extends RecyclerView.ViewHolder {
+ private final AccountRowBinding binding;
+
+ BackupFileViewHolder(AccountRowBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+ }
+
+ public interface OnItemClickedListener {
+ void onClick(ImportBackupService.BackupFile backupFile);
+ }
+
+ static class BitmapWorkerTask extends AsyncTask<Jid, Void, Bitmap> {
+ private final WeakReference<ImageView> imageViewReference;
+ private Jid jid = null;
+ private final int size;
+
+ BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<>(imageView);
+ DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics();
+ this.size = ((int) (48 * metrics.density));
+ }
+
+ @Override
+ protected Bitmap doInBackground(Jid... params) {
+ this.jid = params[0];
+ return AvatarService.get(this.jid, size);
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null && !isCancelled()) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ }
+ }
+ }
+ }
+
+ private void loadAvatar(Jid jid, ImageView imageView) {
+ if (cancelPotentialWork(jid, imageView)) {
+ imageView.setBackgroundColor(UIHelper.getColorForName(jid.asBareJid().toString()));
+ imageView.setImageDrawable(null);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getContext().getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(jid);
+ } catch (final RejectedExecutionException ignored) {
+ }
+ }
+ }
+
+ private static boolean cancelPotentialWork(Jid jid, ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final Jid oldJid = bitmapWorkerTask.jid;
+ if (oldJid == null || jid != oldJid) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+ AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+ }
+
+ BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/utils/BackupFileHeader.java b/src/main/java/de/pixart/messenger/utils/BackupFileHeader.java
new file mode 100644
index 000000000..204ec45eb
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/BackupFileHeader.java
@@ -0,0 +1,85 @@
+package de.pixart.messenger.utils;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import rocks.xmpp.addr.Jid;
+
+public class BackupFileHeader {
+
+ private static final int VERSION = 1;
+
+ private String app;
+ private Jid jid;
+ private long timestamp;
+ private byte[] iv;
+ private byte[] salt;
+
+
+ @Override
+ public String toString() {
+ return "BackupFileHeader{" +
+ "app='" + app + '\'' +
+ ", jid=" + jid +
+ ", timestamp=" + timestamp +
+ ", iv=" + CryptoHelper.bytesToHex(iv) +
+ ", salt=" + CryptoHelper.bytesToHex(salt) +
+ '}';
+ }
+
+ public BackupFileHeader(String app, Jid jid, long timestamp, byte[] iv, byte[] salt) {
+ this.app = app;
+ this.jid = jid;
+ this.timestamp = timestamp;
+ this.iv = iv;
+ this.salt = salt;
+ }
+
+ public void write(DataOutputStream dataOutputStream) throws IOException {
+ dataOutputStream.writeInt(VERSION);
+ dataOutputStream.writeUTF(app);
+ dataOutputStream.writeUTF(jid.asBareJid().toEscapedString());
+ dataOutputStream.writeLong(timestamp);
+ dataOutputStream.write(iv);
+ dataOutputStream.write(salt);
+ }
+
+ public static BackupFileHeader read(DataInputStream inputStream) throws IOException {
+ final int version = inputStream.readInt();
+ if (version > VERSION) {
+ throw new IllegalArgumentException("Backup File version was " + version + " but app only supports up to version " + VERSION);
+ }
+ String app = inputStream.readUTF();
+ String jid = inputStream.readUTF();
+ long timestamp = inputStream.readLong();
+ byte[] iv = new byte[12];
+ inputStream.readFully(iv);
+ byte[] salt = new byte[16];
+ inputStream.readFully(salt);
+
+ return new BackupFileHeader(app, Jid.of(jid), timestamp, iv, salt);
+
+ }
+
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public byte[] getIv() {
+ return iv;
+ }
+
+ public Jid getJid() {
+ return jid;
+ }
+
+ public String getApp() {
+ return app;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/utils/PermissionUtils.java b/src/main/java/de/pixart/messenger/utils/PermissionUtils.java
new file mode 100644
index 000000000..b21ca8869
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/PermissionUtils.java
@@ -0,0 +1,34 @@
+package de.pixart.messenger.utils;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+
+public class PermissionUtils {
+
+ public static boolean allGranted(int[] grantResults) {
+ for (int grantResult : grantResults) {
+ if (grantResult != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean writeGranted(int[] grantResults, String[] permission) {
+ for (int i = 0; i < grantResults.length; ++i) {
+ if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) {
+ return grantResults[i] == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ return false;
+ }
+
+ public static String getFirstDenied(int[] grantResults, String[] permissions) {
+ for (int i = 0; i < grantResults.length; ++i) {
+ if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
+ return permissions[i];
+ }
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/main/res/drawable-hdpi/ic_archive_white_24dp.png b/src/main/res/drawable-hdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..1f109a958
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-hdpi/ic_unarchive_white_24dp.png b/src/main/res/drawable-hdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..a32579aa8
--- /dev/null
+++ b/src/main/res/drawable-hdpi/ic_unarchive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_archive_white_24dp.png b/src/main/res/drawable-mdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..7944feb2a
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-mdpi/ic_unarchive_white_24dp.png b/src/main/res/drawable-mdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..0480fea98
--- /dev/null
+++ b/src/main/res/drawable-mdpi/ic_unarchive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_archive_white_24dp.png b/src/main/res/drawable-xhdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..9d1d46a5d
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-xhdpi/ic_unarchive_white_24dp.png b/src/main/res/drawable-xhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..04e607bd4
--- /dev/null
+++ b/src/main/res/drawable-xhdpi/ic_unarchive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..d526d6a1d
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-xxhdpi/ic_unarchive_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..1a426fb0b
--- /dev/null
+++ b/src/main/res/drawable-xxhdpi/ic_unarchive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..62d27e00f
--- /dev/null
+++ b/src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..067e413b7
--- /dev/null
+++ b/src/main/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png
Binary files differ
diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml
index eaacbde5e..d6831ac26 100644
--- a/src/main/res/layout/account_row.xml
+++ b/src/main/res/layout/account_row.xml
@@ -1,60 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="?android:attr/activatedBackgroundIndicator"
- android:paddingStart="8dp"
- android:paddingLeft="8dp"
- android:paddingTop="8dp"
- android:paddingBottom="8dp">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
- <com.makeramen.roundedimageview.RoundedImageView
- android:id="@+id/account_image"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
- android:contentDescription="@string/account_image_description"
- android:padding="1dp"
- app:riv_corner_radius="@dimen/rounded_image_border" />
-
- <LinearLayout
- android:layout_width="fill_parent"
+ <RelativeLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_centerVertical="true"
- android:layout_toStartOf="@+id/tgl_account_status"
- android:layout_toLeftOf="@+id/tgl_account_status"
- android:layout_toEndOf="@+id/account_image"
- android:layout_toRightOf="@+id/account_image"
- android:orientation="vertical"
- android:paddingStart="@dimen/avatar_item_distance"
- android:paddingLeft="@dimen/avatar_item_distance">
+ android:background="?android:selectableItemBackground"
+ android:paddingLeft="8dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp">
- <TextView
- android:id="@+id/account_jid"
- android:layout_width="wrap_content"
+ <com.makeramen.roundedimageview.RoundedImageView
+ android:id="@+id/account_image"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentLeft="true"
+ android:contentDescription="@string/account_image_description"
+ app:riv_corner_radius="2dp" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:scrollHorizontally="false"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
+ android:layout_centerVertical="true"
+ android:layout_toStartOf="@+id/tgl_account_status"
+ android:layout_toLeftOf="@+id/tgl_account_status"
+ android:layout_toRightOf="@+id/account_image"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/avatar_item_distance">
+
+ <TextView
+ android:id="@+id/account_jid"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scrollHorizontally="false"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
- <TextView
- android:id="@+id/account_status"
+ <TextView
+ android:id="@+id/account_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/account_status_unknown"
+ android:textAppearance="@style/TextAppearance.Conversations.Body2" />
+ </LinearLayout>
+
+ <android.support.v7.widget.SwitchCompat
+ android:id="@+id/tgl_account_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/account_status_unknown"
- android:textAppearance="@style/TextAppearance.Conversations.Body2" />
- </LinearLayout>
-
- <android.support.v7.widget.SwitchCompat
- android:id="@+id/tgl_account_status"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentEnd="true"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:focusable="false"
- android:padding="16dp" />
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:focusable="false"
+ android:padding="16dp" />
-</RelativeLayout> \ No newline at end of file
+ </RelativeLayout>
+</layout> \ No newline at end of file
diff --git a/src/main/res/layout/activity_import_backup.xml b/src/main/res/layout/activity_import_backup.xml
new file mode 100644
index 000000000..bc5ccecc1
--- /dev/null
+++ b/src/main/res/layout/activity_import_backup.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="?attr/color_background_primary"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/toolbar"
+ layout="@layout/toolbar" />
+
+ <android.support.design.widget.CoordinatorLayout
+ android:id="@+id/coordinator"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/color_background_primary">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/color_background_primary"
+ android:orientation="vertical"
+ app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
+ </android.support.design.widget.CoordinatorLayout>
+
+ </LinearLayout>
+</layout> \ No newline at end of file
diff --git a/src/main/res/layout/dialog_enter_password.xml b/src/main/res/layout/dialog_enter_password.xml
new file mode 100644
index 000000000..935ebad6c
--- /dev/null
+++ b/src/main/res/layout/dialog_enter_password.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="?dialogPreferredPadding">
+
+ <TextView
+ android:id="@+id/explain"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/enter_password_to_restore"
+ android:textAppearance="@style/TextAppearance.Conversations.Body2" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?TextSizeBody1"
+ android:text="@string/restore_warning"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1" />
+
+ <android.support.design.widget.TextInputLayout
+ android:id="@+id/account_password_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"
+ app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
+ app:passwordToggleDrawable="@drawable/visibility_toggle_drawable"
+ app:passwordToggleEnabled="true"
+ app:passwordToggleTint="?android:textColorSecondary">
+
+ <de.pixart.messenger.ui.widget.TextInputEditText
+ android:id="@+id/account_password"
+ style="@style/Widget.Conversations.EditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/password"
+ android:inputType="textPassword"
+ android:textColor="?attr/edit_text_color" />
+
+ </android.support.design.widget.TextInputLayout>
+ </LinearLayout>
+</layout> \ No newline at end of file
diff --git a/src/main/res/layout/dialog_quickedit.xml b/src/main/res/layout/dialog_quickedit.xml
index 4ce88b2ba..b309bc173 100644
--- a/src/main/res/layout/dialog_quickedit.xml
+++ b/src/main/res/layout/dialog_quickedit.xml
@@ -6,11 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- android:paddingStart="?attr/dialog_horizontal_padding"
- android:paddingLeft="?attr/dialog_horizontal_padding"
- android:paddingTop="?attr/dialog_vertical_padding"
- android:paddingEnd="?attr/dialog_horizontal_padding"
- android:paddingRight="?attr/dialog_horizontal_padding">
+ android:padding="?dialogPreferredPadding">
<android.support.design.widget.TextInputLayout
android:id="@+id/input_layout"
diff --git a/src/main/res/menu/editaccount.xml b/src/main/res/menu/editaccount.xml
index 09919b9fe..59667c1c7 100644
--- a/src/main/res/menu/editaccount.xml
+++ b/src/main/res/menu/editaccount.xml
@@ -26,6 +26,11 @@
</item>
<item
+ android:id="@+id/action_import_backup"
+ android:title="@string/restore_backup"
+ app:showAsAction="never" />
+
+ <item
android:id="@+id/action_show_qr_code"
android:icon="@drawable/ic_qrcode_white_24dp"
android:title="@string/show_qr_code"
diff --git a/src/main/res/menu/manageaccounts.xml b/src/main/res/menu/manageaccounts.xml
index 044c715c5..3199fc2be 100644
--- a/src/main/res/menu/manageaccounts.xml
+++ b/src/main/res/menu/manageaccounts.xml
@@ -5,18 +5,22 @@
<item
android:id="@+id/action_add_account"
android:icon="?attr/icon_add_person"
- app:showAsAction="always"
- android:title="@string/action_add_account" />
+ android:title="@string/action_add_account"
+ app:showAsAction="always" />
+ <item
+ android:id="@+id/action_import_backup"
+ android:title="@string/restore_backup"
+ app:showAsAction="never" />
<item
android:id="@+id/action_add_account_with_cert"
- app:showAsAction="never"
android:icon="?attr/icon_add_person"
android:title="@string/action_add_account_with_certificate"
- android:visible="true" />
+ android:visible="true"
+ app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
- app:showAsAction="never"
- android:title="@string/action_settings" />
+ android:title="@string/action_settings"
+ app:showAsAction="never" />
</menu> \ No newline at end of file
diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml
index e3f355814..ecf8eccd1 100644
--- a/src/main/res/values/attrs.xml
+++ b/src/main/res/values/attrs.xml
@@ -78,7 +78,6 @@
<attr name="icon_settings" format="reference" />
<attr name="icon_share" format="reference" />
<attr name="icon_delete" format="reference" />
- <attr name="icon_import_export" format="reference" />
<attr name="icon_scan_qr_code" format="reference" />
<attr name="icon_enable_undecided_device" format="reference" />
<attr name="icon_scroll_down" format="reference" />
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 8867502a4..b0912e1c9 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -314,9 +314,12 @@
<string name="smp_requested">Contact requested SMP verification</string>
<string name="no_otr_session_found">No valid OTR session has been found!</string>
<string name="conversations_foreground_service" translatable="false">Pix-Art Messenger</string>
- <string name="pref_export_logs">Export history</string>
- <string name="pref_export_logs_summary">Write conversations history logs to SD card</string>
- <string name="notification_export_logs_title">Writing conversations to SD card</string>
+ <string name="pref_create_backup">Create backup</string>
+ <string name="pref_create_backup_summary">Write backup files to %s</string>
+ <string name="notification_create_backup_title">Creating backup files</string>
+ <string name="notification_restore_backup_title">Restoring backup</string>
+ <string name="notification_restored_backup_title">Your backup has been restored</string>
+ <string name="notification_restored_backup_subtitle">Do not forget to enable the account.</string>
<string name="choose_file">Choose file</string>
<string name="receiving_x_file">Receiving %1$s (%2$d%% completed)</string>
<string name="download_x_file">Download %s</string>
@@ -575,9 +578,7 @@
<string name="import_canceled">Import canceled</string>
<string name="Import_failed">Database import failed, an import is not possible</string>
<string name="Password_wrong">Wrong password, try again</string>
- <string name="enter_account_password">Please enter your account password to import your backup.</string>
<string name="please_wait">Please wait…</string>
- <string name="databaseimport_started">Backup will be imported, this may take awhile.</string>
<string name="pref_export_plain_text_logs_summary">Enable an export of chat logs as human readable text files</string>
<string name="pref_export_plain_text_logs">Export human readable chat logs</string>
<string name="payment_required">Payment required</string>
@@ -823,7 +824,7 @@
<string name="pref_more_notification_settings">Notification Settings</string>
<string name="pref_more_notification_settings_summary">Importance, Sound, Vibrate</string>
<string name="video_compression_channel_name">Video compression</string>
- <string name="backup_channel_name">Database backup</string>
+ <string name="backup_channel_name"><![CDATA[Backup & Restore]]></string>
<string name="app_update_channel_name">App update</string>
<string name="action_group_details">Group details</string>
<string name="view_media">View media</string>
@@ -862,4 +863,10 @@
<string name="open_with">Open with…</string>
<string name="choose_account">Choose account</string>
<string name="set_profile_picture">Pix-Art Messenger profile picture</string>
+ <string name="restore_backup">Restore backup</string>
+ <string name="restore">Restore</string>
+ <string name="enter_password_to_restore">Enter your password for the account %s to restore the backup.</string>
+ <string name="restore_warning">Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you have lost the original device.</string>
+ <string name="unable_to_restore_backup">Unable to restore backup</string>
+ <string name="unable_to_decrypt_backup">Unable to decrypt backup</string>
</resources>
diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml
index 92e7cd49c..8060687ac 100644
--- a/src/main/res/values/themes.xml
+++ b/src/main/res/values/themes.xml
@@ -99,8 +99,6 @@
<item name="icon_search" type="reference">@drawable/ic_search_white_24dp</item>
<item name="icon_secure" type="reference">@drawable/ic_lock_open_white_24dp</item>
<item name="icon_settings" type="reference">@drawable/ic_settings_black_24dp</item>
- <item name="icon_import_export" type="reference">@drawable/ic_import_export_white_24dp
- </item>
<item name="icon_delete" type="reference">@drawable/ic_delete_black_24dp</item>
<item name="icon_share" type="reference">@drawable/ic_share_black_24dp</item>
<item name="icon_scan_qr_code" type="reference">@drawable/ic_qrcode_scan_white_24dp</item>
@@ -274,8 +272,6 @@
<item name="icon_search" type="reference">@drawable/ic_search_white_24dp</item>
<item name="icon_secure" type="reference">@drawable/ic_lock_open_white_24dp</item>
<item name="icon_settings" type="reference">@drawable/ic_settings_white_24dp</item>
- <item name="icon_import_export" type="reference">@drawable/ic_import_export_white_24dp
- </item>
<item name="icon_delete" type="reference">@drawable/ic_delete_white_24dp</item>
<item name="icon_share" type="reference">@drawable/ic_share_white_24dp</item>
<item name="icon_scan_qr_code" type="reference">@drawable/ic_qrcode_scan_white_24dp</item>
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index 747338435..0244da459 100644
--- a/src/main/res/xml/preferences.xml
+++ b/src/main/res/xml/preferences.xml
@@ -307,11 +307,11 @@
android:key="crashreport"
android:summary="@string/pref_send_crash_summary"
android:title="@string/pref_send_crash" />
- <PreferenceCategory android:title="@string/pref_export_logs">
+ <PreferenceCategory android:title="@string/pref_create_backup">
<Preference
- android:key="export_logs"
- android:summary="@string/pref_export_logs_summary"
- android:title="@string/pref_export_logs" />
+ android:key="create_backup"
+ android:summary="@string/pref_create_backup_summary"
+ android:title="@string/pref_create_backup" />
<CheckBoxPreference
android:defaultValue="@bool/plain_text_logs"
android:key="export_plain_text_logs"