Base backup restore activity.

This commit is contained in:
Anton Chekulaev 2020-10-20 15:39:01 +11:00
parent 0cd24905b7
commit 255271bfaf
9 changed files with 388 additions and 11 deletions

View File

@ -105,6 +105,11 @@
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.BackupRestoreActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity <activity
android:name="org.thoughtcrime.securesms.loki.activities.LinkDeviceActivity" android:name="org.thoughtcrime.securesms.loki.activities.LinkDeviceActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"

View File

@ -149,6 +149,8 @@ dependencies {
implementation "com.fasterxml.jackson.core:jackson-databind:2.9.8" implementation "com.fasterxml.jackson.core:jackson-databind:2.9.8"
implementation "com.squareup.okhttp3:okhttp:3.12.1" implementation "com.squareup.okhttp3:okhttp:3.12.1"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
implementation 'androidx.activity:activity-ktx:1.1.0'
implementation "nl.komponents.kovenant:kovenant:$kovenant_version" implementation "nl.komponents.kovenant:kovenant:$kovenant_version"
implementation "nl.komponents.kovenant:kovenant-android:$kovenant_version" implementation "nl.komponents.kovenant:kovenant-android:$kovenant_version"
implementation "com.github.lelloman:android-identicons:v11" implementation "com.github.lelloman:android-identicons:v11"
@ -347,6 +349,19 @@ android {
includeAndroidResources = true includeAndroidResources = true
} }
} }
buildFeatures {
dataBinding true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
} }
/* /*

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="org.thoughtcrime.securesms.loki.activities.RestoreBackupViewModel"/>
<import type="android.view.View"/>
<variable
name="viewModel"
type="org.thoughtcrime.securesms.loki.activities.RestoreBackupViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginRight="@dimen/very_large_spacing"
android:text="Restore from backup"
android:textColor="@color/text"
android:textSize="@dimen/large_font_size"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="4dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:text="Go on and pick the backup file to restore from."
android:textColor="@color/text"
android:textSize="@dimen/small_font_size" />
<Button
android:id="@+id/buttonSelectFile"
style="@style/Button.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="4dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:text="@{RestoreBackupViewModel.uriToFileName(buttonSelectFile, viewModel.backupFile), default=`Select a file`}"/>
<EditText
android:id="@+id/backupCode"
style="@style/SmallSessionEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="10dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:hint="Backup code"
android:inputType="numberDecimal"
android:text="@={viewModel.backupPassphrase}" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/restoreButton"
style="@style/Widget.Session.Button.Common.ProminentFilled"
android:layout_width="match_parent"
android:layout_height="@dimen/medium_button_height"
android:layout_marginLeft="@dimen/massive_spacing"
android:layout_marginRight="@dimen/massive_spacing"
android:text="@string/continue_2"
android:visibility="@{RestoreBackupViewModel.validateData(viewModel.backupFile, viewModel.backupPassphrase) ? View.VISIBLE : View.INVISIBLE}"/>
<TextView
android:id="@+id/termsTextView"
android:layout_width="match_parent"
android:layout_height="@dimen/onboarding_button_bottom_offset"
android:layout_marginLeft="@dimen/massive_spacing"
android:layout_marginRight="@dimen/massive_spacing"
android:gravity="center"
android:text="By using this service, you agree to our Terms of Service and Privacy Policy"
android:textColor="@color/text"
android:textColorLink="@color/text"
android:textSize="@dimen/very_small_font_size" /> <!-- Intentionally not yet translated -->
</LinearLayout>
</layout>

View File

@ -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/small_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"

View File

@ -5,6 +5,7 @@ import android.animation.Animator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -328,7 +329,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
FullBackupImporter.importFile(context, FullBackupImporter.importFile(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database, backup.getFile(), passphrase); database, Uri.fromFile(backup.getFile()), passphrase);
DatabaseFactory.upgradeRestored(context, database); DatabaseFactory.upgradeRestored(context, database);
NotificationChannels.restoreContactNotificationChannels(context); NotificationChannels.restoreContactNotificationChannels(context);

View File

@ -7,6 +7,8 @@ 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 androidx.annotation.NonNull;
import android.net.Uri;
import android.util.Pair; import android.util.Pair;
import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabase;
@ -36,6 +38,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.kdf.HKDFv3; import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil; import org.whispersystems.libsignal.util.ByteUtil;
import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -63,13 +66,18 @@ public class FullBackupImporter extends FullBackupBase {
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,
@NonNull SQLiteDatabase db, @NonNull File file, @NonNull String passphrase) @NonNull SQLiteDatabase db, @NonNull Uri fileUri, @NonNull String passphrase)
throws IOException throws IOException
{ {
BackupRecordInputStream inputStream = new BackupRecordInputStream(file, passphrase); InputStream baseInputStream = context.getContentResolver().openInputStream(fileUri);
if (baseInputStream == null) {
throw new IOException("Cannot open an input stream for the file URI: " + fileUri.toString());
}
int count = 0; int count = 0;
try { try (BackupRecordInputStream inputStream = new BackupRecordInputStream(baseInputStream, passphrase)) {
db.beginTransaction(); db.beginTransaction();
dropAllTables(db); dropAllTables(db);
@ -91,8 +99,10 @@ public class FullBackupImporter extends FullBackupBase {
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
if (db.inTransaction()) {
db.endTransaction(); db.endTransaction();
} }
}
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
} }
@ -213,7 +223,7 @@ public class FullBackupImporter extends FullBackupBase {
} }
private static class BackupRecordInputStream extends BackupStream { private static class BackupRecordInputStream extends BackupStream implements Closeable {
private final InputStream in; private final InputStream in;
private final Cipher cipher; private final Cipher cipher;
@ -225,9 +235,9 @@ public class FullBackupImporter extends FullBackupBase {
private byte[] iv; private byte[] iv;
private int counter; private int counter;
private BackupRecordInputStream(@NonNull File file, @NonNull String passphrase) throws IOException { private BackupRecordInputStream(@NonNull InputStream inputStream, @NonNull String passphrase) throws IOException {
try { try {
this.in = new FileInputStream(file); this.in = inputStream;
byte[] headerLengthBytes = new byte[4]; byte[] headerLengthBytes = new byte[4];
Util.readFully(in, headerLengthBytes); Util.readFully(in, headerLengthBytes);
@ -349,6 +359,11 @@ public class FullBackupImporter extends FullBackupBase {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@Override
public void close() throws IOException {
in.close();
}
} }
public static class DatabaseDowngradeException extends IOException { public static class DatabaseDowngradeException extends IOException {

View File

@ -0,0 +1,231 @@
package org.thoughtcrime.securesms.loki.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.provider.OpenableColumns
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.StyleSpan
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
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.databinding.ActivityBackupRestoreBinding
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.RegistrationActivity
import org.thoughtcrime.securesms.backup.FullBackupImporter
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.loki.utilities.show
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.io.IOException
class BackupRestoreActivity : BaseActionBarActivity() {
companion object {
private const val TAG = "BackupRestoreActivity"
private const val REQUEST_CODE_BACKUP_FILE = 779955
}
private val viewModel by viewModels<RestoreBackupViewModel>()
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpActionBarSessionLogo()
val dataBinding = DataBindingUtil.setContentView<ActivityBackupRestoreBinding>(this, R.layout.activity_backup_restore)
dataBinding.lifecycleOwner = this
dataBinding.viewModel = viewModel
// setContentView(R.layout.activity_backup_restore)
dataBinding.restoreButton.setOnClickListener { restore() }
dataBinding.buttonSelectFile.setOnClickListener {
// Let user pick a file.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
// type = BackupUtil.BACKUP_FILE_MIME_TYPE
type = "*/*"
}
startActivityForResult(intent, REQUEST_CODE_BACKUP_FILE)
}
dataBinding.backupCode.addTextChangedListener { text -> viewModel.backupPassphrase.value = text.toString() }
//region Legal info views
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
openURL("https://getsession.org/terms-of-service/")
}
}, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
openURL("https://getsession.org/privacy-policy/")
}
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
dataBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
dataBinding.termsTextView.text = termsExplanation
//endregion
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
REQUEST_CODE_BACKUP_FILE -> {
if (resultCode == Activity.RESULT_OK && data != null && data.data != null) {
// // Acquire persistent access permissions for the file selected.
// val persistentFlags: Int = data.flags and
// (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
// context.contentResolver.takePersistableUriPermission(data.data!!, persistentFlags)
viewModel.onBackupFileSelected(data.data!!)
}
}
}
}
// endregion
// region Interaction
private fun restore() {
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 passphrase = viewModel.backupPassphrase.value!!.trim()
object : AsyncTask<Void?, Void?, BackupImportResult>() {
override fun doInBackground(vararg params: Void?): BackupImportResult {
return try {
val context: Context = this@BackupRestoreActivity
val database = DatabaseFactory.getBackupDatabase(context)
FullBackupImporter.importFile(
context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
DatabaseFactory.getBackupDatabase(context),
backupFile,
passphrase
)
DatabaseFactory.upgradeRestored(context, database)
NotificationChannels.restoreContactNotificationChannels(context)
TextSecurePreferences.setBackupEnabled(context, true)
TextSecurePreferences.setBackupPassphrase(context, passphrase)
BackupImportResult.SUCCESS
} catch (e: DatabaseDowngradeException) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
BackupImportResult.FAILURE_VERSION_DOWNGRADE
} catch (e: IOException) {
Log.w(TAG, e)
BackupImportResult.FAILURE_UNKNOWN
}
}
override fun onPostExecute(result: BackupImportResult) {
val context = this@BackupRestoreActivity
when (result) {
BackupImportResult.SUCCESS -> {
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
TextSecurePreferences.setPromptedPushRegistration(context, true)
TextSecurePreferences.setIsUsingFCM(context, true)
TextSecurePreferences.setHasSeenMultiDeviceRemovalSheet(context)
TextSecurePreferences.setHasSeenLightThemeIntroSheet(context)
val application = ApplicationContext.getInstance(context)
application.setUpStorageAPIIfNeeded()
application.setUpP2PAPIIfNeeded()
val intent = Intent(context, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
show(intent)
}
BackupImportResult.FAILURE_VERSION_DOWNGRADE ->
Toast.makeText(context, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
BackupImportResult.FAILURE_UNKNOWN ->
Toast.makeText(context, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
}
}
}.execute()
}
private fun openURL(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
}
}
enum class BackupImportResult {
SUCCESS, FAILURE_VERSION_DOWNGRADE, FAILURE_UNKNOWN
}
// endregion
}
class RestoreBackupViewModel(application: Application): AndroidViewModel(application) {
companion object {
@JvmStatic
fun uriToFileName(view: View, fileUri: Uri?): String? {
fileUri ?: return null
view.context.contentResolver.query(fileUri, null, null, null, null).use {
val nameIndex = it!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
it.moveToFirst()
return it.getString(nameIndex)
}
}
@JvmStatic
fun validateData(fileUri: Uri?, passphrase: String?): Boolean {
return fileUri != null && !Strings.isEmptyOrWhitespace(passphrase)
}
}
val backupFile = MutableLiveData<Uri>()
val backupPassphrase = MutableLiveData<String>()
fun onBackupFileSelected(backupFile: Uri) {
//TODO Check if backup file is correct.
this.backupFile.value = backupFile
}
}

