Skip to content
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

Improve app language switching locic and workaround google locale issue #2437

Merged
merged 1 commit into from Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/docs/help/en/Advanced preferences.md
Expand Up @@ -74,7 +74,7 @@ Show the menu buttons at the bottom of the screen. Default: _on_. You need to re

### App language

Select a language for the user interface that is different from the device default. On devices running Android 13 and later the app language can be changed in the system settings too. Preset translations can be disabled in the preset configurations.
Select a language for the user interface that is different from the device default, setting the value to _Device language_ will revert to using your preference for the whole device. On devices running Android 13 and later the app language can be changed in the system settings too. Preset translations can be disabled in the preset configurations.

### Max. number of inline values

Expand Down
139 changes: 139 additions & 0 deletions src/androidTest/java/de/blau/android/prefs/AppLocalePrefTest.java
@@ -0,0 +1,139 @@
package de.blau.android.prefs;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.Locale;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import android.app.Instrumentation;
import android.app.Instrumentation.ActivityMonitor;
import android.view.View;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.os.LocaleListCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import de.blau.android.LayerUtils;
import de.blau.android.Main;
import de.blau.android.Map;
import de.blau.android.R;
import de.blau.android.TestUtils;
import de.blau.android.prefs.AdvancedPrefDatabase.Geocoder;

/**
* Note these tests are not mocked
*
* @author simon
*
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class AppLocalePrefTest {

Main main = null;
View v = null;
ActivityMonitor monitor = null;
Instrumentation instrumentation = null;
UiDevice device = null;

/**
* Manual start of activity so that we can set up the monitor for main
*/
@Rule
public ActivityTestRule<Main> mActivityRule = new ActivityTestRule<>(Main.class);

/**
* Pre-test setup
*
* Note that there doesn't seem to be an easy way to set the app locale from the test
*/
@Before
public void setup() {
instrumentation = InstrumentationRegistry.getInstrumentation();
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
main = mActivityRule.getActivity();
Preferences prefs = new Preferences(main);
LayerUtils.removeImageryLayers(main);
Map map = main.getMap();
map.setPrefs(main, prefs);

TestUtils.grantPermissons(device);
TestUtils.dismissStartUpDialogs(device, main);
TestUtils.stopEasyEdit(main);
}

/**
* Post-test teardown
*/
@After
public void teardown() {
instrumentation.removeMonitor(monitor);
}

