diff --git a/app/build.gradle b/app/build.gradle index 2a7e039b302..b6da0b779f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,6 +54,9 @@ ext { icepickLibVersion = '3.2.0' stethoLibVersion = '1.5.0' markwonVersion = '4.2.1' + work_version = '2.3.2' + zip4j_version = '2.3.2' + preference_version = '1.1.0' } dependencies { @@ -112,4 +115,13 @@ 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}" + + 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 @@ { + 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 | 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..c2a1a763429 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/AutoBackupWorker.java @@ -0,0 +1,41 @@ +package org.schabi.newpipe.settings; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import org.schabi.newpipe.R; +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() { + Context ctx = getApplicationContext(); + BackupRestoreHelper backupRestoreHelper = new BackupRestoreHelper(ctx); + String autoBackupPath = backupRestoreHelper.getAutoBackupPath(); + try { + new File(autoBackupPath).mkdirs(); + String path = autoBackupPath + File.separator + "NewPipeData-" + Build.MODEL + ".zip"; + SharedPreferences encryptedSharedPreferences = EncryptedSharedPreferencesHelper.create(ctx); + String password = (encryptedSharedPreferences != null) ? encryptedSharedPreferences.getString(ctx.getString(R.string.backup_password_key), null) : null; + if(TextUtils.isEmpty(password)) return Result.failure(); + backupRestoreHelper.exportDatabase(path, password.toCharArray()); + } 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..887fbdb7e3a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupSettingsFragment.java @@ -0,0 +1,348 @@ +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.content.SharedPreferences; +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; +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.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; +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.Arrays; +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 = 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) { + + 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 { + 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); + } + } + + private void scheduleWork(String tag) { + Boolean autoBackup = defaultPreferences.getBoolean(getString(R.string.scheduled_backups_key), false); + if(autoBackup){ + SharedPreferences encryptedSharedPreferences = EncryptedSharedPreferencesHelper.create(requireContext()); + if(encryptedSharedPreferences == null || TextUtils.isEmpty(encryptedSharedPreferences.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{ + WorkManager.getInstance(ctx).cancelUniqueWork(tag); + } + } + + + @Override + public void onPause() { + scheduleWork(TAG_AUTO_BACKUP_WORK); + super.onPause(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Error + //////////////////////////////////////////////////////////////////////////*/ + + protected void onError(Throwable e) { + + 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/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/java/org/schabi/newpipe/settings/EncryptedSharedPreferencesHelper.java b/app/src/main/java/org/schabi/newpipe/settings/EncryptedSharedPreferencesHelper.java new file mode 100644 index 00000000000..fef6daa19f1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/EncryptedSharedPreferencesHelper.java @@ -0,0 +1,40 @@ +package org.schabi.newpipe.settings; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.schabi.newpipe.MainActivity; +import org.yausername.encryptedsharedpreferences.EncryptedSharedPreferences; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +public class EncryptedSharedPreferencesHelper { + + private static final String TAG = "EncSharedPrefHelper"; + private static final boolean DEBUG = MainActivity.DEBUG; + + private static final String FILE_NAME = "encryptedPrefs"; + private static final String MASTER_KEY_ALIAS = "_newpipe_security_master_key_"; + + @Nullable + public static SharedPreferences create(@NonNull Context context){ + try { + EncryptedSharedPreferences.PrefValueEncryptionScheme prefValueEncryptionScheme; + if (Build.VERSION.SDK_INT >= 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/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/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/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/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..826089de2fe 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -193,6 +193,23 @@ downloads_storage_ask storage_use_saf + scheduled_backups_key + backup_path_key + backup_file_name_key + backup_frequency_key + backup_password_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..a6deb76d628 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,14 @@ Switch to Background Switch to Popup Switch to Main + Auto backup + 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 @@ -399,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/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/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 new file mode 100644 index 00000000000..0ea38d4fcf8 --- /dev/null +++ b/app/src/main/res/xml/backup_restore_settings.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + 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"/> + +