diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java index 44b69934..555a8ed3 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java @@ -536,16 +536,9 @@ 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(); return true; @@ -553,22 +546,9 @@ 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(); return true; @@ -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 { + private WeakReference refSettingsActivity; + private WeakReference 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 { + private WeakReference refSettingsFragment; + private WeakReference 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. diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java b/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java index b4d8c879..7bc9847f 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java @@ -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. */ diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java index 2d55e9cb..fbea0363 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java @@ -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 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); } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java b/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java index 404a9044..d58dda01 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/util/FileUtils.java @@ -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); + } }