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