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 b7b98f26038..8dac29abdf0 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);
+ }
+}