Merge branch '1160-language-setting' into 'master'

Add language setting

Closes #1160 and #1222

See merge request akwizgran/briar!679
This commit is contained in:
akwizgran
2018-06-08 11:20:39 +00:00
13 changed files with 308 additions and 19 deletions

View File

@@ -4,3 +4,4 @@ build
local.properties
.settings
src/main/assets/*.zip
src/main/res/values-iw

View File

@@ -285,3 +285,46 @@ 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-") && !dir.name.endsWith("night")) {
folders.add(dir.name.substring(7).replace("-r", "-"))
}
}
folders.each { n ->
if (!translations.remove(n) && n != 'iw') {
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")
// Some devices use iw instead of he for hebrew
def hebrew_legacy = new File("briar-android/src/main/res/values-iw")
def hebrew = new File("briar-android/src/main/res/values-he")
// Copy values-he to values-iw
if (hebrew.exists()) {
hebrew_legacy.mkdir()
copy {
from 'src/main/res/values-he'
into 'src/main/res/values-iw'
}
}
}
}
project.afterEvaluate {
preBuild.dependsOn.add(verifyTranslations)
}

View File

@@ -2,9 +2,12 @@ package org.briarproject.briar.android;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
import android.os.StrictMode.VmPolicy;
import android.preference.PreferenceManager;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
@@ -75,7 +78,12 @@ public class BriarApplicationImpl extends Application
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(base);
// Loading the language needs to be done here.
Localizer.initialize(prefs);
super.attachBaseContext(
Localizer.getInstance().setLocale(base));
ACRA.init(this);
}
@@ -108,6 +116,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();

View File

@@ -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(

View File

@@ -0,0 +1,79 @@
package org.briarproject.briar.android;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Locale;
import javax.annotation.Nullable;
import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE;
@NotNullByDefault
public class Localizer {
// Locking: class
@Nullable
private static Localizer INSTANCE;
@Nullable
private final Locale locale;
private Localizer(SharedPreferences sharedPreferences) {
locale = getLocaleFromTag(
sharedPreferences.getString(LANGUAGE, "default"));
}
public static synchronized void initialize(SharedPreferences prefs) {
if (INSTANCE == null)
INSTANCE = new Localizer(prefs);
}
public static synchronized Localizer getInstance() {
if (INSTANCE == null)
throw new IllegalStateException("Localizer not initialized");
return INSTANCE;
}
// 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;
}
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -36,5 +36,4 @@ public class SettingsActivity extends BriarActivity {
}
return false;
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.media.Ringtone;
@@ -8,6 +9,7 @@ import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.v4.text.TextUtilsCompat;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
@@ -30,8 +32,13 @@ 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.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -50,6 +57,7 @@ import static android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -58,6 +66,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 +89,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 +130,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 +150,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
setSettingsEnabled(false);
language.setOnPreferenceChangeListener(this);
enableBluetooth.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this);
if (SDK_INT >= 21) {
@@ -180,6 +194,51 @@ public class SettingsFragment extends PreferenceFragmentCompat
eventBus.removeListener(this);
}
private void setLanguageEntries() {
CharSequence[] tags = language.getEntryValues();
List<CharSequence> entries = new ArrayList<>(tags.length);
List<CharSequence> entryValues = new ArrayList<>(tags.length);
for (CharSequence cs : tags) {
String tag = cs.toString();
if (tag.equals("default")) {
entries.add(getString(R.string.pref_language_default));
entryValues.add(tag);
continue;
}
Locale locale = Localizer.getLocaleFromTag(tag);
if (locale == null)
throw new IllegalStateException();
// Exclude RTL locales on API < 17, they won't be laid out correctly
if (SDK_INT < 17 && !isLeftToRight(locale)) {
if (LOG.isLoggable(INFO))
LOG.info("Skipping RTL locale " + tag);
continue;
}
String nativeName = locale.getDisplayName(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;
}
// Prefix with LRM marker to prevent any RTL direction
entries.add("\u200E" + nativeName.substring(0, 1).toUpperCase()
+ nativeName.substring(1));
entryValues.add(tag);
}
language.setEntries(entries.toArray(new CharSequence[0]));
language.setEntryValues(entryValues.toArray(new CharSequence[0]));
}
private boolean isLeftToRight(Locale locale) {
// TextUtilsCompat returns the wrong direction for Hebrew on some phones
String language = locale.getLanguage();
if (language.equals("iw") || language.equals("he")) return false;
int direction = TextUtilsCompat.getLayoutDirectionFromLocale(locale);
return direction == LAYOUT_DIRECTION_LTR;
}
private void loadSettings() {
listener.runOnDbThread(() -> {
try {
@@ -312,41 +371,65 @@ 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);
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 {

View File

@@ -18,4 +18,37 @@
<item>1</item>
<item>2</item>
</string-array>
</resources>
<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>pl</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>

View File

@@ -323,6 +323,12 @@
<string name="blogs_rss_feeds_manage_empty_state">No RSS feeds to show\n\nTap the + icon to import a feed</string>
<string name="blogs_rss_feeds_manage_error">There was a problem loading your feeds. Please try again later.</string>
<!-- Settings Display -->
<string name="pref_language_title">Language &amp; region</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>
<!-- Settings Network -->
<string name="network_settings_title">Networks</string>
<string name="bluetooth_setting">Connect via Bluetooth</string>

View File

@@ -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"

View File

@@ -1,6 +1,8 @@
package org.briarproject.briar.android;
import android.app.Application;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.briar.BriarCoreModule;
@@ -27,6 +29,8 @@ public class TestBriarApplication extends Application
super.onCreate();
LOG.info("Created");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Localizer.initialize(prefs);
applicationComponent = DaggerAndroidComponent.builder()
.appModule(new AppModule(this))
.build();