/**
* Start prefs, advanced prefs, app locale
*/
@Test
public void setToEnglishUS() {
monitor = instrumentation.addMonitor(PrefEditor.class.getName(), null, false);
assertTrue(TestUtils.clickButton(device, device.getCurrentPackageName() + ":id/menu_config", true));
instrumentation.waitForMonitorWithTimeout(monitor, 40000); //

if (!TestUtils.scrollToAndSelect(device, main.getString(R.string.config_advancedprefs), new UiSelector().scrollable(true))) {
fail("Didn't find " + main.getString(R.string.config_advancedprefs));
}

assertTrue(TestUtils.clickText(device, false, main.getString(R.string.config_advancedprefs), true));
assertTrue(TestUtils.findText(device, false, main.getString(R.string.config_advancedprefs)));

assertTrue(TestUtils.clickText(device, false, main.getString(R.string.config_category_view), true, false));
assertTrue(TestUtils.findText(device, false, main.getString(R.string.config_category_view)));

if (!TestUtils.scrollToAndSelect(device, main.getString(R.string.config_appLocale_title), new UiSelector().scrollable(true))) {
fail("Didn't find " + main.getString(R.string.config_appLocale_title));
}

assertTrue(TestUtils.clickText(device, false, main.getString(R.string.config_appLocale_title), true, false));
assertTrue(TestUtils.findText(device, false, main.getString(R.string.config_appLocale_title)));

UiObject2 deviceLangCheck = TestUtils.findObjectWithText(device, false, main.getString(R.string.config_appLocale_device_language), 500, false);
assertTrue(deviceLangCheck.isChecked());

TestUtils.scrollTo(Locale.US.getDisplayName(Locale.getDefault()), false);
UiObject2 german = TestUtils.findObjectWithText(device, false, Locale.US.getDisplayName(Locale.getDefault()), 500, false);
assertFalse(german.isChecked());
german.click();

assertTrue(TestUtils.findText(device, false, main.getString(R.string.config_category_view), 5000));
assertTrue(TestUtils.clickText(device, false, main.getString(R.string.config_category_view), true, false));
assertTrue(TestUtils.findText(device, false, main.getString(R.string.config_category_view)));

if (!TestUtils.scrollToAndSelect(device, main.getString(R.string.config_appLocale_title), new UiSelector().scrollable(true))) {
fail("Didn't find " + main.getString(R.string.config_appLocale_title));
}

assertTrue(TestUtils.clickText(device, false, main.getString(R.string.config_appLocale_title), true, false));
assertTrue(TestUtils.findText(device, false, main.getString(R.string.config_appLocale_title)));

TestUtils.scrollTo(main.getString(R.string.config_appLocale_device_language), false);
deviceLangCheck = TestUtils.findObjectWithText(device, false, main.getString(R.string.config_appLocale_device_language), 500, false);
assertFalse(deviceLangCheck.isChecked());
deviceLangCheck.click();

TestUtils.clickHome(device, false);
}
}
2 changes: 1 addition & 1 deletion src/main/assets/help/en/Advanced preferences.html
Expand Up @@ -43,7 +43,7 @@ <h3>Theme</h3>
<h3>Enable split action bar</h3>
<p>Show the menu buttons at the bottom of the screen. Default: <em>on</em>. You need to restart the app for changes to this setting to take effect.</p>
<h3>App language</h3>
<p>Select a language for the user interface that is different from the device default. On devices running Android 13 and later the app language can be changed in the system settings too. Preset translations can be disabled in the preset configurations.</p>
<p>Select a language for the user interface that is different from the device default, setting the value to <em>Device language</em> will revert to using your preference for the whole device. On devices running Android 13 and later the app language can be changed in the system settings too. Preset translations can be disabled in the preset configurations.</p>
<h3>Max. number of inline values</h3>
<p>Maximum number of values that will directly be displayed in the form based editor for Tags with pre-determined values. Default: <em>4</em>.</p>
<h3>Long string limit</h3>
Expand Down
45 changes: 27 additions & 18 deletions src/main/java/de/blau/android/prefs/AdvancedPrefEditorFragment.java
Expand Up @@ -91,37 +91,46 @@ public void onResume() {
}

