0
0
mirror of https://github.com/TrianguloY/UrlChecker.git synced 2024-09-19 20:02:16 +02:00

backup and restore

This commit is contained in:
TrianguloY 2024-01-19 18:04:19 +01:00
parent 05091220fc
commit 52842af9d3
19 changed files with 946 additions and 13 deletions

View File

@ -116,6 +116,24 @@
</intent-filter>
</activity>
<activity
android:name=".activities.BackupActivity"
android:parentActivityName=".activities.SettingsActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:pathPattern=".*\\.ucbckp" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<service
android:name=".services.CustomTabs"
android:exported="true">

View File

@ -0,0 +1,8 @@
Hi!
Yes, the backup is just a simple zip file, and the files themselves are plain text.
You can edit it if you want, and the app will try to load it anyway, but be careful because it may become unusable.
If it does, you'll need to delete the corrupted data (for example by loading the original backup).
For more information, and to know how it is created/loaded, you can just check the source code:
https://github.com/TrianguloY/UrlChecker/blob/master/app/src/main/java/com/trianguloy/urlchecker/activities/BackupActivity.java

View File

@ -0,0 +1,472 @@
package com.trianguloy.urlchecker.activities;
import static com.trianguloy.urlchecker.utilities.methods.JavaUtils.negate;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.Switch;
import android.widget.Toast;
import com.trianguloy.urlchecker.BuildConfig;
import com.trianguloy.urlchecker.R;
import com.trianguloy.urlchecker.fragments.ResultCodeInjector;
import com.trianguloy.urlchecker.modules.companions.Hosts;
import com.trianguloy.urlchecker.modules.companions.VersionManager;
import com.trianguloy.urlchecker.modules.list.LogModule;
import com.trianguloy.urlchecker.modules.list.VirusTotalModule;
import com.trianguloy.urlchecker.utilities.AndroidSettings;
import com.trianguloy.urlchecker.utilities.generics.GenericPref;
import com.trianguloy.urlchecker.utilities.methods.AndroidUtils;
import com.trianguloy.urlchecker.utilities.methods.JavaUtils;
import com.trianguloy.urlchecker.utilities.methods.JavaUtils.Function;
import com.trianguloy.urlchecker.utilities.methods.PackageUtils;
import com.trianguloy.urlchecker.utilities.wrappers.ProgressDialog;
import com.trianguloy.urlchecker.utilities.wrappers.ZipReader;
import com.trianguloy.urlchecker.utilities.wrappers.ZipWriter;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class BackupActivity extends Activity {
private final ResultCodeInjector resultCodeInjector = new ResultCodeInjector();
private Switch chk_data;
private Switch chk_data_prefs;
private Switch chk_data_files;
private Switch chk_secrets;
private Switch chk_cache;
private Switch chk_delete;
private Switch chk_ignoreNewer;
private Button btn_backup;
private Button btn_restore;
private Button btn_delete;
private SharedPreferences prefs;
/* ------------------- activity ------------------- */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidSettings.setTheme(this, false);
AndroidSettings.setLocale(this);
setContentView(R.layout.activity_backup);
setTitle(R.string.btn_backupRestore);
AndroidUtils.configureUp(this);
prefs = GenericPref.getPrefs(this);
chk_data = findViewById(R.id.chk_data);
chk_data_prefs = findViewById(R.id.chk_data_prefs);
chk_data_files = findViewById(R.id.chk_data_files);
chk_secrets = findViewById(R.id.chk_secrets);
chk_cache = findViewById(R.id.chk_cache);
chk_delete = findViewById(R.id.chk_delete);
chk_ignoreNewer = findViewById(R.id.chk_ignoreNewer);
btn_backup = findViewById(R.id.btn_backup);
btn_restore = findViewById(R.id.btn_restore);
btn_delete = findViewById(R.id.btn_delete);
// if this app was reloaded, some settings may have changed, so reload previous one too
if (AndroidSettings.wasReloaded(this)) AndroidSettings.markForReloading(this);
// sync data switches
chk_data.setOnCheckedChangeListener((v, checked) -> {
chk_data_prefs.setChecked(checked);
chk_data_files.setChecked(checked);
});
// sync button enabled status
var chks = List.of(chk_cache, chk_secrets, chk_data_files, chk_data_prefs);
for (var chk : chks)
chk.setOnCheckedChangeListener((b, c) -> {
var enabled = false;
for (var chkk : chks) if (chkk.isChecked()) enabled = true;
btn_backup.setEnabled(enabled);
btn_restore.setEnabled(enabled);
btn_delete.setEnabled(enabled);
});
// restore advanced status
if (getIntent().getBooleanExtra(ADVANCED_EXTRA, false)) {
showAdvanced();
}
// ask to restore if a file was opened
var data = getIntent().getData();
if (data != null) {
getIntent().setData(null); // avoid restoring again after reload
askRestore(data);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_backup, menu);
// restore advanced status
if (getIntent().getBooleanExtra(ADVANCED_EXTRA, false)) {
menu.findItem(R.id.menu_advanced).setVisible(false);
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home -> {
// press the 'back' button in the action bar to go back
onBackPressed();
return true;
}
case R.id.menu_advanced -> {
// show advanced
item.setVisible(false);
getIntent().putExtra(ADVANCED_EXTRA, true);
showAdvanced();
}
}
return super.onOptionsItemSelected(item);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (!resultCodeInjector.onActivityResult(requestCode, resultCode, data))
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (!resultCodeInjector.onRequestPermissionsResult(requestCode, permissions, grantResults))
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
/* ------------------- backup ------------------- */
public void backup(View ignored) {
chooseFile(Intent.ACTION_CREATE_DOCUMENT, this::backup);
}
/**
* Creates a backup and saves it to [uri]
*/
private void backup(Uri uri) {
ProgressDialog.run(this, R.string.btn_backup, progress -> {
progress.setMax(7);
progress.setMessage("Initializing backup");
try (var zip = new ZipWriter(uri, this)) {
zip.setComment(getString(R.string.app_name) + " (" + getPackageName() + ") " + getString(R.string.btn_backup));
// version
progress.setMessage("Adding version");
progress.increaseProgress();
zip.addStringFile(FILE_VERSION, BuildConfig.VERSION_NAME);
// readme
progress.setMessage("Adding readme");
progress.increaseProgress();
try (var readme = getAssets().open("backup_readme.txt")) {
zip.addStreamFile("readme.txt", readme);
}
// rest of preferences
progress.setMessage("Adding preferences");
progress.increaseProgress();
if (chk_data_prefs.isChecked()) backupPreferencesMatching(FILE_PREFERENCES, negate(IS_PREF_SECRET), zip);
// secret preferences
progress.setMessage("Adding secrets");
progress.increaseProgress();
if (chk_secrets.isChecked()) backupPreferencesMatching(FILE_SECRETS, IS_PREF_SECRET, zip);
// rest of files
progress.setMessage("Adding files");
progress.increaseProgress();
if (chk_data_files.isChecked()) backupFilesMatching(FILES_FOLDER, negate(IS_FILE_CACHE), zip);
// cache files
progress.setMessage("Adding cache");
progress.increaseProgress();
if (chk_cache.isChecked()) backupFilesMatching(CACHE_FOLDER, IS_FILE_CACHE, zip);
runOnUiThread(() -> Toast.makeText(this, R.string.bck_backupOk, Toast.LENGTH_SHORT).show());
} catch (Exception e) {
e.printStackTrace();
runOnUiThread(() -> Toast.makeText(this, R.string.bck_backupError, Toast.LENGTH_LONG).show());
}
});
}
private void backupPreferencesMatching(String fileName, Function<String, Boolean> predicate, ZipWriter zip) throws IOException, JSONException {
var jsonPrefs = new JSONObject();
for (var entry : prefs.getAll().entrySet()) {
if (!predicate.apply(entry.getKey())) continue;
jsonPrefs.put(entry.getKey(), new JSONObject()
.put(PREF_VALUE, entry.getValue())
.put(PREF_TYPE, entry.getValue().getClass().getSimpleName()));
}
zip.addStringFile(fileName, jsonPrefs.toString());
}
private void backupFilesMatching(String folder, Function<String, Boolean> predicate, ZipWriter zip) throws IOException {
var empty = true;
for (var file : fileList()) {
if (!predicate.apply(file)) continue;
try (var in = openFileInput(file)) {
zip.addStreamFile(folder + file, in);
empty = false;
}
}
if (empty) zip.addStringFile(folder + EMPTY, "");
}
/* ------------------- restore ------------------- */
public void restore(View ignored) {
chooseFile(Intent.ACTION_OPEN_DOCUMENT, this::askRestore);
}
/**
* Asks to restore a backup from [uri]
*/
private void askRestore(Uri uri) {
new AlertDialog.Builder(this)
.setTitle(R.string.bck_restoreTitle)
.setMessage(R.string.bck_restoreMessage)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.bck_restoreConfirm, (d, w) -> restore(uri))
.show();
}
/**
* Restores a backup from [uri]
*/
private void restore(Uri uri) {
ProgressDialog.run(this, R.string.btn_restore, progress -> {
progress.setMax(5);
progress.setMessage("Loading backup");
try (var zip = new ZipReader(uri, this)) {
// check version
if (!chk_ignoreNewer.isChecked() && VersionManager.isNewerThanCurrent(zip.getFileString(FILE_VERSION))) {
runOnUiThread(() -> Toast.makeText(this, R.string.bck_newer, Toast.LENGTH_LONG).show());
return;
}
// rest of preferences
progress.setMessage("Restoring preferences");
progress.increaseProgress();
if (chk_data_prefs.isChecked()) restorePreferencesMatching(FILE_PREFERENCES, negate(IS_PREF_SECRET), zip);
// secret preferences
progress.setMessage("Restoring secrets");
progress.increaseProgress();
if (chk_secrets.isChecked()) restorePreferencesMatching(FILE_SECRETS, IS_PREF_SECRET, zip);
// rest of files
progress.setMessage("Restoring files");
progress.increaseProgress();
if (chk_data_files.isChecked()) restoreFilesMatching(FILES_FOLDER, negate(IS_FILE_CACHE), zip);
// cache files
progress.setMessage("Restoring cache");
progress.increaseProgress();
if (chk_cache.isChecked()) restoreFilesMatching(CACHE_FOLDER, IS_FILE_CACHE, zip);
runOnUiThread(() -> Toast.makeText(this, R.string.bck_restoreOk, Toast.LENGTH_LONG).show());
} catch (Exception e) {
e.printStackTrace();
runOnUiThread(() -> Toast.makeText(this, R.string.bck_restoreError, Toast.LENGTH_LONG).show());
}
runOnUiThread(() -> AndroidSettings.reload(this));
});
}
private void restorePreferencesMatching(String fileName, Function<String, Boolean> predicate, ZipReader zip) throws IOException, JSONException {
var preferences = zip.getFileString(fileName);
if (preferences == null) return;
var jsonPrefs = new JSONObject(preferences);
var editor = prefs.edit();
// remove
if (chk_delete.isChecked()) {
for (var key : prefs.getAll().keySet()) {
if (predicate.apply(key)) editor.remove(key);
}
}
// add
for (var key : JavaUtils.toList(jsonPrefs.keys())) {
var ent = jsonPrefs.getJSONObject(key);
switch (ent.getString(PREF_TYPE)) {
case "String" -> editor.putString(key, ent.getString(PREF_VALUE));
case "Integer" -> editor.putInt(key, ent.getInt(PREF_VALUE));
case "Long" -> editor.putLong(key, ent.getLong(PREF_VALUE));
case "Boolean" -> editor.putBoolean(key, ent.getBoolean(PREF_VALUE));
default -> AndroidUtils.assertError("Unknown type: " + ent.getString(PREF_TYPE));
}
}
editor.apply();
}
private void restoreFilesMatching(String folder, Function<String, Boolean> predicate, ZipReader zip) throws IOException {
var fileNames = zip.fileNames(folder);
if (fileNames.isEmpty()) return;
// delete
if (chk_delete.isChecked()) {
for (var file : fileList()) {
if (predicate.apply(file)) deleteFile(file);
}
}
// create
for (var fileName : fileNames) {
if ((folder + EMPTY).equals(fileName)) continue; // ignore marker
try (var out = openFileOutput(fileName.substring(folder.length()), MODE_PRIVATE)) {
zip.getFileStream(fileName, out);
}
}
}
/* ------------------- delete ------------------- */
public void delete(View ignored) {
new AlertDialog.Builder(this)
.setTitle(R.string.bck_deleteTitle)
.setMessage(R.string.bck_deleteMessage)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.bck_deleteConfirm, (d, w) -> delete())
.show();
}
private void delete() {
ProgressDialog.run(this, R.string.btn_delete, progress -> {
progress.setMax(4);
try {
// rest of preferences
progress.setMessage("Deleting preferences");
if (chk_data_prefs.isChecked()) deletePreferencesMatching(negate(IS_PREF_SECRET));
// secret preferences
progress.setMessage("Deleting secrets");
progress.increaseProgress();
if (chk_secrets.isChecked()) deletePreferencesMatching(IS_PREF_SECRET);
// rest of files
progress.setMessage("Deleting files");
progress.increaseProgress();
if (chk_data_files.isChecked()) deleteFilesMatching(negate(IS_FILE_CACHE));
// cache files
progress.setMessage("Deleting cache");
progress.increaseProgress();
if (chk_cache.isChecked()) deleteFilesMatching(IS_FILE_CACHE);
runOnUiThread(() -> Toast.makeText(this, R.string.bck_deleteOk, Toast.LENGTH_SHORT).show());
} catch (Exception e) {
e.printStackTrace();
runOnUiThread(() -> Toast.makeText(this, R.string.bck_deleteError, Toast.LENGTH_SHORT).show());
}
runOnUiThread(() -> AndroidSettings.reload(this));
});
}
private void deletePreferencesMatching(Function<String, Boolean> predicate) {
var editor = prefs.edit();
for (var key : prefs.getAll().keySet()) {
if (predicate.apply(key)) editor.remove(key);
}
editor.apply();
}
private void deleteFilesMatching(Function<String, Boolean> predicate) {
for (var file : fileList()) {
if (predicate.apply(file)) deleteFile(file);
}
}
/* ------------------- common ------------------- */
private static final String FILE_VERSION = "version";
private static final String FILE_PREFERENCES = "preferences";
private static final String FILE_SECRETS = "secrets";
private static final String FILES_FOLDER = "files/";
private static final String CACHE_FOLDER = "cache/";
private static final String PREF_VALUE = "value";
private static final String PREF_TYPE = "type";
private static final String EMPTY = ".empty";
private static final String ADVANCED_EXTRA = "advanced";
private static final Function<String, Boolean> IS_PREF_SECRET = List.of(VirusTotalModule.PREF, LogModule.PREF)::contains;
private static final Function<String, Boolean> IS_FILE_CACHE = s -> s.startsWith(Hosts.PREFIX);
private void chooseFile(String action, JavaUtils.Consumer<Uri> listener) {
// choose backup file
var intent = new Intent(action);
intent.putExtra(Intent.EXTRA_TITLE, getInitialFile());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, getInitialFolder());
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
PackageUtils.startActivityForResult(
Intent.createChooser(intent, getString(R.string.bck_chooseFile)),
resultCodeInjector.registerActivityResult((resultCode, data) -> {
// file selected?
if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) listener.accept(data.getData());
else Toast.makeText(this, R.string.canceled, Toast.LENGTH_SHORT).show();
}),
R.string.toast_noApp,
this);
}
private File getInitialFolder() {
var folder = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
? new File(getSystemService(StorageManager.class).getPrimaryStorageVolume().getDirectory(), Environment.DIRECTORY_DOWNLOADS)
: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
folder.mkdirs();
return folder;
}
private String getInitialFile() {
return "UrlChecker_" + new SimpleDateFormat("yyyyMMddHHmmss", Locale.US).format(new Date()) + ".ucbckp";
}
private void showAdvanced() {
chk_data.setThumbResource(android.R.color.transparent);
chk_data_prefs.setVisibility(View.VISIBLE);
chk_data_files.setVisibility(View.VISIBLE);
chk_delete.setVisibility(View.VISIBLE);
chk_ignoreNewer.setVisibility(View.VISIBLE);
btn_delete.setVisibility(View.VISIBLE);
}
}

