[Settings] Create new controller for app language: suggested locale

Bug: 388199937
Test: manual
Flag: EXEMPT refactor
Change-Id: I45ddbfb460365e3ff4858de0c0411c7a46d49302
This commit is contained in:
Zoey Chen
2025-01-09 15:32:13 +00:00
parent d44314f253
commit 1daa1bb380
4 changed files with 452 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<!--
Copyright (C) 2016 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"
android:tint="?android:attr/colorControlNormal">
<path android:fillColor="@android:color/white"
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,473 799.5,465.5Q799,458 799,453Q794,482 772,501Q750,520 720,520L640,520Q607,520 583.5,496.5Q560,473 560,440L560,400L400,400L400,320Q400,287 423.5,263.5Q447,240 480,240L520,240L520,240Q520,217 532.5,199.5Q545,182 563,171Q543,166 522.5,163Q502,160 480,160Q346,160 253,253Q160,346 160,480Q160,480 160,480Q160,480 160,480L360,480Q426,480 473,527Q520,574 520,640L520,680L400,680L400,790Q420,795 439.5,797.5Q459,800 480,800Z"/>
</vector>

View File

@@ -527,6 +527,10 @@
<string name="top_intro_region_title">The region you choose affects how your phone displays time, dates, temperature, and more</string> <string name="top_intro_region_title">The region you choose affects how your phone displays time, dates, temperature, and more</string>
<!-- Category for more language settings. [CHAR LIMIT=NONE]--> <!-- Category for more language settings. [CHAR LIMIT=NONE]-->
<string name="more_language_settings_category">More language settings</string> <string name="more_language_settings_category">More language settings</string>
<!-- Title for asking to change system locale region or not. [CHAR LIMIT=50]-->
<string name="title_change_system_locale_region">Change region to %s ?</string>
<!-- Message for asking to change system locale region or not. [CHAR LIMIT=50]-->
<string name="body_change_system_locale_region">Your device will keep %s as a system language</string>
<!-- Regional Preferences begin --> <!-- Regional Preferences begin -->
<!-- The title of the menu entry of regional preferences. [CHAR LIMIT=50] --> <!-- The title of the menu entry of regional preferences. [CHAR LIMIT=50] -->

View File

