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

Commit 61629937 authored by Allen Su's avatar Allen Su Committed by Android (Google) Code Review
Browse files

Merge "[Panlingual]Enhance App's suggested language"

parents 3d3f264c 3467d1ba
Loading
Loading
Loading
Loading
+228 −30
Original line number Diff line number Diff line
@@ -19,49 +19,184 @@ package com.android.internal.app;
import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET;
import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;

import android.app.LocaleManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.LocaleList;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;

import com.android.internal.annotations.VisibleForTesting;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;

/** The Locale data collector for per-app language. */
class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
    private static final String TAG = AppLocaleCollector.class.getSimpleName();
    private final Context mContext;
    private final String mAppPackageName;
    private final LocaleStore.LocaleInfo mAppCurrentLocale;
    private LocaleStore.LocaleInfo mAppCurrentLocale;
    private Set<LocaleStore.LocaleInfo> mAllAppActiveLocales;
    private Set<LocaleStore.LocaleInfo> mImeLocales;
    private static final String PROP_APP_LANGUAGE_SUGGESTION =
            "android.app.language.suggestion.enhanced";
    private static final boolean ENABLED = true;

    AppLocaleCollector(Context context, String appPackageName) {
    public AppLocaleCollector(Context context, String appPackageName) {
        mContext = context;
        mAppPackageName = appPackageName;
        mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo(
                mContext, mAppPackageName);
    }

    @VisibleForTesting
    public LocaleStore.LocaleInfo getAppCurrentLocale() {
        return LocaleStore.getAppActivatedLocaleInfo(mContext, mAppPackageName, true);
    }

    /**
     * Get all applications' activated locales.
     * @return A set which includes all applications' activated LocaleInfo.
     */
    @VisibleForTesting
    public Set<LocaleStore.LocaleInfo> getAllAppActiveLocales() {
        PackageManager pm = mContext.getPackageManager();
        LocaleManager lm = mContext.getSystemService(LocaleManager.class);
        HashSet<LocaleStore.LocaleInfo> result = new HashSet<>();
        if (pm != null && lm != null) {
            HashMap<String, LocaleStore.LocaleInfo> map = new HashMap<>();
            for (ApplicationInfo appInfo : pm.getInstalledApplications(
                    PackageManager.ApplicationInfoFlags.of(0))) {
                LocaleStore.LocaleInfo localeInfo = LocaleStore.getAppActivatedLocaleInfo(
                        mContext, appInfo.packageName, false);
                // For the locale to be added into the suggestion area, its country could not be
                // empty.
                if (localeInfo != null && localeInfo.getLocale().getCountry().length() > 0) {
                    map.put(localeInfo.getId(), localeInfo);
                }
            }
            map.forEach((language, localeInfo) -> result.add(localeInfo));
        }
        return result;
    }

    /**
     * Get all locales that active IME supports.
     *
     * @return A set which includes all LocaleInfo that active IME supports.
     */
    @VisibleForTesting
    public Set<LocaleStore.LocaleInfo> getActiveImeLocales() {
        Set<LocaleStore.LocaleInfo> activeImeLocales = null;
        InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
        if (imm != null) {
            InputMethodInfo activeIme = getActiveIme(imm);
            if (activeIme != null) {
                activeImeLocales = LocaleStore.transformImeLanguageTagToLocaleInfo(
                        imm.getEnabledInputMethodSubtypeList(activeIme, true));
            }
        }
        if (activeImeLocales == null) {
            return Set.of();
        } else {
            return activeImeLocales.stream().filter(
                    // For the locale to be added into the suggestion area, its country could not be
                    // empty.
                    info -> info.getLocale().getCountry().length() > 0).collect(
                    Collectors.toSet());
        }
    }

    private InputMethodInfo getActiveIme(InputMethodManager imm) {
        InputMethodInfo activeIme = null;
        List<InputMethodInfo> infoList = imm.getEnabledInputMethodList();
        String imeId = Settings.Secure.getStringForUser(mContext.getContentResolver(),
                Settings.Secure.DEFAULT_INPUT_METHOD, mContext.getUserId());
        for (InputMethodInfo method : infoList) {
            if (method.getId().equals(imeId)) {
                activeIme = method;
            }
        }
        return activeIme;
    }

    /**
     * Get the AppLocaleResult that the application supports.
     * @return The AppLocaleResult that the application supports.
     */
    @VisibleForTesting
    public AppLocaleStore.AppLocaleResult getAppSupportedLocales() {
        return AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
    }

    /**
     * Get the locales that system supports excluding langTagsToIgnore.
     *
     * @param langTagsToIgnore A language set to be ignored.
     * @param parent The parent locale.
     * @param translatedOnly specified if is is only for translation.
     * @return A set which includes the LocaleInfo that system supports, excluding langTagsToIgnore.
     */
    @VisibleForTesting
    public Set<LocaleStore.LocaleInfo> getSystemSupportedLocale(Set<String> langTagsToIgnore,
            LocaleStore.LocaleInfo parent, boolean translatedOnly) {
        return LocaleStore.getLevelLocales(mContext, langTagsToIgnore, parent, translatedOnly);
    }

    /**
     * Get the locales that system activates.
     * @return A set which includes all the locales that system activates.
     */
    @VisibleForTesting
    public List<LocaleStore.LocaleInfo> getSystemCurrentLocale() {
        return LocaleStore.getSystemCurrentLocaleInfo();
    }

    @Override
    public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
        HashSet<String> langTagsToIgnore = new HashSet<>();

        if (mAppCurrentLocale != null) {
            langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag());
        }

        if (SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION, ENABLED)) {
            // Add the locale that other App activated
            mAllAppActiveLocales.forEach(
                    info -> langTagsToIgnore.add(info.getLocale().toLanguageTag()));
            // Add the locale that active IME enabled
            mImeLocales.forEach(info -> langTagsToIgnore.add(info.getLocale().toLanguageTag()));
        }

        // Add System locales
        LocaleList systemLangList = LocaleList.getDefault();
        for (int i = 0; i < systemLangList.size(); i++) {
            langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
        }

        if (mAppCurrentLocale != null) {
            langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag());
        }
        return langTagsToIgnore;
    }

    @Override
    public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
            boolean translatedOnly, boolean isForCountryMode) {
        AppLocaleStore.AppLocaleResult result =
                AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
        if (mAppCurrentLocale == null) {
            mAppCurrentLocale = getAppCurrentLocale();
        }
        if (mAllAppActiveLocales == null) {
            mAllAppActiveLocales = getAllAppActiveLocales();
        }
        if (mImeLocales == null) {
            mImeLocales = getActiveImeLocales();
        }
        AppLocaleStore.AppLocaleResult result = getAppSupportedLocales();
        Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly);
        Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>();
        Set<LocaleStore.LocaleInfo> systemLocaleList;
