mirror of
https://github.com/syncthing/syncthing-android.git
synced 2025-01-09 11:41:29 +00:00
* Add export of index database * Add import of index database * Shutdown service before export and restart it afterwards. * Do not import database if it doesn't exist on sdcard * Do not attempt to delete the database export directory on export if it does not exist. * Return to MainActivity after successful export * Import/Export using an AsyncTask * Fix compatibility with Android 5.x
This commit is contained in:
parent
79d0d7cc4c
commit
12bc08c6dd
4 changed files with 218 additions and 38 deletions
|
@ -536,15 +536,8 @@ public class SettingsActivity extends SyncthingActivity {
|
|||
new AlertDialog.Builder(getActivity())
|
||||
.setMessage(R.string.dialog_confirm_export)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
if (mSyncthingService.exportConfig()) {
|
||||
Toast.makeText(getActivity(),
|
||||
getString(R.string.config_export_successful,
|
||||
Constants.EXPORT_PATH), Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(getActivity(),
|
||||
getString(R.string.config_export_failed),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
new ExportConfigTask((SettingsActivity) getActivity(), mSyncthingService)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
|
@ -553,21 +546,8 @@ public class SettingsActivity extends SyncthingActivity {
|
|||
new AlertDialog.Builder(getActivity())
|
||||
.setMessage(R.string.dialog_confirm_import)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
// Shutdown syncthing, import config, if run conditions applied restart syncthing.
|
||||
if (!mSyncthingService.importConfig()) {
|
||||
Toast.makeText(getActivity(),
|
||||
getString(R.string.config_import_failed,
|
||||
Constants.EXPORT_PATH), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
Toast.makeText(getActivity(),
|
||||
getString(R.string.config_imported_successful),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
// We don't have to send the config via REST on leaving activity.
|
||||
mPendingConfig = false;
|
||||
// We have to evaluate run conditions, they may have changed by the imported prefs.
|
||||
mPendingRunConditions = true;
|
||||
getActivity().finish();
|
||||
new ImportConfigTask(this, mSyncthingService)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
|
@ -658,6 +638,107 @@ public class SettingsActivity extends SyncthingActivity {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs export of settings, config and database in the background.
|
||||
*/
|
||||
private static class ExportConfigTask extends AsyncTask<Void, String, Void> {
|
||||
private WeakReference<SettingsActivity> refSettingsActivity;
|
||||
private WeakReference<SyncthingService> refSyncthingService;
|
||||
Boolean actionSucceeded = false;
|
||||
|
||||
ExportConfigTask(SettingsActivity context, SyncthingService service) {
|
||||
refSettingsActivity = new WeakReference<>(context);
|
||||
refSyncthingService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
SyncthingService syncthingService = refSyncthingService.get();
|
||||
if (syncthingService == null) {
|
||||
cancel(true);
|
||||
return null;
|
||||
}
|
||||
actionSucceeded = syncthingService.exportConfig();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
// Get a reference to the activity if it is still there.
|
||||
SettingsActivity settingsActivity = refSettingsActivity.get();
|
||||
if (settingsActivity == null) {
|
||||
return;
|
||||
}
|
||||
if (!actionSucceeded) {
|
||||
Toast.makeText(settingsActivity,
|
||||
settingsActivity.getString(R.string.config_export_failed),
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
Toast.makeText(settingsActivity,
|
||||
settingsActivity.getString(R.string.config_export_successful,
|
||||
Constants.EXPORT_PATH_OBJ), Toast.LENGTH_LONG).show();
|
||||
settingsActivity.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs import of settings, config and database in the background.
|
||||
*/
|
||||
private static class ImportConfigTask extends AsyncTask<Void, String, Void> {
|
||||
private WeakReference<SettingsFragment> refSettingsFragment;
|
||||
private WeakReference<SyncthingService> refSyncthingService;
|
||||
Boolean actionSucceeded = false;
|
||||
|
||||
ImportConfigTask(SettingsFragment context, SyncthingService service) {
|
||||
refSettingsFragment = new WeakReference<>(context);
|
||||
refSyncthingService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
SyncthingService syncthingService = refSyncthingService.get();
|
||||
if (syncthingService == null) {
|
||||
cancel(true);
|
||||
return null;
|
||||
}
|
||||
actionSucceeded = syncthingService.importConfig();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
// Get a reference to the activity if it is still there.
|
||||
SettingsFragment settingsFragment = refSettingsFragment.get();
|
||||
if (settingsFragment == null) {
|
||||
return;
|
||||
}
|
||||
settingsFragment.afterConfigImport(actionSucceeded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calley by {@link #ImportConfigTask} after config import.
|
||||
*/
|
||||
private void afterConfigImport(Boolean actionSucceeded) {
|
||||
if (!actionSucceeded) {
|
||||
Toast.makeText(getActivity(),
|
||||
getString(R.string.config_import_failed,
|
||||
Constants.EXPORT_PATH_OBJ), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
Toast.makeText(getActivity(),
|
||||
getString(R.string.config_imported_successful,
|
||||
Constants.EXPORT_PATH_OBJ), Toast.LENGTH_LONG).show();
|
||||
|
||||
// We don't have to send the config via REST on leaving activity.
|
||||
mPendingConfig = false;
|
||||
|
||||
// We have to evaluate run conditions, they may have changed by the imported prefs.
|
||||
mPendingRunConditions = true;
|
||||
getActivity().finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a new user input for the SOCKS proxy preference.
|
||||
* Returns if the changed setting requires a restart.
|
||||
|
|
|
@ -68,8 +68,9 @@ public class Constants {
|
|||
/**
|
||||
* Directory where config is exported to and imported from.
|
||||
*/
|
||||
public static final File EXPORT_PATH =
|
||||
new File(Environment.getExternalStorageDirectory(), "backups/syncthing");
|
||||
public static final String EXPORT_PATH = Environment.getExternalStorageDirectory() + "/backups/syncthing";
|
||||
|
||||
public static final File EXPORT_PATH_OBJ = new File(EXPORT_PATH);
|
||||
|
||||
/**
|
||||
* File in the config folder that contains configuration.
|
||||
|
@ -107,6 +108,11 @@ public class Constants {
|
|||
return new File(context.getFilesDir(), PRIVATE_KEY_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the folder containing the index database.
|
||||
*/
|
||||
static final String INDEX_DB_FOLDER = "index-v0.14.0.db";
|
||||
|
||||
/**
|
||||
* Name of the public HTTPS CA file in the data directory.
|
||||
*/
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
|
|||
import android.content.SharedPreferences;
|
||||
import android.Manifest;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.Nullable;
|
||||
|
@ -21,6 +22,7 @@ import com.nutomic.syncthingandroid.SyncthingApp;
|
|||
import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask;
|
||||
import com.nutomic.syncthingandroid.model.Folder;
|
||||
import com.nutomic.syncthingandroid.util.ConfigXml;
|
||||
import com.nutomic.syncthingandroid.util.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
@ -30,6 +32,8 @@ import java.io.ObjectInputStream;
|
|||
import java.io.ObjectOutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
@ -664,20 +668,28 @@ public class SyncthingService extends Service {
|
|||
|
||||
/**
|
||||
* Exports the local config and keys to {@link Constants#EXPORT_PATH}.
|
||||
*
|
||||
* Test with Android Virtual Device using emulator.
|
||||
* cls & adb shell su 0 "ls -a -l -R /data/data/com.github.catfriend1.syncthingandroid.debug/files; echo === SDCARD ===; ls -a -l -R /storage/emulated/0/backups/syncthing"
|
||||
*
|
||||
*/
|
||||
public boolean exportConfig() {
|
||||
Boolean failSuccess = true;
|
||||
Log.v(TAG, "exportConfig BEGIN");
|
||||
|
||||
// Shutdown synchronously.
|
||||
shutdown(State.DISABLED, () -> {
|
||||
});
|
||||
|
||||
// Copy config, privateKey and/or publicKey to export path.
|
||||
Constants.EXPORT_PATH.mkdirs();
|
||||
Constants.EXPORT_PATH_OBJ.mkdirs();
|
||||
try {
|
||||
Files.copy(Constants.getConfigFile(this),
|
||||
new File(Constants.EXPORT_PATH, Constants.CONFIG_FILE));
|
||||
new File(Constants.EXPORT_PATH_OBJ, Constants.CONFIG_FILE));
|
||||
Files.copy(Constants.getPrivateKeyFile(this),
|
||||
new File(Constants.EXPORT_PATH, Constants.PRIVATE_KEY_FILE));
|
||||
new File(Constants.EXPORT_PATH_OBJ, Constants.PRIVATE_KEY_FILE));
|
||||
Files.copy(Constants.getPublicKeyFile(this),
|
||||
new File(Constants.EXPORT_PATH, Constants.PUBLIC_KEY_FILE));
|
||||
new File(Constants.EXPORT_PATH_OBJ, Constants.PUBLIC_KEY_FILE));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to export config", e);
|
||||
failSuccess = false;
|
||||
|
@ -688,7 +700,7 @@ public class SyncthingService extends Service {
|
|||
FileOutputStream fileOutputStream = null;
|
||||
ObjectOutputStream objectOutputStream = null;
|
||||
try {
|
||||
file = new File(Constants.EXPORT_PATH, Constants.SHARED_PREFS_EXPORT_FILE);
|
||||
file = new File(Constants.EXPORT_PATH_OBJ, Constants.SHARED_PREFS_EXPORT_FILE);
|
||||
fileOutputStream = new FileOutputStream(file);
|
||||
if (!file.exists()) {
|
||||
file.createNewFile();
|
||||
|
@ -712,12 +724,49 @@ public class SyncthingService extends Service {
|
|||
Log.e(TAG, "exportConfig: Failed to export SharedPreferences #2", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* java.nio.file library is available since API level 26, see
|
||||
* https://developer.android.com/reference/java/nio/file/package-summary
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
Log.v(TAG, "exportConfig: Exporting index database");
|
||||
Path databaseSourcePath = Paths.get(this.getFilesDir() + "/" + Constants.INDEX_DB_FOLDER);
|
||||
Path databaseExportPath = Paths.get(Constants.EXPORT_PATH + "/" + Constants.INDEX_DB_FOLDER);
|
||||
if (java.nio.file.Files.exists(databaseExportPath)) {
|
||||
try {
|
||||
FileUtils.deleteDirectoryRecursively(databaseExportPath);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to delete directory '" + databaseExportPath + "'" + e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
java.nio.file.Files.walk(databaseSourcePath).forEach(source -> {
|
||||
try {
|
||||
java.nio.file.Files.copy(source, databaseExportPath.resolve(databaseSourcePath.relativize(source)));
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to copy file '" + source + "' to '" + databaseExportPath + "'");
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to copy directory '" + databaseSourcePath + "' to '" + databaseExportPath + "'");
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "exportConfig END");
|
||||
|
||||
// Start syncthing after export if run conditions apply.
|
||||
if (mLastDeterminedShouldRun) {
|
||||
launchStartupTask(SyncthingRunnable.Command.main);
|
||||
}
|
||||
return failSuccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports config and keys from {@link Constants#EXPORT_PATH}.
|
||||
*
|
||||
* Test with Android Virtual Device using emulator.
|
||||
* cls & adb shell su 0 "ls -a -l -R /data/data/com.github.catfriend1.syncthingandroid.debug/files; echo === SDCARD ===; ls -a -l -R /storage/emulated/0/backups/syncthing"
|
||||
*
|
||||
* @return True if the import was successful, false otherwise (eg if files aren't found).
|
||||
*/
|
||||
public boolean importConfig() {
|
||||
|
@ -730,9 +779,9 @@ public class SyncthingService extends Service {
|
|||
|
||||
// Import config, privateKey and/or publicKey.
|
||||
try {
|
||||
File config = new File(Constants.EXPORT_PATH, Constants.CONFIG_FILE);
|
||||
File privateKey = new File(Constants.EXPORT_PATH, Constants.PRIVATE_KEY_FILE);
|
||||
File publicKey = new File(Constants.EXPORT_PATH, Constants.PUBLIC_KEY_FILE);
|
||||
File config = new File(Constants.EXPORT_PATH_OBJ, Constants.CONFIG_FILE);
|
||||
File privateKey = new File(Constants.EXPORT_PATH_OBJ, Constants.PRIVATE_KEY_FILE);
|
||||
File publicKey = new File(Constants.EXPORT_PATH_OBJ, Constants.PUBLIC_KEY_FILE);
|
||||
|
||||
// Check if necessary files for import are available.
|
||||
if (config.exists() && privateKey.exists() && publicKey.exists()) {
|
||||
|
@ -754,7 +803,7 @@ public class SyncthingService extends Service {
|
|||
ObjectInputStream objectInputStream = null;
|
||||
Map<String, Object> sharedPrefsMap = null;
|
||||
try {
|
||||
file = new File(Constants.EXPORT_PATH, Constants.SHARED_PREFS_EXPORT_FILE);
|
||||
file = new File(Constants.EXPORT_PATH_OBJ, Constants.SHARED_PREFS_EXPORT_FILE);
|
||||
if (file.exists()) {
|
||||
// Read, deserialize shared preferences.
|
||||
fileInputStream = new FileInputStream(file);
|
||||
|
@ -831,7 +880,36 @@ public class SyncthingService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
// Start syncthing after successful import if run conditions apply.
|
||||
/**
|
||||
* java.nio.file library is available since API level 26, see
|
||||
* https://developer.android.com/reference/java/nio/file/package-summary
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
Path databaseImportPath = Paths.get(Constants.EXPORT_PATH + "/" + Constants.INDEX_DB_FOLDER);
|
||||
if (java.nio.file.Files.exists(databaseImportPath)) {
|
||||
Log.v(TAG, "importConfig: Importing index database");
|
||||
Path databaseTargetPath = Paths.get(this.getFilesDir() + "/" + Constants.INDEX_DB_FOLDER);
|
||||
try {
|
||||
FileUtils.deleteDirectoryRecursively(databaseTargetPath);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to delete directory '" + databaseTargetPath + "'" + e);
|
||||
}
|
||||
try {
|
||||
java.nio.file.Files.walk(databaseImportPath).forEach(source -> {
|
||||
try {
|
||||
java.nio.file.Files.copy(source, databaseTargetPath.resolve(databaseImportPath.relativize(source)));
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to copy file '" + source + "' to '" + databaseTargetPath + "'");
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to copy directory '" + databaseImportPath + "' to '" + databaseTargetPath + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "importConfig END");
|
||||
|
||||
// Start syncthing after import if run conditions apply.
|
||||
if (mLastDeterminedShouldRun) {
|
||||
launchStartupTask(SyncthingRunnable.Command.main);
|
||||
}
|
||||
|
|
|
@ -12,10 +12,12 @@ import android.support.annotation.Nullable;
|
|||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Utils for dealing with Storage Access Framework URIs.
|
||||
|
@ -204,4 +206,17 @@ public class FileUtils {
|
|||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a directory recursively.
|
||||
* java.nio.file library is available since API level 26, see
|
||||
* https://developer.android.com/reference/java/nio/file/package-summary
|
||||
*/
|
||||
@TargetApi(26)
|
||||
public static void deleteDirectoryRecursively(java.nio.file.Path pathToDelete) throws IOException {
|
||||
java.nio.file.Files.walk(pathToDelete)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(java.nio.file.Path::toFile)
|
||||
.forEach(File::delete);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue