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