From a19d22bee6332daad2441d27194d2e382e2b296b Mon Sep 17 00:00:00 2001 From: Martin/Geno Date: Sat, 17 Nov 2018 21:57:23 +0100 Subject: [PATCH] add backup import and export --- .../conversations/ui/WelcomeActivity.java | 244 +++++++++++++++++- src/conversations/res/layout/welcome.xml | 19 +- src/main/AndroidManifest.xml | 1 + .../persistance/DatabaseBackend.java | 4 +- .../persistance/FileBackend.java | 4 + .../conversations/services/BackupService.java | 141 ++++++++++ .../services/NotificationService.java | 7 + .../conversations/ui/SettingsActivity.java | 20 +- .../utils/EncryptDecryptFile.java | 73 ++++++ src/main/res/layout/backup_password.xml | 21 ++ src/main/res/values-de/strings.xml | 9 + src/main/res/values-es/strings.xml | 8 + src/main/res/values-fr/strings.xml | 1 + src/main/res/values-id/strings.xml | 7 + src/main/res/values-ru/strings.xml | 1 + src/main/res/values/strings.xml | 11 + src/main/res/xml/preferences.xml | 5 + 17 files changed, 571 insertions(+), 5 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/services/BackupService.java create mode 100644 src/main/java/eu/siacs/conversations/utils/EncryptDecryptFile.java create mode 100644 src/main/res/layout/backup_password.xml diff --git a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java index 4eb614d55..1ea3cd519 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -1,20 +1,49 @@ package eu.siacs.conversations.ui; +import android.Manifest; +import android.app.ProgressDialog; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.os.Build; import android.os.Bundle; import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.List; +import javax.crypto.NoSuchPaddingException; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.XmppUri; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.utils.EncryptDecryptFile; public class WelcomeActivity extends XmppActivity { + boolean importSuccessful = false; + @Override protected void refreshUiReal() { @@ -25,6 +54,8 @@ public class WelcomeActivity extends XmppActivity { } + private static final int REQUEST_READ_EXTERNAL_STORAGE = 0XD737; + @Override public void onStart() { super.onStart(); @@ -54,7 +85,19 @@ public class WelcomeActivity extends XmppActivity { ab.setDisplayShowHomeEnabled(false); ab.setDisplayHomeAsUpEnabled(false); } + //check if there is a backed up database -- + if (hasStoragePermission(REQUEST_READ_EXTERNAL_STORAGE)) { + backupAvailable(); + } + final Button importDatabase = findViewById(R.id.import_database); + final TextView importText = findViewById(R.id.import_text); final Button createAccount = findViewById(R.id.create_account); + if (backupAvailable()) { + importDatabase.setVisibility(View.VISIBLE); + importText.setVisibility(View.VISIBLE); + } + + importDatabase.setOnClickListener(v -> enterPasswordDialog()); createAccount.setOnClickListener(v -> { final Intent intent = new Intent(WelcomeActivity.this, MagicCreateActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); @@ -77,6 +120,205 @@ public class WelcomeActivity extends XmppActivity { } + public void enterPasswordDialog() { + LayoutInflater li = LayoutInflater.from(WelcomeActivity.this); + View promptsView = li.inflate(R.layout.backup_password, null); + final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(WelcomeActivity.this); + alertDialogBuilder.setView(promptsView); + final EditText userInput = promptsView + .findViewById(R.id.password); + alertDialogBuilder.setTitle(R.string.enter_password); + alertDialogBuilder.setMessage(R.string.enter_backup_password); + alertDialogBuilder + .setCancelable(false) + .setPositiveButton(R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + final String password = userInput.getText().toString(); + final ProgressDialog pd = ProgressDialog.show(WelcomeActivity.this, getString(R.string.please_wait), getString(R.string.import_started), true); + if (!password.isEmpty()) { + new Thread(new Runnable() { + @Override + public void run() { + try { + checkDatabase(password); + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + pd.dismiss(); + } + }).start(); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(WelcomeActivity.this); + builder.setTitle(R.string.error); + builder.setMessage(R.string.password_should_not_be_empty); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.try_again, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + enterPasswordDialog(); + } + }); + builder.create().show(); + } + } + }) + .setNegativeButton(R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Toast.makeText(WelcomeActivity.this, R.string.import_canceled, Toast.LENGTH_LONG).show(); + dialog.dismiss(); + } + } + ); + WelcomeActivity.this.runOnUiThread(new Runnable() { + public void run() { + // create alert dialog + AlertDialog alertDialog = alertDialogBuilder.create(); + // show it + alertDialog.show(); + } + }); + } + + private boolean backupAvailable() { + // Set the folder on the SDcard + File filePath = new File(FileBackend.getBackupDirectory() + "/database.db.crypt"); + Log.d(Config.LOGTAG, "DB Path: " + filePath.toString()); + if (filePath.exists()) { + Log.d(Config.LOGTAG, "DB Path existing"); + return true; + } else { + Log.d(Config.LOGTAG, "DB Path not existing"); + return false; + } + } + + private void checkDatabase(String DecryptionKey) throws IOException { + // Set the folder on the SDcard + File directory = new File(FileBackend.getBackupDirectory()); + // Set the input file stream up: + FileInputStream InputFile = new FileInputStream(directory.getPath() + "/database.db.crypt"); + // Temp output for DB checks + File TempFile = new File(directory.getPath() + "database.bak"); + FileOutputStream OutputTemp = new FileOutputStream(TempFile); + + try { + EncryptDecryptFile.decrypt(InputFile, OutputTemp, DecryptionKey); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "Database importer: decryption failed with " + e); + e.printStackTrace(); + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "Database importer: decryption failed with " + e); + e.printStackTrace(); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Database importer: decryption failed (invalid key) with " + e); + e.printStackTrace(); + } catch (IOException e) { + Log.d(Config.LOGTAG, "Database importer: decryption failed (IO) with " + e); + e.printStackTrace(); + } catch (Exception e) { + Log.d(Config.LOGTAG, "Database importer: Error " + e); + e.printStackTrace(); + } + + SQLiteDatabase checkDB = null; + int DB_Version = DatabaseBackend.DATABASE_VERSION; + int Backup_DB_Version = 0; + + try { + String dbPath = TempFile.toString(); + checkDB = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY); + Backup_DB_Version = checkDB.getVersion(); + Log.d(Config.LOGTAG, "Backup found: " + checkDB + " Version: " + checkDB.getVersion()); + } catch (SQLiteException e) { + //database does't exist yet. + Log.d(Config.LOGTAG, "No backup found: " + checkDB); + } catch (Exception e) { + Log.d(Config.LOGTAG, "Error importing backup: " + e); + } + + if (checkDB != null) { + checkDB.close(); + } + if (checkDB != null) { + Log.d(Config.LOGTAG, "checkDB = " + checkDB.toString() + ", Backup DB = " + Backup_DB_Version + ", DB = " + DB_Version); + } + if (checkDB != null && Backup_DB_Version != 0 && Backup_DB_Version <= DB_Version) { + try { + importDatabase(); + importSuccessful = true; + } catch (Exception e) { + importSuccessful = false; + e.printStackTrace(); + } finally { + if (importSuccessful) { + restart(); + } + } + } else if (checkDB != null && Backup_DB_Version == 0) { + WelcomeActivity.this.runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(WelcomeActivity.this, R.string.password_wrong, Toast.LENGTH_LONG).show(); + enterPasswordDialog(); + } + }); + } else { + WelcomeActivity.this.runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(WelcomeActivity.this, R.string.import_failed, Toast.LENGTH_LONG).show(); + } + }); + } + } + + private void importDatabase() throws Exception { + // Set location for the db: + final OutputStream OutputFile = new FileOutputStream(this.getDatabasePath(DatabaseBackend.DATABASE_NAME)); + // Set the folder on the SDcard + File directory = new File(FileBackend.getBackupDirectory()); + // Set the input file stream up: + final InputStream InputFile = new FileInputStream(directory.getPath() + "database.bak"); + //set temp file + File TempFile = new File(directory.getPath() + "database.bak"); + + // Transfer bytes from the input file to the output file + byte[] buffer = new byte[1024]; + int length; + while ((length = InputFile.read(buffer)) > 0) { + OutputFile.write(buffer, 0, length); + } + if (TempFile.exists()) { + Log.d(Config.LOGTAG, "Delete temp file from " + TempFile.toString()); + TempFile.delete(); + } + } + + private void restart() { + //restart app + Log.d(Config.LOGTAG, "Restarting " + getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName())); + Intent intent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName()); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + System.exit(0); + } + + public boolean hasStoragePermission(int requestCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, requestCode); + return false; + } else { + return true; + } + } else { + return true; + } + } + public void addInviteUri(Intent intent) { StartConversationActivity.addInviteUri(intent, getIntent()); } diff --git a/src/conversations/res/layout/welcome.xml b/src/conversations/res/layout/welcome.xml index e33c1abe8..e84113b1d 100644 --- a/src/conversations/res/layout/welcome.xml +++ b/src/conversations/res/layout/welcome.xml @@ -42,6 +42,23 @@ android:layout_marginTop="8dp" android:text="@string/welcome_text" android:textAppearance="@style/TextAppearance.Conversations.Body1"/> + +