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