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<>();