@@ -0,0 +1,203 @@
/**
* Copyright (C) 2025 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.settings.localepicker;
import static com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_APPS_LOCALE;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.internal.app.AppLocaleCollector;
import com.android.internal.app.LocaleStore;
import com.android.settings.R;
import com.android.settings.applications.manageapplications.ManageApplicationsUtil;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/** A controller for handling suggested locale of app. */
public class AppLocaleSuggestedListPreferenceController extends
BasePreferenceController implements LocaleListSearchCallback {
private static final String TAG = "AppLocaleSuggestedListPreferenceController";
private static final String KEY_PREFERENCE_CATEGORY_APP_LANGUAGE_SUGGESTED =
"app_language_suggested_category";
private static final String KEY_PREFERENCE_APP_LOCALE_SUGGESTED_LIST =
"app_locale_suggested_list";
private static final String KEY_PREFERENCE_CATEGORY_ADD_A_LANGUAGE_SUGGESTED =
"system_language_suggested_category";
@SuppressWarnings("NullAway")
private PreferenceCategory mPreferenceCategory;
private Set<LocaleStore.LocaleInfo> mLocaleList;
private List<LocaleStore.LocaleInfo> mLocaleOptions;
private Map<String, Preference> mSuggestedPreferences;
private boolean mIsCountryMode;
@Nullable private LocaleStore.LocaleInfo mParentLocale;
private AppLocaleCollector mAppLocaleCollector;
@SuppressWarnings("NullAway")
private String mPackageName;
private boolean mIsNumberingSystemMode;
@SuppressWarnings("NullAway")
public AppLocaleSuggestedListPreferenceController(@NonNull Context context,
@NonNull String preferenceKey) {
super(context, preferenceKey);
}
@SuppressWarnings("NullAway")
public AppLocaleSuggestedListPreferenceController(@NonNull Context context,
@NonNull String preferenceKey, @Nullable String packageName,
boolean isNumberingSystemMode, @NonNull LocaleStore.LocaleInfo parentLocale) {
super(context, preferenceKey);
mPackageName = packageName;
mIsNumberingSystemMode = isNumberingSystemMode;
mParentLocale = parentLocale;
mIsCountryMode = mParentLocale != null;
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceCategory = screen.findPreference(
(mIsNumberingSystemMode || mIsCountryMode)
? KEY_PREFERENCE_CATEGORY_ADD_A_LANGUAGE_SUGGESTED
: KEY_PREFERENCE_CATEGORY_APP_LANGUAGE_SUGGESTED);
mAppLocaleCollector = new AppLocaleCollector(mContext, mPackageName);
mSuggestedPreferences = new ArrayMap<>();
mLocaleOptions = new ArrayList<>();
updatePreferences();
}
private void updatePreferences() {
if (mPreferenceCategory == null) {
Log.d(TAG, "updatePreferences, mPreferenceCategory is null");
return;
}
List<LocaleStore.LocaleInfo> result = LocaleUtils.getSortedLocaleList(
getSuggestedLocaleList(), mIsCountryMode);
final Map<String, Preference> existingSuggestedPreferences = mSuggestedPreferences;
mSuggestedPreferences = new ArrayMap<>();
setupSuggestedPreference(result, existingSuggestedPreferences);
for (Preference pref : existingSuggestedPreferences.values()) {
mPreferenceCategory.removePreference(pref);
}
}
@Override
public void onSearchListChanged(@NonNull List<LocaleStore.LocaleInfo> newList,
@Nullable CharSequence prefix) {
if (mPreferenceCategory == null) {
Log.d(TAG, "onSearchListChanged, mPreferenceCategory is null");
return;
}
mPreferenceCategory.removeAll();
final Map<String, Preference> existingSuggestedPreferences = mSuggestedPreferences;
List<LocaleStore.LocaleInfo> sortedList = getSuggestedLocaleList();
newList = LocaleUtils.getSortedLocaleFromSearchList(newList, sortedList, mIsCountryMode);
setupSuggestedPreference(newList, existingSuggestedPreferences);
}
private void setupSuggestedPreference(List<LocaleStore.LocaleInfo> localeInfoList,
Map<String, Preference> existingSuggestedPreferences) {
for (LocaleStore.LocaleInfo locale : localeInfoList) {
if (mIsNumberingSystemMode || mIsCountryMode) {
Preference pref = existingSuggestedPreferences.remove(locale.getId());
if (pref == null) {
pref = new Preference(mContext);
setupPreference(pref, locale);
mPreferenceCategory.addPreference(pref);
}
} else {
SelectorWithWidgetPreference pref =
(SelectorWithWidgetPreference) existingSuggestedPreferences.remove(
locale.getId());
if (pref == null) {
pref = new SelectorWithWidgetPreference(mContext);
setupPreference(pref, locale);
mPreferenceCategory.addPreference(pref);
}
}
}
Log.d(TAG, "setupSuggestedPreference, mPreferenceCategory setVisible"
+ (mPreferenceCategory.getPreferenceCount() > 0));
mPreferenceCategory.setVisible(mPreferenceCategory.getPreferenceCount() > 0);
}
private void setupPreference(Preference pref, LocaleStore.LocaleInfo locale) {
String localeName = mIsCountryMode ? locale.getFullCountryNameNative()
: locale.getFullNameNative();
if (pref instanceof SelectorWithWidgetPreference) {
((SelectorWithWidgetPreference) pref).setChecked(locale.isAppCurrentLocale());
}
pref.setTitle(locale.isSystemLocale()
? mContext.getString(R.string.preference_of_system_locale_summary)
: localeName);
pref.setKey(locale.toString());
pref.setOnPreferenceClickListener(clickedPref -> {
LocaleUtils.onLocaleSelected(mContext, locale, mPackageName);
((Activity) mContext).finish();
return true;
});
mSuggestedPreferences.put(locale.getId(), pref);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
protected List<LocaleStore.LocaleInfo> getSuggestedLocaleList() {
setupLocaleList();
if (mLocaleList != null && !mLocaleList.isEmpty()) {
mLocaleOptions.addAll(
mLocaleList.stream().filter(localeInfo -> (localeInfo.isSuggested())).collect(
Collectors.toList()));
} else {
Log.d(TAG, "Can not get suggested locales because the locale list is null or empty.");
}
return mLocaleOptions;
}
private void setupLocaleList() {
mLocaleList = mAppLocaleCollector.getSupportedLocaleList(mParentLocale,
false, mIsCountryMode);
mLocaleOptions.clear();
}
@Override
public @NonNull String getPreferenceKey() {
return KEY_PREFERENCE_APP_LOCALE_SUGGESTED_LIST;
}
}

View File

@@ -16,16 +16,53 @@
package com.android.settings.localepicker; package com.android.settings.localepicker;
import static com.android.settings.flags.Flags.localeNotificationEnabled;
import static com.android.settings.localepicker.LocaleListEditor.EXTRA_RESULT_LOCALE;
import static com.android.settings.localepicker.RegionAndNumberingSystemPickerFragment.EXTRA_IS_NUMBERING_SYSTEM;
import static com.android.settings.localepicker.RegionAndNumberingSystemPickerFragment.EXTRA_TARGET_LOCALE;
import android.app.Dialog;
import android.app.LocaleManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.LocaleList; import android.os.LocaleList;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import com.android.internal.app.LocaleHelper;
import com.android.internal.app.LocaleStore;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Collectors;
/** /**
* A locale utility class. * A locale utility class.
*/ */
public class LocaleUtils { public class LocaleUtils {
private static final String TAG = "LocaleUtils";
private static final String CHANNEL_ID_SUGGESTION = "suggestion";
private static final String CHANNEL_ID_SUGGESTION_TO_USER = "Locale suggestion";
private static final String EXTRA_APP_LOCALE = "app_locale";
private static final String EXTRA_NOTIFICATION_ID = "notification_id";
private static final int SIM_LOCALE = 1 << 0;
private static final int SYSTEM_LOCALE = 1 << 1;
private static final int APP_LOCALE = 1 << 2;
private static final int IME_LOCALE = 1 << 3;
/** /**
* Checks if the languageTag is in the system locale. Since in the current design, the system * Checks if the languageTag is in the system locale. Since in the current design, the system
* language list would not show two locales with the same language and region but different * language list would not show two locales with the same language and region but different
@@ -50,4 +87,191 @@ public class LocaleUtils {
} }
return false; return false;
} }
/**
* Logs the locale, sets the default locale for the app then broadcasts it.
*
* @param context Context
* @param localeInfo locale info
*/
public static void onLocaleSelected(@NonNull Context context,
@NonNull LocaleStore.LocaleInfo localeInfo,
@NonNull String packageName) {
if (localeInfo.getLocale() == null || localeInfo.isSystemLocale()) {
setAppDefaultLocale(context, "", packageName);
} else {
logLocaleSource(context, localeInfo);
setAppDefaultLocale(context, localeInfo.getLocale().toLanguageTag(),
packageName);
broadcastAppLocaleChange(context, localeInfo, packageName);
}
}
private static void logLocaleSource(Context context, LocaleStore.LocaleInfo localeInfo) {
if (!localeInfo.isSuggested() || localeInfo.isAppCurrentLocale()) {
return;
}
int localeSource = 0;
if (hasSuggestionType(localeInfo,
LocaleStore.LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE)) {
localeSource |= SYSTEM_LOCALE;
}
if (hasSuggestionType(localeInfo,
LocaleStore.LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE)) {
localeSource |= APP_LOCALE;
}
if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE)) {
localeSource |= IME_LOCALE;
}
if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_SIM)) {
localeSource |= SIM_LOCALE;
}
MetricsFeatureProvider metricsFeatureProvider =
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
metricsFeatureProvider.action(context,
SettingsEnums.ACTION_CHANGE_APP_LANGUAGE_FROM_SUGGESTED, localeSource);
}
private static boolean hasSuggestionType(LocaleStore.LocaleInfo localeInfo,
int suggestionType) {
return localeInfo.isSuggestionOfType(suggestionType);
}
private static void setAppDefaultLocale(Context context, String languageTag,
String packageName) {
LocaleManager localeManager = context.getSystemService(LocaleManager.class);
if (localeManager == null) {
Log.w(TAG, "LocaleManager is null, cannot set default app locale");
return;
}
localeManager.setApplicationLocales(packageName,
LocaleList.forLanguageTags(languageTag));
}
private static void broadcastAppLocaleChange(Context context, LocaleStore.LocaleInfo localeInfo,
String packageName) {
if (!localeNotificationEnabled()) {
Log.w(TAG, "Locale notification is not enabled");
return;
}
if (localeInfo.isAppCurrentLocale()) {
return;
}
try {
NotificationController notificationController = NotificationController.getInstance(
context);
String localeTag = localeInfo.getLocale().toLanguageTag();
int uid = context.getPackageManager().getApplicationInfo(packageName,
PackageManager.GET_META_DATA).uid;
boolean launchNotification = notificationController.shouldTriggerNotification(
uid, localeTag);
if (launchNotification) {
triggerNotification(
context,
notificationController.getNotificationId(localeTag),
context.getString(R.string.title_system_locale_addition,
localeInfo.getFullNameNative()),
context.getString(R.string.desc_system_locale_addition),
localeTag);
MetricsFeatureProvider metricsFeatureProvider =
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
metricsFeatureProvider.action(context,
SettingsEnums.ACTION_NOTIFICATION_FOR_SYSTEM_LOCALE);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to find info for package: " + packageName);
}
}
private static void triggerNotification(
Context context,
int notificationId,
String title,
String description,
String localeTag) {
NotificationManager notificationManager = context.getSystemService(
NotificationManager.class);
final boolean channelExist =
notificationManager.getNotificationChannel(CHANNEL_ID_SUGGESTION) != null;
// Create an alert channel if it does not exist
if (!channelExist) {
NotificationChannel channel =
new NotificationChannel(
CHANNEL_ID_SUGGESTION,
CHANNEL_ID_SUGGESTION_TO_USER,
NotificationManager.IMPORTANCE_DEFAULT);
channel.setSound(/* sound */ null, /* audioAttributes */ null); // silent notification
notificationManager.createNotificationChannel(channel);
}
final NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, CHANNEL_ID_SUGGESTION)
.setSmallIcon(R.drawable.ic_settings_language)
.setAutoCancel(true)
.setContentTitle(title)
.setContentText(description)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(
createPendingIntent(context, localeTag, notificationId, false))
.setDeleteIntent(
createPendingIntent(context, localeTag, notificationId, true));
notificationManager.notify(notificationId, builder.build());
}
private static PendingIntent createPendingIntent(Context context, String locale,
int notificationId,
boolean isDeleteIntent) {
Intent intent = isDeleteIntent
? new Intent(context, NotificationCancelReceiver.class)
: new Intent(context, NotificationActionActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra(EXTRA_APP_LOCALE, locale)
.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
int flag = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT;
int elapsedTime = (int) SystemClock.elapsedRealtimeNanos();
return isDeleteIntent
? PendingIntent.getBroadcast(context, elapsedTime, intent, flag)
: PendingIntent.getActivity(context, elapsedTime, intent, flag);
}
/**
* Sort the locale's list.
*
* @param localeInfos list of locale Infos
* @param isCountryMode Whether the locale page is in country mode or not.
* @return localeInfos list of locale Infos
*/
public static @NonNull List<LocaleStore.LocaleInfo> getSortedLocaleList(
@NonNull List<LocaleStore.LocaleInfo> localeInfos, boolean isCountryMode) {
final Locale sortingLocale = Locale.getDefault();
final LocaleHelper.LocaleInfoComparator comp = new LocaleHelper.LocaleInfoComparator(
sortingLocale, isCountryMode);
Collections.sort(localeInfos, comp);
return localeInfos;
}
/**
* Sort the locale's list by keywords in search.
*
* @param searchList locale Infos in search bar
* @param localeList list of locale Infos
* @param isCountryMode Whether the locale page is in country mode or not.
* @return localeInfos list of locale Infos
*/
public static @NonNull List<LocaleStore.LocaleInfo> getSortedLocaleFromSearchList(
@NonNull List<LocaleStore.LocaleInfo> searchList,
@NonNull List<LocaleStore.LocaleInfo> localeList,
boolean isCountryMode) {
List<LocaleStore.LocaleInfo> searchItem = localeList.stream()
.filter(suggested -> searchList.stream()
.anyMatch(option -> option.getLocale() != null
&& option.getLocale().getLanguage().equals(
suggested.getLocale().getLanguage())))
.distinct()
.collect(Collectors.toList());
return getSortedLocaleList(searchItem, isCountryMode);
}
} }