@@ -71,11 +206,9 @@ class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {

        // Get system supported locale list
        if (isForCountryMode) {
            systemLocaleList = LocaleStore.getLevelLocales(mContext,
                    langTagsToIgnore, parent, translatedOnly);
            systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, parent, translatedOnly);
        } else {
            systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore,
                    null /* no parent */, translatedOnly);
            systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, null, translatedOnly);
        }

        // Add current app locale
@@ -84,19 +217,46 @@ class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
        }

        // Add current system language into suggestion list
        for(LocaleStore.LocaleInfo localeInfo:
                LocaleStore.getSystemCurrentLocaleInfo()) {
            boolean isNotCurrentLocale = mAppCurrentLocale == null
                    || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
            if (!isForCountryMode && isNotCurrentLocale) {
        if (!isForCountryMode) {
            boolean isCurrentLocale, isInAppOrIme;
            for (LocaleStore.LocaleInfo localeInfo : getSystemCurrentLocale()) {
                isCurrentLocale = mAppCurrentLocale != null
                        && localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
                isInAppOrIme = existsInAppOrIme(localeInfo.getLocale());
                if (!isCurrentLocale && !isInAppOrIme) {
                    appLocaleList.add(localeInfo);
                }
            }
        }

        // Add the languages that included in system supported locale
        // Add the languages that are included in system supported locale
        Set<LocaleStore.LocaleInfo> suggestedSet = null;
        if (shouldShowList) {
            appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale(
                    systemLocaleList, result.mAppSupportedLocales));
            appLocaleList.addAll(filterSupportedLocales(systemLocaleList,
                    result.mAppSupportedLocales));
            suggestedSet = getSuggestedLocales(appLocaleList);
        }

        if (!isForCountryMode && SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION,
                ENABLED)) {
            // Add the language that other apps activate into the suggestion list.
            Set<LocaleStore.LocaleInfo> localeSet = filterSupportedLocales(mAllAppActiveLocales,
                    result.mAppSupportedLocales);
            if (suggestedSet != null) {
                // Filter out the locale with the same language and country
                // like zh-TW vs zh-Hant-TW.
                localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet);
            }
            appLocaleList.addAll(localeSet);
            suggestedSet.addAll(localeSet);

            // Add the language that the active IME enables into the suggestion list.
            localeSet = filterSupportedLocales(mImeLocales, result.mAppSupportedLocales);
            if (suggestedSet != null) {
                localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet);
            }
            appLocaleList.addAll(localeSet);
            suggestedSet.addAll(localeSet);
        }

        // Add "system language" option