/**
* Setup the app local preference
* Setup the app locale preference
*
* @param appLocalePref the preference
*
*/
private void setupAppLocalePref(@NonNull ListPreference appLocalePref) {
appLocalePref.setPersistent(false); // stored by the device
Locale currentLocale = Locale.getDefault();
LocaleListCompat appLocales = LocaleUtils.getSupportedLocales(getContext());
LocaleListCompat currentLocales = AppCompatDelegate.getApplicationLocales();
if (!currentLocales.isEmpty()) {
LocaleListCompat temp = LocaleListCompat.getAdjustedDefault();
if (!temp.isEmpty()) {
currentLocale = temp.get(0);
}
LocaleListCompat currentAppLocales = AppCompatDelegate.getApplicationLocales();
boolean hasAppLocale = !currentAppLocales.isEmpty();
if (hasAppLocale) {
currentLocale = currentAppLocales.get(0);
}
String[] entries = new String[appLocales.size()];
String[] values = new String[appLocales.size()];
LocaleListCompat appLocales = LocaleUtils.getSupportedLocales(getContext());
final String[] entries = new String[appLocales.size() + 1];
String[] values = new String[appLocales.size() + 1];
entries[0] = getContext().getString(R.string.config_appLocale_device_language);
values[0] = "";
for (int i = 0; i < appLocales.size(); i++) {
Locale l = appLocales.get(i);
entries[i] = l.getDisplayName(currentLocale);
values[i] = l.toString();
entries[i + 1] = l.getDisplayName(currentLocale);
values[i + 1] = l.toString();
}
appLocalePref.setEntryValues(values);
appLocalePref.setEntries(entries);
appLocalePref.setDefaultValue(currentLocale.toString());
final String currentLocaleString = currentLocale.toString();
appLocalePref.setValue(hasAppLocale ? currentLocaleString : "");
appLocalePref.setSummary(hasAppLocale ? currentLocale.getDisplayName() : entries[0]);
OnPreferenceChangeListener p = (preference, newValue) -> {
Log.d(DEBUG_TAG, "onPreferenceChange appLocale " + newValue);
LocaleListCompat newDefaultList = LocaleListCompat.forLanguageTags((String) newValue);
Locale newDefault = newDefaultList.get(0);
AppCompatDelegate.setApplicationLocales(newDefaultList);
preference.setSummary(newDefault.getDisplayName(newDefault));
// Note google is very confused about Android with _ and proper, with - locale values, we try to circumvent
// that here
if (Util.notEmpty((String) newValue)) {
Locale newLocale = LocaleUtils.localeFromAndroidLocaleTag((String) newValue);
LocaleListCompat newAppList = LocaleListCompat.create(newLocale);
AppCompatDelegate.setApplicationLocales(newAppList);
preference.setSummary(newLocale.getDisplayName());
} else {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
preference.setSummary(entries[0]);
}
return true;
};
appLocalePref.setOnPreferenceChangeListener(p);
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/de/blau/android/util/LocaleUtils.java
Expand Up @@ -23,6 +23,8 @@ public final class LocaleUtils {

private static final String DEBUG_TAG = LocaleUtils.class.getSimpleName();

private static final String ANDROID_LOCALE_SEPARATOR = "_";

// list of languages that use Latin script from https://gist.github.com/phil-brown/8056700
private static Set<String> latin = new HashSet<>(Arrays.asList("aa", "ace", "ach", "ada", "af", "agq", "ak", "ale", "amo", "an", "arn", "arp", "arw", "asa",
"ast", "ay", "az", "bal", "ban", "bas", "bbc", "bem", "bez", "bi", "bik", "bin", "bku", "bla", "bm", "bqv", "br", "bs", "buc", "bug", "bya", "ca",
Expand Down Expand Up @@ -212,4 +214,16 @@ public static LocaleListCompat getSupportedLocales(@NonNull Context context) {
}
return LocaleListCompat.forLanguageTags(String.join(",", locales));
}

/**
* Construct a Locale from an Android format locale string
*
* @param localeString the Android format locale string
* @return a Locale
*/
@NonNull
public static Locale localeFromAndroidLocaleTag(@NonNull String localeString) {
String[] code = localeString.split(ANDROID_LOCALE_SEPARATOR);
return code.length == 1 ? new Locale(code[0]) : new Locale(code[0], code[1]);
}
}
1 change: 1 addition & 0 deletions src/main/res/values/strings.xml
Expand Up @@ -1205,6 +1205,7 @@
<string name="config_splitActionBarEnabled_summary">Show the menu buttons at the bottom of the screen. Requires starting Vespucci again, and Android 4.0 or later.</string>
<string name="config_appLocale_title">App language</string>
<string name="config_appLocale_summary">Select non-standard app language</string>
<string name="config_appLocale_device_language">Device language</string>
<string name="config_maxInlineValues_title">Max. number of inline values</string>
<string name="config_maxInlineValues_summary">Maximum number of inline values displayed in tag form.</string>
<string name="config_maxInlineValues_current">%1$d values</string>
Expand Down
4 changes: 2 additions & 2 deletions src/main/res/xml-v19/advancedpreferences.xml
Expand Up @@ -45,7 +45,7 @@
android:summary="@string/config_useInternalPhotoViewer_summary"
android:title="@string/config_useInternalPhotoViewer_title" />
<androidx.preference.CheckBoxPreference
android:defaultValue="alse"
android:defaultValue="false"
android:key="@string/config_indexMediaStore_key"
android:summary="@string/config_indexMediaStore_summary"
android:title="@string/config_indexMediaStore_title" />
Expand Down Expand Up @@ -101,7 +101,7 @@
android:key="@string/config_appLocale_key"
android:title="@string/config_appLocale_title"
android:summary="@string/config_appLocale_summary"
android:defaultValue="en" />
android:defaultValue="" />
<ch.poole.android.numberpickerpreference.NumberPickerPreference
android:defaultValue="4"
android:key="@string/config_maxInlineValues_key"
Expand Down
4 changes: 2 additions & 2 deletions src/main/res/xml-v24/advancedpreferences.xml
Expand Up @@ -45,7 +45,7 @@
android:summary="@string/config_useInternalPhotoViewer_summary"
android:title="@string/config_useInternalPhotoViewer_title" />
<androidx.preference.CheckBoxPreference
android:defaultValue="alse"
android:defaultValue="false"
android:key="@string/config_indexMediaStore_key"
android:summary="@string/config_indexMediaStore_summary"
android:title="@string/config_indexMediaStore_title" />
Expand Down Expand Up @@ -101,7 +101,7 @@
android:key="@string/config_appLocale_key"
android:title="@string/config_appLocale_title"
android:summary="@string/config_appLocale_summary"
android:defaultValue="en" />
android:defaultValue="" />
<ch.poole.android.numberpickerpreference.NumberPickerPreference
android:defaultValue="4"
android:key="@string/config_maxInlineValues_key"
Expand Down
4 changes: 2 additions & 2 deletions src/main/res/xml-v29/advancedpreferences.xml
Expand Up @@ -45,7 +45,7 @@
android:summary="@string/config_useInternalPhotoViewer_summary"
android:title="@string/config_useInternalPhotoViewer_title" />
<androidx.preference.CheckBoxPreference
android:defaultValue="alse"
android:defaultValue="false"
android:key="@string/config_indexMediaStore_key"
android:summary="@string/config_indexMediaStore_summary"
android:title="@string/config_indexMediaStore_title" />
Expand Down Expand Up @@ -101,7 +101,7 @@
android:key="@string/config_appLocale_key"
android:title="@string/config_appLocale_title"
android:summary="@string/config_appLocale_summary"
android:defaultValue="en" />
android:defaultValue="" />
<ch.poole.android.numberpickerpreference.NumberPickerPreference
android:defaultValue="4"
android:key="@string/config_maxInlineValues_key"
Expand Down
4 changes: 2 additions & 2 deletions src/main/res/xml/advancedpreferences.xml
Expand Up @@ -45,7 +45,7 @@
android:summary="@string/config_useInternalPhotoViewer_summary"
android:title="@string/config_useInternalPhotoViewer_title" />
<androidx.preference.CheckBoxPreference
android:defaultValue="alse"
android:defaultValue="false"
android:key="@string/config_indexMediaStore_key"
android:summary="@string/config_indexMediaStore_summary"
android:title="@string/config_indexMediaStore_title" />
Expand Down Expand Up @@ -101,7 +101,7 @@
android:key="@string/config_appLocale_key"
android:title="@string/config_appLocale_title"
android:summary="@string/config_appLocale_summary"
android:defaultValue="en" />
android:defaultValue="" />
<ch.poole.android.numberpickerpreference.NumberPickerPreference
android:defaultValue="4"
android:key="@string/config_maxInlineValues_key"
Expand Down