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