New dynamic locale system.

- Fixes #7619
This commit is contained in:
Alan Evans 2019-03-25 17:23:38 -03:00
parent 6a0a419f0c
commit a7aa980e58
12 changed files with 370 additions and 44 deletions

View File

@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions; import org.webrtc.PeerConnectionFactory.InitializationOptions;
import org.webrtc.voiceengine.WebRtcAudioManager; import org.webrtc.voiceengine.WebRtcAudioManager;
@ -319,4 +320,9 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
BlobProvider.getInstance().onSessionStart(this); BlobProvider.getInstance().onSessionStart(this);
}); });
} }
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
}
} }

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
@ -16,6 +17,8 @@ import android.view.WindowManager;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import java.lang.reflect.Field; import java.lang.reflect.Field;
@ -35,6 +38,7 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
initializeScreenshotSecurity(); initializeScreenshotSecurity();
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
} }
@Override @Override
@ -90,4 +94,8 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
} }
} }
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
}
} }

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.content.Context;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
@ -7,6 +8,10 @@ import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent; import android.view.KeyEvent;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
public abstract class BaseActivity extends FragmentActivity { public abstract class BaseActivity extends FragmentActivity {
@Override @Override
public boolean onKeyDown(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent event) {
@ -27,4 +32,15 @@ public abstract class BaseActivity extends FragmentActivity {
VERSION.SDK_INT > VERSION_CODES.GINGERBREAD_MR1 && VERSION.SDK_INT > VERSION_CODES.GINGERBREAD_MR1 &&
("LGE".equalsIgnoreCase(Build.MANUFACTURER) || "E6710".equalsIgnoreCase(Build.DEVICE)); ("LGE".equalsIgnoreCase(Build.MANUFACTURER) || "E6710".equalsIgnoreCase(Build.DEVICE));
} }
@Override
protected void onResume() {
super.onResume();
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
}
} }

View File

@ -119,6 +119,13 @@ public class Log {
} }
} }
public static String tag(Class<?> clazz) {
String simpleName = clazz.getSimpleName();
if (simpleName.length() > 23) {
return simpleName.substring(0, 23);
}
return simpleName;
}
public static abstract class Logger { public static abstract class Logger {
public abstract void v(String tag, String message, Throwable t); public abstract void v(String tag, String message, Throwable t);

View File

@ -1,50 +1,35 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import org.thoughtcrime.securesms.util.dynamiclanguage.LanguageString;
import android.support.annotation.RequiresApi;
import android.text.TextUtils;
import java.util.Locale; import java.util.Locale;
/**
* @deprecated Use a base activity that uses the {@link org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper}
*/
@Deprecated
public class DynamicLanguage { public class DynamicLanguage {
private static final String DEFAULT = "zz";
private Locale currentLocale;
public void onCreate(Activity activity) { public void onCreate(Activity activity) {
currentLocale = getSelectedLocale(activity);
setContextLocale(activity, currentLocale);
} }
public void onResume(Activity activity) { public void onResume(Activity activity) {
if (!currentLocale.equals(getSelectedLocale(activity))) {
Intent intent = activity.getIntent();
activity.finish();
OverridePendingTransition.invoke(activity);
activity.startActivity(intent);
OverridePendingTransition.invoke(activity);
}
} }
public void updateServiceLocale(Service service) { public void updateServiceLocale(Service service) {
currentLocale = getSelectedLocale(service); setContextLocale(service, getSelectedLocale(service));
setContextLocale(service, currentLocale);
} }
public Locale getCurrentLocale() { public Locale getCurrentLocale() {
return currentLocale; return Locale.getDefault();
} }
@RequiresApi(VERSION_CODES.JELLY_BEAN_MR1) static int getLayoutDirection(Context context) {
public static int getLayoutDirection(Context context) {
Configuration configuration = context.getResources().getConfiguration(); Configuration configuration = context.getResources().getConfiguration();
return configuration.getLayoutDirection(); return configuration.getLayoutDirection();
} }
@ -53,34 +38,18 @@ public class DynamicLanguage {
Configuration configuration = context.getResources().getConfiguration(); Configuration configuration = context.getResources().getConfiguration();
if (!configuration.locale.equals(selectedLocale)) { if (!configuration.locale.equals(selectedLocale)) {
configuration.locale = selectedLocale; configuration.setLocale(selectedLocale);
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
configuration.setLayoutDirection(selectedLocale);
}
context.getResources().updateConfiguration(configuration, context.getResources().updateConfiguration(configuration,
context.getResources().getDisplayMetrics()); context.getResources().getDisplayMetrics());
} }
} }
private static Locale getActivityLocale(Activity activity) {
return activity.getResources().getConfiguration().locale;
}
private static Locale getSelectedLocale(Context context) { private static Locale getSelectedLocale(Context context) {
String language[] = TextUtils.split(TextSecurePreferences.getLanguage(context), "_"); Locale locale = LanguageString.parseLocale(TextSecurePreferences.getLanguage(context));
if (locale == null) {
if (language[0].equals(DEFAULT)) {
return Locale.getDefault(); return Locale.getDefault();
} else if (language.length == 2) {
return new Locale(language[0], language[1]);
} else { } else {
return new Locale(language[0]); return locale;
}
}
private static final class OverridePendingTransition {
static void invoke(Activity activity) {
activity.overridePendingTransition(0, 0);
} }
} }
} }

View File

@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.util.dynamiclanguage;
import android.app.Activity;
import android.content.Intent;
import android.support.annotation.MainThread;
import android.support.v4.os.ConfigurationCompat;
import org.thoughtcrime.securesms.logging.Log;
import java.util.Locale;
public final class DynamicLanguageActivityHelper {
private static final String TAG = Log.tag(DynamicLanguageActivityHelper.class);
private static String reentryProtection;
/**
* If the activity isn't in the specified language, it will restart the activity.
*/
@MainThread
public static void recreateIfNotInCorrectLanguage(Activity activity, String language) {
Locale currentActivityLocale = ConfigurationCompat.getLocales(activity.getResources().getConfiguration()).get(0);
Locale selectedLocale = LocaleParser.findBestMatchingLocaleForLanguage(language);
if (currentActivityLocale.equals(selectedLocale)) {
reentryProtection = "";
return;
}
String reentryKey = activity.getClass().getName() + ":" + selectedLocale;
if (!reentryKey.equals(reentryProtection)) {
reentryProtection = reentryKey;
Log.d(TAG, String.format("Activity Locale %s, Selected locale %s, restarting", currentActivityLocale, selectedLocale));
activity.recreate();
} else {
Log.d(TAG, String.format("Skipping recreate as looks like looping, Activity Locale %s, Selected locale %s", currentActivityLocale, selectedLocale));
}
}
}

View File

@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.util.dynamiclanguage;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import java.util.Locale;
/**
* Updates a context with an alternative language.
*/
public final class DynamicLanguageContextWrapper {
public static Context updateContext(Context context, String language) {
final Locale newLocale = LocaleParser.findBestMatchingLocaleForLanguage(language);
Locale.setDefault(newLocale);
final Resources resources = context.getResources();
final Configuration config = resources.getConfiguration();
final Configuration newConfig = copyWithNewLocale(config, newLocale);
resources.updateConfiguration(newConfig, resources.getDisplayMetrics());
return context;
}
private static Configuration copyWithNewLocale(Configuration config, Locale locale) {
final Configuration copy = new Configuration(config);
copy.setLocale(locale);
return copy;
}
}

View File

@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.util.dynamiclanguage;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Locale;
public final class LanguageString {
private LanguageString() {
}
/**
* @param languageString String in format language_REGION, e.g. en_US
* @return Locale, or null if cannot parse
*/
@Nullable
public static Locale parseLocale(@Nullable String languageString) {
if (languageString == null || languageString.isEmpty()) {
return null;
}
final Locale locale = createLocale(languageString);
if (!isValid(locale)) {
return null;
} else {
return locale;
}
}
private static Locale createLocale(@NonNull String languageString) {
final String language[] = languageString.split("_");
if (language.length == 2) {
return new Locale(language[0], language[1]);
} else {
return new Locale(language[0]);
}
}
private static boolean isValid(@NonNull Locale locale) {
try {
return locale.getISO3Language() != null && locale.getISO3Country() != null;
} catch (Exception ex) {
return false;
}
}
}

View File

