diff --git a/Parse/src/main/java/com/parse/ParseInstallation.java b/Parse/src/main/java/com/parse/ParseInstallation.java index 24105b17f..70019e314 100644 --- a/Parse/src/main/java/com/parse/ParseInstallation.java +++ b/Parse/src/main/java/com/parse/ParseInstallation.java @@ -11,10 +11,12 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.text.TextUtils; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.TimeZone; import bolts.Continuation; @@ -36,12 +38,13 @@ public class ParseInstallation extends ParseObject { private static final String KEY_DEVICE_TOKEN = "deviceToken"; private static final String KEY_PUSH_TYPE = "pushType"; private static final String KEY_TIME_ZONE = "timeZone"; + private static final String KEY_LOCALE = "localeIdentifier"; private static final String KEY_APP_VERSION = "appVersion"; /* package */ static final String KEY_CHANNELS = "channels"; private static final List READ_ONLY_FIELDS = Collections.unmodifiableList( Arrays.asList(KEY_DEVICE_TYPE, KEY_INSTALLATION_ID, KEY_DEVICE_TOKEN, KEY_PUSH_TYPE, - KEY_TIME_ZONE, KEY_APP_VERSION, KEY_APP_NAME, KEY_PARSE_VERSION, + KEY_TIME_ZONE, KEY_LOCALE, KEY_APP_VERSION, KEY_APP_NAME, KEY_PARSE_VERSION, KEY_APP_IDENTIFIER)); // TODO(mengyan): Inject into ParseInstallationInstanceController @@ -108,6 +111,7 @@ public String getInstallationId() { updateTimezone(); updateVersionInfo(); updateDeviceInfo(); + updateLocaleIdentifier(); } } @@ -165,8 +169,8 @@ public Task then(Task task) throws Exception { // time zones from devices reporting other formats. private void updateTimezone() { String zone = TimeZone.getDefault().getID(); - if ((zone.indexOf('/') > 0 || zone.equals("GMT")) && !zone.equals(get("timeZone"))) { - performPut("timeZone", zone); + if ((zone.indexOf('/') > 0 || zone.equals("GMT")) && !zone.equals(get(KEY_TIME_ZONE))) { + performPut(KEY_TIME_ZONE, zone); } } @@ -180,25 +184,65 @@ private void updateVersionInfo() { String appVersion = pkgInfo.versionName; String appName = pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString(); - if (packageName != null && !packageName.equals(get("appIdentifier"))) { + if (packageName != null && !packageName.equals(get(KEY_APP_IDENTIFIER))) { performPut(KEY_APP_IDENTIFIER, packageName); } - if (appName != null && !appName.equals(get("appName"))) { + if (appName != null && !appName.equals(get(KEY_APP_NAME))) { performPut(KEY_APP_NAME, appName); } - if (appVersion != null && !appVersion.equals(get("appVersion"))) { + if (appVersion != null && !appVersion.equals(get(KEY_APP_VERSION))) { performPut(KEY_APP_VERSION, appVersion); } } catch (PackageManager.NameNotFoundException e) { PLog.w(TAG, "Cannot load package info; will not be saved to installation"); } - if (!VERSION_NAME.equals(get("parseVersion"))) { + if (!VERSION_NAME.equals(get(KEY_PARSE_VERSION))) { performPut(KEY_PARSE_VERSION, VERSION_NAME); } } } + /* + * Save locale in the following format: + * [language code]-[country code] + * + * The language codes are two-letter lowercase ISO language codes (such as "en") as defined by + * ISO 639-1. + * The country codes are two-letter uppercase ISO country codes (such as "US") as defined by + * ISO 3166-1. + * + * Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language + * code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This + * rewriting happens even if you construct your own {@code Locale} object, not just for + * instances returned by the various lookup methods. + */ + private void updateLocaleIdentifier() { + final Locale locale = Locale.getDefault(); + + String language = locale.getLanguage(); + String country = locale.getCountry(); + + if (TextUtils.isEmpty(language)) { + return; + } + + // rewrite depreciated two-letter codes + if (language.equals("iw")) language = "he"; // Hebrew + if (language.equals("in")) language = "id"; // Indonesian + if (language.equals("ji")) language = "yi"; // Yiddish + + String localeString = language; + + if (!TextUtils.isEmpty(country)) { + localeString = String.format(Locale.US, "%s-%s", language, country); + } + + if (!localeString.equals(get(KEY_LOCALE))) { + performPut(KEY_LOCALE, localeString); + } + } + // TODO(mengyan): Move to ParseInstallationInstanceController /* package */ void updateDeviceInfo() { updateDeviceInfo(ParsePlugins.get().installationId()); diff --git a/Parse/src/test/java/com/parse/ParseInstallationTest.java b/Parse/src/test/java/com/parse/ParseInstallationTest.java index 9097eb4e4..9aa74a18d 100644 --- a/Parse/src/test/java/com/parse/ParseInstallationTest.java +++ b/Parse/src/test/java/com/parse/ParseInstallationTest.java @@ -23,6 +23,7 @@ import org.robolectric.res.builder.RobolectricPackageManager; import java.util.Arrays; +import java.util.Locale; import java.util.TimeZone; import bolts.Task; @@ -46,17 +47,25 @@ public class ParseInstallationTest { private static final String KEY_APP_NAME = "appName"; private static final String KEY_APP_IDENTIFIER = "appIdentifier"; private static final String KEY_TIME_ZONE = "timeZone"; + private static final String KEY_LOCALE_IDENTIFIER = "localeIdentifier"; private static final String KEY_APP_VERSION = "appVersion"; + private Locale defaultLocale; + @Before public void setUp() { ParseObject.registerSubclass(ParseInstallation.class); + + defaultLocale = Locale.getDefault(); } @After public void tearDown() { ParseObject.unregisterSubclass(ParseInstallation.class); ParseCorePlugins.getInstance().reset(); + ParsePlugins.reset(); + + Locale.setDefault(defaultLocale); } @Test @@ -71,6 +80,7 @@ public void testImmutableKeys() { "deviceTokenLastModified", "pushType", "timeZone", + "localeIdentifier", "appVersion" }; @@ -146,24 +156,9 @@ public void testHandleFetchResultAsync() throws Exception { @Test public void testUpdateBeforeSave() throws Exception { - // Mock currentInstallationController to make setAsync work - ParseCurrentInstallationController controller = - mock(ParseCurrentInstallationController.class); - when(controller.isCurrent(any(ParseInstallation.class))).thenReturn(true); - ParseCorePlugins.getInstance().registerCurrentInstallationController(controller); - // Mock package manager - RobolectricPackageManager packageManager = - spy(RuntimeEnvironment.getRobolectricPackageManager()); - doReturn("parseTest").when(packageManager).getApplicationLabel(any(ApplicationInfo.class)); - RuntimeEnvironment.setRobolectricPackageManager(packageManager); - ParsePlugins.Android plugins = mock(ParsePlugins.Android.class); - // Mock installationId - InstallationId installationId = mock(InstallationId.class); - when(installationId.get()).thenReturn("installationId"); - when(plugins.installationId()).thenReturn(installationId); - // Mock application context - when(plugins.applicationContext()).thenReturn(RuntimeEnvironment.application); - ParsePlugins.set(plugins); + mocksForUpdateBeforeSave(); + + Locale.setDefault(new Locale("en", "US")); ParseInstallation installation = new ParseInstallation(); installation.updateBeforeSave(); @@ -183,7 +178,9 @@ public void testUpdateBeforeSave() throws Exception { assertEquals(appVersion, installation.getString(KEY_APP_VERSION)); // Make sure we update device info assertEquals("android", installation.getString(KEY_DEVICE_TYPE)); - assertEquals(installationId.get(), installation.getString(KEY_INSTALLATION_ID)); + assertEquals("installationId", installation.getString(KEY_INSTALLATION_ID)); + // Make sure we update the locale identifier + assertEquals("en-US", installation.getString(KEY_LOCALE_IDENTIFIER)); } // TODO(mengyan): Add other testUpdateBeforeSave cases to cover all branches @@ -255,6 +252,54 @@ public void testGetCurrentInstallation() throws Exception { verify(controller, times(1)).getAsync(); } + @Test + public void testLocaleIdentifierSpecialCases() throws Exception { + mocksForUpdateBeforeSave(); + + ParseInstallation installation = new ParseInstallation(); + + // Deprecated two-letter codes (Java issue). + Locale.setDefault(new Locale("iw", "US")); + installation.updateBeforeSave(); + assertEquals("he-US", installation.getString(KEY_LOCALE_IDENTIFIER)); + + Locale.setDefault(new Locale("in", "US")); + installation.updateBeforeSave(); + assertEquals("id-US", installation.getString(KEY_LOCALE_IDENTIFIER)); + + Locale.setDefault(new Locale("ji", "US")); + installation.updateBeforeSave(); + assertEquals("yi-US", installation.getString(KEY_LOCALE_IDENTIFIER)); + + // No country code. + Locale.setDefault(new Locale("en")); + installation.updateBeforeSave(); + assertEquals("en", installation.getString(KEY_LOCALE_IDENTIFIER)); + } + + + // TODO(mengyan): Add testFetchAsync, right now we can not test super methods inside // testFetchAsync + + private static void mocksForUpdateBeforeSave() { + // Mock currentInstallationController to make setAsync work + ParseCurrentInstallationController controller = + mock(ParseCurrentInstallationController.class); + when(controller.isCurrent(any(ParseInstallation.class))).thenReturn(true); + ParseCorePlugins.getInstance().registerCurrentInstallationController(controller); + // Mock package manager + RobolectricPackageManager packageManager = + spy(RuntimeEnvironment.getRobolectricPackageManager()); + doReturn("parseTest").when(packageManager).getApplicationLabel(any(ApplicationInfo.class)); + RuntimeEnvironment.setRobolectricPackageManager(packageManager); + ParsePlugins.Android plugins = mock(ParsePlugins.Android.class); + // Mock installationId + InstallationId installationId = mock(InstallationId.class); + when(installationId.get()).thenReturn("installationId"); + when(plugins.installationId()).thenReturn(installationId); + // Mock application context + when(plugins.applicationContext()).thenReturn(RuntimeEnvironment.application); + ParsePlugins.set(plugins); + } }