• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.localepicker;
18 
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.os.Bundle;
22 import android.os.LocaleList;
23 import android.text.TextUtils;
24 import android.util.Log;
25 import android.util.TypedValue;
26 import android.view.LayoutInflater;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.widget.CheckBox;
31 import android.widget.CompoundButton;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.core.view.MotionEventCompat;
35 import androidx.recyclerview.widget.ItemTouchHelper;
36 import androidx.recyclerview.widget.RecyclerView;
37 
38 import com.android.internal.app.LocalePicker;
39 import com.android.internal.app.LocaleStore;
40 import com.android.settings.R;
41 import com.android.settings.shortcut.ShortcutsUpdateTask;
42 
43 import java.text.NumberFormat;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Locale;
47 
48 class LocaleDragAndDropAdapter
49         extends RecyclerView.Adapter<LocaleDragAndDropAdapter.CustomViewHolder> {
50 
51     private static final String TAG = "LocaleDragAndDropAdapter";
52     private static final String CFGKEY_SELECTED_LOCALES = "selectedLocales";
53     private static final String CFGKEY_DRAG_LOCALE = "dragLocales";
54 
55     private final Context mContext;
56     private final ItemTouchHelper mItemTouchHelper;
57 
58     private List<LocaleStore.LocaleInfo> mFeedItemList;
59     private List<LocaleStore.LocaleInfo> mCacheItemList;
60     private RecyclerView mParentView = null;
61     private boolean mRemoveMode = false;
62     private boolean mDragEnabled = true;
63     private NumberFormat mNumberFormatter = NumberFormat.getNumberInstance();
64     private LocaleStore.LocaleInfo mDragLocale;
65 
66     class CustomViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener {
67         private final LocaleDragCell mLocaleDragCell;
68 
CustomViewHolder(LocaleDragCell view)69         public CustomViewHolder(LocaleDragCell view) {
70             super(view);
71             mLocaleDragCell = view;
72             mLocaleDragCell.getDragHandle().setOnTouchListener(this);
73         }
74 
getLocaleDragCell()75         public LocaleDragCell getLocaleDragCell() {
76             return mLocaleDragCell;
77         }
78 
79         @Override
onTouch(View v, MotionEvent event)80         public boolean onTouch(View v, MotionEvent event) {
81             if (mDragEnabled) {
82                 switch (MotionEventCompat.getActionMasked(event)) {
83                     case MotionEvent.ACTION_DOWN:
84                         mItemTouchHelper.startDrag(this);
85                 }
86             }
87             return false;
88         }
89     }
90 
LocaleDragAndDropAdapter(LocaleListEditor parent, List<LocaleStore.LocaleInfo> feedItemList)91     LocaleDragAndDropAdapter(LocaleListEditor parent, List<LocaleStore.LocaleInfo> feedItemList) {
92         mFeedItemList = feedItemList;
93         mCacheItemList = new ArrayList<>(feedItemList);
94         mContext = parent.getContext();
95 
96         final float dragElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
97                 mContext.getResources().getDisplayMetrics());
98 
99         mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
100                 ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0 /* no swipe */) {
101 
102             @Override
103             public boolean onMove(RecyclerView view, RecyclerView.ViewHolder source,
104                     RecyclerView.ViewHolder target) {
105                 onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
106                 return true;
107             }
108 
109             @Override
110             public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
111                 // Swipe is disabled, this is intentionally empty.
112             }
113 
114             private static final int SELECTION_GAINED = 1;
115             private static final int SELECTION_LOST = 0;
116             private static final int SELECTION_UNCHANGED = -1;
117             private int mSelectionStatus = SELECTION_UNCHANGED;
118 
119             @Override
120             public void onChildDraw(Canvas c, RecyclerView recyclerView,
121                     RecyclerView.ViewHolder viewHolder, float dX, float dY,
122                     int actionState, boolean isCurrentlyActive) {
123 
124                 super.onChildDraw(c, recyclerView, viewHolder, dX, dY,
125                         actionState, isCurrentlyActive);
126                 // We change the elevation if selection changed
127                 if (mSelectionStatus != SELECTION_UNCHANGED) {
128                     viewHolder.itemView.setElevation(
129                             mSelectionStatus == SELECTION_GAINED ? dragElevation : 0);
130                     mSelectionStatus = SELECTION_UNCHANGED;
131                 }
132             }
133 
134             @Override
135             public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
136                 super.onSelectedChanged(viewHolder, actionState);
137                 if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
138                     mSelectionStatus = SELECTION_GAINED;
139                 } else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
140                     mSelectionStatus = SELECTION_LOST;
141                 }
142             }
143         });
144     }
145 
setRecyclerView(RecyclerView rv)146     public void setRecyclerView(RecyclerView rv) {
147         mParentView = rv;
148         mItemTouchHelper.attachToRecyclerView(rv);
149     }
150 
151     @Override
onCreateViewHolder(ViewGroup viewGroup, int i)152     public CustomViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
153         final LocaleDragCell item = (LocaleDragCell) LayoutInflater.from(mContext)
154                 .inflate(R.layout.locale_drag_cell, viewGroup, false);
155         return new CustomViewHolder(item);
156     }
157 
158     @Override
onBindViewHolder(final CustomViewHolder holder, int i)159     public void onBindViewHolder(final CustomViewHolder holder, int i) {
160         final LocaleStore.LocaleInfo feedItem = mFeedItemList.get(i);
161         final LocaleDragCell dragCell = holder.getLocaleDragCell();
162         final String label = feedItem.getFullNameNative();
163         final String description = feedItem.getFullNameInUiLanguage();
164 
165         dragCell.setLabelAndDescription(label, description);
166         dragCell.setLocalized(feedItem.isTranslated());
167         dragCell.setCurrentDefault(feedItem.getLocale().equals(Locale.getDefault()));
168         dragCell.setMiniLabel(mNumberFormatter.format(i + 1));
169         dragCell.setShowCheckbox(mRemoveMode);
170         dragCell.setShowMiniLabel(!mRemoveMode);
171         dragCell.setShowHandle(!mRemoveMode && mDragEnabled);
172         dragCell.setTag(feedItem);
173         CheckBox checkbox = dragCell.getCheckbox();
174         // clear listener before setChecked() in case another item already bind to
175         // current ViewHolder and checked event is triggered on stale listener mistakenly.
176         checkbox.setOnCheckedChangeListener(null);
177         boolean isChecked = mRemoveMode ? feedItem.getChecked() : false;
178         checkbox.setChecked(isChecked);
179         setCheckBoxDescription(dragCell, checkbox, isChecked);
180 
181         checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
182             @Override
183             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
184                 LocaleStore.LocaleInfo feedItem =
185                         (LocaleStore.LocaleInfo) dragCell.getTag();
186                 feedItem.setChecked(isChecked);
187                 setCheckBoxDescription(dragCell, checkbox, isChecked);
188             }
189         });
190     }
191 
192     @VisibleForTesting
setCheckBoxDescription(LocaleDragCell dragCell, CheckBox checkbox, boolean isChecked)193     protected void setCheckBoxDescription(LocaleDragCell dragCell, CheckBox checkbox,
194             boolean isChecked) {
195         CharSequence checkedStatus = mContext.getText(
196                 isChecked ? com.android.internal.R.string.checked
197                         : com.android.internal.R.string.not_checked);
198         // Talkback
199         dragCell.setStateDescription(checkedStatus);
200         // Select to Speak
201         checkbox.setContentDescription(checkedStatus);
202     }
203 
204     @Override
getItemCount()205     public int getItemCount() {
206         int itemCount = (null != mFeedItemList ? mFeedItemList.size() : 0);
207         if (itemCount < 2 || mRemoveMode) {
208             setDragEnabled(false);
209         } else {
210             setDragEnabled(true);
211         }
212         return itemCount;
213     }
214 
onItemMove(int fromPosition, int toPosition)215     void onItemMove(int fromPosition, int toPosition) {
216         if (fromPosition >= 0 && toPosition >= 0) {
217             final LocaleStore.LocaleInfo saved = mFeedItemList.get(fromPosition);
218             mFeedItemList.remove(fromPosition);
219             mFeedItemList.add(toPosition, saved);
220             mDragLocale = saved;
221         } else {
222             // TODO: It looks like sometimes the RecycleView tries to swap item -1
223             // I did not see it in a while, but if it happens, investigate and file a bug.
224             Log.e(TAG, String.format(Locale.US,
225                     "Negative position in onItemMove %d -> %d", fromPosition, toPosition));
226         }
227 
228         notifyItemChanged(fromPosition); // to update the numbers
229         notifyItemChanged(toPosition);
230         notifyItemMoved(fromPosition, toPosition);
231         // We don't call doTheUpdate() here because this method is called for each item swap.
232         // So if we drag something across several positions it will be called several times.
233     }
234 
setRemoveMode(boolean removeMode)235     void setRemoveMode(boolean removeMode) {
236         mRemoveMode = removeMode;
237         int itemCount = mFeedItemList.size();
238         for (int i = 0; i < itemCount; i++) {
239             mFeedItemList.get(i).setChecked(false);
240             notifyItemChanged(i);
241         }
242     }
243 
isRemoveMode()244     boolean isRemoveMode() {
245         return mRemoveMode;
246     }
247 
removeItem(int position)248     void removeItem(int position) {
249         int itemCount = mFeedItemList.size();
250         if (itemCount <= 1) {
251             return;
252         }
253         if (position < 0 || position >= itemCount) {
254             return;
255         }
256         mFeedItemList.remove(position);
257         notifyDataSetChanged();
258     }
259 
removeChecked()260     void removeChecked() {
261         int itemCount = mFeedItemList.size();
262         LocaleStore.LocaleInfo localeInfo;
263         for (int i = itemCount - 1; i >= 0; i--) {
264             localeInfo = mFeedItemList.get(i);
265             if (localeInfo.getChecked()) {
266                 mFeedItemList.remove(i);
267             }
268         }
269         notifyDataSetChanged();
270         doTheUpdate();
271     }
272 
getCheckedCount()273     int getCheckedCount() {
274         int result = 0;
275         for (LocaleStore.LocaleInfo li : mFeedItemList) {
276             if (li.getChecked()) {
277                 result++;
278             }
279         }
280         return result;
281     }
282 
isFirstLocaleChecked()283     boolean isFirstLocaleChecked() {
284         return mFeedItemList != null && mFeedItemList.get(0).getChecked();
285     }
286 
addLocale(LocaleStore.LocaleInfo li)287     void addLocale(LocaleStore.LocaleInfo li) {
288         mFeedItemList.add(li);
289         notifyItemInserted(mFeedItemList.size() - 1);
290         doTheUpdate();
291     }
292 
doTheUpdate()293     public void doTheUpdate() {
294         int count = mFeedItemList.size();
295         final Locale[] newList = new Locale[count];
296 
297         for (int i = 0; i < count; i++) {
298             final LocaleStore.LocaleInfo li = mFeedItemList.get(i);
299             newList[i] = li.getLocale();
300         }
301 
302         final LocaleList ll = new LocaleList(newList);
303         updateLocalesWhenAnimationStops(ll);
304     }
305 
306     private LocaleList mLocalesToSetNext = null;
307     private LocaleList mLocalesSetLast = null;
308 
updateLocalesWhenAnimationStops(final LocaleList localeList)309     public void updateLocalesWhenAnimationStops(final LocaleList localeList) {
310         if (localeList.equals(mLocalesToSetNext)) {
311             return;
312         }
313 
314         // This will only update the Settings application to make things feel more responsive,
315         // the system will be updated later, when animation stopped.
316         LocaleList.setDefault(localeList);
317 
318         mLocalesToSetNext = localeList;
319         final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();
320         itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
321             @Override
322             public void onAnimationsFinished() {
323                 if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {
324                     // All animations finished, but the locale list did not change
325                     return;
326                 }
327 
328                 LocalePicker.updateLocales(mLocalesToSetNext);
329                 mLocalesSetLast = mLocalesToSetNext;
330                 new ShortcutsUpdateTask(mContext).execute();
331 
332                 mLocalesToSetNext = null;
333 
334                 mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());
335             }
336         });
337     }
338 
notifyListChanged(LocaleStore.LocaleInfo localeInfo)339     public void notifyListChanged(LocaleStore.LocaleInfo localeInfo) {
340         if (!localeInfo.getLocale().equals(mCacheItemList.get(0).getLocale())) {
341             mFeedItemList = new ArrayList<>(mCacheItemList);
342             notifyDataSetChanged();
343         }
344     }
345 
setCacheItemList()346     public void setCacheItemList() {
347         mCacheItemList = new ArrayList<>(mFeedItemList);
348     }
349 
getFeedItemList()350     public List<LocaleStore.LocaleInfo> getFeedItemList() {
351         return mFeedItemList;
352     }
setDragEnabled(boolean enabled)353     private void setDragEnabled(boolean enabled) {
354         mDragEnabled = enabled;
355     }
356 
357     /**
358      * Saves the list of checked locales to preserve status when the list is destroyed.
359      * (for instance when the device is rotated)
360      *
361      * @param outInstanceState Bundle in which to place the saved state
362      */
saveState(Bundle outInstanceState)363     public void saveState(Bundle outInstanceState) {
364         if (outInstanceState != null) {
365             final ArrayList<String> selectedLocales = new ArrayList<>();
366             for (LocaleStore.LocaleInfo li : mFeedItemList) {
367                 if (li.getChecked()) {
368                     selectedLocales.add(li.getId());
369                 }
370             }
371             outInstanceState.putStringArrayList(CFGKEY_SELECTED_LOCALES, selectedLocales);
372             // Save the dragged locale before rotation
373             outInstanceState.putSerializable(CFGKEY_DRAG_LOCALE, mDragLocale);
374         }
375     }
376 
377     /**
378      * Restores the list of checked locales to preserve status when the list is recreated.
379      * (for instance when the device is rotated)
380      *
381      * @param savedInstanceState Bundle with the data saved by {@link #saveState(Bundle)}
382      * @param isDialogShowing A flag indicating whether the dialog is showing or not.
383      */
restoreState(Bundle savedInstanceState, boolean isDialogShowing)384     public void restoreState(Bundle savedInstanceState, boolean isDialogShowing) {
385         if (savedInstanceState != null) {
386             if (mRemoveMode) {
387                 final ArrayList<String> selectedLocales =
388                         savedInstanceState.getStringArrayList(CFGKEY_SELECTED_LOCALES);
389                 if (selectedLocales == null || selectedLocales.isEmpty()) {
390                     return;
391                 }
392                 for (LocaleStore.LocaleInfo li : mFeedItemList) {
393                     li.setChecked(selectedLocales.contains(li.getId()));
394                 }
395                 notifyItemRangeChanged(0, mFeedItemList.size());
396             } else if (isDialogShowing) {
397                 // After rotation, the dragged position will be restored to original. Restore the
398                 // drag locale's original position to the top.
399                 mDragLocale = (LocaleStore.LocaleInfo) savedInstanceState.getSerializable(
400                         CFGKEY_DRAG_LOCALE);
401                 if (mDragLocale != null) {
402                     mFeedItemList.removeIf(
403                             localeInfo -> TextUtils.equals(localeInfo.getId(),
404                                     mDragLocale.getId()));
405                     mFeedItemList.add(0, mDragLocale);
406                     notifyItemRangeChanged(0, mFeedItemList.size());
407                 }
408             }
409         }
410     }
411 }
412