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