From 5f758208c88521d9a7b153ef388a8967811ac9e9 Mon Sep 17 00:00:00 2001 From: yausername Date: Sat, 29 Feb 2020 04:54:22 +0530 Subject: [PATCH 1/4] added autobackup setting --- app/build.gradle | 5 + .../newpipe/database/BackupRestoreHelper.java | 192 +++++++++++++ .../newpipe/settings/AutoBackupWorker.java | 34 +++ .../settings/BackupSettingsFragment.java | 257 ++++++++++++++++++ .../settings/ContentSettingsFragment.java | 225 --------------- .../main/res/drawable/ic_sync_black_24dp.xml | 9 + .../main/res/drawable/ic_sync_white_24dp.xml | 9 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 16 ++ app/src/main/res/values/strings.xml | 5 + app/src/main/res/values/styles.xml | 2 + .../main/res/xml/backup_restore_settings.xml | 39 +++ app/src/main/res/xml/content_settings.xml | 12 - app/src/main/res/xml/main_settings.xml | 6 + 14 files changed, 575 insertions(+), 237 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java create mode 100644 app/src/main/res/drawable/ic_sync_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_sync_white_24dp.xml create mode 100644 app/src/main/res/xml/backup_restore_settings.xml diff --git a/app/build.gradle b/app/build.gradle index 2a7e039b302..9fe1716a71c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,6 +54,7 @@ ext { icepickLibVersion = '3.2.0' stethoLibVersion = '1.5.0' markwonVersion = '4.2.1' + work_version = '2.3.2' } dependencies { @@ -112,4 +113,8 @@ dependencies { implementation "io.noties.markwon:core:${markwonVersion}" implementation "io.noties.markwon:linkify:${markwonVersion}" + + implementation "androidx.work:work-runtime:${work_version}" + implementation "androidx.work:work-rxjava2:${work_version}" + } diff --git a/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java b/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java new file mode 100644 index 00000000000..4718ac31d36 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java @@ -0,0 +1,192 @@ +package org.schabi.newpipe.database; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import androidx.annotation.MainThread; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.ZipHelper; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Map; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class BackupRestoreHelper { + + private File databasesDir; + private File newpipe_db; + private File newpipe_db_journal; + private File newpipe_db_shm; + private File newpipe_db_wal; + private File newpipe_settings; + + private Context ctx; + + public BackupRestoreHelper(Context ctx) { + this.ctx = ctx; + String homeDir = ctx.getApplicationInfo().dataDir; + databasesDir = new File(homeDir + "/databases"); + newpipe_db = new File(homeDir + "/databases/newpipe.db"); + newpipe_db_journal = new File(homeDir + "/databases/newpipe.db-journal"); + newpipe_db_shm = new File(homeDir + "/databases/newpipe.db-shm"); + newpipe_db_wal = new File(homeDir + "/databases/newpipe.db-wal"); + + newpipe_settings = new File(homeDir + "/databases/newpipe.settings"); + newpipe_settings.delete(); + } + + public String getAutoBackupPath(){ + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx); + String autoBackupPath = sharedPreferences.getString(ctx.getString(R.string.backup_path_key), null); + if(null == autoBackupPath){ + autoBackupPath = new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS + File.separator + "NewPipeAutoBackup").getAbsolutePath(); + } + return autoBackupPath; + } + + public void exportDatabase(String path) throws Exception { + ZipOutputStream outZip = new ZipOutputStream( + new BufferedOutputStream( + new FileOutputStream(path))); + ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db"); + + saveSharedPreferencesToFile(newpipe_settings); + ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "newpipe.settings"); + + outZip.close(); + } + + private void saveSharedPreferencesToFile(File dst) { + ObjectOutputStream output = null; + try { + output = new ObjectOutputStream(new FileOutputStream(dst)); + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx); + output.writeObject(pref.getAll()); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (output != null) { + output.flush(); + output.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + + @MainThread + public void importDatabase(String filePath) throws Exception { + // check if file is supported + ZipFile zipFile = null; + try { + zipFile = new ZipFile(filePath); + } catch (IOException ioe) { + Toast.makeText(ctx, R.string.no_valid_zip_file, Toast.LENGTH_SHORT) + .show(); + return; + } finally { + try { + zipFile.close(); + } catch (Exception ignored) { + } + } + + if (!databasesDir.exists() && !databasesDir.mkdir()) { + throw new Exception("Could not create databases dir"); + } + + final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, + newpipe_db.getPath(), "newpipe.db"); + + if (isDbFileExtracted) { + newpipe_db_journal.delete(); + newpipe_db_wal.delete(); + newpipe_db_shm.delete(); + + } else { + + Toast.makeText(ctx, R.string.could_not_import_all_files, Toast.LENGTH_LONG) + .show(); + } + + //If settings file exist, ask if it should be imported. + if (ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) { + AlertDialog.Builder alert = new AlertDialog.Builder(ctx); + alert.setTitle(R.string.import_settings); + + alert.setNegativeButton(android.R.string.no, (dialog, which) -> { + dialog.dismiss(); + // restart app to properly load db + System.exit(0); + }); + alert.setPositiveButton(ctx.getString(R.string.finish), (dialog, which) -> { + dialog.dismiss(); + loadSharedPreferences(newpipe_settings); + // restart app to properly load db + System.exit(0); + }); + alert.show(); + } else { + // restart app to properly load db + System.exit(0); + } + } + + private void loadSharedPreferences(File src) { + ObjectInputStream input = null; + try { + input = new ObjectInputStream(new FileInputStream(src)); + SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(ctx).edit(); + prefEdit.clear(); + Map entries = (Map) input.readObject(); + for (Map.Entry entry : entries.entrySet()) { + Object v = entry.getValue(); + String key = entry.getKey(); + + if (v instanceof Boolean) + prefEdit.putBoolean(key, (Boolean) v); + else if (v instanceof Float) + prefEdit.putFloat(key, (Float) v); + else if (v instanceof Integer) + prefEdit.putInt(key, (Integer) v); + else if (v instanceof Long) + prefEdit.putLong(key, (Long) v); + else if (v instanceof String) + prefEdit.putString(key, ((String) v)); + } + prefEdit.commit(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } finally { + try { + if (input != null) { + input.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java b/app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java new file mode 100644 index 00000000000..27c9cc46db6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java @@ -0,0 +1,34 @@ +package org.schabi.newpipe.settings; + +import android.content.Context; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import org.schabi.newpipe.database.BackupRestoreHelper; + +import java.io.File; + +public class AutoBackupWorker extends Worker { + + public AutoBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + try { + BackupRestoreHelper backupRestoreHelper = new BackupRestoreHelper(getApplicationContext()); + String autoBackupPath = backupRestoreHelper.getAutoBackupPath(); + new File(autoBackupPath).mkdirs(); + String path = autoBackupPath + File.separator + "NewPipeData-" + Build.MODEL + ".zip"; + backupRestoreHelper.exportDatabase(path); + } catch (Exception e) { + return Result.failure(); + } + return Result.success(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java new file mode 100644 index 00000000000..8a55a06c732 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java @@ -0,0 +1,257 @@ +package org.schabi.newpipe.settings; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; + +import com.nononsenseapps.filepicker.Utils; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.BackupRestoreHelper; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.FilePickerActivityHelper; +import org.schabi.newpipe.util.PermissionHelper; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + +public class BackupSettingsFragment extends BasePreferenceFragment { + + private static final int REQUEST_IMPORT_PATH = 8945; + private static final int REQUEST_EXPORT_PATH = 30945; + private static final int REQUEST_AUTO_BACKUP_PATH = 40945; + + private static final int BACKUP_STORAGE_PERMISSION_REQUEST_CODE = 44543; + + public static final String TAG_AUTO_BACKUP_WORK = "TAG_AUTO_BACKUP_WORK"; + + private Context ctx; + private BackupRestoreHelper backupRestoreHelper; + + private String BACKUP_PATH_PREFERENCE_KEY; + private Preference autoBackupPathPreference; + private SwitchPreference autoBackupSwitchPreference; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + private void showMessageDialog(@StringRes int title, @StringRes int message) { + AlertDialog.Builder msg = new AlertDialog.Builder(ctx); + msg.setTitle(title); + msg.setMessage(message); + msg.setPositiveButton(getString(R.string.finish), null); + msg.show(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + + backupRestoreHelper = new BackupRestoreHelper(ctx); + + BACKUP_PATH_PREFERENCE_KEY = getString(R.string.backup_path_key); + + addPreferencesFromResource(R.xml.backup_restore_settings); + + Preference importDataPreference = findPreference(getString(R.string.import_data)); + importDataPreference.setOnPreferenceClickListener((Preference p) -> { + Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); + startActivityForResult(i, REQUEST_IMPORT_PATH); + return true; + }); + + Preference exportDataPreference = findPreference(getString(R.string.export_data)); + exportDataPreference.setOnPreferenceClickListener((Preference p) -> { + Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); + startActivityForResult(i, REQUEST_EXPORT_PATH); + return true; + }); + + autoBackupPathPreference = findPreference(BACKUP_PATH_PREFERENCE_KEY); + autoBackupPathPreference.setOnPreferenceClickListener(preference -> { + Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); + startActivityForResult(i, REQUEST_AUTO_BACKUP_PATH); + return true; + }); + autoBackupPathPreference.setSummary(backupRestoreHelper.getAutoBackupPath()); + + + autoBackupSwitchPreference = (SwitchPreference) findPreference(getString(R.string.scheduled_backups_key)); + autoBackupSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if((Boolean) newValue) PermissionHelper.checkStoragePermissions(getActivity(), BACKUP_STORAGE_PERMISSION_REQUEST_CODE); + return true; + }); + Boolean autoBackup = defaultPreferences.getBoolean(getString(R.string.scheduled_backups_key), false); + if(autoBackup) PermissionHelper.checkStoragePermissions(getActivity(), BACKUP_STORAGE_PERMISSION_REQUEST_CODE); + + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + ctx = context; + } + + @Override + public void onDetach() { + super.onDetach(); + ctx = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) { + String rawUri = defaultPreferences.getString(prefKey, null); + if (rawUri == null || rawUri.isEmpty()) { + target.setSummary(getString(defaultString)); + return; + } + + if (rawUri.charAt(0) == File.separatorChar) { + target.setSummary(rawUri); + return; + } + if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) { + target.setSummary(new File(URI.create(rawUri)).getPath()); + return; + } + + try { + rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + // nothing to do + } + + target.setSummary(rawUri); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @NonNull Intent data) { + assureCorrectAppLanguage(getContext()); + super.onActivityResult(requestCode, resultCode, data); + + if (resultCode != Activity.RESULT_OK) return; + + if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) && data.getData() != null) { + String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); + if (requestCode == REQUEST_EXPORT_PATH) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip"); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(R.string.override_current_data) + .setPositiveButton(getString(R.string.finish), + (DialogInterface d, int id) -> importDatabase(path)) + .setNegativeButton(android.R.string.cancel, + (DialogInterface d, int id) -> d.cancel()); + builder.create().show(); + } + } else if (requestCode == REQUEST_AUTO_BACKUP_PATH){ + Uri uri = data.getData(); + if (uri == null) { + showMessageDialog(R.string.general_error, R.string.invalid_directory); + return; + } + + File target = Utils.getFileForUri(uri); + if (!target.canWrite()) { + showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); + return; + } + uri = Uri.fromFile(target); + + defaultPreferences.edit().putString(BACKUP_PATH_PREFERENCE_KEY, uri.toString()).apply(); + showPathInSummary(BACKUP_PATH_PREFERENCE_KEY, R.string.download_path_summary, autoBackupPathPreference); + } else{ + return; + } + } + + private void exportDatabase(String path) { + try { + backupRestoreHelper.exportDatabase(path); + Toast.makeText(ctx, R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + onError(e); + } + } + + private void importDatabase(String filePath) { + try { + backupRestoreHelper.importDatabase(filePath); + } catch (Exception e) { + onError(e); + } + } + + private void scheduleWork(String tag) { + Boolean autoBackup = defaultPreferences.getBoolean(getString(R.string.scheduled_backups_key), false); + if(autoBackup){ + Integer interval = Integer.valueOf(defaultPreferences.getString(getString(R.string.backup_frequency_key), "24")); + PeriodicWorkRequest.Builder autoBackupRequestBuilder = + new PeriodicWorkRequest.Builder(AutoBackupWorker.class, interval, TimeUnit.HOURS); + PeriodicWorkRequest request = autoBackupRequestBuilder.build(); + WorkManager.getInstance(ctx).enqueueUniquePeriodicWork(tag, ExistingPeriodicWorkPolicy.REPLACE , request); + }else{ + WorkManager.getInstance(ctx).cancelUniqueWork(tag); + } + } + + @Override + public void onPause() { + scheduleWork(TAG_AUTO_BACKUP_WORK); + super.onPause(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Error + //////////////////////////////////////////////////////////////////////////*/ + + protected void onError(Throwable e) { + final Activity activity = getActivity(); + ErrorActivity.reportError(activity, e, + activity.getClass(), + null, + ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, + "none", "", R.string.app_ui_crash)); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 0be72d0eb4c..62d12c99abd 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -1,10 +1,7 @@ package org.schabi.newpipe.settings; import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; @@ -14,48 +11,19 @@ import androidx.annotation.Nullable; import androidx.preference.Preference; -import com.nononsenseapps.filepicker.Utils; import com.nostra13.universalimageloader.core.ImageLoader; -import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.FilePickerActivityHelper; -import org.schabi.newpipe.util.ZipHelper; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.Map; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class ContentSettingsFragment extends BasePreferenceFragment { - private static final int REQUEST_IMPORT_PATH = 8945; - private static final int REQUEST_EXPORT_PATH = 30945; - - private File databasesDir; - private File newpipe_db; - private File newpipe_db_journal; - private File newpipe_db_shm; - private File newpipe_db_wal; - private File newpipe_settings; - private String thumbnailLoadToggleKey; private Localization initialSelectedLocalization; @@ -90,37 +58,7 @@ public boolean onPreferenceTreeClick(Preference preference) { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - String homeDir = getActivity().getApplicationInfo().dataDir; - databasesDir = new File(homeDir + "/databases"); - newpipe_db = new File(homeDir + "/databases/newpipe.db"); - newpipe_db_journal = new File(homeDir + "/databases/newpipe.db-journal"); - newpipe_db_shm = new File(homeDir + "/databases/newpipe.db-shm"); - newpipe_db_wal = new File(homeDir + "/databases/newpipe.db-wal"); - - newpipe_settings = new File(homeDir + "/databases/newpipe.settings"); - newpipe_settings.delete(); - addPreferencesFromResource(R.xml.content_settings); - - Preference importDataPreference = findPreference(getString(R.string.import_data)); - importDataPreference.setOnPreferenceClickListener((Preference p) -> { - Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); - startActivityForResult(i, REQUEST_IMPORT_PATH); - return true; - }); - - Preference exportDataPreference = findPreference(getString(R.string.export_data)); - exportDataPreference.setOnPreferenceClickListener((Preference p) -> { - Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); - startActivityForResult(i, REQUEST_EXPORT_PATH); - return true; - }); } @Override @@ -148,169 +86,6 @@ public void onActivityResult(int requestCode, int resultCode, @NonNull Intent da if (DEBUG) { Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]"); } - - if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) - && resultCode == Activity.RESULT_OK && data.getData() != null) { - String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); - if (requestCode == REQUEST_EXPORT_PATH) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip"); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(R.string.override_current_data) - .setPositiveButton(getString(R.string.finish), - (DialogInterface d, int id) -> importDatabase(path)) - .setNegativeButton(android.R.string.cancel, - (DialogInterface d, int id) -> d.cancel()); - builder.create().show(); - } - } - } - - private void exportDatabase(String path) { - try { - //checkpoint before export - NewPipeDatabase.checkpoint(); - - ZipOutputStream outZip = new ZipOutputStream( - new BufferedOutputStream( - new FileOutputStream(path))); - ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db"); - - saveSharedPreferencesToFile(newpipe_settings); - ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "newpipe.settings"); - - outZip.close(); - - Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT) - .show(); - } catch (Exception e) { - onError(e); - } - } - - private void saveSharedPreferencesToFile(File dst) { - ObjectOutputStream output = null; - try { - output = new ObjectOutputStream(new FileOutputStream(dst)); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext()); - output.writeObject(pref.getAll()); - - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (output != null) { - output.flush(); - output.close(); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } - } - - private void importDatabase(String filePath) { - // check if file is supported - ZipFile zipFile = null; - try { - zipFile = new ZipFile(filePath); - } catch (IOException ioe) { - Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) - .show(); - return; - } finally { - try { - zipFile.close(); - } catch (Exception ignored){} - } - - try { - if (!databasesDir.exists() && !databasesDir.mkdir()) { - throw new Exception("Could not create databases dir"); - } - - final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, - newpipe_db.getPath(), "newpipe.db"); - - if (isDbFileExtracted) { - newpipe_db_journal.delete(); - newpipe_db_wal.delete(); - newpipe_db_shm.delete(); - - } else { - - Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) - .show(); - } - - //If settings file exist, ask if it should be imported. - if (ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) { - AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); - alert.setTitle(R.string.import_settings); - - alert.setNegativeButton(android.R.string.no, (dialog, which) -> { - dialog.dismiss(); - // restart app to properly load db - System.exit(0); - }); - alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { - dialog.dismiss(); - loadSharedPreferences(newpipe_settings); - // restart app to properly load db - System.exit(0); - }); - alert.show(); - } else { - // restart app to properly load db - System.exit(0); - } - - } catch (Exception e) { - onError(e); - } - } - - private void loadSharedPreferences(File src) { - ObjectInputStream input = null; - try { - input = new ObjectInputStream(new FileInputStream(src)); - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit(); - prefEdit.clear(); - Map entries = (Map) input.readObject(); - for (Map.Entry entry : entries.entrySet()) { - Object v = entry.getValue(); - String key = entry.getKey(); - - if (v instanceof Boolean) - prefEdit.putBoolean(key, (Boolean) v); - else if (v instanceof Float) - prefEdit.putFloat(key, (Float) v); - else if (v instanceof Integer) - prefEdit.putInt(key, (Integer) v); - else if (v instanceof Long) - prefEdit.putLong(key, (Long) v); - else if (v instanceof String) - prefEdit.putString(key, ((String) v)); - } - prefEdit.commit(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } finally { - try { - if (input != null) { - input.close(); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/res/drawable/ic_sync_black_24dp.xml b/app/src/main/res/drawable/ic_sync_black_24dp.xml new file mode 100644 index 00000000000..3f88dd4fcd7 --- /dev/null +++ b/app/src/main/res/drawable/ic_sync_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sync_white_24dp.xml b/app/src/main/res/drawable/ic_sync_white_24dp.xml new file mode 100644 index 00000000000..d35ff11ec10 --- /dev/null +++ b/app/src/main/res/drawable/ic_sync_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 88925a59855..da1397b576c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -45,6 +45,7 @@ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index fe096c9fddd..e18890f63ec 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -193,6 +193,22 @@ downloads_storage_ask storage_use_saf + scheduled_backups_key + backup_path_key + backup_file_name_key + backup_frequency_key + + + 24 + 48 + 168 + + + 24 hours + 72 hours + 168 hours + + file_rename_charset file_replacement_character diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c9ede7b6bdf..9b44fee2b90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,6 +130,7 @@ Other Debug Updates + Backup & Restore Playing in background Playing in popup mode Queued on background player @@ -177,6 +178,10 @@ Switch to Background Switch to Popup Switch to Main + Auto backup + Enable scheduled backups + Backup location + Backup frequency Import database Export database Overrides your current history and subscriptions diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 633003092f9..3645561a51c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -60,6 +60,7 @@ @drawable/ic_grid_black_24dp @drawable/ic_delete_black_24dp @drawable/ic_settings_update_black + @drawable/ic_sync_black_24dp @drawable/ic_done_black_24dp @color/light_separator_color @@ -130,6 +131,7 @@ @drawable/ic_delete_white_24dp @drawable/ic_pause_white_24dp @drawable/ic_settings_update_white + @drawable/ic_sync_white_24dp @drawable/ic_done_white_24dp @color/dark_separator_color diff --git a/app/src/main/res/xml/backup_restore_settings.xml b/app/src/main/res/xml/backup_restore_settings.xml new file mode 100644 index 00000000000..bf00bba616f --- /dev/null +++ b/app/src/main/res/xml/backup_restore_settings.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index fd87de9efb5..9adb26d8a12 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -77,16 +77,4 @@ android:key="@string/show_comments_key" android:title="@string/show_comments_title" android:summary="@string/show_comments_summary"/> - - - - diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml index cd9dc3278ba..9b51ed8c220 100644 --- a/app/src/main/res/xml/main_settings.xml +++ b/app/src/main/res/xml/main_settings.xml @@ -35,6 +35,12 @@ android:icon="?attr/language" android:title="@string/content"/> + + Date: Wed, 4 Mar 2020 04:01:36 +0530 Subject: [PATCH 2/4] added password for encrypting backups --- app/build.gradle | 5 + .../newpipe/database/BackupRestoreHelper.java | 47 +++---- .../newpipe/settings/AutoBackupWorker.java | 12 +- .../settings/BackupSettingsFragment.java | 117 +++++++++++++++--- .../org/schabi/newpipe/util/ZipHelper.java | 89 ++++++------- app/src/main/res/layout/dialog_password.xml | 23 ++++ app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 6 + .../main/res/xml/backup_restore_settings.xml | 9 ++ 9 files changed, 208 insertions(+), 101 deletions(-) create mode 100644 app/src/main/res/layout/dialog_password.xml diff --git a/app/build.gradle b/app/build.gradle index 9fe1716a71c..09758d1ed33 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,6 +55,8 @@ ext { stethoLibVersion = '1.5.0' markwonVersion = '4.2.1' work_version = '2.3.2' + zip4j_version = '2.3.2' + preference_version = '1.1.0' } dependencies { @@ -117,4 +119,7 @@ dependencies { implementation "androidx.work:work-runtime:${work_version}" implementation "androidx.work:work-rxjava2:${work_version}" + implementation "net.lingala.zip4j:zip4j:${zip4j_version}" + + implementation "androidx.preference:preference:${preference_version}" } diff --git a/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java b/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java index 4718ac31d36..b371a7412d7 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java +++ b/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java @@ -12,7 +12,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.util.ZipHelper; -import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -21,8 +20,6 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Map; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; public class BackupRestoreHelper { @@ -57,25 +54,23 @@ public String getAutoBackupPath(){ return autoBackupPath; } - public void exportDatabase(String path) throws Exception { - ZipOutputStream outZip = new ZipOutputStream( - new BufferedOutputStream( - new FileOutputStream(path))); - ZipHelper.addFileToZip(outZip, newpipe_db.getPath(), "newpipe.db"); - + public void exportDatabase(String path, char[] password) throws Exception { + ZipHelper.addFileToZip(path, newpipe_db.getPath(), "newpipe.db", password); saveSharedPreferencesToFile(newpipe_settings); - ZipHelper.addFileToZip(outZip, newpipe_settings.getPath(), "newpipe.settings"); - - outZip.close(); + ZipHelper.addFileToZip(path, newpipe_settings.getPath(), "newpipe.settings", password); } private void saveSharedPreferencesToFile(File dst) { ObjectOutputStream output = null; + SharedPreferences pref = null; + String password = null; try { output = new ObjectOutputStream(new FileOutputStream(dst)); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx); + pref = PreferenceManager.getDefaultSharedPreferences(ctx); + // remove password from shared prefs before taking backup + password = pref.getString(ctx.getString(R.string.backup_password_key), null); + pref.edit().remove(ctx.getString(R.string.backup_password_key)).apply(); output.writeObject(pref.getAll()); - } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { @@ -88,33 +83,21 @@ private void saveSharedPreferencesToFile(File dst) { } } catch (IOException ex) { ex.printStackTrace(); + } finally { + // add password again after taking backup + if(pref != null) pref.edit().putString(ctx.getString(R.string.backup_password_key), password).apply(); } } } @MainThread - public void importDatabase(String filePath) throws Exception { - // check if file is supported - ZipFile zipFile = null; - try { - zipFile = new ZipFile(filePath); - } catch (IOException ioe) { - Toast.makeText(ctx, R.string.no_valid_zip_file, Toast.LENGTH_SHORT) - .show(); - return; - } finally { - try { - zipFile.close(); - } catch (Exception ignored) { - } - } + public void importDatabase(String filePath, char[] password) throws Exception { if (!databasesDir.exists() && !databasesDir.mkdir()) { throw new Exception("Could not create databases dir"); } - final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, - newpipe_db.getPath(), "newpipe.db"); + final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, "newpipe.db", databasesDir.getPath(), password); if (isDbFileExtracted) { newpipe_db_journal.delete(); @@ -128,7 +111,7 @@ public void importDatabase(String filePath) throws Exception { } //If settings file exist, ask if it should be imported. - if (ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) { + if (ZipHelper.extractFileFromZip(filePath, "newpipe.settings", databasesDir.getPath(), password)) { AlertDialog.Builder alert = new AlertDialog.Builder(ctx); alert.setTitle(R.string.import_settings); diff --git a/app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java b/app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java index 27c9cc46db6..a6d1ba425bd 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java @@ -2,11 +2,14 @@ import android.content.Context; import android.os.Build; +import android.text.TextUtils; import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; import androidx.work.Worker; import androidx.work.WorkerParameters; +import org.schabi.newpipe.R; import org.schabi.newpipe.database.BackupRestoreHelper; import java.io.File; @@ -20,12 +23,15 @@ public AutoBackupWorker(@NonNull Context context, @NonNull WorkerParameters work @NonNull @Override public Result doWork() { + Context ctx = getApplicationContext(); + BackupRestoreHelper backupRestoreHelper = new BackupRestoreHelper(ctx); + String autoBackupPath = backupRestoreHelper.getAutoBackupPath(); try { - BackupRestoreHelper backupRestoreHelper = new BackupRestoreHelper(getApplicationContext()); - String autoBackupPath = backupRestoreHelper.getAutoBackupPath(); new File(autoBackupPath).mkdirs(); String path = autoBackupPath + File.separator + "NewPipeData-" + Build.MODEL + ".zip"; - backupRestoreHelper.exportDatabase(path); + String password = PreferenceManager.getDefaultSharedPreferences(ctx).getString(ctx.getString(R.string.backup_password_key), null); + if(TextUtils.isEmpty(password)) return Result.failure(); + backupRestoreHelper.exportDatabase(path, password.toCharArray()); } catch (Exception e) { return Result.failure(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java index 8a55a06c732..db07fd6c58f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java @@ -8,6 +8,9 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; @@ -19,8 +22,12 @@ import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; +import com.google.android.material.textfield.TextInputEditText; import com.nononsenseapps.filepicker.Utils; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.exception.ZipException; + import org.schabi.newpipe.R; import org.schabi.newpipe.database.BackupRestoreHelper; import org.schabi.newpipe.report.ErrorActivity; @@ -34,6 +41,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -110,8 +118,7 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { }); autoBackupPathPreference.setSummary(backupRestoreHelper.getAutoBackupPath()); - - autoBackupSwitchPreference = (SwitchPreference) findPreference(getString(R.string.scheduled_backups_key)); + autoBackupSwitchPreference = findPreference(getString(R.string.scheduled_backups_key)); autoBackupSwitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { if((Boolean) newValue) PermissionHelper.checkStoragePermissions(getActivity(), BACKUP_STORAGE_PERMISSION_REQUEST_CODE); return true; @@ -206,17 +213,88 @@ public void onActivityResult(int requestCode, int resultCode, @NonNull Intent da } private void exportDatabase(String path) { - try { - backupRestoreHelper.exportDatabase(path); - Toast.makeText(ctx, R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - onError(e); - } + + AlertDialog.Builder alert = new AlertDialog.Builder(ctx); + LayoutInflater inflater = LayoutInflater.from(ctx); + View view = inflater.inflate(R.layout.dialog_password, null); + TextInputEditText editText = view.findViewById(android.R.id.edit); + alert.setView(view); + alert.setTitle(R.string.auto_backup_password_title); + alert.setMessage(R.string.backup_password_message); + + alert.setNegativeButton(R.string.backup_no_password, (dialog, which) -> { + dialog.dismiss(); + try { + backupRestoreHelper.exportDatabase(path, null); + Toast.makeText(ctx, R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + onError(e); + } + }); + alert.setPositiveButton(R.string.finish, (dialog, which) -> { + dialog.dismiss(); + char[] password = getPassword(editText); + try { + backupRestoreHelper.exportDatabase(path, password); + Toast.makeText(ctx, R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + onError(e); + } finally { + clearPassword(password); + } + }); + + alert.show(); + } + + private char[] getPassword(TextInputEditText editText) { + int length = editText.length(); + char[] password = new char[length]; + editText.getText().getChars(0, length, password, 0); + return password; + } + + private void clearPassword(char[] password){ + Arrays.fill(password,'0'); } private void importDatabase(String filePath) { + + ZipFile zipFile = new ZipFile(filePath); + if(!zipFile.isValidZipFile()){ + Toast.makeText(ctx, R.string.no_valid_zip_file, Toast.LENGTH_SHORT).show(); + return; + } + try { - backupRestoreHelper.importDatabase(filePath); + if(zipFile.isEncrypted()){ + AlertDialog.Builder alert = new AlertDialog.Builder(ctx); + LayoutInflater inflater = LayoutInflater.from(ctx); + View view = inflater.inflate(R.layout.dialog_password, null); + TextInputEditText editText = view.findViewById(android.R.id.edit); + alert.setView(view); + + alert.setTitle(R.string.auto_backup_password_title); + + alert.setNegativeButton(android.R.string.no, (dialog, which) -> { + dialog.dismiss(); + }); + alert.setPositiveButton(R.string.finish, (dialog, which) -> { + dialog.dismiss(); + char[] password = getPassword(editText); + try { + backupRestoreHelper.importDatabase(filePath, password); + } catch (Exception e) { + onError(e); + } finally { + clearPassword(password); + } + }); + + alert.show(); + }else{ + backupRestoreHelper.importDatabase(filePath, null); + } } catch (Exception e) { onError(e); } @@ -225,9 +303,13 @@ private void importDatabase(String filePath) { private void scheduleWork(String tag) { Boolean autoBackup = defaultPreferences.getBoolean(getString(R.string.scheduled_backups_key), false); if(autoBackup){ + if(TextUtils.isEmpty(defaultPreferences.getString(ctx.getString(R.string.backup_password_key), null))){ + Toast.makeText(ctx, R.string.auto_backup_password_mandatory, Toast.LENGTH_LONG).show(); + } Integer interval = Integer.valueOf(defaultPreferences.getString(getString(R.string.backup_frequency_key), "24")); PeriodicWorkRequest.Builder autoBackupRequestBuilder = new PeriodicWorkRequest.Builder(AutoBackupWorker.class, interval, TimeUnit.HOURS); + autoBackupRequestBuilder.setInitialDelay(15, TimeUnit.MINUTES); PeriodicWorkRequest request = autoBackupRequestBuilder.build(); WorkManager.getInstance(ctx).enqueueUniquePeriodicWork(tag, ExistingPeriodicWorkPolicy.REPLACE , request); }else{ @@ -246,12 +328,17 @@ public void onPause() { //////////////////////////////////////////////////////////////////////////*/ protected void onError(Throwable e) { - final Activity activity = getActivity(); - ErrorActivity.reportError(activity, e, - activity.getClass(), - null, - ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, - "none", "", R.string.app_ui_crash)); + + if(e instanceof ZipException && ((ZipException)e).getType() == ZipException.Type.WRONG_PASSWORD){ + Toast.makeText(ctx, R.string.no_valid_password, Toast.LENGTH_SHORT).show(); + }else{ + final Activity activity = getActivity(); + ErrorActivity.reportError(activity, e, + activity.getClass(), + null, + ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, + "none", "", R.string.app_ui_crash)); + } } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java index 3142ad8dcb6..160fc476f36 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java @@ -1,12 +1,11 @@ package org.schabi.newpipe.util; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.model.ZipParameters; +import net.lingala.zip4j.model.enums.EncryptionMethod; + +import org.schabi.newpipe.MainActivity; /** * Created by Christian Schabesberger on 28.01.18. @@ -30,69 +29,57 @@ public class ZipHelper { - private static final int BUFFER_SIZE = 2048; + private static final String TAG = "ZipHelper"; + private static final boolean DEBUG = MainActivity.DEBUG; /** * This function helps to create zip files. * Caution this will override the original file. - * @param outZip The ZipOutputStream where the data should be stored in + * @param zipPath The zip path where the data should be stored in * @param file The path of the file that should be added to zip. * @param name The path of the file inside the zip. + * @param password The password of zip file. * @throws Exception */ - public static void addFileToZip(ZipOutputStream outZip, String file, String name) throws Exception { - byte data[] = new byte[BUFFER_SIZE]; - FileInputStream fi = new FileInputStream(file); - BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE); - ZipEntry entry = new ZipEntry(name); - outZip.putNextEntry(entry); - int count; - while((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) { - outZip.write(data, 0, count); + public static void addFileToZip(String zipPath, String file, String name, char[] password) throws Exception { + ZipParameters zipParameters = new ZipParameters(); + zipParameters.setFileNameInZip(name); + ZipFile zipFile; + if(password != null){ + zipParameters.setEncryptFiles(true); + zipParameters.setEncryptionMethod(EncryptionMethod.AES); + zipFile = new ZipFile(zipPath, password); + }else{ + zipFile = new ZipFile(zipPath); } - inputStream.close(); + zipFile.addFile(file, zipParameters); } /** * This will extract data from Zipfiles. * Caution this will override the original file. - * @param file The path of the file on the disk where the data should be extracted to. + * @param zipPath The path of the zip file. * @param name The path of the file inside the zip. + * @param destDir The path of the directory on disk where the data should be extracted to. + * @param password The password of zip file. * @return will return true if the file was found within the zip file * @throws Exception */ - public static boolean extractFileFromZip(String filePath, String file, String name) throws Exception { - - ZipInputStream inZip = new ZipInputStream( - new BufferedInputStream( - new FileInputStream(filePath))); - - byte data[] = new byte[BUFFER_SIZE]; - - boolean found = false; - - ZipEntry ze; - while((ze = inZip.getNextEntry()) != null) { - if(ze.getName().equals(name)) { - found = true; - // delete old file first - File oldFile = new File(file); - if(oldFile.exists()) { - if(!oldFile.delete()) { - throw new Exception("Could not delete " + file); - } - } - - FileOutputStream outFile = new FileOutputStream(file); - int count = 0; - while((count = inZip.read(data)) != -1) { - outFile.write(data, 0, count); - } - - outFile.close(); - inZip.closeEntry(); + public static boolean extractFileFromZip(String zipPath, String name, String destDir, char[] password) throws Exception { + ZipFile zipFile; + if(password != null){ + zipFile = new ZipFile(zipPath, password); + }else{ + zipFile = new ZipFile(zipPath); + } + try { + zipFile.extractFile(name, destDir); + } catch (ZipException e) { + if(("No file found with name " + name + " in zip file").equals(e.getMessage())){ + return false; } + throw e; } - return found; + return true; } } diff --git a/app/src/main/res/layout/dialog_password.xml b/app/src/main/res/layout/dialog_password.xml new file mode 100644 index 00000000000..919bdf9211c --- /dev/null +++ b/app/src/main/res/layout/dialog_password.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index e18890f63ec..826089de2fe 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -197,6 +197,7 @@ backup_path_key backup_file_name_key backup_frequency_key + backup_password_key 24 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b44fee2b90..a6deb76d628 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -182,6 +182,10 @@ Enable scheduled backups Backup location Backup frequency + Password + Auto backup password (required) + Set password to encrypt zip + No password Import database Export database Overrides your current history and subscriptions @@ -404,6 +408,8 @@ Exported Imported No valid ZIP file + Invalid password + Auto backup will not work without a password Warning: Could not import all files. This will override your current setup. Do you want to also import settings? diff --git a/app/src/main/res/xml/backup_restore_settings.xml b/app/src/main/res/xml/backup_restore_settings.xml index bf00bba616f..f4c517725d0 100644 --- a/app/src/main/res/xml/backup_restore_settings.xml +++ b/app/src/main/res/xml/backup_restore_settings.xml @@ -25,6 +25,15 @@ android:summary="%s" android:title="@string/auto_backup_frequency_title"/> + + Date: Mon, 9 Mar 2020 23:07:08 +0530 Subject: [PATCH 3/4] use encrypted shared prefs for saving password --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 1 + .../newpipe/database/BackupRestoreHelper.java | 14 +--- .../newpipe/settings/AutoBackupWorker.java | 5 +- .../settings/BackupSettingsFragment.java | 6 +- .../EncryptedSharedPreferencesHelper.java | 40 +++++++++ .../newpipe/settings/PasswordPreference.java | 83 +++++++++++++++++++ .../res/xml/android_auto_backup_rules.xml | 4 + .../main/res/xml/backup_restore_settings.xml | 3 +- 9 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/EncryptedSharedPreferencesHelper.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/PasswordPreference.java create mode 100644 app/src/main/res/xml/android_auto_backup_rules.xml diff --git a/app/build.gradle b/app/build.gradle index 09758d1ed33..b6da0b779f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -122,4 +122,6 @@ dependencies { implementation "net.lingala.zip4j:zip4j:${zip4j_version}" implementation "androidx.preference:preference:${preference_version}" + + implementation 'com.github.yausername:EncryptedSharedPreferences:1.0.0-beta01' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7ca04eed0b3..845a6d802c7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ = Build.VERSION_CODES.M) { + prefValueEncryptionScheme = EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM; + }else{ + prefValueEncryptionScheme = EncryptedSharedPreferences.PrefValueEncryptionScheme.XCHACHA20_POLY1305; + } + return EncryptedSharedPreferences.create(FILE_NAME, MASTER_KEY_ALIAS, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, prefValueEncryptionScheme); + } catch (GeneralSecurityException | IOException e) { + if(DEBUG) Log.e(TAG, "failed to create encrypted preferences", e); + return null; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/PasswordPreference.java b/app/src/main/java/org/schabi/newpipe/settings/PasswordPreference.java new file mode 100644 index 00000000000..f1ef2fbad45 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/PasswordPreference.java @@ -0,0 +1,83 @@ +package org.schabi.newpipe.settings; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.widget.Toast; + +import androidx.preference.EditTextPreference; + +public class PasswordPreference extends EditTextPreference { + + private final SharedPreferences encryptedSharedPreferences; + + public PasswordPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + encryptedSharedPreferences = initEncryptedSharedPreferences(); + } + + public PasswordPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + encryptedSharedPreferences = initEncryptedSharedPreferences(); + } + + public PasswordPreference(Context context, AttributeSet attrs) { + super(context, attrs); + encryptedSharedPreferences = initEncryptedSharedPreferences(); + } + + public PasswordPreference(Context context) { + super(context); + encryptedSharedPreferences = initEncryptedSharedPreferences(); + } + + private SharedPreferences initEncryptedSharedPreferences(){ + return EncryptedSharedPreferencesHelper.create(getContext()); + } + + @Override + public void setText(String text) { + final boolean wasBlocking = shouldDisableDependents(); + + setPassword(text); + + final boolean isBlocking = shouldDisableDependents(); + if (isBlocking != wasBlocking) { + notifyDependencyChange(isBlocking); + } + + notifyChanged(); + } + + @Override + public String getText() { + return getPassword(); + } + + @Override + protected void onSetInitialValue(Object defaultValue) { + String password = getPassword(); + setText(password != null ? password : (String) defaultValue); + } + + private String getPassword() { + if (encryptedSharedPreferences == null){ + return null; + } + return encryptedSharedPreferences.getString(getKey(), null); + } + + private void setPassword(String password){ + if(encryptedSharedPreferences == null){ + Toast.makeText(getContext(), "Failed to save password", Toast.LENGTH_SHORT).show(); + return; + } + encryptedSharedPreferences.edit().putString(getKey(), password).apply(); + } + + @Override + public boolean shouldDisableDependents() { + return TextUtils.isEmpty(getText()) || !isEnabled(); + } +} diff --git a/app/src/main/res/xml/android_auto_backup_rules.xml b/app/src/main/res/xml/android_auto_backup_rules.xml new file mode 100644 index 00000000000..af9e1244fdf --- /dev/null +++ b/app/src/main/res/xml/android_auto_backup_rules.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_restore_settings.xml b/app/src/main/res/xml/backup_restore_settings.xml index f4c517725d0..0ea38d4fcf8 100644 --- a/app/src/main/res/xml/backup_restore_settings.xml +++ b/app/src/main/res/xml/backup_restore_settings.xml @@ -25,8 +25,9 @@ android:summary="%s" android:title="@string/auto_backup_frequency_title"/> - Date: Mon, 9 Mar 2020 23:25:09 +0530 Subject: [PATCH 4/4] checkpoint db before export --- .../org/schabi/newpipe/database/BackupRestoreHelper.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java b/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java index 064bd6d0371..e550fb7bb96 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java +++ b/app/src/main/java/org/schabi/newpipe/database/BackupRestoreHelper.java @@ -9,6 +9,7 @@ import androidx.annotation.MainThread; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.util.ZipHelper; @@ -55,6 +56,10 @@ public String getAutoBackupPath(){ } public void exportDatabase(String path, char[] password) throws Exception { + + //checkpoint before export + NewPipeDatabase.checkpoint(); + ZipHelper.addFileToZip(path, newpipe_db.getPath(), "newpipe.db", password); saveSharedPreferencesToFile(newpipe_settings); ZipHelper.addFileToZip(path, newpipe_settings.getPath(), "newpipe.settings", password);