1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-11-30 00:01:19 +00:00

Add import/export for app settings (SharedPreferences) (#61)

* Add export of SharedPreferences app settings
Add import/export to drawer

* Add import for app settings (SharedPreferences)
Move last_sync_id pref to Constants.java

* Add drawer icon for import / export feature

* Start or stay stopped according to run conditions after import

* Close SettingsActivity after sharedPref import
This commit is contained in:
Catfriend1 2018-09-17 09:13:07 +02:00 committed by GitHub
parent 3bb227379c
commit 38db4d9c32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 219 additions and 44 deletions

View file

@ -122,8 +122,6 @@ public class SettingsActivity extends SyncthingActivity {
private CheckBoxPreference mRestartOnWakeup; private CheckBoxPreference mRestartOnWakeup;
private CheckBoxPreference mUrAccepted; private CheckBoxPreference mUrAccepted;
private Preference mCategoryBackup;
/* Experimental options */ /* Experimental options */
private CheckBoxPreference mUseRoot; private CheckBoxPreference mUseRoot;
private CheckBoxPreference mUseWakelock; private CheckBoxPreference mUseWakelock;
@ -213,7 +211,6 @@ public class SettingsActivity extends SyncthingActivity {
mRestartOnWakeup = (CheckBoxPreference) findPreference("restartOnWakeup"); mRestartOnWakeup = (CheckBoxPreference) findPreference("restartOnWakeup");
mUrAccepted = (CheckBoxPreference) findPreference("urAccepted"); mUrAccepted = (CheckBoxPreference) findPreference("urAccepted");
mCategoryBackup = findPreference("category_backup");
Preference exportConfig = findPreference("export_config"); Preference exportConfig = findPreference("export_config");
Preference importConfig = findPreference("import_config"); Preference importConfig = findPreference("import_config");
@ -319,7 +316,6 @@ public class SettingsActivity extends SyncthingActivity {
mApi.isConfigLoaded() && mApi.isConfigLoaded() &&
(currentState == SyncthingService.State.ACTIVE); (currentState == SyncthingService.State.ACTIVE);
mCategorySyncthingOptions.setEnabled(isSyncthingRunning); mCategorySyncthingOptions.setEnabled(isSyncthingRunning);
mCategoryBackup.setEnabled(isSyncthingRunning);
if (!isSyncthingRunning) if (!isSyncthingRunning)
return; return;
@ -539,10 +535,15 @@ public class SettingsActivity extends SyncthingActivity {
new AlertDialog.Builder(getActivity()) new AlertDialog.Builder(getActivity())
.setMessage(R.string.dialog_confirm_export) .setMessage(R.string.dialog_confirm_export)
.setPositiveButton(android.R.string.yes, (dialog, which) -> { .setPositiveButton(android.R.string.yes, (dialog, which) -> {
mSyncthingService.exportConfig(); if (mSyncthingService.exportConfig()) {
Toast.makeText(getActivity(), Toast.makeText(getActivity(),
getString(R.string.config_export_successful, getString(R.string.config_export_successful,
Constants.EXPORT_PATH), Toast.LENGTH_LONG).show(); Constants.EXPORT_PATH), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(),
getString(R.string.config_export_failed),
Toast.LENGTH_LONG).show();
}
}) })
.setNegativeButton(android.R.string.no, null) .setNegativeButton(android.R.string.no, null)
.show(); .show();
@ -551,17 +552,21 @@ public class SettingsActivity extends SyncthingActivity {
new AlertDialog.Builder(getActivity()) new AlertDialog.Builder(getActivity())
.setMessage(R.string.dialog_confirm_import) .setMessage(R.string.dialog_confirm_import)
.setPositiveButton(android.R.string.yes, (dialog, which) -> { .setPositiveButton(android.R.string.yes, (dialog, which) -> {
if (mSyncthingService.importConfig()) { // Shutdown syncthing, import config, if run conditions applied restart syncthing.
Toast.makeText(getActivity(), if (!mSyncthingService.importConfig()) {
getString(R.string.config_imported_successful),
Toast.LENGTH_SHORT).show();
// No need to restart, as we shutdown to import the config, and
// then have to start Syncthing again.
} else {
Toast.makeText(getActivity(), Toast.makeText(getActivity(),
getString(R.string.config_import_failed, getString(R.string.config_import_failed,
Constants.EXPORT_PATH), Toast.LENGTH_LONG).show(); 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();
}) })
.setNegativeButton(android.R.string.no, null) .setNegativeButton(android.R.string.no, null)
.show(); .show();

View file

@ -39,6 +39,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
private TextView mVersion = null; private TextView mVersion = null;
private TextView mDrawerActionShowQrCode; private TextView mDrawerActionShowQrCode;
private TextView mDrawerActionWebGui; private TextView mDrawerActionWebGui;
private TextView mDrawerActionImportExport;
private TextView mDrawerActionRestart; private TextView mDrawerActionRestart;
private TextView mDrawerActionSettings; private TextView mDrawerActionSettings;
private TextView mExitButton; private TextView mExitButton;
@ -80,6 +81,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
mVersion = view.findViewById(R.id.version); mVersion = view.findViewById(R.id.version);
mDrawerActionShowQrCode = view.findViewById(R.id.drawerActionShowQrCode); mDrawerActionShowQrCode = view.findViewById(R.id.drawerActionShowQrCode);
mDrawerActionWebGui = view.findViewById(R.id.drawerActionWebGui); mDrawerActionWebGui = view.findViewById(R.id.drawerActionWebGui);
mDrawerActionImportExport = view.findViewById(R.id.drawerActionImportExport);
mDrawerActionRestart = view.findViewById(R.id.drawerActionRestart); mDrawerActionRestart = view.findViewById(R.id.drawerActionRestart);
mDrawerActionSettings = view.findViewById(R.id.drawerActionSettings); mDrawerActionSettings = view.findViewById(R.id.drawerActionSettings);
mExitButton = view.findViewById(R.id.drawerActionExit); mExitButton = view.findViewById(R.id.drawerActionExit);
@ -87,6 +89,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
// Add listeners to buttons. // Add listeners to buttons.
mDrawerActionShowQrCode.setOnClickListener(this); mDrawerActionShowQrCode.setOnClickListener(this);
mDrawerActionWebGui.setOnClickListener(this); mDrawerActionWebGui.setOnClickListener(this);
mDrawerActionImportExport.setOnClickListener(this);
mDrawerActionRestart.setOnClickListener(this); mDrawerActionRestart.setOnClickListener(this);
mDrawerActionSettings.setOnClickListener(this); mDrawerActionSettings.setOnClickListener(this);
mExitButton.setOnClickListener(this); mExitButton.setOnClickListener(this);
@ -155,6 +158,7 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Intent intent;
switch (v.getId()) { switch (v.getId()) {
case R.id.drawerActionWebGui: case R.id.drawerActionWebGui:
startActivity(new Intent(mActivity, WebGuiActivity.class)); startActivity(new Intent(mActivity, WebGuiActivity.class));
@ -164,6 +168,12 @@ public class DrawerFragment extends Fragment implements SyncthingService.OnServi
startActivity(new Intent(mActivity, SettingsActivity.class)); startActivity(new Intent(mActivity, SettingsActivity.class));
mActivity.closeDrawer(); mActivity.closeDrawer();
break; break;
case R.id.drawerActionImportExport:
intent = new Intent(mActivity, SettingsActivity.class);
intent.putExtra(SettingsActivity.EXTRA_OPEN_SUB_PREF_SCREEN, "category_import_export");
startActivity(intent);
mActivity.closeDrawer();
break;
case R.id.drawerActionRestart: case R.id.drawerActionRestart:
mActivity.showRestartDialog(); mActivity.showRestartDialog();
mActivity.closeDrawer(); mActivity.closeDrawer();

View file

@ -23,7 +23,7 @@ public class PollWebGuiAvailableTask extends ApiRequest {
* Interval in ms, at which connections to the web gui are performed on first start * Interval in ms, at which connections to the web gui are performed on first start
* to find out if it's online. * to find out if it's online.
*/ */
private static final long WEB_GUI_POLL_INTERVAL = 100; private static final long WEB_GUI_POLL_INTERVAL = 150;
private final Handler mHandler = new Handler(); private final Handler mHandler = new Handler();

View file

@ -9,7 +9,6 @@ import java.util.concurrent.TimeUnit;
public class Constants { public class Constants {
public static final String FILENAME_SYNCTHING_BINARY = "libsyncthing.so"; public static final String FILENAME_SYNCTHING_BINARY = "libsyncthing.so";
public static final String PREF_LAST_BINARY_VERSION = "lastBinaryVersion";
// Preferences - Run conditions // Preferences - Run conditions
public static final String PREF_ALWAYS_RUN_IN_BACKGROUND = "always_run_in_background"; public static final String PREF_ALWAYS_RUN_IN_BACKGROUND = "always_run_in_background";
@ -33,6 +32,16 @@ public class Constants {
public static final String PREF_SOCKS_PROXY_ADDRESS = "socks_proxy_address"; public static final String PREF_SOCKS_PROXY_ADDRESS = "socks_proxy_address";
public static final String PREF_HTTP_PROXY_ADDRESS = "http_proxy_address"; public static final String PREF_HTTP_PROXY_ADDRESS = "http_proxy_address";
/**
* Cached information which is not available on SettingsActivity.
*/
public static final String PREF_LAST_BINARY_VERSION = "lastBinaryVersion";
/**
* {@link EventProcessor}
*/
public static final String PREF_EVENT_PROCESSOR_LAST_SYNC_ID = "last_sync_id";
/** /**
* Available options cache for preference {@link app_settings#debug_facilities_enabled} * Available options cache for preference {@link app_settings#debug_facilities_enabled}
* Read via REST API call in {@link RestApi#updateDebugFacilitiesCache} after first successful binary startup. * Read via REST API call in {@link RestApi#updateDebugFacilitiesCache} after first successful binary startup.
@ -106,6 +115,11 @@ public class Constants {
return new File(context.getFilesDir(), "https-cert.pem"); return new File(context.getFilesDir(), "https-cert.pem");
} }
/**
* Name of the export file holding the SharedPreferences backup.
*/
static final String SHARED_PREFS_EXPORT_FILE = "sharedpreferences.dat";
static File getSyncthingBinary(Context context) { static File getSyncthingBinary(Context context) {
return new File(context.getApplicationInfo().nativeLibraryDir, FILENAME_SYNCTHING_BINARY); return new File(context.getApplicationInfo().nativeLibraryDir, FILENAME_SYNCTHING_BINARY);
} }

View file

@ -36,7 +36,6 @@ import javax.inject.Inject;
public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener { public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener {
private static final String TAG = "EventProcessor"; private static final String TAG = "EventProcessor";
private static final String PREF_LAST_SYNC_ID = "last_sync_id";
/** /**
* Minimum interval in seconds at which the events are polled from syncthing and processed. * Minimum interval in seconds at which the events are polled from syncthing and processed.
@ -68,7 +67,7 @@ public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener
public void run() { public void run() {
// Restore the last event id if the event processor may have been restarted. // Restore the last event id if the event processor may have been restarted.
if (mLastEventId == 0) { if (mLastEventId == 0) {
mLastEventId = mPreferences.getLong(PREF_LAST_SYNC_ID, 0); mLastEventId = mPreferences.getLong(Constants.PREF_EVENT_PROCESSOR_LAST_SYNC_ID, 0);
} }
// First check if the event number ran backwards. // First check if the event number ran backwards.
@ -178,7 +177,7 @@ public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener
mLastEventId = id; mLastEventId = id;
// Store the last EventId in case we get killed // Store the last EventId in case we get killed
mPreferences.edit().putLong(PREF_LAST_SYNC_ID, mLastEventId).apply(); mPreferences.edit().putLong(Constants.PREF_EVENT_PROCESSOR_LAST_SYNC_ID, mLastEventId).apply();
} }
synchronized (mMainThreadHandler) { synchronized (mMainThreadHandler) {

View file

@ -23,12 +23,18 @@ import com.nutomic.syncthingandroid.model.Folder;
import com.nutomic.syncthingandroid.util.ConfigXml; import com.nutomic.syncthingandroid.util.ConfigXml;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.net.URL; import java.net.URL;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject; import javax.inject.Inject;
@ -626,7 +632,11 @@ public class SyncthingService extends Service {
/** /**
* Exports the local config and keys to {@link Constants#EXPORT_PATH}. * Exports the local config and keys to {@link Constants#EXPORT_PATH}.
*/ */
public void exportConfig() { public boolean exportConfig() {
Boolean failSuccess = true;
Log.v(TAG, "exportConfig BEGIN");
// Copy config, privateKey and/or publicKey to export path.
Constants.EXPORT_PATH.mkdirs(); Constants.EXPORT_PATH.mkdirs();
try { try {
Files.copy(Constants.getConfigFile(this), Files.copy(Constants.getConfigFile(this),
@ -637,7 +647,39 @@ public class SyncthingService extends Service {
new File(Constants.EXPORT_PATH, Constants.PUBLIC_KEY_FILE)); new File(Constants.EXPORT_PATH, Constants.PUBLIC_KEY_FILE));
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "Failed to export config", e); Log.w(TAG, "Failed to export config", e);
failSuccess = false;
} }
// Export SharedPreferences.
File file;
FileOutputStream fileOutputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
file = new File(Constants.EXPORT_PATH, Constants.SHARED_PREFS_EXPORT_FILE);
fileOutputStream = new FileOutputStream(file);
if (!file.exists()) {
file.createNewFile();
}
objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(mPreferences.getAll());
objectOutputStream.flush();
fileOutputStream.flush();
} catch (IOException e) {
Log.e(TAG, "exportConfig: Failed to export SharedPreferences #1", e);
failSuccess = false;
} finally {
try {
if (objectOutputStream != null) {
objectOutputStream.close();
}
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
Log.e(TAG, "exportConfig: Failed to export SharedPreferences #2", e);
}
}
return failSuccess;
} }
/** /**
@ -646,21 +688,116 @@ public class SyncthingService extends Service {
* @return True if the import was successful, false otherwise (eg if files aren't found). * @return True if the import was successful, false otherwise (eg if files aren't found).
*/ */
public boolean importConfig() { public boolean importConfig() {
Boolean failSuccess = true;
Log.v(TAG, "importConfig BEGIN");
// Shutdown synchronously.
shutdown(State.DISABLED, () -> {});
// Import config, privateKey and/or publicKey.
try {
File config = new File(Constants.EXPORT_PATH, Constants.CONFIG_FILE); File config = new File(Constants.EXPORT_PATH, Constants.CONFIG_FILE);
File privateKey = new File(Constants.EXPORT_PATH, Constants.PRIVATE_KEY_FILE); File privateKey = new File(Constants.EXPORT_PATH, Constants.PRIVATE_KEY_FILE);
File publicKey = new File(Constants.EXPORT_PATH, Constants.PUBLIC_KEY_FILE); File publicKey = new File(Constants.EXPORT_PATH, Constants.PUBLIC_KEY_FILE);
if (!config.exists() || !privateKey.exists() || !publicKey.exists())
return false; // Check if necessary files for import are available.
shutdown(State.INIT, () -> { if (config.exists() && privateKey.exists() && publicKey.exists()) {
try {
Files.copy(config, Constants.getConfigFile(this)); Files.copy(config, Constants.getConfigFile(this));
Files.copy(privateKey, Constants.getPrivateKeyFile(this)); Files.copy(privateKey, Constants.getPrivateKeyFile(this));
Files.copy(publicKey, Constants.getPublicKeyFile(this)); Files.copy(publicKey, Constants.getPublicKeyFile(this));
} else {
Log.e(TAG, "importConfig: config, privateKey and/or publicKey files missing");
failSuccess = false;
}
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "Failed to import config", e); Log.w(TAG, "importConfig: Failed to import config", e);
failSuccess = false;
} }
// Import SharedPreferences.
File file;
FileInputStream fileInputStream = null;
ObjectInputStream objectInputStream = null;
Map<String, Object> sharedPrefsMap = null;
try {
file = new File(Constants.EXPORT_PATH, Constants.SHARED_PREFS_EXPORT_FILE);
if (file.exists()) {
// Read, deserialize shared preferences.
fileInputStream = new FileInputStream(file);
objectInputStream = new ObjectInputStream(fileInputStream);
sharedPrefsMap = (Map) objectInputStream.readObject();
// Prepare a SharedPreferences commit.
SharedPreferences.Editor editor = mPreferences.edit();
editor.clear();
for (Map.Entry<String, Object> e : sharedPrefsMap.entrySet()) {
String prefKey = e.getKey();
switch (prefKey) {
// Preferences that are no longer used and left-overs from previous versions of the app.
case "first_start":
case "notify_crashes":
// Cached information which is not available on SettingsActivity.
case Constants.PREF_DEBUG_FACILITIES_AVAILABLE:
case Constants.PREF_EVENT_PROCESSOR_LAST_SYNC_ID:
case Constants.PREF_LAST_BINARY_VERSION:
Log.v(TAG, "importConfig: Ignoring pref \"" + prefKey + "\".");
break;
default:
Log.v(TAG, "importConfig: Adding pref \"" + prefKey + "\" to commit ...");
// The editor only provides typed setters.
if (e.getValue() instanceof Boolean) {
editor.putBoolean(prefKey, (Boolean) e.getValue());
} else if (e.getValue() instanceof String) {
editor.putString(prefKey, (String) e.getValue());
} else if (e.getValue() instanceof Integer) {
editor.putInt(prefKey, (int) e.getValue());
} else if (e.getValue() instanceof Float) {
editor.putFloat(prefKey, (float) e.getValue());
} else if (e.getValue() instanceof Long) {
editor.putLong(prefKey, (Long) e.getValue());
} else if (e.getValue() instanceof Set) {
editor.putStringSet(prefKey, (Set<String>) e.getValue());
} else {
Log.v(TAG, "importConfig: SharedPref type " + e.getValue().getClass().getName() + " is unknown");
}
break;
}
}
/**
* If all shared preferences have been added to the commit successfully,
* apply the commit.
*/
failSuccess = failSuccess && editor.commit();
} else {
// File not found.
Log.w(TAG, "importConfig: SharedPreferences file missing. This is expected if you migrate from the official app to the forked app.");
/**
* Don't fail as the file might be expectedly missing when users migrate
* to the forked app.
*/
}
} catch (IOException | ClassNotFoundException e) {
Log.e(TAG, "importConfig: Failed to import SharedPreferences #1", e);
failSuccess = false;
} finally {
try {
if (objectInputStream != null) {
objectInputStream.close();
}
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
Log.e(TAG, "importConfig: Failed to import SharedPreferences #2", e);
}
}
// Start syncthing after successful import if run conditions apply.
if (mLastDeterminedShouldRun) {
launchStartupTask(SyncthingRunnable.Command.main); launchStartupTask(SyncthingRunnable.Command.main);
}); }
return true; return failSuccess;
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View file

@ -114,6 +114,17 @@
android:clickable="true" android:clickable="true"
android:focusable="true" /> android:focusable="true" />
<TextView
android:id="@+id/drawerActionImportExport"
style="@style/Widget.Syncthing.TextView.Label"
android:layout_width="match_parent"
android:layout_height="48dp"
android:drawableLeft="@drawable/ic_import_export_black_24dp"
android:drawableStart="@drawable/ic_import_export_black_24dp"
android:text="@string/category_backup"
android:clickable="true"
android:focusable="true" />
<TextView <TextView
android:id="@+id/drawerActionRestart" android:id="@+id/drawerActionRestart"
style="@style/Widget.Syncthing.TextView.Label" style="@style/Widget.Syncthing.TextView.Label"

View file

@ -294,7 +294,7 @@ Please report any problems you encounter via Github.</string>
<string name="category_run_conditions">Run Conditions</string> <string name="category_run_conditions">Run Conditions</string>
<string name="category_behaviour">Behaviour</string> <string name="category_behaviour">Behaviour</string>
<string name="category_syncthing_options">Syncthing Options</string> <string name="category_syncthing_options">Syncthing Options</string>
<string name="category_backup">Backup</string> <string name="category_backup">Import and Export</string>
<string name="category_debug">Debug</string> <string name="category_debug">Debug</string>
<string name="category_experimental">Experimental</string> <string name="category_experimental">Experimental</string>
@ -466,15 +466,14 @@ Please report any problems you encounter via Github.</string>
<!-- Dialog shown before config import --> <!-- Dialog shown before config import -->
<string name="dialog_confirm_import">Do you really want to import a new configuration? Existing files will be overwritten.</string> <string name="dialog_confirm_import">Do you really want to import a new configuration? Existing files will be overwritten.</string>
<!-- Toast shown after config was successfully exported --> <!-- Toast shown after config was exported -->
<string name="config_export_successful">Config was exported to %1$s</string> <string name="config_export_successful">Config was exported to %1$s</string>
<string name="config_export_failed">Config export failed, check logcat output.</string>
<string name="import_config">Import Configuration</string> <string name="import_config">Import Configuration</string>
<!-- Toast shown after config was successfully imported --> <!-- Toast shown after config was imported -->
<string name="config_imported_successful">Config was imported</string> <string name="config_imported_successful">Config was imported</string>
<!-- Toast shown after config was successfully imported -->
<string name="config_import_failed">Config import failed, make sure files are in %1$s</string> <string name="config_import_failed">Config import failed, make sure files are in %1$s</string>
<!-- Title for the preference to set STTRACE parameters --> <!-- Title for the preference to set STTRACE parameters -->

View file

@ -193,7 +193,7 @@
</PreferenceScreen> </PreferenceScreen>
<PreferenceScreen <PreferenceScreen
android:key="category_backup" android:key="category_import_export"
android:title="@string/category_backup"> android:title="@string/category_backup">
<Preference <Preference