From 75e910e1d910b79784e53a2885b78763e7c0128f Mon Sep 17 00:00:00 2001 From: goapunk <goapunk@riseup.net> Date: Tue, 15 May 2018 19:03:25 +0200 Subject: [PATCH] Add a language setting --- briar-android/build.gradle | 31 +++++++ .../briar/android/BriarApplicationImpl.java | 12 ++- .../briar/android/BriarService.java | 5 ++ .../briarproject/briar/android/Localizer.java | 85 ++++++++++++++++++ .../briar/android/activity/BaseActivity.java | 8 ++ .../android/navdrawer/NavDrawerActivity.java | 15 ++-- .../android/settings/SettingsActivity.java | 1 - .../android/settings/SettingsFragment.java | 86 ++++++++++++++++--- briar-android/src/main/res/values/arrays.xml | 34 +++++++- briar-android/src/main/res/values/strings.xml | 6 ++ briar-android/src/main/res/xml/settings.xml | 11 +++ .../briar/android/TestBriarApplication.java | 1 + 12 files changed, 276 insertions(+), 19 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/Localizer.java diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 839eab3ef5..2a5d6b97bf 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -284,3 +284,34 @@ android { warning 'ExtraTranslation' } } + +task verifyTranslations { + doLast { + def file = "briar-android/src/main/res/values/arrays.xml" + def arrays = new XmlParser().parse(file) + def lc = arrays.children().find { it.@name == "pref_language_values" } + def translations = [] + lc.children().each { value -> translations.add(value.text()) } + + def folders = ["default", "en-US"] + new File("briar-android/src/main/res").eachDir { dir -> + if (dir.name.startsWith("values-")) { + folders.add(dir.name.substring(7).replace("-r", "-")) + } + } + folders.each { n -> + if (!translations.remove(n)) { + throw new GradleException("Translation " + n + " is missing in $file") + } + } + if (translations.size() != 0) + throw new GradleException("Translations\n" + translations.join("\n") + + "\nhave no matching value folder") + } +} + +project.afterEvaluate { + preBuild.dependsOn.add(verifyTranslations) +} + + diff --git a/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java index 97e19062a9..7b7e358460 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java @@ -2,6 +2,7 @@ package org.briarproject.briar.android; import android.app.Application; import android.content.Context; +import android.content.res.Configuration; import android.os.StrictMode; import android.os.StrictMode.ThreadPolicy; import android.os.StrictMode.VmPolicy; @@ -75,7 +76,10 @@ public class BriarApplicationImpl extends Application @Override protected void attachBaseContext(Context base) { - super.attachBaseContext(base); + // Loading the language needs to be done here. + Localizer.initialize(base); + super.attachBaseContext( + Localizer.getInstance().setLocale(base)); ACRA.init(this); } @@ -108,6 +112,12 @@ public class BriarApplicationImpl extends Application AndroidEagerSingletons.initEagerSingletons(applicationComponent); } + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + Localizer.getInstance().setLocale(this); + } + private void enableStrictMode() { ThreadPolicy.Builder threadPolicy = new ThreadPolicy.Builder(); threadPolicy.detectAll(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java b/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java index f3cc72c5b7..b6aaad4b93 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/BriarService.java @@ -168,6 +168,11 @@ public class BriarService extends Service { registerReceiver(receiver, filter); } + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(Localizer.getInstance().setLocale(base)); + } + private void showStartupFailureNotification(StartResult result) { androidExecutor.runOnUiThread(() -> { NotificationCompat.Builder b = new NotificationCompat.Builder( diff --git a/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java b/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java new file mode 100644 index 0000000000..c734f13f0e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/Localizer.java @@ -0,0 +1,85 @@ +package org.briarproject.briar.android; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.preference.PreferenceManager; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.Locale; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import static android.os.Build.VERSION.SDK_INT; +import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE; + +@NotNullByDefault +public class Localizer { + + private static Localizer INSTANCE; + @Nullable + private final Locale locale; + private final SharedPreferences sharedPreferences; + + private Localizer(Context context) { + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + locale = getLocaleFromTag( + sharedPreferences.getString(LANGUAGE, "default")); + } + + public static synchronized void initialize(Context context) { + if (INSTANCE == null) + INSTANCE = new Localizer(context); + } + + public static synchronized Localizer getInstance() { + if (INSTANCE == null) + throw new IllegalStateException("Localizer not initialized"); + return INSTANCE; + } + + public SharedPreferences getSharedPreferences() { + return sharedPreferences; + } + + // Get Locale from BCP-47 tag + @Nullable + public static Locale getLocaleFromTag(String tag) { + if (tag.equals("default")) + return null; + if (SDK_INT >= 21) { + return Locale.forLanguageTag(tag); + } + if (tag.contains("-")) { + String[] langArray = tag.split("-"); + return new Locale(langArray[0], langArray[1]); + } else + return new Locale(tag); + } + + public Context setLocale(Context context) { + if (locale == null) + return context; + Resources res = context.getResources(); + Configuration conf = res.getConfiguration(); + Locale currentLocale; + if (SDK_INT >= 24) { + currentLocale = conf.getLocales().get(0); + } else + currentLocale = conf.locale; + if (locale.equals(currentLocale)) + return context; + Locale.setDefault(locale); + if (SDK_INT >= 17) { + conf.setLocale(locale); + context.createConfigurationContext(conf); + } else + conf.locale = locale; + //noinspection deprecation + res.updateConfiguration(conf, res.getDisplayMetrics()); + return context; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java index 7c8f5061ae..c693ccbfc2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java @@ -1,5 +1,6 @@ package org.briarproject.briar.android.activity; +import android.content.Context; import android.os.Bundle; import android.os.IBinder; import android.support.annotation.LayoutRes; @@ -18,6 +19,7 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.AndroidComponent; import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.DestroyableContext; +import org.briarproject.briar.android.Localizer; import org.briarproject.briar.android.controller.ActivityLifecycleController; import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.fragment.BaseFragment; @@ -84,6 +86,12 @@ public abstract class BaseActivity extends AppCompatActivity } } + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext( + Localizer.getInstance().setLocale(base)); + } + public ActivityComponent getActivityComponent() { return activityComponent; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java index af09db7004..e7800f1db0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java @@ -68,6 +68,7 @@ public class NavDrawerActivity extends BriarActivity implements public static final String INTENT_GROUPS = "intent_groups"; public static final String INTENT_FORUMS = "intent_forums"; public static final String INTENT_BLOGS = "intent_blogs"; + public static final String INTENT_SIGN_OUT = "intent_sign_out"; private static final Logger LOG = Logger.getLogger(NavDrawerActivity.class.getName()); @@ -99,6 +100,8 @@ public class NavDrawerActivity extends BriarActivity implements R.id.nav_btn_contacts); } else if (intent.getBooleanExtra(INTENT_BLOGS, false)) { startFragment(FeedFragment.newInstance(), R.id.nav_btn_blogs); + } else if (intent.getBooleanExtra(INTENT_SIGN_OUT, false)) { + signOut(false); } setIntent(null); } @@ -225,12 +228,12 @@ public class NavDrawerActivity extends BriarActivity implements finish(); } else if (fm.getBackStackEntryCount() == 0 && fm.findFragmentByTag(ContactListFragment.TAG) == null) { - /* - * This makes sure that the first fragment (ContactListFragment) the - * user sees is the same as the last fragment the user sees before - * exiting. This models the typical Google navigation behaviour such - * as in Gmail/Inbox. - */ + /* + * This makes sure that the first fragment (ContactListFragment) the + * user sees is the same as the last fragment the user sees before + * exiting. This models the typical Google navigation behaviour such + * as in Gmail/Inbox. + */ startFragment(ContactListFragment.newInstance(), R.id.nav_btn_contacts); } else { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java index 37870a9831..48b4b7fa90 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsActivity.java @@ -36,5 +36,4 @@ public class SettingsActivity extends BriarActivity { } return false; } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java index c69dd65deb..f75ba97e30 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java @@ -1,8 +1,10 @@ package org.briarproject.briar.android.settings; import android.annotation.TargetApi; +import android.app.AlertDialog; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; @@ -30,8 +32,11 @@ import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent; import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.R; +import org.briarproject.briar.android.Localizer; +import org.briarproject.briar.android.navdrawer.NavDrawerActivity; import org.briarproject.briar.android.util.UserFeedback; +import java.util.Locale; import java.util.logging.Logger; import javax.inject.Inject; @@ -58,6 +63,7 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_ALWAYS; import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE; +import static org.briarproject.briar.android.navdrawer.NavDrawerActivity.INTENT_SIGN_OUT; import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID; import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID; import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID; @@ -80,11 +86,13 @@ public class SettingsFragment extends PreferenceFragmentCompat public static final String SETTINGS_NAMESPACE = "android-ui"; public static final String BT_NAMESPACE = BluetoothConstants.ID.getString(); public static final String TOR_NAMESPACE = TorConstants.ID.getString(); + public static final String LANGUAGE = "pref_key_language"; private static final Logger LOG = Logger.getLogger(SettingsFragment.class.getName()); private SettingsActivity listener; + private ListPreference language; private ListPreference enableBluetooth; private ListPreference torNetwork; private CheckBoxPreference notifyPrivateMessages; @@ -119,6 +127,8 @@ public class SettingsFragment extends PreferenceFragmentCompat public void onCreatePreferences(Bundle bundle, String s) { addPreferencesFromResource(R.xml.settings); + language = (ListPreference) findPreference(LANGUAGE); + setLanguageEntries(); enableBluetooth = (ListPreference) findPreference("pref_key_bluetooth"); torNetwork = (ListPreference) findPreference("pref_key_tor_network"); notifyPrivateMessages = (CheckBoxPreference) findPreference( @@ -137,6 +147,7 @@ public class SettingsFragment extends PreferenceFragmentCompat setSettingsEnabled(false); + language.setOnPreferenceChangeListener(this); enableBluetooth.setOnPreferenceChangeListener(this); torNetwork.setOnPreferenceChangeListener(this); if (SDK_INT >= 21) { @@ -180,6 +191,33 @@ public class SettingsFragment extends PreferenceFragmentCompat eventBus.removeListener(this); } + private void setLanguageEntries() { + CharSequence[] tags = language.getEntryValues(); + CharSequence[] nativeNames = new CharSequence[tags.length]; + for (int i = 0; i < tags.length; i++) { + String tag = tags[i].toString(); + if (tag.equals("default")) { + nativeNames[i] = getString(R.string.pref_language_default); + continue; + } + Locale locale = Localizer.getLocaleFromTag(tag); + if (locale == null) + throw new IllegalStateException(); + String nativeName = locale.getDisplayLanguage(locale); + // Fallback to English if the name is unknown in both native and + // current locale. + if (nativeName.equals(tag)) { + String tmp = locale.getDisplayLanguage(Locale.ENGLISH); + if (!tmp.isEmpty() && !tmp.equals(nativeName)) + nativeName = tmp; + } + nativeNames[i] = + nativeName.substring(0, 1).toUpperCase() + + nativeName.substring(1); + } + language.setEntries(nativeNames); + } + private void loadSettings() { listener.runOnDbThread(() -> { try { @@ -312,41 +350,69 @@ public class SettingsFragment extends PreferenceFragmentCompat } @Override - public boolean onPreferenceChange(Preference preference, Object o) { - if (preference == enableBluetooth) { - boolean btSetting = Boolean.valueOf((String) o); + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == language) { + if (!language.getValue().equals(newValue)) + languageChanged((String) newValue); + return false; + } else if (preference == enableBluetooth) { + boolean btSetting = Boolean.valueOf((String) newValue); storeBluetoothSettings(btSetting); } else if (preference == torNetwork) { - int torSetting = Integer.valueOf((String) o); + int torSetting = Integer.valueOf((String) newValue); storeTorSettings(torSetting); } else if (preference == notifyPrivateMessages) { Settings s = new Settings(); - s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) o); + s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) newValue); storeSettings(s); } else if (preference == notifyGroupMessages) { Settings s = new Settings(); - s.putBoolean(PREF_NOTIFY_GROUP, (Boolean) o); + s.putBoolean(PREF_NOTIFY_GROUP, (Boolean) newValue); storeSettings(s); } else if (preference == notifyForumPosts) { Settings s = new Settings(); - s.putBoolean(PREF_NOTIFY_FORUM, (Boolean) o); + s.putBoolean(PREF_NOTIFY_FORUM, (Boolean) newValue); storeSettings(s); } else if (preference == notifyBlogPosts) { Settings s = new Settings(); - s.putBoolean(PREF_NOTIFY_BLOG, (Boolean) o); + s.putBoolean(PREF_NOTIFY_BLOG, (Boolean) newValue); storeSettings(s); } else if (preference == notifyVibration) { Settings s = new Settings(); - s.putBoolean(PREF_NOTIFY_VIBRATION, (Boolean) o); + s.putBoolean(PREF_NOTIFY_VIBRATION, (Boolean) newValue); storeSettings(s); } else if (preference == notifyLockscreen) { Settings s = new Settings(); - s.putBoolean(PREF_NOTIFY_LOCK_SCREEN, (Boolean) o); + s.putBoolean(PREF_NOTIFY_LOCK_SCREEN, (Boolean) newValue); storeSettings(s); } return true; } + private void languageChanged(String newValue) { + AlertDialog.Builder builder = + new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.pref_language_title); + builder.setMessage(R.string.pref_language_changed); + builder.setPositiveButton(R.string.sign_out_button, + (dialogInterface, i) -> { + language.setValue(newValue); + SharedPreferences prefs = + Localizer.getInstance().getSharedPreferences(); + prefs.edit().putString(LANGUAGE, newValue) + .commit(); + Intent intent = new Intent(getContext(), + NavDrawerActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(INTENT_SIGN_OUT, true); + getActivity().startActivity(intent); + getActivity().finish(); + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setCancelable(false); + builder.show(); + } + private void storeTorSettings(int torSetting) { listener.runOnDbThread(() -> { try { diff --git a/briar-android/src/main/res/values/arrays.xml b/briar-android/src/main/res/values/arrays.xml index 3eea71ecd7..6ce3f24255 100644 --- a/briar-android/src/main/res/values/arrays.xml +++ b/briar-android/src/main/res/values/arrays.xml @@ -18,4 +18,36 @@ <item>1</item> <item>2</item> </string-array> -</resources> \ No newline at end of file + <string-array name="pref_language_values"> + <item>default</item> + <item>en-US</item> + <item>ast</item> + <item>bg</item> + <item>br</item> + <item>ca</item> + <item>cs</item> + <item>de</item> + <item>es</item> + <item>eu</item> + <item>fa</item> + <item>fi</item> + <item>fr</item> + <item>gl</item> + <item>he</item> + <item>hi</item> + <item>it</item> + <item>ja</item> + <item>ms</item> + <item>nb</item> + <item>nl</item> + <item>oc</item> + <item>pt-BR</item> + <item>ro</item> + <item>ru</item> + <item>sq</item> + <item>sr</item> + <item>sv</item> + <item>tr</item> + <item>zh-CN</item> + </string-array> +</resources> diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 1306feae1d..a0087bedd7 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -421,4 +421,10 @@ <string name="permission_camera_denied_toast">Camera permission was not granted</string> <string name="qr_code">QR code</string> <string name="show_qr_code_fullscreen">Show QR code fullscreen</string> + + <!-- Languages --> + <string name="pref_language_title">Language</string> + <string name="pref_language_changed">This setting will take effect when you restart Briar. Please sign out and restart Briar.</string> + <string name="pref_language_default">System default</string> + <string name="display_settings_title">Display</string> </resources> diff --git a/briar-android/src/main/res/xml/settings.xml b/briar-android/src/main/res/xml/settings.xml index 4528150143..7b3181fa05 100644 --- a/briar-android/src/main/res/xml/settings.xml +++ b/briar-android/src/main/res/xml/settings.xml @@ -1,6 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + <PreferenceCategory + android:layout="@layout/preferences_category" + android:title="@string/display_settings_title"> + + <ListPreference + android:defaultValue="default" + android:entryValues="@array/pref_language_values" + android:key="pref_key_language" + android:summary="%s" + android:title="@string/pref_language_title"/> + </PreferenceCategory> <PreferenceCategory android:layout="@layout/preferences_category" diff --git a/briar-android/src/test/java/org/briarproject/briar/android/TestBriarApplication.java b/briar-android/src/test/java/org/briarproject/briar/android/TestBriarApplication.java index 6f232bef0a..93433123b2 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/TestBriarApplication.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/TestBriarApplication.java @@ -27,6 +27,7 @@ public class TestBriarApplication extends Application super.onCreate(); LOG.info("Created"); + Localizer.initialize(this); applicationComponent = DaggerAndroidComponent.builder() .appModule(new AppModule(this)) .build(); -- GitLab