From 04eae5cc608cccb0b55f396be89d785fc82896ba Mon Sep 17 00:00:00 2001 From: tom hsu Date: Fri, 3 Mar 2023 20:00:27 +0800 Subject: [PATCH] Add confirmation dialog for system locale change. Bug: b/243511340 Test: atest passed. Change-Id: I7ef4ed4557a1064d078e49a372f11a573b81058a --- res/layout/locale_dialog.xml | 65 ++++++ res/values/strings.xml | 15 ++ .../localepicker/LocaleDialogFragment.java | 198 ++++++++++++++++++ .../LocaleDragAndDropAdapter.java | 69 +++++- .../localepicker/LocaleListEditor.java | 2 +- .../localepicker/LocaleRecyclerView.java | 2 +- .../LocaleDialogFragmentTest.java | 146 +++++++++++++ 7 files changed, 484 insertions(+), 13 deletions(-) create mode 100644 res/layout/locale_dialog.xml create mode 100644 src/com/android/settings/localepicker/LocaleDialogFragment.java create mode 100644 tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java diff --git a/res/layout/locale_dialog.xml b/res/layout/locale_dialog.xml new file mode 100644 index 00000000000..cbdb37eb23e --- /dev/null +++ b/res/layout/locale_dialog.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index bd0218608e6..d82498451cf 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -372,6 +372,21 @@ https://support.google.com/android?p=per_language_app_settings + + Change system language to %s ? + + + Your device settings and regional preferences will change. + + + Change + + + %s not available + + + This language can’t be used as a system language, but you’ve let apps and websites know you prefer this language. + Regional preferences diff --git a/src/com/android/settings/localepicker/LocaleDialogFragment.java b/src/com/android/settings/localepicker/LocaleDialogFragment.java new file mode 100644 index 00000000000..63fc1792a46 --- /dev/null +++ b/src/com/android/settings/localepicker/LocaleDialogFragment.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2023 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 android.app.Activity; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; + +import com.android.internal.app.LocaleStore; +import com.android.settings.R; +import com.android.settings.RestrictedSettingsFragment; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Create a dialog for system locale events. + */ +public class LocaleDialogFragment extends InstrumentedDialogFragment { + private static final String TAG = LocaleDialogFragment.class.getSimpleName(); + + static final int DIALOG_CONFIRM_SYSTEM_DEFAULT = 0; + static final int DIALOG_NOT_AVAILABLE_LOCALE = 1; + + static final String ARG_DIALOG_TYPE = "arg_dialog_type"; + static final String ARG_TARGET_LOCALE = "arg_target_locale"; + static final String ARG_RESULT_RECEIVER = "arg_result_receiver"; + + /** + * Show dialog + */ + public static void show( + @NonNull RestrictedSettingsFragment fragment, + int dialogType, + LocaleStore.LocaleInfo localeInfo) { + show(fragment, dialogType, localeInfo, null); + } + + /** + * Show dialog + */ + public static void show( + @NonNull RestrictedSettingsFragment fragment, + int dialogType, + LocaleStore.LocaleInfo localeInfo, + ResultReceiver resultReceiver) { + FragmentManager manager = fragment.getChildFragmentManager(); + Bundle args = new Bundle(); + args.putInt(ARG_DIALOG_TYPE, dialogType); + args.putSerializable(ARG_TARGET_LOCALE, localeInfo); + args.putParcelable(ARG_RESULT_RECEIVER, resultReceiver); + + LocaleDialogFragment localeDialogFragment = new LocaleDialogFragment(); + localeDialogFragment.setArguments(args); + localeDialogFragment.show(manager, TAG); + } + + @Override + public int getMetricsCategory() { + int dialogType = getArguments().getInt(ARG_DIALOG_TYPE); + switch (dialogType) { + case DIALOG_CONFIRM_SYSTEM_DEFAULT: + return SettingsEnums.DIALOG_SYSTEM_LOCALE_CHANGE; + case DIALOG_NOT_AVAILABLE_LOCALE: + return SettingsEnums.DIALOG_SYSTEM_LOCALE_UNAVAILABLE; + default: + return SettingsEnums.DIALOG_SYSTEM_LOCALE_CHANGE; + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + LocaleDialogController controller = new LocaleDialogController(this); + LocaleDialogController.DialogContent dialogContent = controller.getDialogContent(); + ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(getContext()).inflate( + R.layout.locale_dialog, null); + setDialogTitle(viewGroup, dialogContent.mTitle); + setDialogMessage(viewGroup, dialogContent.mMessage); + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()) + .setView(viewGroup); + if (!dialogContent.mPositiveButton.isEmpty()) { + builder.setPositiveButton(dialogContent.mPositiveButton, controller); + } + if (!dialogContent.mNegativeButton.isEmpty()) { + builder.setNegativeButton(dialogContent.mNegativeButton, controller); + } + return builder.create(); + } + + private static void setDialogTitle(View root, String content) { + TextView titleView = root.findViewById(R.id.dialog_title); + if (titleView == null) { + return; + } + titleView.setText(content); + } + + private static void setDialogMessage(View root, String content) { + TextView textView = root.findViewById(R.id.dialog_msg); + if (textView == null) { + return; + } + textView.setText(content); + } + + static class LocaleDialogController implements DialogInterface.OnClickListener { + private final Context mContext; + private final int mDialogType; + private final LocaleStore.LocaleInfo mLocaleInfo; + private final ResultReceiver mResultReceiver; + + LocaleDialogController( + @NonNull Context context, @NonNull LocaleDialogFragment dialogFragment) { + mContext = context; + Bundle arguments = dialogFragment.getArguments(); + mDialogType = arguments.getInt(ARG_DIALOG_TYPE); + mLocaleInfo = (LocaleStore.LocaleInfo) arguments.getSerializable( + ARG_TARGET_LOCALE); + mResultReceiver = (ResultReceiver) arguments.getParcelable(ARG_RESULT_RECEIVER); + } + + LocaleDialogController(@NonNull LocaleDialogFragment dialogFragment) { + this(dialogFragment.getContext(), dialogFragment); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (mResultReceiver != null && mDialogType == DIALOG_CONFIRM_SYSTEM_DEFAULT) { + Bundle bundle = new Bundle(); + bundle.putInt(ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT); + if (which == DialogInterface.BUTTON_POSITIVE) { + mResultReceiver.send(Activity.RESULT_OK, bundle); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + mResultReceiver.send(Activity.RESULT_CANCELED, bundle); + } + } + } + + @VisibleForTesting + DialogContent getDialogContent() { + DialogContent + dialogContent = new DialogContent(); + switch (mDialogType) { + case DIALOG_CONFIRM_SYSTEM_DEFAULT: + dialogContent.mTitle = String.format(mContext.getString( + R.string.title_change_system_locale), mLocaleInfo.getFullNameNative()); + dialogContent.mMessage = mContext.getString( + R.string.desc_notice_device_locale_settings_change); + dialogContent.mPositiveButton = mContext.getString( + R.string.button_label_confirmation_of_system_locale_change); + dialogContent.mNegativeButton = mContext.getString(R.string.cancel); + break; + case DIALOG_NOT_AVAILABLE_LOCALE: + dialogContent.mTitle = String.format(mContext.getString( + R.string.title_unavailable_locale), mLocaleInfo.getFullNameNative()); + dialogContent.mMessage = mContext.getString(R.string.desc_unavailable_locale); + dialogContent.mPositiveButton = mContext.getString(R.string.okay); + break; + default: + break; + } + return dialogContent; + } + + @VisibleForTesting + static class DialogContent { + String mTitle = ""; + String mMessage = ""; + String mPositiveButton = ""; + String mNegativeButton = ""; + } + } +} diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java index b3c2e3071af..bece4140153 100644 --- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java +++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java @@ -16,10 +16,14 @@ package com.android.settings.localepicker; +import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.os.Bundle; +import android.os.Handler; import android.os.LocaleList; +import android.os.Looper; +import android.os.ResultReceiver; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; @@ -49,9 +53,11 @@ class LocaleDragAndDropAdapter private static final String TAG = "LocaleDragAndDropAdapter"; private static final String CFGKEY_SELECTED_LOCALES = "selectedLocales"; private final Context mContext; - private final List mFeedItemList; + private List mFeedItemList; + private List mCacheItemList; private final ItemTouchHelper mItemTouchHelper; private RecyclerView mParentView = null; + private LocaleListEditor mParent; private boolean mRemoveMode = false; private boolean mDragEnabled = true; private NumberFormat mNumberFormatter = NumberFormat.getNumberInstance(); @@ -81,12 +87,15 @@ class LocaleDragAndDropAdapter } } - public LocaleDragAndDropAdapter(Context context, List feedItemList) { + LocaleDragAndDropAdapter(LocaleListEditor parent, + List feedItemList) { mFeedItemList = feedItemList; - mContext = context; + mParent = parent; + mCacheItemList = new ArrayList<>(feedItemList); + mContext = parent.getContext(); final float dragElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, - context.getResources().getDisplayMetrics()); + mContext.getResources().getDisplayMetrics()); mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback( ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0 /* no swipe */) { @@ -168,13 +177,13 @@ class LocaleDragAndDropAdapter checkbox.setOnCheckedChangeListener(null); checkbox.setChecked(mRemoveMode ? feedItem.getChecked() : false); checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - LocaleStore.LocaleInfo feedItem = - (LocaleStore.LocaleInfo) dragCell.getTag(); - feedItem.setChecked(isChecked); - } - }); + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + LocaleStore.LocaleInfo feedItem = + (LocaleStore.LocaleInfo) dragCell.getTag(); + feedItem.setChecked(isChecked); + } + }); } @Override @@ -308,6 +317,42 @@ class LocaleDragAndDropAdapter }); } + public void doTheUpdateWithMovingLocaleItem() { + LocaleStore.LocaleInfo localeInfo = mFeedItemList.get(0); + if (!localeInfo.getLocale().equals(LocalePicker.getLocales().get(0))) { + LocaleDialogFragment.show(mParent, + LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, + localeInfo, + new ResultReceiver(new Handler(Looper.getMainLooper())) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + super.onReceiveResult(resultCode, resultData); + int type = resultData.getInt(LocaleDialogFragment.ARG_DIALOG_TYPE); + if (type == LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT) { + if (resultCode == Activity.RESULT_OK) { + doTheUpdate(); + if (!localeInfo.isTranslated()) { + LocaleDialogFragment.show(mParent, + LocaleDialogFragment + .DIALOG_NOT_AVAILABLE_LOCALE, + localeInfo); + } + } else { + if (!localeInfo.getLocale() + .equals(mCacheItemList.get(0).getLocale())) { + mFeedItemList = new ArrayList<>(mCacheItemList); + notifyDataSetChanged(); + } + } + mCacheItemList = new ArrayList<>(mFeedItemList); + } + } + }); + } else { + doTheUpdate(); + } + } + private void setDragEnabled(boolean enabled) { mDragEnabled = enabled; } @@ -315,6 +360,7 @@ class LocaleDragAndDropAdapter /** * Saves the list of checked locales to preserve status when the list is destroyed. * (for instance when the device is rotated) + * * @param outInstanceState Bundle in which to place the saved state */ public void saveState(Bundle outInstanceState) { @@ -332,6 +378,7 @@ class LocaleDragAndDropAdapter /** * Restores the list of checked locales to preserve status when the list is recreated. * (for instance when the device is rotated) + * * @param savedInstanceState Bundle with the data saved by {@link #saveState(Bundle)} */ public void restoreState(Bundle savedInstanceState) { diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java index 89efe53b4ff..bdb9295807e 100644 --- a/src/com/android/settings/localepicker/LocaleListEditor.java +++ b/src/com/android/settings/localepicker/LocaleListEditor.java @@ -105,7 +105,7 @@ public class LocaleListEditor extends RestrictedSettingsFragment { LocaleStore.fillCache(this.getContext()); final List feedsList = getUserLocaleList(); - mAdapter = new LocaleDragAndDropAdapter(this.getContext(), feedsList); + mAdapter = new LocaleDragAndDropAdapter(this, feedsList); } @Override diff --git a/src/com/android/settings/localepicker/LocaleRecyclerView.java b/src/com/android/settings/localepicker/LocaleRecyclerView.java index d32a735d48c..5d469bf7f10 100644 --- a/src/com/android/settings/localepicker/LocaleRecyclerView.java +++ b/src/com/android/settings/localepicker/LocaleRecyclerView.java @@ -40,7 +40,7 @@ class LocaleRecyclerView extends RecyclerView { if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) { LocaleDragAndDropAdapter adapter = (LocaleDragAndDropAdapter) this.getAdapter(); if (adapter != null) { - adapter.doTheUpdate(); + adapter.doTheUpdateWithMovingLocaleItem(); } } return super.onTouchEvent(e); diff --git a/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java b/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java new file mode 100644 index 00000000000..5b10adf0ff6 --- /dev/null +++ b/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2023 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.localepicker.LocaleDialogFragment.ARG_DIALOG_TYPE; +import static com.android.settings.localepicker.LocaleDialogFragment.ARG_RESULT_RECEIVER; +import static com.android.settings.localepicker.LocaleDialogFragment.ARG_TARGET_LOCALE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.ResultReceiver; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.core.app.ApplicationProvider; + +import com.android.internal.app.LocaleStore; +import com.android.settings.testutils.ResourcesUtils; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Locale; + +@UiThreadTest +public class LocaleDialogFragmentTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private Context mContext; + private LocaleDialogFragment mDialogFragment; + + @Before + public void setUp() throws Exception { + mContext = ApplicationProvider.getApplicationContext(); + mDialogFragment = new LocaleDialogFragment(); + } + + private void setArgument( + int type, ResultReceiver receiver) { + LocaleStore.LocaleInfo localeInfo = LocaleStore.getLocaleInfo(Locale.ENGLISH); + Bundle args = new Bundle(); + args.putInt(ARG_DIALOG_TYPE, type); + args.putSerializable(ARG_TARGET_LOCALE, localeInfo); + args.putParcelable(ARG_RESULT_RECEIVER, receiver); + mDialogFragment.setArguments(args); + } + + @Test + public void getDialogContent_confirmSystemDefault_has2ButtonText() { + setArgument(LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, null); + LocaleDialogFragment.LocaleDialogController controller = + new LocaleDialogFragment.LocaleDialogController(mContext, mDialogFragment); + + LocaleDialogFragment.LocaleDialogController.DialogContent dialogContent = + controller.getDialogContent(); + + assertEquals(ResourcesUtils.getResourcesString( + mContext, "button_label_confirmation_of_system_locale_change"), + dialogContent.mPositiveButton); + assertEquals(ResourcesUtils.getResourcesString(mContext, "cancel"), + dialogContent.mNegativeButton); + } + + @Test + public void getDialogContent_unavailableLocale_has1ButtonText() { + setArgument(LocaleDialogFragment.DIALOG_NOT_AVAILABLE_LOCALE, null); + LocaleDialogFragment.LocaleDialogController controller = + new LocaleDialogFragment.LocaleDialogController(mContext, mDialogFragment); + + LocaleDialogFragment.LocaleDialogController.DialogContent dialogContent = + controller.getDialogContent(); + + assertEquals(ResourcesUtils.getResourcesString(mContext, "okay"), + dialogContent.mPositiveButton); + assertTrue(dialogContent.mNegativeButton.isEmpty()); + } + + @Test + public void onClick_clickPositiveButton_sendOK() { + ResultReceiver resultReceiver = spy(new ResultReceiver(null)); + setArgument(LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, resultReceiver); + LocaleDialogFragment.LocaleDialogController controller = + new LocaleDialogFragment.LocaleDialogController(mContext, mDialogFragment); + + controller.onClick(null, DialogInterface.BUTTON_POSITIVE); + + verify(resultReceiver).send(eq(Activity.RESULT_OK), any()); + } + + @Test + public void onClick_clickNegativeButton_sendCancel() { + ResultReceiver resultReceiver = spy(new ResultReceiver(null)); + setArgument(LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, resultReceiver); + LocaleDialogFragment.LocaleDialogController controller = + new LocaleDialogFragment.LocaleDialogController(mContext, mDialogFragment); + + controller.onClick(null, DialogInterface.BUTTON_NEGATIVE); + + verify(resultReceiver).send(eq(Activity.RESULT_CANCELED), any()); + } + + @Test + public void getMetricsCategory_systemLocaleChange() { + setArgument(LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, null); + + int result = mDialogFragment.getMetricsCategory(); + + assertEquals(SettingsEnums.DIALOG_SYSTEM_LOCALE_CHANGE, result); + } + + @Test + public void getMetricsCategory_unavailableLocale() { + setArgument(LocaleDialogFragment.DIALOG_NOT_AVAILABLE_LOCALE, null); + + int result = mDialogFragment.getMetricsCategory(); + + assertEquals(SettingsEnums.DIALOG_SYSTEM_LOCALE_UNAVAILABLE, result); + } +}