blob: 2fcb06a32b24b8c6640667ff659bac3a7245e24a [file] [log] [blame]
/*
* 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.
*/
package com.android.settings.localepicker;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.MotionEventCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.app.LocalePicker;
import com.android.internal.app.LocaleStore;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.shortcut.ShortcutsUpdateTask;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
class LocaleDragAndDropAdapter
extends RecyclerView.Adapter<LocaleDragAndDropAdapter.CustomViewHolder> {
private static final String TAG = "LocaleDragAndDropAdapter";
private static final String CFGKEY_SELECTED_LOCALES = "selectedLocales";
private static final String CFGKEY_DRAG_LOCALE = "dragLocales";
private final Context mContext;
private final ItemTouchHelper mItemTouchHelper;
private List<LocaleStore.LocaleInfo> mFeedItemList;
private List<LocaleStore.LocaleInfo> mCacheItemList;
private RecyclerView mParentView = null;
private boolean mRemoveMode = false;
private boolean mDragEnabled = true;
private NumberFormat mNumberFormatter = NumberFormat.getNumberInstance();
private LocaleStore.LocaleInfo mDragLocale;
class CustomViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener {
private final LocaleDragCell mLocaleDragCell;
public CustomViewHolder(LocaleDragCell view) {
super(view);
mLocaleDragCell = view;
mLocaleDragCell.getDragHandle().setOnTouchListener(this);
}
public LocaleDragCell getLocaleDragCell() {
return mLocaleDragCell;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mDragEnabled) {
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_DOWN:
mItemTouchHelper.startDrag(this);
}
}
return false;
}
}
LocaleDragAndDropAdapter(LocaleListEditor parent, List<LocaleStore.LocaleInfo> feedItemList) {
mFeedItemList = feedItemList;
mCacheItemList = new ArrayList<>(feedItemList);
mContext = parent.getContext();
final float dragElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
mContext.getResources().getDisplayMetrics());
mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0 /* no swipe */) {
@Override
public boolean onMove(RecyclerView view, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
// Swipe is disabled, this is intentionally empty.
}
private static final int SELECTION_GAINED = 1;
private static final int SELECTION_LOST = 0;
private static final int SELECTION_UNCHANGED = -1;
private int mSelectionStatus = SELECTION_UNCHANGED;
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, float dX, float dY,
int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY,
actionState, isCurrentlyActive);
// We change the elevation if selection changed
if (mSelectionStatus != SELECTION_UNCHANGED) {
viewHolder.itemView.setElevation(
mSelectionStatus == SELECTION_GAINED ? dragElevation : 0);
mSelectionStatus = SELECTION_UNCHANGED;
}
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
mSelectionStatus = SELECTION_GAINED;
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
mSelectionStatus = SELECTION_LOST;
}
}
});
}
public void setRecyclerView(RecyclerView rv) {
mParentView = rv;
mItemTouchHelper.attachToRecyclerView(rv);
}
@Override
public CustomViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
final LocaleDragCell item = (LocaleDragCell) LayoutInflater.from(mContext)
.inflate(R.layout.locale_drag_cell, viewGroup, false);
return new CustomViewHolder(item);
}
@Override
public void onBindViewHolder(final CustomViewHolder holder, int i) {
final LocaleStore.LocaleInfo feedItem = mFeedItemList.get(i);
final LocaleDragCell dragCell = holder.getLocaleDragCell();
final String label = feedItem.getFullNameNative();
final String description = feedItem.getFullNameInUiLanguage();
dragCell.setLabelAndDescription(label, description);
dragCell.setLocalized(feedItem.isTranslated());
dragCell.setCurrentDefault(feedItem.getLocale().equals(Locale.getDefault()));
dragCell.setMiniLabel(mNumberFormatter.format(i + 1));
dragCell.setShowCheckbox(mRemoveMode);
dragCell.setShowMiniLabel(!mRemoveMode);
dragCell.setShowHandle(!mRemoveMode && mDragEnabled);
dragCell.setTag(feedItem);
CheckBox checkbox = dragCell.getCheckbox();
// clear listener before setChecked() in case another item already bind to
// current ViewHolder and checked event is triggered on stale listener mistakenly.
checkbox.setOnCheckedChangeListener(null);
boolean isChecked = mRemoveMode ? feedItem.getChecked() : false;
checkbox.setChecked(isChecked);
setCheckBoxDescription(dragCell, checkbox, isChecked);
checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
LocaleStore.LocaleInfo feedItem =
(LocaleStore.LocaleInfo) dragCell.getTag();
feedItem.setChecked(isChecked);
setCheckBoxDescription(dragCell, checkbox, isChecked);
}
});
}
@VisibleForTesting
protected void setCheckBoxDescription(LocaleDragCell dragCell, CheckBox checkbox,
boolean isChecked) {
if (!mRemoveMode) {
return;
}
CharSequence checkedStatus = mContext.getText(
isChecked ? com.android.internal.R.string.checked
: com.android.internal.R.string.not_checked);
// Talkback
dragCell.setStateDescription(checkedStatus);
// Select to Speak
checkbox.setContentDescription(checkedStatus);
}
@Override
public int getItemCount() {
int itemCount = (null != mFeedItemList ? mFeedItemList.size() : 0);
if (itemCount < 2 || mRemoveMode) {
setDragEnabled(false);
} else {
setDragEnabled(true);
}
return itemCount;
}
void onItemMove(int fromPosition, int toPosition) {
if (fromPosition >= 0 && toPosition >= 0) {
final LocaleStore.LocaleInfo saved = mFeedItemList.get(fromPosition);
mFeedItemList.remove(fromPosition);
mFeedItemList.add(toPosition, saved);
mDragLocale = saved;
} else {
// TODO: It looks like sometimes the RecycleView tries to swap item -1
// I did not see it in a while, but if it happens, investigate and file a bug.
Log.e(TAG, String.format(Locale.US,
"Negative position in onItemMove %d -> %d", fromPosition, toPosition));
}
if (fromPosition != toPosition) {
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.action(mContext, SettingsEnums.ACTION_REORDER_LANGUAGE,
mDragLocale.getLocale().toLanguageTag() + " move to " + toPosition);
}
notifyItemChanged(fromPosition); // to update the numbers
notifyItemChanged(toPosition);
notifyItemMoved(fromPosition, toPosition);
// We don't call doTheUpdate() here because this method is called for each item swap.
// So if we drag something across several positions it will be called several times.
}
void setRemoveMode(boolean removeMode) {
mRemoveMode = removeMode;
int itemCount = mFeedItemList.size();
for (int i = 0; i < itemCount; i++) {
mFeedItemList.get(i).setChecked(false);
notifyItemChanged(i);
}
}
boolean isRemoveMode() {
return mRemoveMode;
}
void removeItem(int position) {
int itemCount = mFeedItemList.size();
if (itemCount <= 1) {
return;
}
if (position < 0 || position >= itemCount) {
return;
}
mFeedItemList.remove(position);
notifyDataSetChanged();
}
void removeChecked() {
int itemCount = mFeedItemList.size();
LocaleStore.LocaleInfo localeInfo;
for (int i = itemCount - 1; i >= 0; i--) {
localeInfo = mFeedItemList.get(i);
if (localeInfo.getChecked()) {
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.action(mContext, SettingsEnums.ACTION_REMOVE_LANGUAGE,
localeInfo.getLocale().toLanguageTag());
mFeedItemList.remove(i);
}
}
notifyDataSetChanged();
doTheUpdate();
}
int getCheckedCount() {
int result = 0;
for (LocaleStore.LocaleInfo li : mFeedItemList) {
if (li.getChecked()) {
result++;
}
}
return result;
}
boolean isFirstLocaleChecked() {
return mFeedItemList != null && mFeedItemList.get(0).getChecked();
}
void addLocale(LocaleStore.LocaleInfo li) {
mFeedItemList.add(li);
notifyItemInserted(mFeedItemList.size() - 1);
doTheUpdate();
}
public void doTheUpdate() {
int count = mFeedItemList.size();
final Locale[] newList = new Locale[count];
for (int i = 0; i < count; i++) {
final LocaleStore.LocaleInfo li = mFeedItemList.get(i);
newList[i] = li.getLocale();
}
final LocaleList ll = new LocaleList(newList);
updateLocalesWhenAnimationStops(ll);
}
private LocaleList mLocalesToSetNext = null;
private LocaleList mLocalesSetLast = null;
public void updateLocalesWhenAnimationStops(final LocaleList localeList) {
if (localeList.equals(mLocalesToSetNext)) {
return;
}
// This will only update the Settings application to make things feel more responsive,
// the system will be updated later, when animation stopped.
LocaleList.setDefault(localeList);
mLocalesToSetNext = localeList;
final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();
itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
@Override
public void onAnimationsFinished() {
if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {
// All animations finished, but the locale list did not change
return;
}
LocalePicker.updateLocales(mLocalesToSetNext);
mLocalesSetLast = mLocalesToSetNext;
new ShortcutsUpdateTask(mContext).execute();
mLocalesToSetNext = null;
mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());
}
});
}
public void notifyListChanged(LocaleStore.LocaleInfo localeInfo) {
if (!localeInfo.getLocale().equals(mCacheItemList.get(0).getLocale())) {
mFeedItemList = new ArrayList<>(mCacheItemList);
notifyDataSetChanged();
}
}
public void setCacheItemList() {
mCacheItemList = new ArrayList<>(mFeedItemList);
}
public List<LocaleStore.LocaleInfo> getFeedItemList() {
return mFeedItemList;
}
private void setDragEnabled(boolean enabled) {
mDragEnabled = enabled;
}
/**
* 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) {
if (outInstanceState != null) {
final ArrayList<String> selectedLocales = new ArrayList<>();
for (LocaleStore.LocaleInfo li : mFeedItemList) {
if (li.getChecked()) {
selectedLocales.add(li.getId());
}
}
outInstanceState.putStringArrayList(CFGKEY_SELECTED_LOCALES, selectedLocales);
// Save the dragged locale before rotation
outInstanceState.putSerializable(CFGKEY_DRAG_LOCALE, mDragLocale);
}
}
/**
* 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)}
* @param isDialogShowing A flag indicating whether the dialog is showing or not.
*/
public void restoreState(Bundle savedInstanceState, boolean isDialogShowing) {
if (savedInstanceState != null) {
if (mRemoveMode) {
final ArrayList<String> selectedLocales =
savedInstanceState.getStringArrayList(CFGKEY_SELECTED_LOCALES);
if (selectedLocales == null || selectedLocales.isEmpty()) {
return;
}
for (LocaleStore.LocaleInfo li : mFeedItemList) {
li.setChecked(selectedLocales.contains(li.getId()));
}
notifyItemRangeChanged(0, mFeedItemList.size());
} else if (isDialogShowing) {
// After rotation, the dragged position will be restored to original. Restore the
// drag locale's original position to the top.
mDragLocale = (LocaleStore.LocaleInfo) savedInstanceState.getSerializable(
CFGKEY_DRAG_LOCALE);
if (mDragLocale != null) {
mFeedItemList.removeIf(
localeInfo -> TextUtils.equals(localeInfo.getId(),
mDragLocale.getId()));
mFeedItemList.add(0, mDragLocale);
notifyItemRangeChanged(0, mFeedItemList.size());
}
}
}
}
}