diff --git a/res/values/config.xml b/res/values/config.xml
index 726aa72707d..7394c4c6a00 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -724,4 +724,10 @@
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 82adb359404..fd5999c0d33 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -381,6 +381,12 @@
Change system language to %s ?
+
+ Add %s to preferred languages?
+
+
+ This lets apps and websites know you also prefer this language.
+
Your device settings and regional preferences will change.
diff --git a/src/com/android/settings/localepicker/AppLocalePickerActivity.java b/src/com/android/settings/localepicker/AppLocalePickerActivity.java
index 9efaf63c687..332f6256522 100644
--- a/src/com/android/settings/localepicker/AppLocalePickerActivity.java
+++ b/src/com/android/settings/localepicker/AppLocalePickerActivity.java
@@ -18,10 +18,13 @@ package com.android.settings.localepicker;
import android.app.FragmentTransaction;
import android.app.LocaleManager;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.LocaleList;
+import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
@@ -29,6 +32,9 @@ import android.view.View;
import android.widget.FrameLayout;
import android.widget.ListView;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+
import com.android.internal.app.LocalePickerWithRegion;
import com.android.internal.app.LocaleStore;
import com.android.settings.R;
@@ -36,9 +42,14 @@ import com.android.settings.applications.AppLocaleUtil;
import com.android.settings.applications.appinfo.AppLocaleDetails;
import com.android.settings.core.SettingsBaseActivity;
+import java.util.Locale;
+
public class AppLocalePickerActivity extends SettingsBaseActivity
implements LocalePickerWithRegion.LocaleSelectedListener, MenuItem.OnActionExpandListener {
private static final String TAG = AppLocalePickerActivity.class.getSimpleName();
+ static final String EXTRA_APP_LOCALE = "app_locale";
+ private static final String PROP_SYSTEM_LOCALE_SUGGESTION = "android.system.locale.suggestion";
+ private static final boolean ENABLED = false;
private String mPackageName;
private LocalePickerWithRegion mLocalePickerWithRegion;
@@ -98,6 +109,7 @@ public class AppLocalePickerActivity extends SettingsBaseActivity
setAppDefaultLocale("");
} else {
setAppDefaultLocale(localeInfo.getLocale().toLanguageTag());
+ broadcastAppLocaleChange(localeInfo);
}
finish();
}
@@ -125,6 +137,58 @@ public class AppLocalePickerActivity extends SettingsBaseActivity
localeManager.setApplicationLocales(mPackageName, LocaleList.forLanguageTags(languageTag));
}
+ private void broadcastAppLocaleChange(LocaleStore.LocaleInfo localeInfo) {
+ if (!SystemProperties.getBoolean(PROP_SYSTEM_LOCALE_SUGGESTION, ENABLED)) {
+ return;
+ }
+ String languageTag = localeInfo.getLocale().toLanguageTag();
+ if (isInSystemLocale(languageTag) || localeInfo.isAppCurrentLocale()) {
+ return;
+ }
+ String intentAction = getString(R.string.config_app_locale_intent_action);
+ if (!TextUtils.isEmpty(intentAction)) {
+ try {
+ PackageManager packageManager = getPackageManager();
+ ApplicationInfo info = packageManager.getApplicationInfo(mPackageName,
+ PackageManager.GET_META_DATA);
+ Intent intent = new Intent(intentAction)
+ .putExtra(Intent.EXTRA_UID, info.uid)
+ .putExtra(EXTRA_APP_LOCALE, languageTag);
+ if (intent.resolveActivity(packageManager) != null) {
+ mStartForResult.launch(intent);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Unable to find info for package: " + mPackageName);
+ }
+ }
+ }
+
+ // Invoke startActivityFroResult so that the calling package can be shared via the intent.
+ private ActivityResultLauncher mStartForResult = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ }
+ );
+
+ /**
+ * Checks if the localeTag 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
+ * numbering system. So, during the comparison, the extension has to be stripped.
+ *
+ * @param languageTag A language tag
+ * @return true if the locale is in the system locale. Otherwise, false.
+ */
+ private static boolean isInSystemLocale(String languageTag) {
+ LocaleList systemLocales = LocaleList.getDefault();
+ Locale locale = Locale.forLanguageTag(languageTag).stripExtensions();
+ for (int i = 0; i < systemLocales.size(); i++) {
+ if (locale.equals(systemLocales.get(i).stripExtensions())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private View launchAppLocaleDetailsPage() {
FrameLayout appLocaleDetailsContainer = new FrameLayout(this);
appLocaleDetailsContainer.setId(R.id.layout_app_locale_details);
@@ -171,4 +235,4 @@ public class AppLocalePickerActivity extends SettingsBaseActivity
return false;
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java
index 7ec08f7300e..1b311ea2b4f 100644
--- a/src/com/android/settings/localepicker/LocaleListEditor.java
+++ b/src/com/android/settings/localepicker/LocaleListEditor.java
@@ -18,6 +18,7 @@ package com.android.settings.localepicker;
import static android.os.UserManager.DISALLOW_CONFIG_LOCALE;
+import static com.android.settings.localepicker.AppLocalePickerActivity.EXTRA_APP_LOCALE;
import static com.android.settings.localepicker.LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT;
import android.app.Activity;
@@ -30,6 +31,7 @@ import android.os.Bundle;
import android.os.LocaleList;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -57,6 +59,7 @@ import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.LayoutPreference;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@@ -65,10 +68,11 @@ import java.util.Locale;
*/
@SearchIndexable
public class LocaleListEditor extends RestrictedSettingsFragment implements View.OnTouchListener {
-
+ private static final String TAG = LocaleListEditor.class.getSimpleName();
protected static final String INTENT_LOCALE_KEY = "localeInfo";
private static final String CFGKEY_REMOVE_MODE = "localeRemoveMode";
private static final String CFGKEY_REMOVE_DIALOG = "showingLocaleRemoveDialog";
+ private static final String CFGKEY_ADD_LOCALE = "localeAdded";
private static final int MENU_ID_REMOVE = Menu.FIRST + 1;
private static final int REQUEST_LOCALE_PICKER = 0;
@@ -76,12 +80,16 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View
private static final String KEY_LANGUAGES_PICKER = "languages_picker";
private static final String TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT = "dialog_confirm_system_default";
private static final String TAG_DIALOG_NOT_AVAILABLE = "dialog_not_available_locale";
+ static final String EXTRA_SYSTEM_LOCALE_DIALOG_TYPE = "system_locale_dialog_type";
+ private static final String LOCALE_SUGGESTION = "locale_suggestion";
private LocaleDragAndDropAdapter mAdapter;
private Menu mMenu;
private View mAddLanguage;
+ private AlertDialog mSuggestionDialog = null;
private boolean mRemoveMode;
private boolean mShowingRemoveDialog;
+ private boolean mLocaleAdditionMode = false;
private boolean mIsUiRestricted;
private LayoutPreference mLocalePickerPreference;
@@ -141,12 +149,21 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View
}
}
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (mSuggestionDialog != null) {
+ mSuggestionDialog.dismiss();
+ }
+ }
+
@Override
public void onViewStateRestored(Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
mRemoveMode = savedInstanceState.getBoolean(CFGKEY_REMOVE_MODE, false);
mShowingRemoveDialog = savedInstanceState.getBoolean(CFGKEY_REMOVE_DIALOG, false);
+ mLocaleAdditionMode = savedInstanceState.getBoolean(CFGKEY_ADD_LOCALE, false);
}
setRemoveMode(mRemoveMode);
@@ -162,6 +179,10 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View
if (mShowingRemoveDialog) {
showRemoveLocaleWarningDialog();
}
+ if (shouldShowConfirmationDialog() && !mLocaleAdditionMode) {
+ getActivity().setResult(Activity.RESULT_OK);
+ showDialogForAddedLocale();
+ }
}
@Override
@@ -169,6 +190,7 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View
super.onSaveInstanceState(outState);
outState.putBoolean(CFGKEY_REMOVE_MODE, mRemoveMode);
outState.putBoolean(CFGKEY_REMOVE_DIALOG, mShowingRemoveDialog);
+ outState.putBoolean(CFGKEY_ADD_LOCALE, mLocaleAdditionMode);
mAdapter.saveState(outState);
}
@@ -248,6 +270,118 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View
updateVisibilityOfRemoveMenu();
}
+ private boolean shouldShowConfirmationDialog() {
+ Intent intent = this.getIntent();
+ String dialogType = intent.getStringExtra(EXTRA_SYSTEM_LOCALE_DIALOG_TYPE);
+ String localeTag = intent.getStringExtra(EXTRA_APP_LOCALE);
+ if (!isAllowedPackage()
+ || isNullOrEmpty(dialogType)
+ || isNullOrEmpty(localeTag)
+ || !LOCALE_SUGGESTION.equals(dialogType)
+ || !isValidLocale(localeTag)
+ || isInSystemLocale(localeTag)) {
+ getActivity().setResult(Activity.RESULT_CANCELED);
+ return false;
+ }
+ getActivity().setResult(Activity.RESULT_OK);
+ return true;
+ }
+
+ private boolean isAllowedPackage() {
+ List allowList = Arrays.asList(getContext().getResources().getStringArray(
+ R.array.allowed_packages_for_locale_confirmation_diallog));
+ String callingPckage = getActivity().getCallingPackage();
+ return !isNullOrEmpty(callingPckage) && allowList.contains(callingPckage);
+ }
+
+ private static boolean isNullOrEmpty(String str) {
+ return str == null || str.isEmpty();
+ }
+
+ private boolean isValidLocale(String tag) {
+ String[] systemLocales = getSupportedLocales();
+ for (String systemTag : systemLocales) {
+ if (systemTag.equals(tag)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected String[] getSupportedLocales() {
+ return LocalePicker.getSupportedLocales(getContext());
+ }
+
+ /**
+ * Check if the localeTag 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
+ * numbering system. So, during the comparison, the u extension has to be stripped out.
+ *
+ * @param languageTag A language tag
+ * @return true if the locale is in the system locale. Otherwise, false.
+ */
+ private boolean isInSystemLocale(String languageTag) {
+ LocaleList systemLocales = LocaleList.getDefault();
+ Locale locale = Locale.forLanguageTag(languageTag).stripExtensions();
+ for (int i = 0; i < systemLocales.size(); i++) {
+ if (systemLocales.get(i).stripExtensions().equals(locale)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void showDialogForAddedLocale() {
+ Intent intent = this.getIntent();
+ String dialogType = intent.getStringExtra(EXTRA_SYSTEM_LOCALE_DIALOG_TYPE);
+ String appLocaleTag = intent.getStringExtra(EXTRA_APP_LOCALE);
+ Log.d(TAG, "Dialog suggested locale: " + appLocaleTag);
+ LocaleStore.LocaleInfo localeInfo = LocaleStore.getLocaleInfo(
+ Locale.forLanguageTag(appLocaleTag));
+ if (LOCALE_SUGGESTION.equals(dialogType)) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
+ customizeLayout(dialogBuilder, localeInfo.getFullNameNative());
+ dialogBuilder
+ .setPositiveButton(R.string.add, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mLocaleAdditionMode = true;
+ String preferencesTags = Settings.System.getString(
+ getContext().getContentResolver(),
+ Settings.System.LOCALE_PREFERENCES);
+ mAdapter.addLocale(mayAppendUnicodeTags(localeInfo, preferencesTags));
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mLocaleAdditionMode = true;
+ }
+ });
+ mSuggestionDialog = dialogBuilder.create();
+ mSuggestionDialog.show();
+ } else {
+ Log.d(TAG, "Invalid parameter, dialogType:" + dialogType);
+ }
+ }
+
+ private void customizeLayout(AlertDialog.Builder dialogBuilder, String language) {
+ View dialogView = getLocaleDialogView();
+ dialogBuilder.setView(dialogView);
+ TextView title = dialogView.findViewById(R.id.dialog_title);
+ title.setText(
+ String.format(getContext().getResources().getString(
+ R.string.title_system_locale_addition), language));
+ TextView message = dialogView.findViewById(R.id.dialog_msg);
+ message.setText(R.string.desc_system_locale_addition);
+ }
+
+ protected View getLocaleDialogView() {
+ LayoutInflater inflater = this.getLayoutInflater();
+ return inflater.inflate(R.layout.locale_dialog, null);
+ }
+
// Show the appropriate warning when the user tries to remove locales.
// Shows no warning if there is no locale checked, shows a warning
// about removing all the locales if all of them are checked, and
diff --git a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java
index 16d51beca64..147b44b5e4d 100644
--- a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java
+++ b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java
@@ -16,23 +16,33 @@
package com.android.settings.localepicker;
+import static com.android.settings.localepicker.AppLocalePickerActivity.EXTRA_APP_LOCALE;
+import static com.android.settings.localepicker.LocaleListEditor.EXTRA_SYSTEM_LOCALE_DIALOG_TYPE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
import android.app.Activity;
import android.app.IActivityManager;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.os.Bundle;
import android.os.LocaleList;
+import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
+import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
@@ -82,6 +92,10 @@ public class LocaleListEditorTest {
@Mock
private LocaleDragAndDropAdapter mAdapter;
@Mock
+ private View mAddLanguage;
+ @Mock
+ private Resources mResources;
+ @Mock
private LocaleStore.LocaleInfo mLocaleInfo;
@Mock
private FragmentManager mFragmentManager;
@@ -95,7 +109,7 @@ public class LocaleListEditorTest {
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mContext = RuntimeEnvironment.application;
+ mContext = spy(RuntimeEnvironment.application);
mLocaleListEditor = spy(new LocaleListEditor());
when(mLocaleListEditor.getContext()).thenReturn(mContext);
mActivity = Robolectric.buildActivity(FragmentActivity.class).get();
@@ -107,6 +121,7 @@ public class LocaleListEditorTest {
ReflectionHelpers.setField(mLocaleListEditor, "mUserManager",
RuntimeEnvironment.application.getSystemService(Context.USER_SERVICE));
ReflectionHelpers.setField(mLocaleListEditor, "mAdapter", mAdapter);
+ ReflectionHelpers.setField(mLocaleListEditor, "mAddLanguage", mAddLanguage);
ReflectionHelpers.setField(mLocaleListEditor, "mFragmentManager", mFragmentManager);
when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
FakeFeatureFactory.setupForTest();
@@ -116,6 +131,8 @@ public class LocaleListEditorTest {
public void tearDown() {
ReflectionHelpers.setField(mLocaleListEditor, "mRemoveMode", false);
ReflectionHelpers.setField(mLocaleListEditor, "mShowingRemoveDialog", false);
+ ReflectionHelpers.setField(mLocaleListEditor, "mLocaleAdditionMode", false);
+ ShadowAlertDialogCompat.reset();
}
@Test
@@ -262,6 +279,107 @@ public class LocaleListEditorTest {
verify(mAdapter).doTheUpdate();
}
+ @Test
+ public void showDiallogForAddedLocale_showConfirmDialog() {
+ initIntentAndResourceForLocaleDialog();
+ mLocaleListEditor.onViewStateRestored(null);
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ final ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+ assertThat(shadowDialog.getView()).isNotNull();
+ TextView message = shadowDialog.getView().findViewById(R.id.dialog_msg);
+ assertThat(message.getText().toString()).isEqualTo(
+ "This lets apps and websites know you also prefer this language.");
+ }
+
+ @Test
+ public void showDiallogForAddedLocale_clickAdd() {
+ initIntentAndResourceForLocaleDialog();
+ mLocaleListEditor.onViewStateRestored(null);
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ positive.performClick();
+ verify(mAdapter).addLocale(any(LocaleStore.LocaleInfo.class));
+ }
+
+ @Test
+ public void showDiallogForAddedLocale_clickCancel() {
+ initIntentAndResourceForLocaleDialog();
+ mLocaleListEditor.onViewStateRestored(null);
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ negative.performClick();
+ verify(mAdapter, never()).addLocale(any(LocaleStore.LocaleInfo.class));
+ }
+
+ @Test
+ public void showDiallogForAddedLocale_invalidLocale_noDialog() {
+ Intent intent = new Intent("ACTION")
+ .putExtra(EXTRA_APP_LOCALE, "ab-CD") // invalid locale
+ .putExtra(EXTRA_SYSTEM_LOCALE_DIALOG_TYPE, "locale_suggestion");
+ mActivity.setIntent(intent);
+
+ mLocaleListEditor.onViewStateRestored(null);
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ @Test
+ public void showDiallogForAddedLocale_noDialogType_noDialog() {
+ Intent intent = new Intent("ACTION")
+ .putExtra(EXTRA_APP_LOCALE, "ja-JP");
+ // no EXTRA_SYSTEM_LOCALE_DIALOG_TYPE in the extra
+ mActivity.setIntent(intent);
+
+ mLocaleListEditor.onViewStateRestored(null);
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ @Test
+ public void showDiallogForAddedLocale_inSystemLocale_noDialog() {
+ LocaleList.setDefault(LocaleList.forLanguageTags("en-US,ar-AE-u-nu-arab"));
+ Intent intent = new Intent("ACTION")
+ .putExtra(EXTRA_APP_LOCALE, "ar-AE")
+ .putExtra(EXTRA_SYSTEM_LOCALE_DIALOG_TYPE, "locale_suggestion");
+ mActivity.setIntent(intent);
+
+ mLocaleListEditor.onViewStateRestored(null);
+
+ final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ private void initIntentAndResourceForLocaleDialog() {
+ Intent intent = new Intent("ACTION")
+ .putExtra(EXTRA_APP_LOCALE, "ja-JP")
+ .putExtra(EXTRA_SYSTEM_LOCALE_DIALOG_TYPE, "locale_suggestion");
+ mActivity.setIntent(intent);
+ shadowOf(mActivity).setCallingPackage("com.a.b");
+ String[] allowedPackage = new String[]{"com.a.b", "com.b.c"};
+ String[] supportedLocales = new String[]{"en-US", "ja-JP"};
+ View contentView = LayoutInflater.from(mActivity).inflate(R.layout.locale_dialog, null);
+ doReturn(contentView).when(mLocaleListEditor).getLocaleDialogView();
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getStringArray(
+ R.array.allowed_packages_for_locale_confirmation_diallog)).thenReturn(
+ allowedPackage);
+ when(mResources.getString(
+ R.string.title_system_locale_addition)).thenReturn(
+ "Add %s to preferred languages?");
+ when(mResources.getString(
+ R.string.desc_system_locale_addition)).thenReturn(
+ "This lets apps and websites know you also prefer this language.");
+ when(mLocaleListEditor.getSupportedLocales()).thenReturn(supportedLocales);
+ }
+
private void setUpLocaleConditions() {
ShadowActivityManager.setService(mActivityService);
mLocaleList = new ArrayList<>();