View File

@ -44,6 +44,10 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
fakeChatView.startAnimating() fakeChatView.startAnimating()
registerButton.setOnClickListener { register() } registerButton.setOnClickListener { register() }
restoreButton.setOnClickListener { restore() } restoreButton.setOnClickListener { restore() }
restoreBackupButton.setOnClickListener {
val intent = Intent(this, BackupRestoreActivity::class.java)
push(intent)
}
// linkButton.setOnClickListener { linkDevice() } // linkButton.setOnClickListener { linkDevice() }
if (TextSecurePreferences.getWasUnlinked(this)) { if (TextSecurePreferences.getWasUnlinked(this)) {
Toast.makeText(this, R.string.activity_landing_device_unlinked_dialog_title, Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.activity_landing_device_unlinked_dialog_title, Toast.LENGTH_LONG).show()

View File

@ -28,6 +28,7 @@ import kotlin.jvm.Throws
object BackupUtil { object BackupUtil {
private const val TAG = "BackupUtil" private const val TAG = "BackupUtil"
const val BACKUP_FILE_MIME_TYPE = "application/x-binary"
/** /**
* Set app-wide configuration to enable the backups and schedule them. * Set app-wide configuration to enable the backups and schedule them.
@ -151,7 +152,7 @@ object BackupUtil {
val fileUri = DocumentsContract.createDocument( val fileUri = DocumentsContract.createDocument(
context.contentResolver, context.contentResolver,
DocumentFile.fromTreeUri(context, dirUri)!!.uri, DocumentFile.fromTreeUri(context, dirUri)!!.uri,
"application/x-binary", BACKUP_FILE_MIME_TYPE,
fileName) fileName)
if (fileUri == null) { if (fileUri == null) {
@ -160,7 +161,7 @@ object BackupUtil {
} }
FullBackupExporter.export(context, FullBackupExporter.export(context,
AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret, AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
DatabaseFactory.getBackupDatabase(context), DatabaseFactory.getBackupDatabase(context),
fileUri, fileUri,
backupPassword) backupPassword)