View File

@ -125,4 +125,13 @@ public class SettingsActivity extends Activity {
PackageUtils.startActivity(new Intent(this, TutorialActivity.class), R.string.toast_noApp, this);
}
/* ------------------- backup ------------------- */
public void openBackup(View view) {
PackageUtils.startActivityForResult(new Intent(this, BackupActivity.class),
AndroidSettings.registerForReloading(resultCodeInjector, this),
R.string.toast_noApp,
this
);
}
}

View File

@ -27,7 +27,7 @@ public class Hosts {
private static final char SEPARATOR = '\t';
private static final int FILES = 128;
private static final String PREFIX = "hosts_";
public static final String PREFIX = "hosts_";
// A custom mapping from a given hash with queryable buckets
private final HashMap<Integer, HashMap<String, Pair<String, String>>> buckets = new HashMap<>();

View File

@ -6,6 +6,11 @@ import com.trianguloy.urlchecker.BuildConfig;
import com.trianguloy.urlchecker.activities.TutorialActivity;
import com.trianguloy.urlchecker.utilities.generics.GenericPref;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
/**
* Manages the app version, to notify of updates
*/
@ -17,6 +22,8 @@ public class VersionManager {
return new GenericPref.Str("changelog_lastVersion", null, cntx);
}
/* ------------------- static ------------------- */
/**
* Check if the version must be updated
*/
@ -25,6 +32,32 @@ public class VersionManager {
new VersionManager(cntx);
}
/** Returns true iff [version] is newer than the current one */
public static boolean isNewerThanCurrent(String version) {
// shortcut to check own version
if (BuildConfig.VERSION_NAME.equals(version)) return false;
var versionSplit = split(version);
// invalid version, consider new just in case
if (versionSplit.isEmpty()) return true;
// compare: "1" < "2", "1" < "1.1"
var currentSplit = split(BuildConfig.VERSION_NAME);
var i = 0;
while (true) {
// end of version, version is older (or equal)
if (versionSplit.size() <= i) return false;
// end of current, version is newer
if (currentSplit.size() <= i) return true;
// version is older
if (versionSplit.get(i) < currentSplit.get(i)) return false;
// version is newer
if (versionSplit.get(i) > currentSplit.get(i)) return true;
i++;
}
}
/* ------------------- instance ------------------- */
public VersionManager(Context cntx) {
lastVersion = LASTVERSION_PREF(cntx);
if (lastVersion.get() == null) {
@ -50,4 +83,17 @@ public class VersionManager {
public void markSeen() {
lastVersion.set(BuildConfig.VERSION_NAME);
}
/* ------------------- private ------------------- */
static private final Pattern INTEGER_PATTERN = Pattern.compile("\\d+");
/** Extracts all numbers from the string: "1.2.34d" -> [1, 2, 34] */
private static List<Integer> split(String version) {
if (version == null) return Collections.emptyList();
var matcher = INTEGER_PATTERN.matcher(version);
var parts = new ArrayList<Integer>();
while (matcher.find()) parts.add(Integer.parseInt(matcher.group()));
return parts;
}
}

View File

@ -26,8 +26,10 @@ import java.util.Date;
*/
public class LogModule extends AModuleData {
public static final String PREF = "log_data";
public static GenericPref.Str LOG_DATA(Context cntx) {
return new GenericPref.Str("log_data", "", cntx);
return new GenericPref.Str(PREF, "", cntx);
}
public static GenericPref.Int LOG_LIMIT(Context cntx) {

View File

@ -26,8 +26,10 @@ import com.trianguloy.urlchecker.utilities.wrappers.DefaultTextWatcher;
*/
public class VirusTotalModule extends AModuleData {
public static final String PREF = "api_key";
static GenericPref.Str API_PREF(Context cntx) {
return new GenericPref.Str("api_key", "", cntx);
return new GenericPref.Str(PREF, "", cntx);
}
@Override

View File

@ -26,6 +26,11 @@ import java.util.Objects;
*/
public abstract class GenericPref<T> {
/** Returns the sharedPrefs used by everything in the app */
public static SharedPreferences getPrefs(Context cntx) {
return cntx.getSharedPreferences(cntx.getPackageName(), Context.MODE_PRIVATE);
}
/**
* android sharedprefs
*/
@ -50,7 +55,7 @@ public abstract class GenericPref<T> {
public GenericPref(String prefName, T defaultValue, Context cntx) {
this.prefName = prefName;
this.defaultValue = defaultValue;
prefs = cntx.getSharedPreferences(cntx.getPackageName(), Context.MODE_PRIVATE);
prefs = getPrefs(cntx);
}
/**

View File

@ -21,6 +21,8 @@ import android.widget.Toast;
import com.trianguloy.urlchecker.BuildConfig;
import com.trianguloy.urlchecker.R;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import java.util.HashSet;
@ -205,4 +207,11 @@ public interface AndroidUtils {
while (matcher.find()) links.add(matcher.group());
return links;
}
/** Copies a Uri to a file. */
static void copyUri2File(Uri uri, File file, Context cntx) throws IOException {
try (var in = cntx.getContentResolver().openInputStream(uri)) {
StreamUtils.inputStream2File(in, file);
}
}
}

View File

@ -130,6 +130,11 @@ public interface JavaUtils {
R apply(T t);
}
/** Negates a boolean Function */
static <T> Function<T, Boolean> negate(Function<T, Boolean> function) {
return t -> !function.apply(t);
}
/**
* java.util.function.UnaryOperator requires api 24
*/

View File

@ -1,9 +1,12 @@
package com.trianguloy.urlchecker.utilities.methods;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
@ -79,6 +82,22 @@ public interface StreamUtils {
}
}
/** Reads an inputStream and transfers its content to a file. The stream is NOT closed */
static void inputStream2File(InputStream in, File file) throws IOException {
try (var out = new FileOutputStream(file)) {
inputStream2OutputStream(in, out);
}
}
/** Reads an inputStream and transfers its content to an output stream. The streams are NOT closed */
static void inputStream2OutputStream(InputStream in, OutputStream out) throws IOException {
var buffer = new byte[10240];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
/**
* Reads an input stream and streams its lines
*/

View File

@ -0,0 +1,65 @@
package com.trianguloy.urlchecker.utilities.wrappers;
import android.content.Context;
import android.net.Uri;
import com.trianguloy.urlchecker.utilities.methods.AndroidUtils;
import com.trianguloy.urlchecker.utilities.methods.StreamUtils;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipFile;
/** Utility class to manage loading zips */
public class ZipReader implements Closeable {
private final ZipFile zip;
private File cacheFile;
/** Opens a zip from a uri */
public ZipReader(Uri uri, Context cntx) throws IOException {
// copy to temporal file to allow using ZipFile
cacheFile = new File(cntx.getCacheDir(), "ZipReader");
try {
AndroidUtils.copyUri2File(uri, cacheFile, cntx);
zip = new ZipFile(cacheFile);
} catch (IOException e) {
cacheFile.delete();
cacheFile = null;
throw e;
}
}
/** Returns the content of file as string */
public String getFileString(String name) throws IOException {
var preferences = zip.getEntry(name);
return preferences != null ? StreamUtils.inputStream2String(zip.getInputStream(preferences)) : null;
}
/** Return the content of a file as stream */
public void getFileStream(String name, OutputStream out) throws IOException {
try (var in = zip.getInputStream(zip.getEntry(name))) {
StreamUtils.inputStream2OutputStream(in, out);
}
}
/** Returns the list of files from a particular folder */
public List<String> fileNames(String folder) {
var fileNames = new ArrayList<String>();
var zipEntries = zip.entries();
while (zipEntries.hasMoreElements()) {
var name = zipEntries.nextElement().getName();
if (name.startsWith(folder)) fileNames.add(name);
}
return fileNames;
}
@Override
public void close() throws IOException {
if (zip != null) zip.close();
if (cacheFile != null) cacheFile.delete();
}
}

View File

@ -0,0 +1,49 @@
package com.trianguloy.urlchecker.utilities.wrappers;
import static com.trianguloy.urlchecker.utilities.methods.StreamUtils.UTF_8;
import android.content.Context;
import android.net.Uri;
import com.trianguloy.urlchecker.utilities.methods.StreamUtils;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/** Utility class to manage creating zips */
public class ZipWriter implements Closeable {
private final ZipOutputStream zip;
/** Creates a zip from a uri */
public ZipWriter(Uri uri, Context cntx) throws FileNotFoundException {
zip = new ZipOutputStream(cntx.getContentResolver().openOutputStream(uri));
}
/** Sets the zip comment */
public void setComment(String comment) {
zip.setComment(comment);
}
/** Adds a string as a file */
public void addStringFile(String name, String content) throws IOException {
var entry = new ZipEntry(name);
zip.putNextEntry(entry);
zip.write(content.getBytes(UTF_8));
}
/** Adds the content of a stream as a file. The stream is NOT closed */
public void addStreamFile(String name, InputStream stream) throws IOException {
zip.putNextEntry(new ZipEntry(name));
StreamUtils.inputStream2OutputStream(stream, zip);
}
@Override
public void close() throws IOException {
if (zip != null) zip.close();
}
}

View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activities.BackupActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:padding="@dimen/smallPadding">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/bkp_description" />
<Switch
android:id="@+id/chk_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:button="@android:color/transparent"
android:checked="true"
android:padding="@dimen/smallPadding"
android:text="@string/bkp_data" />
<Switch
android:id="@+id/chk_data_prefs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding"
android:layout_marginLeft="@dimen/padding"
android:checked="true"
android:padding="@dimen/smallPadding"
android:text="@string/bkp_data_prefs"
android:visibility="gone"
tools:visibility="visible" />
<Switch
android:id="@+id/chk_data_files"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding"
android:layout_marginLeft="@dimen/padding"
android:checked="true"
android:padding="@dimen/smallPadding"
android:text="@string/bkp_data_files"
android:visibility="gone"
tools:visibility="visible" />
<Switch
android:id="@+id/chk_secrets"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:padding="@dimen/smallPadding"
android:text="@string/bkp_secrets" />
<Switch
android:id="@+id/chk_cache"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/smallPadding"
android:text="@string/bjp_cache" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="horizontal">
<Button
android:id="@+id/btn_backup"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="backup"
android:text="@string/btn_backup" />
<Button
android:id="@+id/btn_restore"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="restore"
android:text="@string/btn_restore" />
<Button
android:id="@+id/btn_delete"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="delete"
android:text="@string/btn_delete"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
<Switch
android:id="@+id/chk_delete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:padding="@dimen/smallPadding"
android:text="@string/bck_restoreDelete"
android:visibility="gone"
tools:visibility="visible" />
<Switch
android:id="@+id/chk_ignoreNewer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:padding="@dimen/smallPadding"
android:text="@string/bck_ignoreNewer"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</ScrollView>

View File

@ -17,17 +17,21 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="openTutorial"
android:text="@string/btn_tutorialSettings" /><FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/padding">
android:text="@string/btn_tutorialSettings" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/padding">
<include layout="@layout/separator" />
</FrameLayout><TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/txt_openLinks" />
</FrameLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/txt_openLinks" />
<include layout="@layout/fragment_browser_buttons" />
@ -36,37 +40,46 @@
android:layout_height="wrap_content"
android:paddingTop="@dimen/padding"
android:paddingBottom="@dimen/padding">
<include layout="@layout/separator" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_theme" />
<Spinner
android:id="@+id/theme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:saveEnabled="false" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/padding"
android:paddingBottom="@dimen/padding">
<include layout="@layout/separator" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_locale" />
<Spinner
android:id="@+id/locale"
android:layout_width="match_parent"
@ -74,6 +87,21 @@
android:saveEnabled="false" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/padding">
<include layout="@layout/separator" />
</FrameLayout>
<Button
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="openBackup"
android:text="@string/btn_backupRestore" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_advanced"
android:title="@string/bck_mn_advanced" />
</menu>

View File

@ -336,4 +336,32 @@ Hay algunos enlaces cuyo único propósito es redirigirte a otro enlace. Si el e
<string name="mChg_updated">La aplicación se ha actualizado.</string>
<string name="mChg_current">Versión actual: %s</string>
<string name="mChg_view">Ver los cambios</string>
<string name="btn_backupRestore">Copia de seguridad y restauración</string>
<string name="bck_mn_advanced">Mostrar opciones avanzadas</string>
<string name="btn_backup">Hacer copia</string>
<string name="btn_replace">Reemplazar</string>
<string name="btn_restore">Restaurar copia</string>
<string name="btn_delete">Borrar</string>
<string name="bkp_description">Elige los elementos para hacer copia/restaurar.\nAl restaurar, si la copia se creó sin ellos, los existentes se mantendrán.</string>
<string name="bkp_data">Ajustes: Ajustes generales de la aplicación. Por ejemplo módulos habilitados, configuración json, etc. No incluye secretos ni caché.</string>
<string name="bkp_data_prefs">Preferencias: Ajustes guardados como preferencias (preferences).</string>
<string name="bkp_data_files">Archivos: Ajustes guardados como ficheros.</string>
<string name="bkp_secrets">Secretos: Ajustes considerados \'secretos\', como la clave de VirusTotal o el registro (log).</string>
<string name="bjp_cache">Caché: Datos que pueden ser fácilmente descargados/generados, como la base de datos del etiquetador de hosts. Activar esta opción puede aumentar el tamaño de la copia notablemente.</string>
<string name="bck_restoreDelete">Borrar elementos antes de restaurar. Ten en cuenta que las preferencias y algunos ficheros con valores por defecto pueden no existir, con lo que deshabilitando esta opción combinarán los elementos existentes con los de la copia de formas inesperadas. Deshabilitalo sólo si sabes lo que estás haciendo.</string>
<string name="bck_ignoreNewer">Permitir copias creadas en versiones más recientes de la aplicación.</string>
<string name="bck_newer">Cancelado. La copia fue creada con una versión más reciente de la aplicación.</string>
<string name="bck_chooseFile">Elige fichero de la copia</string>
<string name="bck_backupOk">Copia de seguridad creada</string>
<string name="bck_backupError">No se ha podido crear la copia, prueba a elegir otra ubicación para el fichero.</string>
<string name="bck_restoreTitle">Restaurar copia</string>
<string name="bck_restoreMessage">¡CUIDADO! Esto va a reemplazar los elementos seleccionados con los de la copia de seguridad.\n¿Realmente deseas continuar?</string>
<string name="bck_restoreConfirm">Reemplazar elementos</string>
<string name="bck_restoreOk">Copia restaurada</string>
<string name="bck_restoreError">No se ha podido restaurar la copia. ¿Es un fichero válido?</string>
<string name="bck_deleteTitle">Borrar elementos</string>
<string name="bck_deleteMessage">¡CUIDADO! Esto borrará todos los elementos seleccionados.\n¿Realmente deseas continuar?</string>
<string name="bck_deleteConfirm">Borrar elementos</string>
<string name="bck_deleteOk">Elementos borrados</string>
<string name="bck_deleteError">No se han podido borrar los elementos</string>
</resources>

View File

@ -53,6 +53,40 @@ Translations: %s."</string>
<string name="spin_lightTheme">Light</string>
<string name="txt_locale">Locale:</string>
<string name="btn_tutorialSettings">Repeat tutorial</string>
<string name="btn_backupRestore">Backup / Restore</string>
<!--
Backup / Restore
-->
<string name="bck_mn_advanced">Show advanced options</string>
<string name="btn_backup">Backup</string>
<string name="btn_replace">Replace</string>
<string name="btn_restore">Restore</string>
<string name="btn_delete">Delete</string>
<string name="bkp_description">"Choose which elements to backup/restore.
When restoring, if the backup was created without it, existing ones will be kept."</string>
<string name="bkp_data">Data: Standard application data. Things like enabled modules, json configurations, etc. Will not include secrets nor cache.</string>
<string name="bkp_data_prefs">Preferences: Data saved as preferences.</string>
<string name="bkp_data_files">Files: Data saved as files.</string>
<string name="bkp_secrets">"Secrets: Data considered 'secret', like the VirusTotal api key or the log."</string>
<string name="bjp_cache">Cache: Data that can be easily downloaded/generated, like the hosts database. Enabling this option may increase the backup size notably.</string>
<string name="bck_restoreDelete">Delete elements before restoring. Note that preferences and some files with default values may not exist, so disabling this option will merge the existing elements with the backup ones in unexpected ways. Disable if you know what you are doing.</string>
<string name="bck_ignoreNewer">Allow backups created from newer app versions.</string>
<string name="bck_newer">Canceled. The backup was created with a newer app version.</string>
<string name="bck_chooseFile">Choose backup file</string>
<string name="bck_backupOk">Backup created</string>
<string name="bck_backupError">"Unable to create backup, try to choose a different location for the file."</string>
<string name="bck_restoreTitle">Restore backup</string>
<string name="bck_restoreMessage">"WARNING! This will replace the selected elements from the backup.
Do you really want to continue?"</string>
<string name="bck_restoreConfirm">Replace elements</string>
<string name="bck_restoreOk">Backup restored</string>
<string name="bck_restoreError">Unable to restore the backup. Is it a valid file?</string>
<string name="bck_deleteTitle">Delete elements</string>
<string name="bck_deleteMessage">"WARNING! This will delete all the selected elements.
Do you really want to continue?"</string>
<string name="bck_deleteConfirm">Delete elements</string>
<string name="bck_deleteOk">Elements deleted</string>
<string name="bck_deleteError">Unable to delete elements</string>
<!--
tutorial
-->