@@ -117,12 +277,50 @@ class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
        return true;
    }

    private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale(
            Set<LocaleStore.LocaleInfo> systemLocaleList,
    private Set<LocaleStore.LocaleInfo> getSuggestedLocales(Set<LocaleStore.LocaleInfo> localeSet) {
        return localeSet.stream().filter(localeInfo -> localeInfo.isSuggested()).collect(
                Collectors.toSet());
    }

    private Set<LocaleStore.LocaleInfo> filterSameLanguageAndCountry(
            Set<LocaleStore.LocaleInfo> newLocaleList,
            Set<LocaleStore.LocaleInfo> existingLocaleList) {
        Set<LocaleStore.LocaleInfo> result = new HashSet<>(newLocaleList.size());
        for (LocaleStore.LocaleInfo appLocaleInfo : newLocaleList) {
            boolean same = false;
            Locale appLocale = appLocaleInfo.getLocale();
            for (LocaleStore.LocaleInfo localeInfo : existingLocaleList) {
                Locale suggested = localeInfo.getLocale();
                if (appLocale.getLanguage().equals(suggested.getLanguage())
                        && appLocale.getCountry().equals(suggested.getCountry())) {
                    same = true;
                    break;
                }
            }
            if (!same) {
                result.add(appLocaleInfo);
            }
        }
        return result;
    }

    private boolean existsInAppOrIme(Locale locale) {
        boolean existInApp = mAllAppActiveLocales.stream().anyMatch(
                localeInfo -> localeInfo.getLocale().equals(locale));
        if (existInApp) {
            return true;
        } else {
            return mImeLocales.stream().anyMatch(
                    localeInfo -> localeInfo.getLocale().equals(locale));
        }
    }

    private Set<LocaleStore.LocaleInfo> filterSupportedLocales(
            Set<LocaleStore.LocaleInfo> suggestedLocales,
            HashSet<Locale> appSupportedLocales) {
        Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>();

        for(LocaleStore.LocaleInfo li: systemLocaleList) {
        for (LocaleStore.LocaleInfo li : suggestedLocales) {
            if (appSupportedLocales.contains(li.getLocale())) {
                filteredList.add(li);
            } else {
+10 −4
Original line number Diff line number Diff line
@@ -30,7 +30,10 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.stream.Collectors;

class AppLocaleStore {
/**
 * A class used to access an application's supporting locales.
 */
public class AppLocaleStore {
    private static final String TAG = AppLocaleStore.class.getSimpleName();

    public static AppLocaleResult getAppSupportedLocales(
@@ -148,8 +151,11 @@ class AppLocaleStore {
        return false;
    }

    static class AppLocaleResult {
        enum LocaleStatus {
    /**
     * A class used to store an application's supporting locales.
     */
    public static class AppLocaleResult {
        public enum LocaleStatus {
            UNKNOWN_FAILURE,
            NO_SUPPORTED_LANGUAGE_IN_APP,
            ASSET_LOCALE_IS_EMPTY,
@@ -158,7 +164,7 @@ class AppLocaleStore {
        }

        LocaleStatus mLocaleStatus;
        HashSet<Locale> mAppSupportedLocales;
        public HashSet<Locale> mAppSupportedLocales;

        public AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales) {
            this.mLocaleStatus = localeStatus;
+47 −5
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.os.LocaleList;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.inputmethod.InputMethodSubtype;

import com.android.internal.annotations.VisibleForTesting;

@@ -45,10 +46,11 @@ public class LocaleStore {
        @VisibleForTesting static final int SUGGESTION_TYPE_SIM = 1 << 0;
        @VisibleForTesting static final int SUGGESTION_TYPE_CFG = 1 << 1;
        // Only for per-app language picker
        private static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
        @VisibleForTesting static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
        // Only for per-app language picker
        private static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;

        @VisibleForTesting  static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;
        // Only for per-app language picker
        @VisibleForTesting static final int SUGGESTION_TYPE_OTHER_APP_LANGUAGE = 1 << 4;
        private final Locale mLocale;
        private final Locale mParent;
        private final String mId;
@@ -259,7 +261,16 @@ public class LocaleStore {
        }
    }

    public static LocaleInfo getAppCurrentLocaleInfo(Context context, String appPackageName) {
    /**
     * Get the application's activated locale.
     *
     * @param context UI activity's context.
     * @param appPackageName The application's package name.
     * @param isAppSelected True if the application is selected in the UI; false otherwise.
     * @return A LocaleInfo with the application's activated locale.
     */
    public static LocaleInfo getAppActivatedLocaleInfo(Context context, String appPackageName,
            boolean isAppSelected) {
        if (appPackageName == null) {
            return null;
        }
@@ -272,7 +283,11 @@ public class LocaleStore {

            if (locale != null) {
                LocaleInfo localeInfo = new LocaleInfo(locale);
                if (isAppSelected) {
                    localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT;
                } else {
                    localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
                }
                localeInfo.mIsTranslated = true;
                return localeInfo;
            }
@@ -282,6 +297,24 @@ public class LocaleStore {
        return null;
    }

    /**
     * Transform IME's language tag to LocaleInfo.
     *
     * @param list A list which includes IME's subtype.
     * @return A LocaleInfo set which includes IME's language tags.
     */
    public static Set<LocaleInfo> transformImeLanguageTagToLocaleInfo(
            List<InputMethodSubtype> list) {
        Set<LocaleInfo> imeLocales = new HashSet<>();
        for (InputMethodSubtype subtype : list) {
            LocaleInfo localeInfo = new LocaleInfo(subtype.getLanguageTag());
            localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
            localeInfo.mIsTranslated = true;
            imeLocales.add(localeInfo);
        }
        return imeLocales;
    }

    /**
     * Returns a list of system languages with LocaleInfo
     */
@@ -458,4 +491,13 @@ public class LocaleStore {
        }
        return result;
    }

    /**
     * API for testing.
     */
    @UnsupportedAppUsage
    @VisibleForTesting
    public static LocaleInfo fromLocale(Locale locale) {
        return new LocaleInfo(locale);
    }
}
+170 −0

File added.

Preview size limit exceeded, changes collapsed.

+63 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.app;

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

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

import androidx.test.filters.SmallTest;
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.List;
import java.util.Set;

/**
 * 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(
                new InputMethodSubtypeBuilder().setLanguageTag("en-US").build(),
                new InputMethodSubtypeBuilder().setLanguageTag("zh-TW").build(),
                new InputMethodSubtypeBuilder().setLanguageTag("ja-JP").build());

        Set<LocaleInfo> localeSet = LocaleStore.transformImeLanguageTagToLocaleInfo(list);

        Set<String> expectedLanguageTag = Set.of("en-US", "zh-TW", "ja-JP");
        assertEquals(localeSet.size(), expectedLanguageTag.size());
        for (LocaleInfo info : localeSet) {
            assertEquals(info.mSuggestionFlags, LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE);
            assertTrue(expectedLanguageTag.contains(info.getId()));
        }
    }
}