Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 90729ed3 authored by tom hsu's avatar tom hsu
Browse files

Limit locales in launched locale picker activity.

 - This can be passed as an extra field in an Activity Intent with
 one or more locales as a LocaleList.

Bug: b/249219775
Test: make passed
Test: atest passed
Change-Id: I69a9b1abbcdbb99be6d9c5447a7e05de8456d77b
parent c5fa04ec
Loading
Loading
Loading
Loading
+19 −2
Original line number Diff line number Diff line
@@ -1060,14 +1060,31 @@ public final class Settings {
     * In some cases, a matching Activity may not exist, so ensure you
     * safeguard against this.
     * <p>
     * Input: Nothing.
     * <p>
     * Input: The optional {@code #EXTRA_EXPLICIT_LOCALES} with language tags that contains locales
     * to limit available locales. This is only supported when device is under demo mode.
     * If intent does not contain this extra, it will show system supported locale list.
     * <br/>
     * If {@code #EXTRA_EXPLICIT_LOCALES} contain a unsupported locale, it will still show this
     * locale on list, but may not be supported by the devcie.
     *
     * Output: Nothing.
     */
    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
    public static final String ACTION_LOCALE_SETTINGS =
            "android.settings.LOCALE_SETTINGS";
    /**
     * Activity Extra: Show explicit locales in launched locale picker activity.
     *
     * This can be passed as an extra field in an Activity Intent with one or more language tags
     * as a {@link LocaleList}. This must be passed as an extra field to the
     * {@link #ACTION_LOCALE_SETTINGS}.
     *
     * @hide
     */
    public static final String EXTRA_EXPLICIT_LOCALES =
            "android.provider.extra.EXPLICIT_LOCALES";
    /**
     * Activity Action: Show settings to allow configuration of per application locale.
     * <p>
+11 −4
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.content.Context;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuInflater;
@@ -102,15 +103,21 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O

    public static LocalePickerWithRegion createLanguagePicker(Context context,
            LocaleSelectedListener listener, boolean translatedOnly) {
        return createLanguagePicker(context, listener, translatedOnly, null, null);
        return createLanguagePicker(context, listener, translatedOnly, null, null, null);
    }

    public static LocalePickerWithRegion createLanguagePicker(Context context,
            LocaleSelectedListener listener, boolean translatedOnly, String appPackageName,
            OnActionExpandListener onActionExpandListener) {
            LocaleSelectedListener listener, boolean translatedOnly, LocaleList explicitLocales) {
        return createLanguagePicker(context, listener, translatedOnly, explicitLocales, null, null);
    }

    /** Creates language picker UI */
    public static LocalePickerWithRegion createLanguagePicker(Context context,
            LocaleSelectedListener listener, boolean translatedOnly, LocaleList explicitLocales,
            String appPackageName, OnActionExpandListener onActionExpandListener) {
        LocaleCollectorBase localePickerController;
        if (TextUtils.isEmpty(appPackageName)) {
            localePickerController = new SystemLocaleCollector(context);
            localePickerController = new SystemLocaleCollector(context, explicitLocales);
        } else {
            localePickerController = new AppLocaleCollector(context, appPackageName);
        }
+81 −2
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.content.Context;
import android.os.LocaleList;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.inputmethod.InputMethodSubtype;

@@ -29,6 +30,7 @@ import com.android.internal.annotations.VisibleForTesting;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IllformedLocaleException;
@@ -106,6 +108,9 @@ public class LocaleStore {
            return mParent;
        }

        /**
         * TODO: This method may rename to be more generic i.e. toLanguageTag().
         */
        @UnsupportedAppUsage
        public String getId() {
            return mId;
@@ -456,11 +461,30 @@ public class LocaleStore {
    @UnsupportedAppUsage
    public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
            LocaleInfo parent, boolean translatedOnly) {
        return getLevelLocales(context, ignorables, parent, translatedOnly, null);
    }

    /**
     * @param explicitLocales Indicates only the locales within this list should be shown in the
     *                       locale picker.
     *
     * Returns a list of locales for language or region selection.
     * If the parent is null, then it is the language list.
     * If it is not null, then the list will contain all the locales that belong to that parent.
     * Example: if the parent is "ar", then the region list will contain all Arabic locales.
     * (this is not language based, but language-script, so that it works for zh-Hant and so on.
     */
    public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
            LocaleInfo parent, boolean translatedOnly, LocaleList explicitLocales) {
        fillCache(context);
        String parentId = parent == null ? null : parent.getId();

        HashSet<LocaleInfo> result = new HashSet<>();
        for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
        HashMap<String, LocaleInfo> supportedLcoaleInfos =
                explicitLocales == null
                        ? sLocaleCache
                        : convertExplicitLocales(explicitLocales, sLocaleCache.values());

        for (LocaleStore.LocaleInfo li : supportedLcoaleInfos.values()) {
            int level = getLevel(ignorables, li, translatedOnly);
            if (level == 2) {
                if (parent != null) { // region selection
@@ -479,6 +503,61 @@ public class LocaleStore {
        return result;
    }

    /** Converts string array of explicit locales to HashMap */
    public static HashMap<String, LocaleInfo> convertExplicitLocales(
            LocaleList explicitLocales, Collection<LocaleInfo> localeinfo) {
        // Trys to find the matched locale within android supported locales. If there is no matched
        // locale, it will still keep the unsupported lcoale in list.
        // Note: This currently does not support unicode extension check.
        LocaleList localeList = matchLocaleFromSupportedLocaleList(
                explicitLocales, localeinfo);

        HashMap<String, LocaleInfo> localeInfos = new HashMap<>();
        for (int i = 0; i < localeList.size(); i++) {
            Locale locale = localeList.get(i);
            if (locale.toString().isEmpty()) {
                throw new IllformedLocaleException("Bad locale entry");
            }

            LocaleInfo li = new LocaleInfo(locale);
            if (localeInfos.containsKey(li.getId())) {
                continue;
            }
            localeInfos.put(li.getId(), li);
            Locale parent = li.getParent();
            if (parent != null) {
                String parentId = parent.toLanguageTag();
                if (!localeInfos.containsKey(parentId)) {
                    localeInfos.put(parentId, new LocaleInfo(parent));
                }
            }
        }
        return localeInfos;
    }

    private static LocaleList matchLocaleFromSupportedLocaleList(
            LocaleList explicitLocales, Collection<LocaleInfo> localeinfo) {
        //TODO: Adds a function for unicode extension if needed.
        Locale[] resultLocales = new Locale[explicitLocales.size()];
        for (int i = 0; i < explicitLocales.size(); i++) {
            Locale locale = explicitLocales.get(i).stripExtensions();
            if (!TextUtils.isEmpty(locale.getCountry())) {
                for (LocaleInfo localeInfo :localeinfo) {
                    if (LocaleList.matchesLanguageAndScript(locale, localeInfo.getLocale())
                            && TextUtils.equals(locale.getCountry(),
                            localeInfo.getLocale().getCountry())) {
                        resultLocales[i] = localeInfo.getLocale();
                        continue;
                    }
                }
            }
            if (resultLocales[i] == null) {
                resultLocales[i] = locale;
            }
        }
        return new LocaleList(resultLocales);
    }

    @UnsupportedAppUsage
    public static LocaleInfo getLocaleInfo(Locale locale) {
        String id = locale.toLanguageTag();
+8 −4
Original line number Diff line number Diff line
@@ -26,9 +26,15 @@ import java.util.Set;
/** The Locale data collector for System language. */
class SystemLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
    private final Context mContext;
    private LocaleList mExplicitLocales;

    SystemLocaleCollector(Context context) {
        this(context, null);
    }

    SystemLocaleCollector(Context context, LocaleList explicitLocales) {
        mContext = context;
        mExplicitLocales = explicitLocales;
    }

    @Override
@@ -47,18 +53,16 @@ class SystemLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBas
            boolean translatedOnly, boolean isForCountryMode) {
        Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly);
        Set<LocaleStore.LocaleInfo> localeList;

        if (isForCountryMode) {
            localeList = LocaleStore.getLevelLocales(mContext,
                    langTagsToIgnore, parent, translatedOnly);
                    langTagsToIgnore, parent, translatedOnly, mExplicitLocales);
        } else {
            localeList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore,
                    null /* no parent */, translatedOnly);
                    null /* no parent */, translatedOnly, mExplicitLocales);
        }
        return localeList;
    }


    @Override
    public boolean hasSpecificPackageName() {
        return false;
+103 −8
Original line number Diff line number Diff line
@@ -17,8 +17,10 @@
package com.android.internal.app;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import android.os.LocaleList;
import android.view.inputmethod.InputMethodSubtype;
import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;

@@ -27,23 +29,21 @@ import androidx.test.runner.AndroidJUnit4;

import com.android.internal.app.LocaleStore.LocaleInfo;

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.IllformedLocaleException;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/**
 * Unit tests for the {@link LocaleStore}.
 */
/** Unit tests for the {@link LocaleStore}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class LocaleStoreTest {
    @Before
    public void setUp() {
    }

    @Test
    public void testTransformImeLanguageTagToLocaleInfo() {
        List<InputMethodSubtype> list = List.of(
@@ -60,4 +60,99 @@ public class LocaleStoreTest {
            assertTrue(expectedLanguageTag.contains(info.getId()));
        }
    }

    @Test
    public void convertExplicitLocales_noExplicitLcoales_returnEmptyHashMap() {
        Collection<LocaleInfo> supportedLocale = getFakeSupportedLocales();

        HashMap<String, LocaleInfo> result =
                LocaleStore.convertExplicitLocales(
                        LocaleList.getEmptyLocaleList(), supportedLocale);

        assertTrue(result.isEmpty());
    }

    @Test
    public void convertExplicitLocales_hasEmptyLocale_receiveException() {
        Locale[] locales = {Locale.forLanguageTag(""), Locale.forLanguageTag("en-US")};
        LocaleList localelist = new LocaleList(locales);
        Collection<LocaleInfo> supportedLocale = getFakeSupportedLocales();

        boolean isReceiveException = false;
        try {
            LocaleStore.convertExplicitLocales(localelist, supportedLocale);
        } catch (IllformedLocaleException e) {
            isReceiveException = true;
        }

        assertTrue(isReceiveException);
    }

    @Test
    public void convertExplicitLocales_hasSameLocale_returnNonSameLocales() {
        LocaleList locales = LocaleList.forLanguageTags("en-US,en-US");
        Collection<LocaleInfo> supportedLocale = getFakeSupportedLocales();

        HashMap<String, LocaleInfo> result =
                LocaleStore.convertExplicitLocales(locales, supportedLocale);

        // Only has "en" and "en-US".
        assertTrue(result.size() == 2);
    }

    @Test
    public void convertExplicitLocales_hasEnUs_resultHasParentEn() {
        LocaleList locales = LocaleList.forLanguageTags("en-US,ja-JP");
        Collection<LocaleInfo> supportedLocale = getFakeSupportedLocales();

        HashMap<String, LocaleInfo> result =
                LocaleStore.convertExplicitLocales(locales, supportedLocale);

        assertEquals(result.get("en").getId(), "en");
    }

    @Test
    public void convertExplicitLocales_hasZhTw_resultZhHantTw() {
        LocaleList locales = LocaleList.forLanguageTags("zh-TW,en-US,en");
        Collection<LocaleInfo> supportedLocale = getFakeSupportedLocales();

        HashMap<String, LocaleInfo> result =
                LocaleStore.convertExplicitLocales(locales, supportedLocale);

        assertEquals("zh-Hant-TW", result.get("zh-Hant-TW").getId());
    }

    @Test
    public void convertExplicitLocales_nonRegularFormat_resultEmptyContry() {
        LocaleList locales = LocaleList.forLanguageTags("de-1996,de-1901");
        Collection<LocaleInfo> supportedLocale = getFakeSupportedLocales();

        HashMap<String, LocaleInfo> result =
                LocaleStore.convertExplicitLocales(locales, supportedLocale);

        assertEquals("de-1996", result.get("de-1996").getId());
        assertTrue(result.get("de-1996").getLocale().getCountry().isEmpty());
    }

    @Test
    public void convertExplicitLocales_differentEnFormat() {
        LocaleList locales = LocaleList.forLanguageTags("en-Latn-US,en-US,en");
        Collection<LocaleInfo> supportedLocale = getFakeSupportedLocales();

        HashMap<String, LocaleInfo> result =
                LocaleStore.convertExplicitLocales(locales, supportedLocale);

        assertEquals("en", result.get("en").getId());
        assertEquals("en-US", result.get("en-US").getId());
        assertNull(result.get("en-Latn-US"));
    }

    private ArrayList<LocaleInfo> getFakeSupportedLocales() {
        String[] locales = {"en-US", "zh-Hant-TW", "ja-JP", "en-GB"};
        ArrayList<LocaleInfo> supportedLocales = new ArrayList<>();
        for (String localeTag : locales) {
            supportedLocales.add(LocaleStore.fromLocale(Locale.forLanguageTag(localeTag)));
        }
        return supportedLocales;
    }
}