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