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.util.Log;
24 import android.util.TypedValue;
25 import android.view.LayoutInflater;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.CheckBox;
30 import android.widget.CompoundButton;
31 
32 import androidx.core.view.MotionEventCompat;
33 import androidx.recyclerview.widget.ItemTouchHelper;
34 import androidx.recyclerview.widget.RecyclerView;
35 
36 import com.android.internal.app.LocalePicker;
37 import com.android.internal.app.LocaleStore;
38 import com.android.settings.R;
39 import com.android.settings.shortcut.ShortcutsUpdateTask;
40 
41 import java.text.NumberFormat;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Locale;
45 
46 
47 class LocaleDragAndDropAdapter
48         extends RecyclerView.Adapter<LocaleDragAndDropAdapter.CustomViewHolder> {
49 
50     private static final String TAG = "LocaleDragAndDropAdapter";
51     private static final String CFGKEY_SELECTED_LOCALES = "selectedLocales";
52     private final Context mContext;
53     private final List<LocaleStore.LocaleInfo> mFeedItemList;
54     private final ItemTouchHelper mItemTouchHelper;
55     private RecyclerView mParentView = null;
56     private boolean mRemoveMode = false;
57     private boolean mDragEnabled = true;
58     private NumberFormat mNumberFormatter = NumberFormat.getNumberInstance();
59 
60     class CustomViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener {
61         private final LocaleDragCell mLocaleDragCell;
62 
CustomViewHolder(LocaleDragCell view)63         public CustomViewHolder(LocaleDragCell view) {
64             super(view);
65             mLocaleDragCell = view;
66             mLocaleDragCell.getDragHandle().setOnTouchListener(this);
67         }
68 
getLocaleDragCell()69         public LocaleDragCell getLocaleDragCell() {
70             return mLocaleDragCell;
71         }
72 
73         @Override
onTouch(View v, MotionEvent event)74         public boolean onTouch(View v, MotionEvent event) {
75             if (mDragEnabled) {
76                 switch (MotionEventCompat.getActionMasked(event)) {
77                     case MotionEvent.ACTION_DOWN:
78                         mItemTouchHelper.startDrag(this);
79                 }
80             }
81             return false;
82         }
83     }
84 
LocaleDragAndDropAdapter(Context context, List<LocaleStore.LocaleInfo> feedItemList)85     public LocaleDragAndDropAdapter(Context context, List<LocaleStore.LocaleInfo> feedItemList) {
86         mFeedItemList = feedItemList;
87         mContext = context;
88 
89         final float dragElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
90                 context.getResources().getDisplayMetrics());
91 
92         mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
93                 ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0 /* no swipe */) {
94 
95             @Override
96             public boolean onMove(RecyclerView view, RecyclerView.ViewHolder source,
97                     RecyclerView.ViewHolder target) {
98                 onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
99                 return true;
100             }
101 
102             @Override
103             public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
104                 // Swipe is disabled, this is intentionally empty.
105             }
106 
107             private static final int SELECTION_GAINED = 1;
108             private static final int SELECTION_LOST = 0;
109             private static final int SELECTION_UNCHANGED = -1;
110             private int mSelectionStatus = SELECTION_UNCHANGED;
111 
112             @Override
113             public void onChildDraw(Canvas c, RecyclerView recyclerView,
114                     RecyclerView.ViewHolder viewHolder, float dX, float dY,
115                     int actionState, boolean isCurrentlyActive) {
116 
117                 super.onChildDraw(c, recyclerView, viewHolder, dX, dY,
118                         actionState, isCurrentlyActive);
119                 // We change the elevation if selection changed
120                 if (mSelectionStatus != SELECTION_UNCHANGED) {
121                     viewHolder.itemView.setElevation(
122                             mSelectionStatus == SELECTION_GAINED ? dragElevation : 0);
123                     mSelectionStatus = SELECTION_UNCHANGED;
124                 }
125             }
126 
127             @Override
128             public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
129                 super.onSelectedChanged(viewHolder, actionState);
130                 if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
131                     mSelectionStatus = SELECTION_GAINED;
132                 } else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
133                     mSelectionStatus = SELECTION_LOST;
134                 }
135             }
136         });
137     }
138 
setRecyclerView(RecyclerView rv)139     public void setRecyclerView(RecyclerView rv) {
140         mParentView = rv;
141         mItemTouchHelper.attachToRecyclerView(rv);
142     }
143 
144     @Override
onCreateViewHolder(ViewGroup viewGroup, int i)145     public CustomViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
146         final LocaleDragCell item = (LocaleDragCell) LayoutInflater.from(mContext)
147                 .inflate(R.layout.locale_drag_cell, viewGroup, false);
148         return new CustomViewHolder(item);
149     }
150 
151     @Override
onBindViewHolder(final CustomViewHolder holder, int i)152     public void onBindViewHolder(final CustomViewHolder holder, int i) {
153         final LocaleStore.LocaleInfo feedItem = mFeedItemList.get(i);
154         final LocaleDragCell dragCell = holder.getLocaleDragCell();
155         final String label = feedItem.getFullNameNative();
156         final String description = feedItem.getFullNameInUiLanguage();
157         dragCell.setLabelAndDescription(label, description);
158         dragCell.setLocalized(feedItem.isTranslated());
159         dragCell.setMiniLabel(mNumberFormatter.format(i + 1));
160         dragCell.setShowCheckbox(mRemoveMode);
161         dragCell.setShowMiniLabel(!mRemoveMode);
162         dragCell.setShowHandle(!mRemoveMode && mDragEnabled);
163         dragCell.setTag(feedItem);
164         CheckBox checkbox = dragCell.getCheckbox();
165         // clear listener before setChecked() in case another item already bind to
166         // current ViewHolder and checked event is triggered on stale listener mistakenly.
167         checkbox.setOnCheckedChangeListener(null);
168         checkbox.setChecked(mRemoveMode ? feedItem.getChecked() : false);
169         checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
170                     @Override
171                     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
172                         LocaleStore.LocaleInfo feedItem =
173                                 (LocaleStore.LocaleInfo) dragCell.getTag();
174                         feedItem.setChecked(isChecked);
175                     }
176                 });
177     }
178 
179     @Override
getItemCount()180     public int getItemCount() {
181         int itemCount = (null != mFeedItemList ? mFeedItemList.size() : 0);
182         if (itemCount < 2 || mRemoveMode) {
183             setDragEnabled(false);
184         } else {
185             setDragEnabled(true);
186         }
187         return itemCount;
188     }
189 
onItemMove(int fromPosition, int toPosition)190     void onItemMove(int fromPosition, int toPosition) {
191         if (fromPosition >= 0 && toPosition >= 0) {
192             final LocaleStore.LocaleInfo saved = mFeedItemList.get(fromPosition);
193             mFeedItemList.remove(fromPosition);
194             mFeedItemList.add(toPosition, saved);
195         } else {
196             // TODO: It looks like sometimes the RecycleView tries to swap item -1
197             // I did not see it in a while, but if it happens, investigate and file a bug.
198             Log.e(TAG, String.format(Locale.US,
199                     "Negative position in onItemMove %d -> %d", fromPosition, toPosition));
200         }
201         notifyItemChanged(fromPosition); // to update the numbers
202         notifyItemChanged(toPosition);
203         notifyItemMoved(fromPosition, toPosition);
204         // We don't call doTheUpdate() here because this method is called for each item swap.
205         // So if we drag something across several positions it will be called several times.
206     }
207 
setRemoveMode(boolean removeMode)208     void setRemoveMode(boolean removeMode) {
209         mRemoveMode = removeMode;
210         int itemCount = mFeedItemList.size();
211         for (int i = 0; i < itemCount; i++) {
212             mFeedItemList.get(i).setChecked(false);
213             notifyItemChanged(i);
214         }
215     }
216 
isRemoveMode()217     boolean isRemoveMode() {
218         return mRemoveMode;
219     }
220 
removeItem(int position)221     void removeItem(int position) {
222         int itemCount = mFeedItemList.size();
223         if (itemCount <= 1) {
224             return;
225         }
226         if (position < 0 || position >= itemCount) {
227             return;
228         }
229         mFeedItemList.remove(position);
230         notifyDataSetChanged();
231     }
232 
removeChecked()233     void removeChecked() {
234         int itemCount = mFeedItemList.size();
235         for (int i = itemCount - 1; i >= 0; i--) {
236             if (mFeedItemList.get(i).getChecked()) {
237                 mFeedItemList.remove(i);
238             }
239         }
240         notifyDataSetChanged();
241         doTheUpdate();
242     }
243 
getCheckedCount()244     int getCheckedCount() {
245         int result = 0;
246         for (LocaleStore.LocaleInfo li : mFeedItemList) {
247             if (li.getChecked()) {
248                 result++;
249             }
250         }
251         return result;
252     }
253 
addLocale(LocaleStore.LocaleInfo li)254     void addLocale(LocaleStore.LocaleInfo li) {
255         mFeedItemList.add(li);
256         notifyItemInserted(mFeedItemList.size() - 1);
257         doTheUpdate();
258     }
259 
doTheUpdate()260     public void doTheUpdate() {
261         int count = mFeedItemList.size();
262         final Locale[] newList = new Locale[count];
263 
264         for (int i = 0; i < count; i++) {
265             final LocaleStore.LocaleInfo li = mFeedItemList.get(i);
266             newList[i] = li.getLocale();
267         }
268 
269         final LocaleList ll = new LocaleList(newList);
270         updateLocalesWhenAnimationStops(ll);
271     }
272 
273     private LocaleList mLocalesToSetNext = null;
274     private LocaleList mLocalesSetLast = null;
275 
updateLocalesWhenAnimationStops(final LocaleList localeList)276     public void updateLocalesWhenAnimationStops(final LocaleList localeList) {
277         if (localeList.equals(mLocalesToSetNext)) {
278             return;
279         }
280 
281         // This will only update the Settings application to make things feel more responsive,
282         // the system will be updated later, when animation stopped.
283         LocaleList.setDefault(localeList);
284 
285         mLocalesToSetNext = localeList;
286         final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();
287         itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
288             @Override
289             public void onAnimationsFinished() {
290                 if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {
291                     // All animations finished, but the locale list did not change
292                     return;
293                 }
294 
295                 LocalePicker.updateLocales(mLocalesToSetNext);
296                 mLocalesSetLast = mLocalesToSetNext;
297                 new ShortcutsUpdateTask(mContext).execute();
298 
299                 mLocalesToSetNext = null;
300 
301                 mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());
302             }
303         });
304     }
305 
setDragEnabled(boolean enabled)306     private void setDragEnabled(boolean enabled) {
307         mDragEnabled = enabled;
308     }
309 
310     /**
311      * Saves the list of checked locales to preserve status when the list is destroyed.
312      * (for instance when the device is rotated)
313      * @param outInstanceState Bundle in which to place the saved state
314      */
saveState(Bundle outInstanceState)315     public void saveState(Bundle outInstanceState) {
316         if (outInstanceState != null) {
317             final ArrayList<String> selectedLocales = new ArrayList<>();
318             for (LocaleStore.LocaleInfo li : mFeedItemList) {
319                 if (li.getChecked()) {
320                     selectedLocales.add(li.getId());
321                 }
322             }
323             outInstanceState.putStringArrayList(CFGKEY_SELECTED_LOCALES, selectedLocales);
324         }
325     }
326 
327     /**
328      * Restores the list of checked locales to preserve status when the list is recreated.
329      * (for instance when the device is rotated)
330      * @param savedInstanceState Bundle with the data saved by {@link #saveState(Bundle)}
331      */
restoreState(Bundle savedInstanceState)332     public void restoreState(Bundle savedInstanceState) {
333         if (savedInstanceState != null && mRemoveMode) {
334             final ArrayList<String> selectedLocales =
335                     savedInstanceState.getStringArrayList(CFGKEY_SELECTED_LOCALES);
336             if (selectedLocales == null || selectedLocales.isEmpty()) {
337                 return;
338             }
339             for (LocaleStore.LocaleInfo li : mFeedItemList) {
340                 li.setChecked(selectedLocales.contains(li.getId()));
341             }
342             notifyItemRangeChanged(0, mFeedItemList.size());
343         }
344     }
345 }
346