@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.util.dynamiclanguage;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.support.annotation.Nullable;
import android.support.v4.os.ConfigurationCompat;
import org.thoughtcrime.securesms.BuildConfig;
import java.util.Arrays;
import java.util.Locale;
final class LocaleParser {
private LocaleParser() {
}
/**
* Given a language, gets the best choice from the apps list of supported languages and the
* Systems set of languages.
*/
static Locale findBestMatchingLocaleForLanguage(@Nullable String language) {
final Locale locale = LanguageString.parseLocale(language);
if (appSupportsTheExactLocale(locale)) {
return locale;
} else {
return findBestSystemLocale();
}
}
private static boolean appSupportsTheExactLocale(@Nullable Locale locale) {
if (locale == null) {
return false;
}
return Arrays.asList(BuildConfig.LANGUAGES).contains(locale.toString());
}
/**
* Get the first preferred language the app supports.
*/
private static Locale findBestSystemLocale() {
final Configuration config = Resources.getSystem().getConfiguration();
final Locale firstMatch = ConfigurationCompat.getLocales(config)
.getFirstMatch(BuildConfig.LANGUAGES);
if (firstMatch != null) {
return firstMatch;
}
return Locale.ENGLISH;
}
}

View File

@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.logging;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public final class LogTest {
@Test
public void tag_short_class_name() {
assertEquals("MyClass", Log.tag(MyClass.class));
}
@Test
public void tag_23_character_class_name() {
String tag = Log.tag(TwentyThreeCharacters23.class);
assertEquals("TwentyThreeCharacters23", tag);
assertEquals(23, tag.length());
}
@Test
public void tag_24_character_class_name() {
assertEquals(24, TwentyFour24Characters24.class.getSimpleName().length());
String tag = Log.tag(TwentyFour24Characters24.class);
assertEquals("TwentyFour24Characters2", tag);
assertEquals(23, Log.tag(TwentyThreeCharacters23.class).length());
}
private class MyClass {
}
private class TwentyThreeCharacters23 {
}
private class TwentyFour24Characters24 {
}
}

View File

@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.util.dynamiclanguage;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public final class LanguageStringTest {
private final Locale expected;
private final String input;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
/* Language */
{ new Locale("en"), "en" },
{ new Locale("de"), "de" },
{ new Locale("fr"), "FR" },
/* Language and region */
{ new Locale("en", "US"), "en_US" },
{ new Locale("es", "US"), "es_US" },
{ new Locale("es", "MX"), "es_MX" },
{ new Locale("es", "MX"), "es_mx" },
{ new Locale("de", "DE"), "de_DE" },
/* Not parsable input */
{ null, null },
{ null, "" },
{ null, "zz" },
{ null, "zz_ZZ" },
{ null, "fr_ZZ" },
{ null, "zz_FR" },
});
}
public LanguageStringTest(Locale expected, String input) {
this.expected = expected;
this.input = input;
}
@Test
public void parse() {
assertEquals(expected, LanguageString.parseLocale(input));
}
}

View File

@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.util.dynamiclanguage;
import android.app.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.thoughtcrime.securesms.BuildConfig;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = Application.class)
public final class LocaleParserTest {
@Test
public void findBestMatchingLocaleForLanguage_all_build_config_languages_can_be_resolved() {
for (String lang : buildConfigLanguages()) {
Locale locale = LocaleParser.findBestMatchingLocaleForLanguage(lang);
assertEquals(lang, locale.toString());
}
}
@Test
@Config(qualifiers = "fr")
public void findBestMatchingLocaleForLanguage_a_non_build_config_language_defaults_to_device_value_which_is_supported_directly() {
String unsupportedLanguage = getUnsupportedLanguage();
assertEquals(Locale.FRENCH, LocaleParser.findBestMatchingLocaleForLanguage(unsupportedLanguage));
}
@Test
@Config(qualifiers = "en-rCA")
public void findBestMatchingLocaleForLanguage_a_non_build_config_language_defaults_to_device_value_which_is_not_supported_directly() {
String unsupportedLanguage = getUnsupportedLanguage();
assertEquals(Locale.CANADA, LocaleParser.findBestMatchingLocaleForLanguage(unsupportedLanguage));
}
private static String getUnsupportedLanguage() {
String unsupportedLanguage = "af";
assertFalse("Language should be an unsupported one", buildConfigLanguages().contains(unsupportedLanguage));
return unsupportedLanguage;
}
private static List<String> buildConfigLanguages() {
return Arrays.asList(BuildConfig.LANGUAGES);
}
}