diff --git a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java
index 4eb614d55..13608fe1f 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.setOnClickListener(v -> enterPasswordDialog());
+ importDatabase.setVisibility(View.VISIBLE);
+ importText.setVisibility(View.VISIBLE);
+ }
+
createAccount.setOnClickListener(v -> {
final Intent intent = new Intent(WelcomeActivity.this, MagicCreateActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
@@ -77,6 +120,215 @@ 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.db.tmp");
+ 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 DBVersion = DatabaseBackend.DATABASE_VERSION;
+ int BackupDBVersion = 0;
+
+ try {
+ String dbPath = tempFile.toString();
+ checkDB = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY);
+ BackupDBVersion = 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 = " + BackupDBVersion + ", DB = " + DBVersion);
+ }
+ if (checkDB != null && BackupDBVersion != 0 && BackupDBVersion <= DBVersion) {
+ try {
+ importDatabase();
+ importSuccessful = true;
+ } catch (Exception e) {
+ importSuccessful = false;
+ e.printStackTrace();
+ } finally {
+ if (tempFile.exists()) {
+ Log.d(Config.LOGTAG, "Delete temp file from " + tempFile.toString());
+ tempFile.delete();
+ }
+ if (importSuccessful) {
+ restart();
+ }else{
+ WelcomeActivity.this.runOnUiThread(new Runnable() {
+ public void run() {
+ Toast.makeText(WelcomeActivity.this, R.string.import_failed, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }
+ } else if (checkDB != null && BackupDBVersion == 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();
+ }
+ });
+ }
+ if (tempFile.exists()) {
+ Log.d(Config.LOGTAG, "Delete temp file from " + tempFile.toString());
+ tempFile.delete();
+ }
+ }
+
+ 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.db.tmp");
+ //set temp file
+ File tempFile = new File(directory.getPath() + "database.db.tmp");
+
+ // 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);
+ }
+ }
+
+ 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.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_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"/>
+
+
+ android:textColor="?colorAccent"/>
+
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
index 5359a9f08..69d9c1266 100644
--- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -61,8 +61,8 @@ import rocks.xmpp.addr.Jid;
public class DatabaseBackend extends SQLiteOpenHelper {
- private static final String DATABASE_NAME = "history";
- private static final int DATABASE_VERSION = 42;
+ public static final String DATABASE_NAME = "history";
+ public static final int DATABASE_VERSION = 42;
private static DatabaseBackend instance = null;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index 5fe915d20..34d38b745 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -148,6 +148,10 @@ public class FileBackend {
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + context.getString(R.string.app_name) + "/Media/";
}
+ public static String getBackupDirectory() {
+ return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Conversations for Sum7/Database/";
+ }
+
public static String getConversationsLogsDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Conversations for Sum7/";
}
diff --git a/src/main/java/eu/siacs/conversations/services/BackupService.java b/src/main/java/eu/siacs/conversations/services/BackupService.java
new file mode 100644
index 000000000..f292475c5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/BackupService.java
@@ -0,0 +1,119 @@
+package eu.siacs.conversations.services;
+
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.support.v4.app.NotificationCompat;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.crypto.NoSuchPaddingException;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.utils.EncryptDecryptFile;
+
+public class BackupService extends Service {
+
+ private static final int NOTIFICATION_ID = 1;
+ private static AtomicBoolean running = new AtomicBoolean(false);
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (running.compareAndSet(false, true)) {
+ new Thread(() -> {
+ export();
+ stopForeground(true);
+ running.set(false);
+ stopSelf();
+ }).start();
+ }
+ return START_NOT_STICKY;
+ }
+
+ private void export() {
+ try {
+ NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ mBuilder.setContentTitle(getString(R.string.notification_backup_create))
+ .setSmallIcon(R.drawable.ic_import_export_white_24dp);
+ startForeground(NOTIFICATION_ID, mBuilder.build());
+
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String encryptionKey = prefs.getString("backup_password", null);
+ if (encryptionKey == null || encryptionKey.length() < 3) {
+ Log.d(Config.LOGTAG, "BackupService: failed to write encryted backup to sdcard because of missing password");
+
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(BackupService.this.getApplicationContext(),"Found no password to create backup!", Toast.LENGTH_LONG).show();
+ }
+ });
+ return;
+ }
+
+ Log.d(Config.LOGTAG, "BackupService: start creating backup");
+ // Get hold of the db:
+ FileInputStream inputFile = new FileInputStream(this.getDatabasePath(DatabaseBackend.DATABASE_NAME));
+ // Set the output folder on the SDcard
+ File directory = new File(FileBackend.getBackupDirectory());
+ // Create the folder if it doesn't exist:
+ if (!directory.exists()) {
+ boolean directory_created = directory.mkdirs();
+ Log.d(Config.LOGTAG, "BackupService: backup directory created " + directory_created);
+ }
+ //Delete old database export file
+ File tempDBFile = new File(directory + "database.db.tmp");
+ if (tempDBFile.exists()) {
+ Log.d(Config.LOGTAG, "BackupService: Delete temp database backup file from " + tempDBFile.toString());
+ boolean temp_db_file_deleted = tempDBFile.delete();
+ Log.d(Config.LOGTAG, "BackupService: old backup file deleted " + temp_db_file_deleted);
+ }
+ // Set the output file stream up:
+ FileOutputStream outputFile = new FileOutputStream(directory.getPath() + "database.db.crypt");
+
+ // encrypt database from the input file to the output file
+ try {
+ EncryptDecryptFile.encrypt(inputFile, outputFile, encryptionKey);
+ Log.d(Config.LOGTAG, "BackupService: starting encrypted output to " + outputFile.toString());
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.d(Config.LOGTAG, "BackupService: Database exporter: encryption failed with " + e);
+ e.printStackTrace();
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, "BackupService: Database exporter: encryption failed (invalid key) with " + e);
+ e.printStackTrace();
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG, "BackupService: Database exporter: encryption failed (IO) with " + e);
+ e.printStackTrace();
+ } finally {
+ Log.d(Config.LOGTAG, "BackupService: backup job finished");
+ }
+ mNotifyManager.notify(NOTIFICATION_ID, mBuilder.build());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
index 608b1fd72..a18a67262 100644
--- a/src/main/java/eu/siacs/conversations/services/EventReceiver.java
+++ b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.services;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -7,15 +9,36 @@ import android.preference.PreferenceManager;
import android.support.v4.content.ContextCompat;
import android.util.Log;
+import java.util.Calendar;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.Compatibility;
public class EventReceiver extends BroadcastReceiver {
public static final String SETTING_ENABLED_ACCOUNTS = "enabled_accounts";
+ private static Boolean isAlarmStarted = false;
@Override
public void onReceive(final Context context, final Intent originalIntent) {
+ if (originalIntent.getAction().equals("android.intent.action.BOOT_COMPLETED") || !isAlarmStarted) {
+ PendingIntent alarmIntent = PendingIntent.getService(context, 0, new Intent(context, BackupService.class), 0);
+
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(System.currentTimeMillis());
+ calendar.set(Calendar.HOUR_OF_DAY, 4);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+
+ // With setInexactRepeating(), you have to use one of the AlarmManager interval
+ // constants--in this case, AlarmManager.INTERVAL_DAY.
+ AlarmManager alarmMgr = (AlarmManager)(context.getSystemService(Context.ALARM_SERVICE));
+ alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, alarmIntent);
+
+ Log.d(Config.LOGTAG, "BackupService: alarm started for next run: "+calendar.getTime().toString());
+ isAlarmStarted = true;
+ }
+
final Intent intentForService = new Intent(context, XmppConnectionService.class);
if (originalIntent.getAction() != null) {
intentForService.setAction(originalIntent.getAction());
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index 16800f998..9877f9b21 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -139,6 +139,13 @@ public class NotificationService {
exportChannel.setGroup("status");
notificationManager.createNotificationChannel(exportChannel);
+ final NotificationChannel backupChannel = new NotificationChannel("backup",
+ c.getString(R.string.backup_channel_name),
+ NotificationManager.IMPORTANCE_LOW);
+ backupChannel.setShowBadge(false);
+ backupChannel.setGroup("status");
+ notificationManager.createNotificationChannel(backupChannel);
+
final NotificationChannel messagesChannel = new NotificationChannel("messages",
c.getString(R.string.messages_channel_name),
NotificationManager.IMPORTANCE_HIGH);
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index 43fc763ad..af8d5ac55 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -21,8 +21,9 @@ import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.provider.MediaStore;
import android.util.Log;
+import android.view.LayoutInflater;
import android.view.View;
-import android.view.ViewGroup;
+import android.widget.EditText;
import android.widget.Toast;
import java.io.File;
@@ -36,6 +37,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.BackupService;
import eu.siacs.conversations.services.ExportLogsService;
import eu.siacs.conversations.services.MemorizingTrustManager;
import eu.siacs.conversations.services.QuickConversationsService;
@@ -60,6 +62,7 @@ public class SettingsActivity extends XmppActivity implements
public static final String OMEMO_SETTING = "omemo";
public static final int REQUEST_WRITE_LOGS = 0xbf8701;
+ public static final int REQUEST_WRITE_BACKUP = 0xbf8702;
private SettingsFragment mSettingsFragment;
@Override
@@ -219,6 +222,26 @@ public class SettingsActivity extends XmppActivity implements
});
}
+ final Preference backupPasswordPreference = mSettingsFragment.findPreference("backup_password");
+ if (backupPasswordPreference != null) {
+ backupPasswordPreference.setOnPreferenceClickListener(preference -> {
+ if (hasStoragePermission(REQUEST_WRITE_BACKUP)) {
+ enterPasswordDialog();
+ }
+ return true;
+ });
+ }
+
+ final Preference exportBackupPreference = mSettingsFragment.findPreference("export_backup");
+ if (exportBackupPreference != null) {
+ exportBackupPreference.setOnPreferenceClickListener(preference -> {
+ if (hasStoragePermission(REQUEST_WRITE_BACKUP)) {
+ startBackup();
+ }
+ return true;
+ });
+ }
+
final Preference exportLogsPreference = mSettingsFragment.findPreference("export_logs");
if (exportLogsPreference != null) {
exportLogsPreference.setOnPreferenceClickListener(preference -> {
@@ -350,6 +373,50 @@ public class SettingsActivity extends XmppActivity implements
return true;
}
+ private void enterPasswordDialog() {
+ LayoutInflater li = LayoutInflater.from(this);
+ View promptsView = li.inflate(R.layout.backup_password, null);
+
+ final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
+ alertDialogBuilder.setView(promptsView);
+ final EditText password = promptsView.findViewById(R.id.password);
+ final EditText confirmPassword = promptsView.findViewById(R.id.confirm_password);
+ confirmPassword.setVisibility(View.VISIBLE);
+ alertDialogBuilder.setTitle(R.string.enter_password);
+ alertDialogBuilder.setMessage(R.string.enter_password);
+ alertDialogBuilder
+ .setCancelable(false)
+ .setPositiveButton(R.string.ok,
+ (dialog, id) -> {
+ final String pw1 = password.getText().toString();
+ final String pw2 = confirmPassword.getText().toString();
+ if (!pw1.equals(pw2)) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.error);
+ builder.setMessage(R.string.passwords_do_not_match);
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.setPositiveButton(R.string.try_again, (dialog12, id12) -> enterPasswordDialog());
+ builder.create().show();
+ } else if (pw1.trim().isEmpty()) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(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, (dialog1, id1) -> enterPasswordDialog());
+ builder.create().show();
+ } else {
+ boolean passwordstored = this.getPreferences().edit().putString("backup_password", pw1).commit();
+ Log.d(Config.LOGTAG, "saving password " + passwordstored);
+ if (passwordstored) {
+ recreate();
+ startBackup();
+ }
+ }
+ })
+ .setNegativeButton(R.string.cancel, null);
+ alertDialogBuilder.create().show();
+ }
+
@Override
public void onStop() {
super.onStop();
@@ -399,6 +466,9 @@ public class SettingsActivity extends XmppActivity implements
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (grantResults.length > 0)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (requestCode == REQUEST_WRITE_BACKUP) {
+ startBackup();
+ }
if (requestCode == REQUEST_WRITE_LOGS) {
startExport();
}
@@ -407,6 +477,10 @@ public class SettingsActivity extends XmppActivity implements
}
}
+ private void startBackup() {
+ ContextCompat.startForegroundService(this, new Intent(this, BackupService.class));
+ }
+
private void startExport() {
ContextCompat.startForegroundService(this, new Intent(this, ExportLogsService.class));
}
diff --git a/src/main/java/eu/siacs/conversations/utils/EncryptDecryptFile.java b/src/main/java/eu/siacs/conversations/utils/EncryptDecryptFile.java
new file mode 100644
index 000000000..9819e9d5d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/EncryptDecryptFile.java
@@ -0,0 +1,73 @@
+package eu.siacs.conversations.utils;
+
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.Config;
+
+public class EncryptDecryptFile {
+ private static String cipher_mode = "AES/ECB/PKCS5Padding";
+
+ public static void encrypt(FileInputStream iFile, FileOutputStream oFile, String iKey) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
+ byte[] key = iKey.getBytes("UTF-8");
+ Log.d(Config.LOGTAG, "Cipher key: " + Arrays.toString(key));
+ MessageDigest sha = MessageDigest.getInstance("SHA-1");
+ Log.d(Config.LOGTAG, "Cipher sha: " + sha.toString());
+ key = sha.digest(key);
+ Log.d(Config.LOGTAG, "Cipher sha key: " + Arrays.toString(key));
+ key = Arrays.copyOf(key, 16); // use only first 128 bit
+ Log.d(Config.LOGTAG, "Cipher sha key 16 bytes: " + Arrays.toString(key));
+ SecretKeySpec sks = new SecretKeySpec(key, "AES");
+ Cipher cipher = Cipher.getInstance(cipher_mode);
+ cipher.init(Cipher.ENCRYPT_MODE, sks);
+ Log.d(Config.LOGTAG, "Cipher IV: " + Arrays.toString(cipher.getIV()));
+ CipherOutputStream cos = new CipherOutputStream(oFile, cipher);
+ Log.d(Config.LOGTAG, "Encryption with: " + cos.toString());
+ int b;
+ byte[] d = new byte[8];
+ while ((b = iFile.read(d)) != -1) {
+ cos.write(d, 0, b);
+ }
+ cos.flush();
+ cos.close();
+ iFile.close();
+ }
+
+ public static void decrypt(FileInputStream iFile, FileOutputStream oFile, String iKey) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
+ byte[] key = iKey.getBytes("UTF-8");
+ Log.d(Config.LOGTAG, "Cipher key: " + Arrays.toString(key));
+ MessageDigest sha = MessageDigest.getInstance("SHA-1");
+ Log.d(Config.LOGTAG, "Cipher sha: " + sha.toString());
+ key = sha.digest(key);
+ Log.d(Config.LOGTAG, "Cipher sha key: " + Arrays.toString(key));
+ key = Arrays.copyOf(key, 16); // use only first 128 bit
+ Log.d(Config.LOGTAG, "Cipher sha key 16 bytes: " + Arrays.toString(key));
+ SecretKeySpec sks = new SecretKeySpec(key, "AES");
+ Cipher cipher = Cipher.getInstance(cipher_mode);
+ cipher.init(Cipher.DECRYPT_MODE, sks);
+ Log.d(Config.LOGTAG, "Cipher IV: " + Arrays.toString(cipher.getIV()));
+ CipherInputStream cis = new CipherInputStream(iFile, cipher);
+ Log.d(Config.LOGTAG, "Decryption with: " + cis.toString());
+ int b;
+ byte[] d = new byte[8];
+ while ((b = cis.read(d)) != -1) {
+ oFile.write(d, 0, b);
+ }
+ oFile.flush();
+ oFile.close();
+ cis.close();
+ }
+}
diff --git a/src/main/res/layout/backup_password.xml b/src/main/res/layout/backup_password.xml
new file mode 100644
index 000000000..be664be86
--- /dev/null
+++ b/src/main/res/layout/backup_password.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml
index 877e2dcb1..1027794c6 100644
--- a/src/main/res/values-de/strings.xml
+++ b/src/main/res/values-de/strings.xml
@@ -172,6 +172,9 @@
Sperre Jabber-ID
benutzer@domain.de
Passwort
+ Falsches Passwort, erneut versuchen
+ Passwort bestätigen
+ Passwörter stimmen nicht überein
Ungültige Jabber-ID
Zu wenig Speicher vorhanden. Das Bild ist zu groß
%s zum Telefonbuch hinzufügen
@@ -531,6 +534,12 @@
Gruppenchat erstellen
Mitglieder wählen
Erstelle Gruppenchat…
+ Es wurde ein Backup gefunden, welches importiert werden kann.\nDein Messenger startet während des Importvorgangs neu. Soll das Backup importiert werden?
+ Backup importieren
+ Backup wird importiert, dies wird eine Weile dauern.
+ Import abgebrochen
+ Backup import ist fehlgeschlagen und nicht möglich.
+ Bitte gib das Passwort ein, um das Backup zu importieren.
Erneut einladen
Deaktivieren
kurz
diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml
index 6be013461..ccb11bcdd 100644
--- a/src/main/res/values-es/strings.xml
+++ b/src/main/res/values-es/strings.xml
@@ -172,6 +172,9 @@
Bloquear Identificador Jabber
usuario@ejemplo.com
Contraseña
+ Contraseña incorrecta, inténtalo de nuevo
+ Confirmar contraseña
+ Las contraseñas no coinciden
El identificador no es un identificador Jabber válido
Sin memoria. La imagen es demasiado grande
¿Quieres añadir a %s a tus contactos?
@@ -531,6 +534,11 @@
Crear Conversación en Grupo
Elige a los participantes
Creando conversación en grupo...
+ Hay una copia de seguridad en tu dispositivo que se puede importar.\nTu aplicación se reiniciará durante el proceso. ¿Quieres importar la copia de seguridad?
+ Importar copia de seguridad
+ Se importará la copia de seguridad, esto puede llevar un tiempo.
+ Importación cancelada
+ La importación de la base de datos falló, una importación no es posible
Invitar de nuevo
Deshabilitar
Corto
diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml
index 9a992e02f..3cfa9c2c4 100644
--- a/src/main/res/values-fr/strings.xml
+++ b/src/main/res/values-fr/strings.xml
@@ -172,6 +172,7 @@
Bloquer l\'ID Jabber
nom@exemple.com
Mot de passe
+ Les deux mots de passe ne correspondent pas.
Cet identifiant n\'est pas valide
Plus de mémoire disponible. L\'image est trop volumineuse.
Voulez-vous ajouter %s à votre carnet d\'adresses ?
diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml
index 6db275cd3..2380e0131 100644
--- a/src/main/res/values-id/strings.xml
+++ b/src/main/res/values-id/strings.xml
@@ -139,6 +139,9 @@
Jabber ID
username@example.com
Password
+ Kata sandi salah, coba lagi
+ Konfirmasi sandi
+ Kata sandi tidak cocok
Jabber ID tidak valid
Memori habis. Gambar terlalu besar
Info Server
@@ -357,6 +360,10 @@
Password aman berhasil dibuat
Registrasi gagal: Coba lagi nanti
Undang user
+ Impor cadangan
+ Cadangan akan diimpor, ini mungkin memakan waktu sebentar.
+ Impor dibatalkan
+ Gagal memasukkan penyimpanan data, masukan tidak mungkin dilakukan
Undang lagi
Pendek
Sedang
diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml
index e2f734154..c2dc326fb 100644
--- a/src/main/res/values-ru/strings.xml
+++ b/src/main/res/values-ru/strings.xml
@@ -171,6 +171,7 @@
Заблокировать Jabber ID
username@example.com
Пароль
+ Пароли не совпадают
Недопустимый Jabber ID
Недостаточно памяти. Изображение слишком большое
Вы хотите добавить %s в вашу адресную книгу?
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 2a4b89561..8d30182d0 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -175,6 +175,9 @@
Block Jabber ID
username@example.com
Password
+ Wrong password, try again
+ Confirm password
+ Passwords do not match
This is not a valid Jabber ID
Out of memory. Image is too large
Do you want to add %s to your address book?
@@ -538,6 +541,18 @@
Create Group Chat
Choose participants
Creating group chat…
+ There is a backup on your device which can be imported.\nYour Messenger will be restarted during backup process. Shall the backup be imported?
+ Import backup
+ Backup will be imported, this may take awhile.
+ Import canceled
+ Database import failed, an import is not possible
+ Backups
+ Creating backup
+ Backup
+ Backup password
+ Create backup
+ Export backup with set password now
+ Please enter your password for your backup.
Invite again
Disable
Short
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index a3abe5969..98fc1098c 100644
--- a/src/main/res/xml/preferences.xml
+++ b/src/main/res/xml/preferences.xml
@@ -324,6 +324,18 @@
android:key="enable_foreground_service"
android:summary="@string/pref_keep_foreground_service_summary"
android:title="@string/pref_keep_foreground_service" />
+
+
+
+