mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-22 07:57:30 +00:00
Backup exporter ported to Kotlin with adjustments.
Important preferences included in the backup. Private chat sessions get reset after backup import.
This commit is contained in:
parent
0db5f98cb6
commit
68dbd91152
@ -49,6 +49,16 @@
|
|||||||
android:layout_marginRight="@dimen/massive_spacing"
|
android:layout_marginRight="@dimen/massive_spacing"
|
||||||
android:text="@string/activity_landing_restore_button_title" />
|
android:text="@string/activity_landing_restore_button_title" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||||
|
android:id="@+id/restoreBackupButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/medium_button_height"
|
||||||
|
android:layout_marginLeft="@dimen/massive_spacing"
|
||||||
|
android:layout_marginTop="@dimen/medium_spacing"
|
||||||
|
android:layout_marginRight="@dimen/massive_spacing"
|
||||||
|
android:text="Backup" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/linkButton"
|
android:id="@+id/linkButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -1545,6 +1545,7 @@
|
|||||||
<string name="RegistrationLockDialog_disable_registration_lock_pin">Disable Registration Lock PIN?</string>
|
<string name="RegistrationLockDialog_disable_registration_lock_pin">Disable Registration Lock PIN?</string>
|
||||||
<string name="RegistrationLockDialog_disable">Disable</string>
|
<string name="RegistrationLockDialog_disable">Disable</string>
|
||||||
<string name="preferences_chats__backups">Backups</string>
|
<string name="preferences_chats__backups">Backups</string>
|
||||||
|
<string name="preferences_chats__backup_export_error">An error occurred during exporting a backup. Please try again later.</string>
|
||||||
<string name="prompt_passphrase_activity__signal_is_locked">Session is Locked</string>
|
<string name="prompt_passphrase_activity__signal_is_locked">Session is Locked</string>
|
||||||
<string name="prompt_passphrase_activity__tap_to_unlock">TAP TO UNLOCK</string>
|
<string name="prompt_passphrase_activity__tap_to_unlock">TAP TO UNLOCK</string>
|
||||||
<string name="RegistrationLockDialog_reminder">Reminder:</string>
|
<string name="RegistrationLockDialog_reminder">Reminder:</string>
|
||||||
|
@ -66,6 +66,7 @@ import org.thoughtcrime.securesms.loki.api.BackgroundPollListener;
|
|||||||
import org.thoughtcrime.securesms.loki.api.ClosedGroupPoller;
|
import org.thoughtcrime.securesms.loki.api.ClosedGroupPoller;
|
||||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
||||||
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
||||||
|
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||||
|
@ -29,7 +29,8 @@ public class BackupDialog {
|
|||||||
@NonNull SwitchPreferenceCompat preference,
|
@NonNull SwitchPreferenceCompat preference,
|
||||||
@NonNull BackupDirSelector backupDirSelector) {
|
@NonNull BackupDirSelector backupDirSelector) {
|
||||||
|
|
||||||
String[] password = BackupUtil.generateBackupPassphrase();
|
// String[] password = BackupUtil.generateBackupPassphrase();
|
||||||
|
String[] password = new String[]{"00000", "00000", "00000", "00000", "00000", "00000"};
|
||||||
String passwordSt = Util.join(password, " ");
|
String passwordSt = Util.join(password, " ");
|
||||||
|
|
||||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||||
|
@ -18,7 +18,7 @@ public abstract class FullBackupBase {
|
|||||||
static class BackupStream {
|
static class BackupStream {
|
||||||
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
|
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
|
||||||
try {
|
try {
|
||||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
|
EventBus.getDefault().post(BackupEvent.createProgress(0));
|
||||||
|
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||||
byte[] input = passphrase.replace(" ", "").getBytes();
|
byte[] input = passphrase.replace(" ", "").getBytes();
|
||||||
@ -27,7 +27,7 @@ public abstract class FullBackupBase {
|
|||||||
if (salt != null) digest.update(salt);
|
if (salt != null) digest.update(salt);
|
||||||
|
|
||||||
for (int i=0;i<250000;i++) {
|
for (int i=0;i<250000;i++) {
|
||||||
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
|
if (i % 1000 == 0) EventBus.getDefault().post(BackupEvent.createProgress(0));
|
||||||
digest.update(hash);
|
digest.update(hash);
|
||||||
hash = digest.digest(input);
|
hash = digest.digest(input);
|
||||||
}
|
}
|
||||||
@ -47,10 +47,12 @@ public abstract class FullBackupBase {
|
|||||||
|
|
||||||
private final Type type;
|
private final Type type;
|
||||||
private final int count;
|
private final int count;
|
||||||
|
private final @Nullable Exception exception;
|
||||||
|
|
||||||
BackupEvent(Type type, int count) {
|
private BackupEvent(Type type, int count, @Nullable Exception exception) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.count = count;
|
this.count = count;
|
||||||
|
this.exception = exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Type getType() {
|
public Type getType() {
|
||||||
@ -60,6 +62,22 @@ public abstract class FullBackupBase {
|
|||||||
public int getCount() {
|
public int getCount() {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Exception getException() {
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackupEvent createProgress(int count) {
|
||||||
|
return new BackupEvent(Type.PROGRESS, count, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackupEvent createFinished() {
|
||||||
|
return new BackupEvent(Type.FINISHED, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackupEvent createFinished(@Nullable Exception e) {
|
||||||
|
return new BackupEvent(Type.FINISHED, 0, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,429 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.backup;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.annimon.stream.function.Consumer;
|
|
||||||
import com.annimon.stream.function.Predicate;
|
|
||||||
import com.google.protobuf.ByteString;
|
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
|
||||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
|
||||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
|
||||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
|
||||||
import org.thoughtcrime.securesms.util.Conversions;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
|
||||||
import org.whispersystems.libsignal.util.ByteUtil;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.Flushable;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import javax.crypto.Mac;
|
|
||||||
import javax.crypto.NoSuchPaddingException;
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
public class FullBackupExporter extends FullBackupBase {
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static final String TAG = FullBackupExporter.class.getSimpleName();
|
|
||||||
|
|
||||||
public static void export(@NonNull Context context,
|
|
||||||
@NonNull AttachmentSecret attachmentSecret,
|
|
||||||
@NonNull SQLiteDatabase input,
|
|
||||||
@NonNull Uri fileUri,
|
|
||||||
@NonNull String passphrase)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
OutputStream baseOutputStream = context.getContentResolver().openOutputStream(fileUri);
|
|
||||||
if (baseOutputStream == null) {
|
|
||||||
throw new IOException("Cannot open an output stream for the file URI: " + fileUri.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
try (BackupFrameOutputStream outputStream = new BackupFrameOutputStream(baseOutputStream, passphrase)) {
|
|
||||||
outputStream.writeDatabaseVersion(input.getVersion());
|
|
||||||
|
|
||||||
List<String> tables = exportSchema(input, outputStream);
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
for (String table : tables) {
|
|
||||||
if (table.equals(SmsDatabase.TABLE_NAME) || table.equals(MmsDatabase.TABLE_NAME)) {
|
|
||||||
count = exportTable(table, input, outputStream, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0, null, count);
|
|
||||||
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
|
|
||||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count);
|
|
||||||
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
|
|
||||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), cursor -> exportAttachment(attachmentSecret, cursor, outputStream), count);
|
|
||||||
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
|
|
||||||
count = exportTable(table, input, outputStream, cursor -> true, cursor -> exportSticker(attachmentSecret, cursor, outputStream), count);
|
|
||||||
} else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) &&
|
|
||||||
!table.equals(OneTimePreKeyDatabase.TABLE_NAME) &&
|
|
||||||
!table.equals(SessionDatabase.TABLE_NAME) &&
|
|
||||||
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
|
|
||||||
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
|
|
||||||
!table.startsWith("sqlite_"))
|
|
||||||
{
|
|
||||||
count = exportTable(table, input, outputStream, null, null, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) {
|
|
||||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
|
||||||
outputStream.write(preference);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (File avatar : AvatarHelper.getAvatarFiles(context)) {
|
|
||||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
|
||||||
outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
outputStream.writeEnd();
|
|
||||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
List<String> tables = new LinkedList<>();
|
|
||||||
|
|
||||||
try (Cursor cursor = input.rawQuery("SELECT sql, name, type FROM sqlite_master", null)) {
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
String sql = cursor.getString(0);
|
|
||||||
String name = cursor.getString(1);
|
|
||||||
String type = cursor.getString(2);
|
|
||||||
|
|
||||||
if (sql != null) {
|
|
||||||
|
|
||||||
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
|
|
||||||
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
|
|
||||||
|
|
||||||
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
|
|
||||||
if ("table".equals(type)) {
|
|
||||||
tables.add(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(cursor.getString(0)).build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tables;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int exportTable(@NonNull String table,
|
|
||||||
@NonNull SQLiteDatabase input,
|
|
||||||
@NonNull BackupFrameOutputStream outputStream,
|
|
||||||
@Nullable Predicate<Cursor> predicate,
|
|
||||||
@Nullable Consumer<Cursor> postProcess,
|
|
||||||
int count)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
String template = "INSERT INTO " + table + " VALUES ";
|
|
||||||
|
|
||||||
try (Cursor cursor = input.rawQuery("SELECT * FROM " + table, null)) {
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
|
|
||||||
|
|
||||||
if (predicate == null || predicate.test(cursor)) {
|
|
||||||
StringBuilder statement = new StringBuilder(template);
|
|
||||||
BackupProtos.SqlStatement.Builder statementBuilder = BackupProtos.SqlStatement.newBuilder();
|
|
||||||
|
|
||||||
statement.append('(');
|
|
||||||
|
|
||||||
for (int i=0;i<cursor.getColumnCount();i++) {
|
|
||||||
statement.append('?');
|
|
||||||
|
|
||||||
if (cursor.getType(i) == Cursor.FIELD_TYPE_STRING) {
|
|
||||||
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setStringParamter(cursor.getString(i)));
|
|
||||||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_FLOAT) {
|
|
||||||
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setDoubleParameter(cursor.getDouble(i)));
|
|
||||||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_INTEGER) {
|
|
||||||
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setIntegerParameter(cursor.getLong(i)));
|
|
||||||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
|
|
||||||
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))));
|
|
||||||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_NULL) {
|
|
||||||
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setNullparameter(true));
|
|
||||||
} else {
|
|
||||||
throw new AssertionError("unknown type?" + cursor.getType(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < cursor.getColumnCount()-1) {
|
|
||||||
statement.append(',');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statement.append(')');
|
|
||||||
|
|
||||||
outputStream.write(statementBuilder.setStatement(statement.toString()).build());
|
|
||||||
|
|
||||||
if (postProcess != null) postProcess.accept(cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream) {
|
|
||||||
try {
|
|
||||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
|
|
||||||
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
|
|
||||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE));
|
|
||||||
|
|
||||||
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA));
|
|
||||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM));
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(data) && size <= 0) {
|
|
||||||
size = calculateVeryOldStreamLength(attachmentSecret, random, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(data) && size > 0) {
|
|
||||||
InputStream inputStream;
|
|
||||||
|
|
||||||
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
|
|
||||||
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
|
|
||||||
|
|
||||||
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream) {
|
|
||||||
try {
|
|
||||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
|
|
||||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
|
|
||||||
|
|
||||||
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH));
|
|
||||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(data) && size > 0) {
|
|
||||||
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
|
|
||||||
outputStream.writeSticker(rowId, inputStream, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException {
|
|
||||||
long result = 0;
|
|
||||||
InputStream inputStream;
|
|
||||||
|
|
||||||
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
|
|
||||||
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
|
|
||||||
|
|
||||||
int read;
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
|
|
||||||
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
|
||||||
result += read;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
|
|
||||||
String[] columns = new String[] { MmsDatabase.EXPIRES_IN };
|
|
||||||
String where = MmsDatabase.ID + " = ?";
|
|
||||||
String[] args = new String[] { String.valueOf(mmsId) };
|
|
||||||
|
|
||||||
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
|
|
||||||
if (mmsCursor != null && mmsCursor.moveToFirst()) {
|
|
||||||
return mmsCursor.getLong(0) == 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static class BackupFrameOutputStream extends BackupStream implements Closeable, Flushable {
|
|
||||||
|
|
||||||
private final OutputStream outputStream;
|
|
||||||
private final Cipher cipher;
|
|
||||||
private final Mac mac;
|
|
||||||
|
|
||||||
private final byte[] cipherKey;
|
|
||||||
private final byte[] macKey;
|
|
||||||
|
|
||||||
private byte[] iv;
|
|
||||||
private int counter;
|
|
||||||
|
|
||||||
private BackupFrameOutputStream(@NonNull OutputStream outputStream, @NonNull String passphrase) throws IOException {
|
|
||||||
try {
|
|
||||||
byte[] salt = Util.getSecretBytes(32);
|
|
||||||
byte[] key = getBackupKey(passphrase, salt);
|
|
||||||
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64);
|
|
||||||
byte[][] split = ByteUtil.split(derived, 32, 32);
|
|
||||||
|
|
||||||
this.cipherKey = split[0];
|
|
||||||
this.macKey = split[1];
|
|
||||||
|
|
||||||
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
|
||||||
this.mac = Mac.getInstance("HmacSHA256");
|
|
||||||
this.outputStream = outputStream;
|
|
||||||
this.iv = Util.getSecretBytes(16);
|
|
||||||
this.counter = Conversions.byteArrayToInt(iv);
|
|
||||||
|
|
||||||
mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
|
|
||||||
|
|
||||||
byte[] header = BackupProtos.BackupFrame.newBuilder().setHeader(BackupProtos.Header.newBuilder()
|
|
||||||
.setIv(ByteString.copyFrom(iv))
|
|
||||||
.setSalt(ByteString.copyFrom(salt)))
|
|
||||||
.build().toByteArray();
|
|
||||||
|
|
||||||
outputStream.write(Conversions.intToByteArray(header.length));
|
|
||||||
outputStream.write(header);
|
|
||||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(BackupProtos.SharedPreference preference) throws IOException {
|
|
||||||
write(outputStream, BackupProtos.BackupFrame.newBuilder().setPreference(preference).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(BackupProtos.SqlStatement statement) throws IOException {
|
|
||||||
write(outputStream, BackupProtos.BackupFrame.newBuilder().setStatement(statement).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException {
|
|
||||||
write(outputStream, BackupProtos.BackupFrame.newBuilder()
|
|
||||||
.setAvatar(BackupProtos.Avatar.newBuilder()
|
|
||||||
.setName(avatarName)
|
|
||||||
.setLength(Util.toIntExact(size))
|
|
||||||
.build())
|
|
||||||
.build());
|
|
||||||
|
|
||||||
writeStream(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
|
|
||||||
write(outputStream, BackupProtos.BackupFrame.newBuilder()
|
|
||||||
.setAttachment(BackupProtos.Attachment.newBuilder()
|
|
||||||
.setRowId(attachmentId.getRowId())
|
|
||||||
.setAttachmentId(attachmentId.getUniqueId())
|
|
||||||
.setLength(Util.toIntExact(size))
|
|
||||||
.build())
|
|
||||||
.build());
|
|
||||||
|
|
||||||
writeStream(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException {
|
|
||||||
write(outputStream, BackupProtos.BackupFrame.newBuilder()
|
|
||||||
.setSticker(BackupProtos.Sticker.newBuilder()
|
|
||||||
.setRowId(rowId)
|
|
||||||
.setLength(Util.toIntExact(size))
|
|
||||||
.build())
|
|
||||||
.build());
|
|
||||||
|
|
||||||
writeStream(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeDatabaseVersion(int version) throws IOException {
|
|
||||||
write(outputStream, BackupProtos.BackupFrame.newBuilder()
|
|
||||||
.setVersion(BackupProtos.DatabaseVersion.newBuilder().setVersion(version))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeEnd() throws IOException {
|
|
||||||
write(outputStream, BackupProtos.BackupFrame.newBuilder().setEnd(true).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeStream(@NonNull InputStream inputStream) throws IOException {
|
|
||||||
try {
|
|
||||||
Conversions.intToByteArray(iv, 0, counter++);
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
|
||||||
mac.update(iv);
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int read;
|
|
||||||
|
|
||||||
while ((read = inputStream.read(buffer)) != -1) {
|
|
||||||
byte[] ciphertext = cipher.update(buffer, 0, read);
|
|
||||||
|
|
||||||
if (ciphertext != null) {
|
|
||||||
outputStream.write(ciphertext);
|
|
||||||
mac.update(ciphertext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] remainder = cipher.doFinal();
|
|
||||||
outputStream.write(remainder);
|
|
||||||
mac.update(remainder);
|
|
||||||
|
|
||||||
byte[] attachmentDigest = mac.doFinal();
|
|
||||||
outputStream.write(attachmentDigest, 0, 10);
|
|
||||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void write(@NonNull OutputStream out, @NonNull BackupProtos.BackupFrame frame) throws IOException {
|
|
||||||
try {
|
|
||||||
Conversions.intToByteArray(iv, 0, counter++);
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
|
||||||
|
|
||||||
byte[] frameCiphertext = cipher.doFinal(frame.toByteArray());
|
|
||||||
byte[] frameMac = mac.doFinal(frameCiphertext);
|
|
||||||
byte[] length = Conversions.intToByteArray(frameCiphertext.length + 10);
|
|
||||||
|
|
||||||
out.write(length);
|
|
||||||
out.write(frameCiphertext);
|
|
||||||
out.write(frameMac, 0, 10);
|
|
||||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void flush() throws IOException {
|
|
||||||
outputStream.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
outputStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
440
src/org/thoughtcrime/securesms/backup/FullBackupExporter.kt
Normal file
440
src/org/thoughtcrime/securesms/backup/FullBackupExporter.kt
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
package org.thoughtcrime.securesms.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.TextUtils
|
||||||
|
import com.annimon.stream.function.Consumer
|
||||||
|
import com.annimon.stream.function.Predicate
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import net.sqlcipher.database.SQLiteDatabase
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||||
|
import org.thoughtcrime.securesms.backup.BackupProtos.*
|
||||||
|
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||||
|
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
||||||
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||||
|
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||||
|
import org.thoughtcrime.securesms.database.*
|
||||||
|
import org.thoughtcrime.securesms.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||||
|
import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase
|
||||||
|
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||||
|
import org.thoughtcrime.securesms.util.Conversions
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.util.Util
|
||||||
|
import org.whispersystems.libsignal.kdf.HKDFv3
|
||||||
|
import org.whispersystems.libsignal.util.ByteUtil
|
||||||
|
import java.io.*
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.*
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
object FullBackupExporter : FullBackupBase() {
|
||||||
|
private val TAG = FullBackupExporter::class.java.simpleName
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun export(context: Context,
|
||||||
|
attachmentSecret: AttachmentSecret,
|
||||||
|
input: SQLiteDatabase,
|
||||||
|
fileUri: Uri,
|
||||||
|
passphrase: String) {
|
||||||
|
|
||||||
|
val baseOutputStream = context.contentResolver.openOutputStream(fileUri)
|
||||||
|
?: throw IOException("Cannot open an output stream for the file URI: $fileUri")
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
try {
|
||||||
|
BackupFrameOutputStream(baseOutputStream, passphrase).use { outputStream ->
|
||||||
|
outputStream.writeDatabaseVersion(input.version)
|
||||||
|
val tables = exportSchema(input, outputStream)
|
||||||
|
for (table in tables) if (shouldExportTable(table)) {
|
||||||
|
count = when (table) {
|
||||||
|
SmsDatabase.TABLE_NAME, MmsDatabase.TABLE_NAME -> {
|
||||||
|
exportTable(table, input, outputStream,
|
||||||
|
{ cursor: Cursor ->
|
||||||
|
cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
count)
|
||||||
|
}
|
||||||
|
GroupReceiptDatabase.TABLE_NAME -> {
|
||||||
|
exportTable(table, input, outputStream,
|
||||||
|
{ cursor: Cursor ->
|
||||||
|
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID)))
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
count)
|
||||||
|
}
|
||||||
|
AttachmentDatabase.TABLE_NAME -> {
|
||||||
|
exportTable(table, input, outputStream,
|
||||||
|
{ cursor: Cursor ->
|
||||||
|
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)))
|
||||||
|
},
|
||||||
|
{ cursor: Cursor ->
|
||||||
|
exportAttachment(attachmentSecret, cursor, outputStream)
|
||||||
|
},
|
||||||
|
count)
|
||||||
|
}
|
||||||
|
StickerDatabase.TABLE_NAME -> {
|
||||||
|
exportTable(table, input, outputStream,
|
||||||
|
{ true },
|
||||||
|
{ cursor: Cursor -> exportSticker(attachmentSecret, cursor, outputStream) },
|
||||||
|
count)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
exportTable(table, input, outputStream, null, null, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (preference in IdentityKeyUtil.getBackupRecords(context)) {
|
||||||
|
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||||
|
outputStream.writePreferenceEntry(preference)
|
||||||
|
}
|
||||||
|
for (preference in TextSecurePreferences.getBackupRecords(context)) {
|
||||||
|
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||||
|
outputStream.writePreferenceEntry(preference)
|
||||||
|
}
|
||||||
|
for (avatar in AvatarHelper.getAvatarFiles(context)) {
|
||||||
|
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||||
|
outputStream.writeAvatar(avatar.name, FileInputStream(avatar), avatar.length())
|
||||||
|
}
|
||||||
|
outputStream.writeEnd()
|
||||||
|
}
|
||||||
|
EventBus.getDefault().post(BackupEvent.createFinished())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to make full backup.", e)
|
||||||
|
EventBus.getDefault().post(BackupEvent.createFinished(e))
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun shouldExportTable(table: String): Boolean {
|
||||||
|
return table != SignedPreKeyDatabase.TABLE_NAME &&
|
||||||
|
table != OneTimePreKeyDatabase.TABLE_NAME &&
|
||||||
|
table != SessionDatabase.TABLE_NAME &&
|
||||||
|
table != PushDatabase.TABLE_NAME &&
|
||||||
|
// table != DraftDatabase.TABLE_NAME &&
|
||||||
|
|
||||||
|
table != LokiBackupFilesDatabase.TABLE_NAME &&
|
||||||
|
table != LokiAPIDatabase.openGroupProfilePictureTable &&
|
||||||
|
|
||||||
|
table != JobDatabase.Jobs.TABLE_NAME &&
|
||||||
|
table != JobDatabase.Constraints.TABLE_NAME &&
|
||||||
|
table != JobDatabase.Dependencies.TABLE_NAME &&
|
||||||
|
|
||||||
|
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
|
||||||
|
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
|
||||||
|
!table.startsWith("sqlite_")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun exportSchema(input: SQLiteDatabase, outputStream: BackupFrameOutputStream): List<String> {
|
||||||
|
val tables: MutableList<String> = LinkedList()
|
||||||
|
input.rawQuery("SELECT sql, name, type FROM sqlite_master", null).use { cursor ->
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
val sql = cursor.getString(0)
|
||||||
|
val name = cursor.getString(1)
|
||||||
|
val type = cursor.getString(2)
|
||||||
|
if (sql != null) {
|
||||||
|
val isSmsFtsSecretTable = name != null && name != SearchDatabase.SMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME)
|
||||||
|
val isMmsFtsSecretTable = name != null && name != SearchDatabase.MMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME)
|
||||||
|
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
|
||||||
|
if ("table" == type) {
|
||||||
|
tables.add(name)
|
||||||
|
}
|
||||||
|
outputStream.writeSql(SqlStatement.newBuilder().setStatement(cursor.getString(0)).build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tables
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun exportTable(table: String,
|
||||||
|
input: SQLiteDatabase,
|
||||||
|
outputStream: BackupFrameOutputStream,
|
||||||
|
predicate: Predicate<Cursor>?,
|
||||||
|
postProcess: Consumer<Cursor>?,
|
||||||
|
count: Int): Int {
|
||||||
|
var count = count
|
||||||
|
val template = "INSERT INTO $table VALUES "
|
||||||
|
input.rawQuery("SELECT * FROM $table", null).use { cursor ->
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||||
|
if (predicate != null && !predicate.test(cursor)) continue
|
||||||
|
|
||||||
|
val statement = StringBuilder(template)
|
||||||
|
val statementBuilder = SqlStatement.newBuilder()
|
||||||
|
statement.append('(')
|
||||||
|
for (i in 0 until cursor.columnCount) {
|
||||||
|
statement.append('?')
|
||||||
|
when (cursor.getType(i)) {
|
||||||
|
Cursor.FIELD_TYPE_STRING -> {
|
||||||
|
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||||
|
.setStringParamter(cursor.getString(i)))
|
||||||
|
}
|
||||||
|
Cursor.FIELD_TYPE_FLOAT -> {
|
||||||
|
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||||
|
.setDoubleParameter(cursor.getDouble(i)))
|
||||||
|
}
|
||||||
|
Cursor.FIELD_TYPE_INTEGER -> {
|
||||||
|
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||||
|
.setIntegerParameter(cursor.getLong(i)))
|
||||||
|
}
|
||||||
|
Cursor.FIELD_TYPE_BLOB -> {
|
||||||
|
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||||
|
.setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))))
|
||||||
|
}
|
||||||
|
Cursor.FIELD_TYPE_NULL -> {
|
||||||
|
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||||
|
.setNullparameter(true))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw AssertionError("unknown type?" + cursor.getType(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < cursor.columnCount - 1) {
|
||||||
|
statement.append(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statement.append(')')
|
||||||
|
outputStream.writeSql(statementBuilder.setStatement(statement.toString()).build())
|
||||||
|
postProcess?.accept(cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exportAttachment(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
|
||||||
|
try {
|
||||||
|
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID))
|
||||||
|
val uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))
|
||||||
|
var size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE))
|
||||||
|
val data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA))
|
||||||
|
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM))
|
||||||
|
if (!TextUtils.isEmpty(data) && size <= 0) {
|
||||||
|
size = calculateVeryOldStreamLength(attachmentSecret, random, data)
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(data) && size > 0) {
|
||||||
|
val inputStream: InputStream = if (random != null && random.size == 32) {
|
||||||
|
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
|
||||||
|
} else {
|
||||||
|
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
|
||||||
|
}
|
||||||
|
outputStream.writeAttachment(AttachmentId(rowId, uniqueId), inputStream, size)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exportSticker(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
|
||||||
|
try {
|
||||||
|
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID))
|
||||||
|
val size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH))
|
||||||
|
val data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH))
|
||||||
|
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM))
|
||||||
|
if (!TextUtils.isEmpty(data) && size > 0) {
|
||||||
|
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0).use { inputStream -> outputStream.writeSticker(rowId, inputStream, size) }
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun calculateVeryOldStreamLength(attachmentSecret: AttachmentSecret, random: ByteArray?, data: String): Long {
|
||||||
|
var result: Long = 0
|
||||||
|
val inputStream: InputStream = if (random != null && random.size == 32) {
|
||||||
|
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
|
||||||
|
} else {
|
||||||
|
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
|
||||||
|
}
|
||||||
|
var read: Int
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) {
|
||||||
|
result += read.toLong()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
|
||||||
|
val columns = arrayOf(MmsDatabase.EXPIRES_IN)
|
||||||
|
val where = MmsDatabase.ID + " = ?"
|
||||||
|
val args = arrayOf(mmsId.toString())
|
||||||
|
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
|
||||||
|
if (mmsCursor != null && mmsCursor.moveToFirst()) {
|
||||||
|
return mmsCursor.getLong(0) == 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BackupFrameOutputStream : BackupStream, Closeable, Flushable {
|
||||||
|
|
||||||
|
private val outputStream: OutputStream
|
||||||
|
private var cipher: Cipher
|
||||||
|
private var mac: Mac
|
||||||
|
private val cipherKey: ByteArray
|
||||||
|
private val macKey: ByteArray
|
||||||
|
private val iv: ByteArray
|
||||||
|
|
||||||
|
private var counter: Int = 0
|
||||||
|
|
||||||
|
constructor(outputStream: OutputStream, passphrase: String) : super() {
|
||||||
|
try {
|
||||||
|
val salt = Util.getSecretBytes(32)
|
||||||
|
val key = getBackupKey(passphrase, salt)
|
||||||
|
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
|
||||||
|
val split = ByteUtil.split(derived, 32, 32)
|
||||||
|
cipherKey = split[0]
|
||||||
|
macKey = split[1]
|
||||||
|
cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||||
|
mac = Mac.getInstance("HmacSHA256")
|
||||||
|
this.outputStream = outputStream
|
||||||
|
iv = Util.getSecretBytes(16)
|
||||||
|
counter = Conversions.byteArrayToInt(iv)
|
||||||
|
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
|
||||||
|
val header = BackupFrame.newBuilder().setHeader(Header.newBuilder()
|
||||||
|
.setIv(ByteString.copyFrom(iv))
|
||||||
|
.setSalt(ByteString.copyFrom(salt)))
|
||||||
|
.build().toByteArray()
|
||||||
|
outputStream.write(Conversions.intToByteArray(header.size))
|
||||||
|
outputStream.write(header)
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
} catch (e: NoSuchPaddingException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
} catch (e: InvalidKeyException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun writeSql(statement: SqlStatement) {
|
||||||
|
write(outputStream, BackupFrame.newBuilder().setStatement(statement).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun writePreferenceEntry(preference: SharedPreference?) {
|
||||||
|
write(outputStream, BackupFrame.newBuilder().setPreference(preference).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun writeAvatar(avatarName: String, inputStream: InputStream, size: Long) {
|
||||||
|
write(outputStream, BackupFrame.newBuilder()
|
||||||
|
.setAvatar(Avatar.newBuilder()
|
||||||
|
.setName(avatarName)
|
||||||
|
.setLength(Util.toIntExact(size))
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
writeStream(inputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun writeAttachment(attachmentId: AttachmentId, inputStream: InputStream, size: Long) {
|
||||||
|
write(outputStream, BackupFrame.newBuilder()
|
||||||
|
.setAttachment(Attachment.newBuilder()
|
||||||
|
.setRowId(attachmentId.rowId)
|
||||||
|
.setAttachmentId(attachmentId.uniqueId)
|
||||||
|
.setLength(Util.toIntExact(size))
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
writeStream(inputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun writeSticker(rowId: Long, inputStream: InputStream, size: Long) {
|
||||||
|
write(outputStream, BackupFrame.newBuilder()
|
||||||
|
.setSticker(Sticker.newBuilder()
|
||||||
|
.setRowId(rowId)
|
||||||
|
.setLength(Util.toIntExact(size))
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
writeStream(inputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun writeDatabaseVersion(version: Int) {
|
||||||
|
write(outputStream, BackupFrame.newBuilder()
|
||||||
|
.setVersion(DatabaseVersion.newBuilder().setVersion(version))
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun writeEnd() {
|
||||||
|
write(outputStream, BackupFrame.newBuilder().setEnd(true).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun writeStream(inputStream: InputStream) {
|
||||||
|
try {
|
||||||
|
Conversions.intToByteArray(iv, 0, counter++)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
||||||
|
mac.update(iv)
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var read: Int
|
||||||
|
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||||
|
val ciphertext = cipher.update(buffer, 0, read)
|
||||||
|
if (ciphertext != null) {
|
||||||
|
outputStream.write(ciphertext)
|
||||||
|
mac.update(ciphertext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val remainder = cipher.doFinal()
|
||||||
|
outputStream.write(remainder)
|
||||||
|
mac.update(remainder)
|
||||||
|
val attachmentDigest = mac.doFinal()
|
||||||
|
outputStream.write(attachmentDigest, 0, 10)
|
||||||
|
} catch (e: InvalidKeyException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
} catch (e: InvalidAlgorithmParameterException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
} catch (e: IllegalBlockSizeException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
} catch (e: BadPaddingException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun write(out: OutputStream, frame: BackupFrame) {
|
||||||
|
try {
|
||||||
|
Conversions.intToByteArray(iv, 0, counter++)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
||||||
|
val frameCiphertext = cipher.doFinal(frame.toByteArray())
|
||||||
|
val frameMac = mac.doFinal(frameCiphertext)
|
||||||
|
val length = Conversions.intToByteArray(frameCiphertext.size + 10)
|
||||||
|
out.write(length)
|
||||||
|
out.write(frameCiphertext)
|
||||||
|
out.write(frameMac, 0, 10)
|
||||||
|
} catch (e: InvalidKeyException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
} catch (e: InvalidAlgorithmParameterException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
} catch (e: IllegalBlockSizeException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
} catch (e: BadPaddingException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun flush() {
|
||||||
|
outputStream.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun close() {
|
||||||
|
outputStream.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,15 @@
|
|||||||
package org.thoughtcrime.securesms.backup;
|
package org.thoughtcrime.securesms.backup;
|
||||||
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
@ -40,7 +39,6 @@ import org.whispersystems.libsignal.util.ByteUtil;
|
|||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -62,7 +60,12 @@ import javax.crypto.spec.SecretKeySpec;
|
|||||||
|
|
||||||
public class FullBackupImporter extends FullBackupBase {
|
public class FullBackupImporter extends FullBackupBase {
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
/**
|
||||||
|
* Because BackupProtos.SharedPreference was made only to serialize string values,
|
||||||
|
* we use these 3-char prefixes to explicitly cast the values before inserting to a preference file.
|
||||||
|
*/
|
||||||
|
public static final String PREF_PREFIX_TYPE_INT = "i__";
|
||||||
|
|
||||||
private static final String TAG = FullBackupImporter.class.getSimpleName();
|
private static final String TAG = FullBackupImporter.class.getSimpleName();
|
||||||
|
|
||||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||||
@ -85,7 +88,7 @@ public class FullBackupImporter extends FullBackupBase {
|
|||||||
BackupFrame frame;
|
BackupFrame frame;
|
||||||
|
|
||||||
while (!(frame = inputStream.readFrame()).getEnd()) {
|
while (!(frame = inputStream.readFrame()).getEnd()) {
|
||||||
if (count++ % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count));
|
if (count++ % 100 == 0) EventBus.getDefault().post(BackupEvent.createProgress(count));
|
||||||
|
|
||||||
if (frame.hasVersion()) processVersion(db, frame.getVersion());
|
if (frame.hasVersion()) processVersion(db, frame.getVersion());
|
||||||
else if (frame.hasStatement()) processStatement(db, frame.getStatement());
|
else if (frame.hasStatement()) processStatement(db, frame.getStatement());
|
||||||
@ -104,7 +107,7 @@ public class FullBackupImporter extends FullBackupBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
|
EventBus.getDefault().post(BackupEvent.createFinished());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) throws IOException {
|
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) throws IOException {
|
||||||
@ -185,7 +188,18 @@ public class FullBackupImporter extends FullBackupBase {
|
|||||||
@SuppressLint("ApplySharedPref")
|
@SuppressLint("ApplySharedPref")
|
||||||
private static void processPreference(@NonNull Context context, SharedPreference preference) {
|
private static void processPreference(@NonNull Context context, SharedPreference preference) {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(preference.getFile(), 0);
|
SharedPreferences preferences = context.getSharedPreferences(preference.getFile(), 0);
|
||||||
preferences.edit().putString(preference.getKey(), preference.getValue()).commit();
|
String key = preference.getKey();
|
||||||
|
String value = preference.getValue();
|
||||||
|
|
||||||
|
// See comments next to PREF_PREFIX_TYPE_* constants.
|
||||||
|
if (key.startsWith(PREF_PREFIX_TYPE_INT)) {
|
||||||
|
preferences.edit().putInt(
|
||||||
|
key.substring(3),
|
||||||
|
Integer.parseInt(value)
|
||||||
|
).commit();
|
||||||
|
} else {
|
||||||
|
preferences.edit().putString(key, value).commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void dropAllTables(@NonNull SQLiteDatabase db) {
|
private static void dropAllTables(@NonNull SQLiteDatabase db) {
|
||||||
|
@ -122,21 +122,38 @@ public class IdentityKeyUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<BackupProtos.SharedPreference> getBackupRecord(@NonNull Context context) {
|
public static List<BackupProtos.SharedPreference> getBackupRecords(@NonNull Context context) {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||||
|
|
||||||
return new LinkedList<BackupProtos.SharedPreference>() {{
|
LinkedList<BackupProtos.SharedPreference> prefList = new LinkedList<>();
|
||||||
add(BackupProtos.SharedPreference.newBuilder()
|
|
||||||
|
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||||
.setKey(IDENTITY_PUBLIC_KEY_PREF)
|
.setKey(IDENTITY_PUBLIC_KEY_PREF)
|
||||||
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
|
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
|
||||||
.build());
|
.build());
|
||||||
add(BackupProtos.SharedPreference.newBuilder()
|
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||||
.setKey(IDENTITY_PRIVATE_KEY_PREF)
|
.setKey(IDENTITY_PRIVATE_KEY_PREF)
|
||||||
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
|
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
|
||||||
.build());
|
.build());
|
||||||
}};
|
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||||
|
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||||
|
.setKey(ED25519_PUBLIC_KEY)
|
||||||
|
.setValue(preferences.getString(ED25519_PUBLIC_KEY, null))
|
||||||
|
.build());
|
||||||
|
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||||
|
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||||
|
.setKey(ED25519_SECRET_KEY)
|
||||||
|
.setValue(preferences.getString(ED25519_SECRET_KEY, null))
|
||||||
|
.build());
|
||||||
|
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||||
|
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||||
|
.setKey(LOKI_SEED)
|
||||||
|
.setValue(preferences.getString(LOKI_SEED, null))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return prefList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean hasLegacyIdentityKeys(Context context) {
|
private static boolean hasLegacyIdentityKeys(Context context) {
|
||||||
|
@ -17,11 +17,11 @@ import java.util.Set;
|
|||||||
|
|
||||||
public class DraftDatabase extends Database {
|
public class DraftDatabase extends Database {
|
||||||
|
|
||||||
private static final String TABLE_NAME = "drafts";
|
public static final String TABLE_NAME = "drafts";
|
||||||
public static final String ID = "_id";
|
public static final String ID = "_id";
|
||||||
public static final String THREAD_ID = "thread_id";
|
public static final String THREAD_ID = "thread_id";
|
||||||
public static final String DRAFT_TYPE = "type";
|
public static final String DRAFT_TYPE = "type";
|
||||||
public static final String DRAFT_VALUE = "value";
|
public static final String DRAFT_VALUE = "value";
|
||||||
|
|
||||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||||
THREAD_ID + " INTEGER, " + DRAFT_TYPE + " TEXT, " + DRAFT_VALUE + " TEXT);";
|
THREAD_ID + " INTEGER, " + DRAFT_TYPE + " TEXT, " + DRAFT_VALUE + " TEXT);";
|
||||||
|
@ -22,8 +22,8 @@ public class JobDatabase extends Database {
|
|||||||
Constraints.CREATE_TABLE,
|
Constraints.CREATE_TABLE,
|
||||||
Dependencies.CREATE_TABLE };
|
Dependencies.CREATE_TABLE };
|
||||||
|
|
||||||
private static final class Jobs {
|
public static final class Jobs {
|
||||||
private static final String TABLE_NAME = "job_spec";
|
public static final String TABLE_NAME = "job_spec";
|
||||||
private static final String ID = "_id";
|
private static final String ID = "_id";
|
||||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||||
private static final String FACTORY_KEY = "factory_key";
|
private static final String FACTORY_KEY = "factory_key";
|
||||||
@ -53,8 +53,8 @@ public class JobDatabase extends Database {
|
|||||||
IS_RUNNING + " INTEGER)";
|
IS_RUNNING + " INTEGER)";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Constraints {
|
public static final class Constraints {
|
||||||
private static final String TABLE_NAME = "constraint_spec";
|
public static final String TABLE_NAME = "constraint_spec";
|
||||||
private static final String ID = "_id";
|
private static final String ID = "_id";
|
||||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||||
private static final String FACTORY_KEY = "factory_key";
|
private static final String FACTORY_KEY = "factory_key";
|
||||||
@ -65,8 +65,8 @@ public class JobDatabase extends Database {
|
|||||||
"UNIQUE(" + JOB_SPEC_ID + ", " + FACTORY_KEY + "))";
|
"UNIQUE(" + JOB_SPEC_ID + ", " + FACTORY_KEY + "))";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Dependencies {
|
public static final class Dependencies {
|
||||||
private static final String TABLE_NAME = "dependency_spec";
|
public static final String TABLE_NAME = "dependency_spec";
|
||||||
private static final String ID = "_id";
|
private static final String ID = "_id";
|
||||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||||
private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id";
|
private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id";
|
||||||
|
@ -20,16 +20,16 @@ public class PushDatabase extends Database {
|
|||||||
|
|
||||||
private static final String TAG = PushDatabase.class.getSimpleName();
|
private static final String TAG = PushDatabase.class.getSimpleName();
|
||||||
|
|
||||||
private static final String TABLE_NAME = "push";
|
public static final String TABLE_NAME = "push";
|
||||||
public static final String ID = "_id";
|
public static final String ID = "_id";
|
||||||
public static final String TYPE = "type";
|
public static final String TYPE = "type";
|
||||||
public static final String SOURCE = "source";
|
public static final String SOURCE = "source";
|
||||||
public static final String DEVICE_ID = "device_id";
|
public static final String DEVICE_ID = "device_id";
|
||||||
public static final String LEGACY_MSG = "body";
|
public static final String LEGACY_MSG = "body";
|
||||||
public static final String CONTENT = "content";
|
public static final String CONTENT = "content";
|
||||||
public static final String TIMESTAMP = "timestamp";
|
public static final String TIMESTAMP = "timestamp";
|
||||||
public static final String SERVER_TIMESTAMP = "server_timestamp";
|
public static final String SERVER_TIMESTAMP = "server_timestamp";
|
||||||
public static final String SERVER_GUID = "server_guid";
|
public static final String SERVER_GUID = "server_guid";
|
||||||
|
|
||||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||||
TYPE + " INTEGER, " + SOURCE + " TEXT, " + DEVICE_ID + " INTEGER, " + LEGACY_MSG + " TEXT, " + CONTENT + " TEXT, " + TIMESTAMP + " INTEGER, " +
|
TYPE + " INTEGER, " + SOURCE + " TEXT, " + DEVICE_ID + " INTEGER, " + LEGACY_MSG + " TEXT, " + CONTENT + " TEXT, " + TIMESTAMP + " INTEGER, " +
|
||||||
|
@ -50,12 +50,15 @@ import org.whispersystems.libsignal.util.Pair;
|
|||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import nl.komponents.kovenant.combine.Tuple2;
|
||||||
|
|
||||||
public class ThreadDatabase extends Database {
|
public class ThreadDatabase extends Database {
|
||||||
|
|
||||||
private static final String TAG = ThreadDatabase.class.getSimpleName();
|
private static final String TAG = ThreadDatabase.class.getSimpleName();
|
||||||
@ -618,6 +621,34 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lightweight utility method to retrieve the complete list of non-archived threads coupled with their recipient address.
|
||||||
|
* @return a tuple with non-null values: thread id, recipient address.
|
||||||
|
*/
|
||||||
|
public @NonNull List<Tuple2<Long, String>> getConversationListQuick() {
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
|
||||||
|
ArrayList<Tuple2<Long, String>> result = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Cursor cursor = db.query(
|
||||||
|
TABLE_NAME,
|
||||||
|
new String[]{ID, ADDRESS},
|
||||||
|
ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + ADDRESS + " IS NOT NULL",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null)) {
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
result.add(new Tuple2<>(
|
||||||
|
cursor.getLong(cursor.getColumnIndexOrThrow(ID)),
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
|
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
|
||||||
|
@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
|||||||
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
||||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollJob;
|
import org.thoughtcrime.securesms.loki.api.BackgroundPollJob;
|
||||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||||
|
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
|
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob;
|
import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob;
|
||||||
@ -104,6 +105,7 @@ public class WorkManagerFactoryMappings {
|
|||||||
put(TypingSendJob.class.getName(), TypingSendJob.KEY);
|
put(TypingSendJob.class.getName(), TypingSendJob.KEY);
|
||||||
put(UpdateApkJob.class.getName(), UpdateApkJob.KEY);
|
put(UpdateApkJob.class.getName(), UpdateApkJob.KEY);
|
||||||
put(PrepareAttachmentAudioExtrasJob.class.getName(), PrepareAttachmentAudioExtrasJob.KEY);
|
put(PrepareAttachmentAudioExtrasJob.class.getName(), PrepareAttachmentAudioExtrasJob.KEY);
|
||||||
|
put(ResetThreadSessionJob.class.getName(), ResetThreadSessionJob.KEY);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
public static @Nullable String getFactoryKey(@NonNull String workManagerClass) {
|
public static @Nullable String getFactoryKey(@NonNull String workManagerClass) {
|
||||||
|
@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
|||||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollJob;
|
import org.thoughtcrime.securesms.loki.api.BackgroundPollJob;
|
||||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||||
|
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
|
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob;
|
import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob;
|
||||||
@ -81,6 +82,7 @@ public final class JobManagerFactories {
|
|||||||
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
||||||
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||||
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
|
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
|
||||||
|
put(ResetThreadSessionJob.KEY, new ResetThreadSessionJob.Factory());
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.loki.activities
|
package org.thoughtcrime.securesms.loki.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -23,17 +22,17 @@ import androidx.databinding.DataBindingUtil
|
|||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.google.android.gms.common.util.Strings
|
import com.google.android.gms.common.util.Strings
|
||||||
import kotlinx.android.synthetic.main.activity_pn_mode.*
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityBackupRestoreBinding
|
import network.loki.messenger.databinding.ActivityBackupRestoreBinding
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.RegistrationActivity
|
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
|
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||||
|
import org.thoughtcrime.securesms.database.Address
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.logging.Log
|
import org.thoughtcrime.securesms.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob
|
||||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||||
import org.thoughtcrime.securesms.loki.utilities.show
|
import org.thoughtcrime.securesms.loki.utilities.show
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||||
@ -115,21 +114,6 @@ class BackupRestoreActivity : BaseActionBarActivity() {
|
|||||||
private fun restore() {
|
private fun restore() {
|
||||||
if (viewModel.backupFile.value == null && Strings.isEmptyOrWhitespace(viewModel.backupPassphrase.value)) return
|
if (viewModel.backupFile.value == null && Strings.isEmptyOrWhitespace(viewModel.backupPassphrase.value)) return
|
||||||
|
|
||||||
// val backupFile = viewModel.backupFile.value!!
|
|
||||||
// val password = viewModel.backupPassphrase.value!!.trim()
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// FullBackupImporter.importFile(
|
|
||||||
// this,
|
|
||||||
// AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret(),
|
|
||||||
// DatabaseFactory.getBackupDatabase(this),
|
|
||||||
// backupFile,
|
|
||||||
// password
|
|
||||||
// )
|
|
||||||
// } catch (e: IOException) {
|
|
||||||
// Log.e(TAG, "Failed to restore from the backup file \"$backupFile\"", e)
|
|
||||||
// }
|
|
||||||
|
|
||||||
val backupFile = viewModel.backupFile.value!!
|
val backupFile = viewModel.backupFile.value!!
|
||||||
val passphrase = viewModel.backupPassphrase.value!!.trim()
|
val passphrase = viewModel.backupPassphrase.value!!.trim()
|
||||||
|
|
||||||
@ -147,8 +131,10 @@ class BackupRestoreActivity : BaseActionBarActivity() {
|
|||||||
)
|
)
|
||||||
DatabaseFactory.upgradeRestored(context, database)
|
DatabaseFactory.upgradeRestored(context, database)
|
||||||
NotificationChannels.restoreContactNotificationChannels(context)
|
NotificationChannels.restoreContactNotificationChannels(context)
|
||||||
TextSecurePreferences.setBackupEnabled(context, true)
|
// TextSecurePreferences.setBackupEnabled(context, true)
|
||||||
TextSecurePreferences.setBackupPassphrase(context, passphrase)
|
// TextSecurePreferences.setBackupPassphrase(context, passphrase)
|
||||||
|
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
|
||||||
|
|
||||||
BackupImportResult.SUCCESS
|
BackupImportResult.SUCCESS
|
||||||
} catch (e: DatabaseDowngradeException) {
|
} catch (e: DatabaseDowngradeException) {
|
||||||
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
|
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
|
||||||
@ -163,6 +149,7 @@ class BackupRestoreActivity : BaseActionBarActivity() {
|
|||||||
val context = this@BackupRestoreActivity
|
val context = this@BackupRestoreActivity
|
||||||
when (result) {
|
when (result) {
|
||||||
BackupImportResult.SUCCESS -> {
|
BackupImportResult.SUCCESS -> {
|
||||||
|
TextSecurePreferences.setHasViewedSeed(context, true)
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
||||||
TextSecurePreferences.setPromptedPushRegistration(context, true)
|
TextSecurePreferences.setPromptedPushRegistration(context, true)
|
||||||
TextSecurePreferences.setIsUsingFCM(context, true)
|
TextSecurePreferences.setIsUsingFCM(context, true)
|
||||||
@ -172,6 +159,8 @@ class BackupRestoreActivity : BaseActionBarActivity() {
|
|||||||
application.setUpStorageAPIIfNeeded()
|
application.setUpStorageAPIIfNeeded()
|
||||||
application.setUpP2PAPIIfNeeded()
|
application.setUpP2PAPIIfNeeded()
|
||||||
|
|
||||||
|
HomeActivity.requestResetAllSessionsOnStartup(context)
|
||||||
|
|
||||||
val intent = Intent(context, HomeActivity::class.java)
|
val intent = Intent(context, HomeActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
show(intent)
|
show(intent)
|
||||||
@ -222,7 +211,7 @@ class RestoreBackupViewModel(application: Application): AndroidViewModel(applica
|
|||||||
}
|
}
|
||||||
|
|
||||||
val backupFile = MutableLiveData<Uri>()
|
val backupFile = MutableLiveData<Uri>()
|
||||||
val backupPassphrase = MutableLiveData<String>()
|
val backupPassphrase = MutableLiveData<String>("000000000000000000000000000000")
|
||||||
|
|
||||||
fun onBackupFileSelected(backupFile: Uri) {
|
fun onBackupFileSelected(backupFile: Uri) {
|
||||||
//TODO Check if backup file is correct.
|
//TODO Check if backup file is correct.
|
||||||
|
@ -26,13 +26,13 @@ import kotlinx.android.synthetic.main.activity_home.*
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||||
|
import org.thoughtcrime.securesms.database.Address
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob
|
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob
|
||||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob
|
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob
|
||||||
import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet
|
import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet
|
||||||
import org.thoughtcrime.securesms.loki.dialogs.LightThemeFeatureIntroBottomSheet
|
import org.thoughtcrime.securesms.loki.dialogs.LightThemeFeatureIntroBottomSheet
|
||||||
import org.thoughtcrime.securesms.loki.dialogs.MultiDeviceRemovalBottomSheet
|
import org.thoughtcrime.securesms.loki.dialogs.MultiDeviceRemovalBottomSheet
|
||||||
@ -46,6 +46,8 @@ import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate
|
|||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences.getBooleanPreference
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences.setBooleanPreference
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
|
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
|
||||||
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
|
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
|
||||||
@ -57,6 +59,32 @@ import org.whispersystems.signalservice.loki.utilities.toHexString
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
|
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREF_RESET_ALL_SESSIONS_ON_START_UP = "pref_reset_all_sessions_on_start_up"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun requestResetAllSessionsOnStartup(context: Context) {
|
||||||
|
setBooleanPreference(context, PREF_RESET_ALL_SESSIONS_ON_START_UP, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun scheduleResetAllSessionsIfRequested(context: Context) {
|
||||||
|
if (!getBooleanPreference(context, PREF_RESET_ALL_SESSIONS_ON_START_UP, false)) return
|
||||||
|
setBooleanPreference(context, PREF_RESET_ALL_SESSIONS_ON_START_UP, false)
|
||||||
|
|
||||||
|
val jobManager = ApplicationContext.getInstance(context).jobManager
|
||||||
|
|
||||||
|
DatabaseFactory.getThreadDatabase(context).conversationListQuick.forEach { tuple ->
|
||||||
|
val threadId: Long = tuple.first
|
||||||
|
val recipientAddress: String = tuple.second
|
||||||
|
jobManager.add(ResetThreadSessionJob(
|
||||||
|
Address.fromSerialized(recipientAddress),
|
||||||
|
threadId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var glide: GlideRequests
|
private lateinit var glide: GlideRequests
|
||||||
private var broadcastReceiver: BroadcastReceiver? = null
|
private var broadcastReceiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
@ -196,6 +224,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
|||||||
TextSecurePreferences.setWasUnlinked(this, true)
|
TextSecurePreferences.setWasUnlinked(this, true)
|
||||||
ApplicationContext.getInstance(this).clearData()
|
ApplicationContext.getInstance(this).clearData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Perform chat sessions reset if requested (usually happens after backup restoration).
|
||||||
|
scheduleResetAllSessionsIfRequested(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki.api
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.Address
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||||
|
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||||
|
import org.thoughtcrime.securesms.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.sms.MessageSender
|
||||||
|
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage
|
||||||
|
import org.thoughtcrime.securesms.sms.OutgoingTextMessage
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class ResetThreadSessionJob private constructor(
|
||||||
|
parameters: Parameters,
|
||||||
|
private val address: Address,
|
||||||
|
private val threadId: Long)
|
||||||
|
: BaseJob(parameters) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY = "ResetThreadSessionJob"
|
||||||
|
const val DATA_KEY_ADDRESS = "address"
|
||||||
|
const val DATA_KEY_THREAD_ID = "thread_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(address: Address, threadId: Long) : this(Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setQueue(KEY)
|
||||||
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
.build(),
|
||||||
|
address,
|
||||||
|
threadId)
|
||||||
|
|
||||||
|
override fun serialize(): Data {
|
||||||
|
return Data.Builder()
|
||||||
|
.putParcelable(DATA_KEY_ADDRESS, address)
|
||||||
|
.putLong(DATA_KEY_THREAD_ID, threadId)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFactoryKey(): String { return KEY }
|
||||||
|
|
||||||
|
public override fun onRun() {
|
||||||
|
val recipient = Recipient.from(context, address, false)
|
||||||
|
|
||||||
|
// Only reset sessions for private chats.
|
||||||
|
if (recipient.isGroupRecipient) return
|
||||||
|
|
||||||
|
Log.v(KEY, "Resetting session for thread: \"$threadId\", recipient: \"${address.serialize()}\"")
|
||||||
|
|
||||||
|
val message = OutgoingEndSessionMessage(OutgoingTextMessage(recipient, "TERMINATE", 0, -1))
|
||||||
|
MessageSender.send(context, message, threadId, false, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onShouldRetry(e: Exception): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCanceled() { }
|
||||||
|
|
||||||
|
class Factory : Job.Factory<ResetThreadSessionJob> {
|
||||||
|
|
||||||
|
override fun create(parameters: Parameters, data: Data): ResetThreadSessionJob {
|
||||||
|
val address = data.getParcelable(DATA_KEY_ADDRESS, Address.CREATOR)
|
||||||
|
val threadId = data.getLong(DATA_KEY_THREAD_ID)
|
||||||
|
return ResetThreadSessionJob(parameters, address, threadId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -71,7 +71,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
private val openGroupPublicKeyTable = "open_group_public_keys"
|
private val openGroupPublicKeyTable = "open_group_public_keys"
|
||||||
@JvmStatic val createOpenGroupPublicKeyTableCommand = "CREATE TABLE $openGroupPublicKeyTable ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);"
|
@JvmStatic val createOpenGroupPublicKeyTableCommand = "CREATE TABLE $openGroupPublicKeyTable ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);"
|
||||||
// Open group profile picture cache
|
// Open group profile picture cache
|
||||||
private val openGroupProfilePictureTable = "open_group_avatar_cache"
|
public val openGroupProfilePictureTable = "open_group_avatar_cache"
|
||||||
private val openGroupProfilePicture = "open_group_avatar"
|
private val openGroupProfilePicture = "open_group_avatar"
|
||||||
@JvmStatic val createOpenGroupProfilePictureTableCommand = "CREATE TABLE $openGroupProfilePictureTable ($publicChatID STRING PRIMARY KEY, $openGroupProfilePicture TEXT NULLABLE DEFAULT NULL);"
|
@JvmStatic val createOpenGroupProfilePictureTableCommand = "CREATE TABLE $openGroupProfilePictureTable ($publicChatID STRING PRIMARY KEY, $openGroupProfilePicture TEXT NULLABLE DEFAULT NULL);"
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class LokiBackupFilesDatabase(context: Context, databaseHelper: SQLCipherOpenHel
|
|||||||
: Database(context, databaseHelper) {
|
: Database(context, databaseHelper) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TABLE_NAME = "backup_files"
|
public const val TABLE_NAME = "backup_files"
|
||||||
private const val COLUMN_ID = "_id"
|
private const val COLUMN_ID = "_id"
|
||||||
private const val COLUMN_URI = "uri"
|
private const val COLUMN_URI = "uri"
|
||||||
private const val COLUMN_FILE_SIZE = "file_size"
|
private const val COLUMN_FILE_SIZE = "file_size"
|
||||||
|
@ -6,6 +6,7 @@ import android.content.DialogInterface;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -104,7 +105,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
public void onEvent(BackupEvent event) {
|
public void onEvent(BackupEvent event) {
|
||||||
ProgressPreference preference = (ProgressPreference)findPreference(TextSecurePreferences.BACKUP_NOW);
|
ProgressPreference preference = findPreference(TextSecurePreferences.BACKUP_NOW);
|
||||||
|
|
||||||
if (event.getType() == BackupEvent.Type.PROGRESS) {
|
if (event.getType() == BackupEvent.Type.PROGRESS) {
|
||||||
preference.setEnabled(false);
|
preference.setEnabled(false);
|
||||||
@ -114,6 +115,14 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||||||
preference.setEnabled(true);
|
preference.setEnabled(true);
|
||||||
preference.setProgressVisible(false);
|
preference.setProgressVisible(false);
|
||||||
setBackupSummary();
|
setBackupSummary();
|
||||||
|
|
||||||
|
if (event.getException() != null) {
|
||||||
|
Toast.makeText(
|
||||||
|
getActivity(),
|
||||||
|
getString(R.string.preferences_chats__backup_export_error),
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
|
import org.thoughtcrime.securesms.backup.BackupProtos;
|
||||||
|
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||||
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
|
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
@ -24,10 +26,14 @@ import java.security.SecureRandom;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
|
import static org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_INT;
|
||||||
|
|
||||||
public class TextSecurePreferences {
|
public class TextSecurePreferences {
|
||||||
|
|
||||||
private static final String TAG = TextSecurePreferences.class.getSimpleName();
|
private static final String TAG = TextSecurePreferences.class.getSimpleName();
|
||||||
@ -1334,4 +1340,38 @@ public class TextSecurePreferences {
|
|||||||
setBooleanPreference(context, "has_seen_light_theme_intro_sheet", true);
|
setBooleanPreference(context, "has_seen_light_theme_intro_sheet", true);
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region Backup related
|
||||||
|
public static List<BackupProtos.SharedPreference> getBackupRecords(@NonNull Context context) {
|
||||||
|
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
final String prefsFileName;
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||||
|
prefsFileName = PreferenceManager.getDefaultSharedPreferencesName(context);
|
||||||
|
} else {
|
||||||
|
prefsFileName = context.getPackageName() + "_preferences";
|
||||||
|
}
|
||||||
|
|
||||||
|
final LinkedList<BackupProtos.SharedPreference> prefList = new LinkedList<>();
|
||||||
|
|
||||||
|
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||||
|
.setFile(prefsFileName)
|
||||||
|
.setKey(PREF_PREFIX_TYPE_INT + LOCAL_REGISTRATION_ID_PREF)
|
||||||
|
.setValue(String.valueOf(preferences.getInt(LOCAL_REGISTRATION_ID_PREF, 0)))
|
||||||
|
.build());
|
||||||
|
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||||
|
.setFile(prefsFileName)
|
||||||
|
.setKey(LOCAL_NUMBER_PREF)
|
||||||
|
.setValue(preferences.getString(LOCAL_NUMBER_PREF, null))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||||
|
.setFile(prefsFileName)
|
||||||
|
.setKey(PROFILE_NAME_PREF)
|
||||||
|
.setValue(preferences.getString(PROFILE_NAME_PREF, null))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return prefList;
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user