From f2d502518ea3de673c7f0ebf425f53295f620f2f Mon Sep 17 00:00:00 2001 From: Christian Schneppe Date: Sat, 26 Jan 2019 15:07:28 +0100 Subject: rework backup & restore use the implementation from Conversations --- .../messenger/persistance/DatabaseBackend.java | 4 +- .../pixart/messenger/services/AlarmReceiver.java | 2 +- .../pixart/messenger/services/AvatarService.java | 84 +++--- .../messenger/services/ExportBackupService.java | 287 +++++++++++++++++++ .../messenger/services/ExportLogsService.java | 248 ---------------- .../messenger/services/ImportBackupService.java | 290 +++++++++++++++++++ .../messenger/services/NotificationService.java | 1 - .../pixart/messenger/ui/ConversationFragment.java | 30 +- .../pixart/messenger/ui/EditAccountActivity.java | 31 ++ .../pixart/messenger/ui/ImportBackupActivity.java | 124 ++++++++ .../pixart/messenger/ui/ManageAccountActivity.java | 36 ++- .../de/pixart/messenger/ui/SettingsActivity.java | 27 +- .../de/pixart/messenger/ui/UriHandlerActivity.java | 2 +- .../de/pixart/messenger/ui/WelcomeActivity.java | 311 ++------------------- .../messenger/ui/adapter/AccountAdapter.java | 148 +++++----- .../messenger/ui/adapter/BackupFileAdapter.java | 167 +++++++++++ .../pixart/messenger/utils/BackupFileHeader.java | 85 ++++++ .../de/pixart/messenger/utils/PermissionUtils.java | 34 +++ 18 files changed, 1218 insertions(+), 693 deletions(-) create mode 100644 src/main/java/de/pixart/messenger/services/ExportBackupService.java delete mode 100644 src/main/java/de/pixart/messenger/services/ExportLogsService.java create mode 100644 src/main/java/de/pixart/messenger/services/ImportBackupService.java create mode 100644 src/main/java/de/pixart/messenger/ui/ImportBackupActivity.java create mode 100644 src/main/java/de/pixart/messenger/ui/adapter/BackupFileAdapter.java create mode 100644 src/main/java/de/pixart/messenger/utils/BackupFileHeader.java create mode 100644 src/main/java/de/pixart/messenger/utils/PermissionUtils.java (limited to 'src/main/java') 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 getAccountJids() { + public List getAccountJids(final boolean enabledOnly) { SQLiteDatabase db = this.getReadableDatabase(); final List 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 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 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 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 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 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 accounts = mDatabaseBackend.getAccountJids(false); + final ArrayList backupFiles = new ArrayList<>(); + final Set 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 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) { @@ -1443,6 +1454,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()) { 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 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: @@ -199,6 +211,26 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda return super.onOptionsItemSelected(item); } + @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) { 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 accounts = DatabaseBackend.getInstance(this).getAccountJids(); //TODO only look at enabled accounts + final List 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 { 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 { return view; } + private static class ViewHolder { + private final AccountRowBinding binding; + + private ViewHolder(AccountRowBinding binding) { + this.binding = binding; + } + } + + class BitmapWorkerTask extends AsyncTask { + private final WeakReference 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 { 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 bitmapWorkerTaskReference; @@ -154,30 +186,4 @@ public class AccountAdapter extends ArrayAdapter { return bitmapWorkerTaskReference.get(); } } - - class BitmapWorkerTask extends AsyncTask { - private final WeakReference 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 { + + private OnItemClickedListener listener; + + private final List 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 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 { + private final WeakReference 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 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 -- cgit v1.2.3