-
-
Notifications
You must be signed in to change notification settings - Fork 351
Export/Import settings #1337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Export/Import settings #1337
Changes from 2 commits
2accff9
4f54667
6585426
641c167
6219a07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,75 @@ | ||
| package juloo.keyboard2; | ||
|
|
||
| import android.app.Activity; | ||
| import android.content.Intent; | ||
| import android.content.SharedPreferences; | ||
| import android.content.res.Configuration; | ||
| import android.net.Uri; | ||
| import android.os.Build; | ||
| import android.os.Bundle; | ||
| import android.os.Handler; | ||
| import android.preference.Preference; | ||
| import android.preference.PreferenceActivity; | ||
| import android.preference.PreferenceManager; | ||
| import android.util.Log; | ||
| import android.widget.Toast; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.InputStreamReader; | ||
| import java.io.OutputStream; | ||
| import java.io.OutputStreamWriter; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Arrays; | ||
| import java.util.HashSet; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
| public class SettingsActivity extends PreferenceActivity | ||
| { | ||
| // Request code for file picker | ||
| private static final int REQUEST_EXPORT = 1001; | ||
| private static final int REQUEST_IMPORT = 1002; | ||
|
|
||
| private SharedPreferences sharedPreferences; | ||
| @Override | ||
| public void onCreate(Bundle savedInstanceState) | ||
| { | ||
| super.onCreate(savedInstanceState); | ||
| this.sharedPreferences = getPreferenceManager().getSharedPreferences(); | ||
| // The preferences can't be read when in direct-boot mode. Avoid crashing | ||
| // and don't allow changing the settings. | ||
| // Run the config migration on this prefs as it might be different from the | ||
| // one used by the keyboard, which have been migrated. | ||
| try | ||
| { | ||
| Config.migrate(getPreferenceManager().getSharedPreferences()); | ||
| Config.migrate(sharedPreferences); | ||
| } | ||
| catch (Exception _e) { fallbackEncrypted(); return; } | ||
| addPreferencesFromResource(R.xml.settings); | ||
|
|
||
| final Preference importDataPreference = findPreference("settings_import"); | ||
| importDataPreference.setOnPreferenceClickListener((Preference p) -> { | ||
| Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); | ||
| intent.addCategory(Intent.CATEGORY_OPENABLE); | ||
| intent.setType("text/*"); | ||
| this.startActivityForResult(Intent.createChooser(intent, getString(R.string.pref_settings_import)), REQUEST_IMPORT); | ||
|
|
||
| return true; | ||
| }); | ||
|
|
||
| final Preference exportDataPreference = findPreference("settings_export"); | ||
| exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { | ||
| Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); | ||
| intent.addCategory(Intent.CATEGORY_OPENABLE); | ||
| intent.setType("text/plain"); | ||
| intent.putExtra(Intent.EXTRA_TITLE, "prefs.txt"); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's name it
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| this.startActivityForResult(Intent.createChooser(intent, getString(R.string.pref_settings_export)), REQUEST_EXPORT); | ||
|
|
||
| return true; | ||
| }); | ||
|
|
||
| boolean foldableDevice = FoldStateTracker.isFoldableDevice(this); | ||
| findPreference("margin_bottom_portrait_unfolded").setEnabled(foldableDevice); | ||
| findPreference("margin_bottom_landscape_unfolded").setEnabled(foldableDevice); | ||
|
|
@@ -33,6 +79,25 @@ public void onCreate(Bundle savedInstanceState) | |
| findPreference("keyboard_height_landscape_unfolded").setEnabled(foldableDevice); | ||
| } | ||
|
|
||
| @Override | ||
| protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||
| super.onActivityResult(requestCode, resultCode, data); | ||
|
|
||
| if (resultCode != RESULT_OK || data == null) { | ||
| return; | ||
| } | ||
|
|
||
| Uri uri = data.getData(); | ||
| if (uri != null) { | ||
| if (requestCode == REQUEST_IMPORT) { | ||
| importFromFile(uri); | ||
| } | ||
| if (requestCode == REQUEST_EXPORT) { | ||
| exportToFile(uri); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void fallbackEncrypted() | ||
| { | ||
| // Can't communicate with the user here. | ||
|
|
@@ -43,7 +108,83 @@ protected void onStop() | |
| { | ||
| DirectBootAwarePreferences | ||
| .copy_preferences_to_protected_storage(this, | ||
| getPreferenceManager().getSharedPreferences()); | ||
| sharedPreferences); | ||
| super.onStop(); | ||
| } | ||
|
|
||
| private void exportToFile(Uri uri) { | ||
| try (OutputStream stream = this.getContentResolver().openOutputStream(uri); | ||
| OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8)) { | ||
| Map<String, ?> allPrefs = sharedPreferences.getAll(); | ||
| for (String key : allPrefs.keySet()) { | ||
| Object value = allPrefs.get(key); | ||
| if (value == null) continue; | ||
| String valueType = value.getClass().getSimpleName(); | ||
| writer.write(key + "=" + valueType + ";" + value + "\n"); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The format breaks if To avoid any format problems, I suggest switching to JSON. It might be worth looking at this constructor, which takes a Map as input: https://developer.android.com/reference/kotlin/org/json/JSONObject#JSONObject(kotlin.collections.MutableMap)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I replaced the format with a JSONArray. |
||
| } | ||
|
|
||
| post_toast(R.string.export_success); | ||
| } catch (IOException e) { | ||
| Log.e("Settings", "Error exporting prefs", e); | ||
| post_toast(R.string.export_fail); | ||
| } | ||
| } | ||
| private void importFromFile(Uri uri) { | ||
| try (InputStream inputStream = this.getContentResolver().openInputStream(uri); | ||
| BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { | ||
|
|
||
| // Clear all existing preferences | ||
| SharedPreferences.Editor editor = sharedPreferences.edit(); | ||
| editor.clear(); | ||
| editor.apply(); | ||
|
matthiakl marked this conversation as resolved.
Outdated
|
||
|
|
||
| editor = sharedPreferences.edit(); | ||
| String line; | ||
| while ((line = reader.readLine()) != null) { | ||
| String[] keyValue = line.split("=", 2); | ||
|
matthiakl marked this conversation as resolved.
Outdated
|
||
| if (keyValue.length == 2) { | ||
| String[] typeAndValue = keyValue[1].split(";", 2); | ||
| if (typeAndValue.length == 2) { | ||
| String type = typeAndValue[0]; | ||
| String value = typeAndValue[1]; | ||
| switch (type) { | ||
| case "Integer": | ||
| editor.putInt(keyValue[0], Integer.parseInt(value)); | ||
| break; | ||
| case "Float": | ||
| editor.putFloat(keyValue[0], Float.parseFloat(value)); | ||
| break; | ||
| case "Boolean": | ||
| editor.putBoolean(keyValue[0], Boolean.parseBoolean(value)); | ||
| break; | ||
| case "String": | ||
| editor.putString(keyValue[0], value); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| editor.apply(); | ||
|
|
||
| post_toast(R.string.import_success); | ||
| // Restart app | ||
| new Handler().postDelayed( | ||
| () -> { | ||
| Intent intent = new Intent(this, SettingsActivity.class); | ||
| intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); | ||
| this.startActivity(intent); | ||
| this.finish(); | ||
| Runtime.getRuntime().exit(0); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't the settings updated automatically after
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it somehow is updated in memory, but the UI does not receive an update. I will try to find another way.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After some searching I found a simple solution. Instead of restarting, only the activity is refreshed by recalling onCreate(). |
||
| }, 2000 | ||
| ); | ||
| } catch (IOException e) { | ||
|
matthiakl marked this conversation as resolved.
Outdated
|
||
| Log.e("Settings", "Error importing prefs", e); | ||
| post_toast(R.string.import_fail); | ||
| } | ||
| } | ||
|
|
||
| private void post_toast(int msg_id) | ||
| { | ||
| Toast.makeText(this, msg_id, Toast.LENGTH_SHORT).show(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's too many strings. Do we need all these toast message ? Importing and exporting should be instantaneous.
I think only the error case need a toast message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I also merged the failure into a single message.