Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,11 @@
<string name="pref_physical_keyboard_behavior">When a physical keyboard is connected</string>
<string name="pref_physical_keyboard_behavior_hide">Hide everything</string>
<string name="pref_physical_keyboard_behavior_show">Show everything</string>
<string name="pref_category_backup">Backup</string>
<string name="pref_settings_import">Import settings</string>
<string name="pref_settings_export">Export settings</string>
<string name="export_success">Settings exported</string>
<string name="export_fail">Settings export failed</string>
<string name="import_success">Settings imported, app will restart</string>
<string name="import_fail">Settings import failed</string>

Copy link
Copy Markdown
Owner

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.

Copy link
Copy Markdown
Contributor Author

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.

</resources>
6 changes: 5 additions & 1 deletion res/xml/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,8 @@
<PreferenceCategory android:title="@string/pref_category_clipboard">
<ListPreference android:key="clipboard_history_duration" android:title="@string/pref_clipboard_history_duration" android:summary="%s" android:defaultValue="5" android:entries="@array/pref_clipboard_duration_entries" android:entryValues="@array/pref_clipboard_duration_values"/>
</PreferenceCategory>
</PreferenceScreen>
<PreferenceCategory android:title="@string/pref_category_backup">
<Preference android:key="settings_import" android:title="@string/pref_settings_import"/>
<Preference android:key="settings_export" android:title="@string/pref_settings_export"/>
</PreferenceCategory>
</PreferenceScreen>
145 changes: 143 additions & 2 deletions srcs/juloo.keyboard2/SettingsActivity.java
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");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's name it "unexpected-keyboard-prefs.txt" ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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);
Expand All @@ -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.
Expand All @@ -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");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format breaks if value ever contains a newline character. For some reason this doesn't happen but nothing in this code prevents it.
Custom layouts can contain newlines but by chance, when converted to JSON (in ListGroupPreference), we use the compact encoding. This was not made on purpose and there's chance that we break this in the future.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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();
Comment thread
matthiakl marked this conversation as resolved.
Outdated

editor = sharedPreferences.edit();
String line;
while ((line = reader.readLine()) != null) {
String[] keyValue = line.split("=", 2);
Comment thread
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);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the settings updated automatically after editor.apply() ? I'd prefer if the activity was not restarted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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) {
Comment thread
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();
}
}
Loading