From 2accff95f4a02ac712c1da2f61f5d01030bd5c0c Mon Sep 17 00:00:00 2001 From: matthiakl Date: Sun, 21 Jun 2026 14:22:13 +0200 Subject: [PATCH 1/4] Export/Import settings --- res/values/strings.xml | 7 + res/xml/settings.xml | 6 +- srcs/juloo.keyboard2/SettingsActivity.java | 145 ++++++++++++++++++++- 3 files changed, 155 insertions(+), 3 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 028491c0e..4428c11f9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -165,4 +165,11 @@ When a physical keyboard is connected Hide everything Show everything + Backup + Import settings + Export settings + Settings exported + Settings export failed + Settings imported, app will restart + Settings import failed diff --git a/res/xml/settings.xml b/res/xml/settings.xml index 2fc12a05d..ecc8ecb82 100644 --- a/res/xml/settings.xml +++ b/res/xml/settings.xml @@ -67,4 +67,8 @@ - + + + + + \ No newline at end of file diff --git a/srcs/juloo.keyboard2/SettingsActivity.java b/srcs/juloo.keyboard2/SettingsActivity.java index dffc986dc..7dcb177af 100644 --- a/srcs/juloo.keyboard2/SettingsActivity.java +++ b/srcs/juloo.keyboard2/SettingsActivity.java @@ -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"); + 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 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 + "=" + value + ";" + valueType + "\n"); + } + + post_toast(R.string.import_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(); + + editor = sharedPreferences.edit(); + String line; + while ((line = reader.readLine()) != null) { + String[] keyValue = line.split("=", 2); + if (keyValue.length == 2) { + String[] valueAndType = keyValue[1].split(";", 2); + if (valueAndType.length == 2) { + String value = valueAndType[0]; + String type = valueAndType[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); + }, 2000 + ); + } catch (IOException e) { + 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(); + } } From 4f5466727972ec4e6f699f49862a993f6e64f2dd Mon Sep 17 00:00:00 2001 From: matthiakl Date: Sun, 21 Jun 2026 15:08:07 +0200 Subject: [PATCH 2/4] Fix custom layout handling --- srcs/juloo.keyboard2/SettingsActivity.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/srcs/juloo.keyboard2/SettingsActivity.java b/srcs/juloo.keyboard2/SettingsActivity.java index 7dcb177af..8a2737fec 100644 --- a/srcs/juloo.keyboard2/SettingsActivity.java +++ b/srcs/juloo.keyboard2/SettingsActivity.java @@ -120,10 +120,10 @@ private void exportToFile(Uri uri) { Object value = allPrefs.get(key); if (value == null) continue; String valueType = value.getClass().getSimpleName(); - writer.write(key + "=" + value + ";" + valueType + "\n"); + writer.write(key + "=" + valueType + ";" + value + "\n"); } - post_toast(R.string.import_success); + post_toast(R.string.export_success); } catch (IOException e) { Log.e("Settings", "Error exporting prefs", e); post_toast(R.string.export_fail); @@ -143,10 +143,10 @@ private void importFromFile(Uri uri) { while ((line = reader.readLine()) != null) { String[] keyValue = line.split("=", 2); if (keyValue.length == 2) { - String[] valueAndType = keyValue[1].split(";", 2); - if (valueAndType.length == 2) { - String value = valueAndType[0]; - String type = valueAndType[1]; + 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)); From 65854261ba2d0bbe10b5af0568c0583a1c222c96 Mon Sep 17 00:00:00 2001 From: matthiakl Date: Sat, 4 Jul 2026 17:00:08 +0200 Subject: [PATCH 3/4] Address review --- res/values/strings.xml | 5 +- srcs/juloo.keyboard2/SettingsActivity.java | 68 ++++++++++++---------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 4428c11f9..64b662297 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -168,8 +168,5 @@ Backup Import settings Export settings - Settings exported - Settings export failed - Settings imported, app will restart - Settings import failed + Operation failed diff --git a/srcs/juloo.keyboard2/SettingsActivity.java b/srcs/juloo.keyboard2/SettingsActivity.java index 8a2737fec..2c85c9ada 100644 --- a/srcs/juloo.keyboard2/SettingsActivity.java +++ b/srcs/juloo.keyboard2/SettingsActivity.java @@ -14,6 +14,9 @@ import android.util.Log; import android.widget.Toast; +import org.json.JSONArray; +import org.json.JSONObject; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -64,7 +67,7 @@ public void onCreate(Bundle savedInstanceState) Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TITLE, "prefs.txt"); + intent.putExtra(Intent.EXTRA_TITLE, "unexpected-keyboard-prefs.txt"); this.startActivityForResult(Intent.createChooser(intent, getString(R.string.pref_settings_export)), REQUEST_EXPORT); return true; @@ -116,17 +119,22 @@ private void exportToFile(Uri uri) { try (OutputStream stream = this.getContentResolver().openOutputStream(uri); OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8)) { Map allPrefs = sharedPreferences.getAll(); + JSONArray json = new JSONArray(); 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"); + JSONObject entry = new JSONObject(); + entry.put("key", key); + entry.put("type", valueType); + entry.put("value", value.toString()); + json.put(entry); } + writer.write(json.toString(2)); - post_toast(R.string.export_success); - } catch (IOException e) { + } catch (Exception e) { Log.e("Settings", "Error exporting prefs", e); - post_toast(R.string.export_fail); + post_toast(R.string.export_import_fail); } } private void importFromFile(Uri uri) { @@ -138,35 +146,35 @@ private void importFromFile(Uri uri) { editor.clear(); editor.apply(); - editor = sharedPreferences.edit(); + StringBuilder fileContent = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { - String[] keyValue = line.split("=", 2); - 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; - } - } + fileContent.append(line); + } + + JSONArray json = new JSONArray(fileContent.toString()); + for (int i = 0; i < json.length(); i++) { + JSONObject entry = json.getJSONObject(i); + String key = entry.getString("key"); + String type = entry.getString("type"); + String value = entry.getString("value"); + switch (type) { + case "Integer": + editor.putInt(key, Integer.parseInt(value)); + break; + case "Float": + editor.putFloat(key, Float.parseFloat(value)); + break; + case "Boolean": + editor.putBoolean(key, Boolean.parseBoolean(value)); + break; + case "String": + editor.putString(key, value); + break; } } editor.apply(); - post_toast(R.string.import_success); // Restart app new Handler().postDelayed( () -> { @@ -177,9 +185,9 @@ private void importFromFile(Uri uri) { Runtime.getRuntime().exit(0); }, 2000 ); - } catch (IOException e) { + } catch (Exception e) { Log.e("Settings", "Error importing prefs", e); - post_toast(R.string.import_fail); + post_toast(R.string.export_import_fail); } } From 6219a07f227877ebfb1effcfeb8655093c2900f0 Mon Sep 17 00:00:00 2001 From: matthiakl Date: Sat, 4 Jul 2026 17:35:58 +0200 Subject: [PATCH 4/4] Only refresh UI instead of restarting --- srcs/juloo.keyboard2/SettingsActivity.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/srcs/juloo.keyboard2/SettingsActivity.java b/srcs/juloo.keyboard2/SettingsActivity.java index 2c85c9ada..d0ad9c61a 100644 --- a/srcs/juloo.keyboard2/SettingsActivity.java +++ b/srcs/juloo.keyboard2/SettingsActivity.java @@ -143,8 +143,6 @@ private void importFromFile(Uri uri) { // Clear all existing preferences SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.clear(); - editor.apply(); StringBuilder fileContent = new StringBuilder(); String line; @@ -175,16 +173,8 @@ private void importFromFile(Uri uri) { } editor.apply(); - // 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); - }, 2000 - ); + // Refresh UI + onCreate(null); } catch (Exception e) { Log.e("Settings", "Error importing prefs", e); post_toast(R.string.export_import_fail);