1 /* 2 * Copyright (C) 2015 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 package android.car.ui.provider; 17 18 import android.content.Context; 19 import android.content.res.ColorStateList; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.os.SystemProperties; 25 import android.support.car.ui.CarListItemViewHolder; 26 import android.support.car.ui.PagedListView; 27 import android.support.car.ui.R; 28 import android.support.v7.widget.RecyclerView; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.CompoundButton; 34 import android.widget.ImageView; 35 import android.widget.RemoteViews; 36 import android.widget.TextView; 37 38 import android.support.car.app.menu.CarMenu; 39 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 44 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.FLAG_BROWSABLE; 45 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.FLAG_FIRSTITEM; 46 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_EMPTY_PLACEHOLDER; 47 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_FLAGS; 48 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_ID; 49 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_LEFTICON; 50 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_REMOTEVIEWS; 51 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_RIGHTICON; 52 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_RIGHTTEXT; 53 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_TEXT; 54 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_TITLE; 55 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_WIDGET; 56 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_WIDGET_STATE; 57 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.WIDGET_CHECKBOX; 58 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.WIDGET_TEXT_VIEW; 59 60 public class DrawerApiAdapter extends RecyclerView.Adapter<CarListItemViewHolder> 61 implements PagedListView.ItemCap { 62 private static final String TAG = "CAR.UI.ADAPTER"; 63 private static final String INDEX_OUT_OF_BOUNDS_MESSAGE = "invalid item position"; 64 private static final String KEY_ID_UNAVAILABLE_CATEGORY = "UNAVAILABLE_CATEGORY"; 65 private static final String UNLIMITED_MODE_PROPERTY = "android.car.drawer.unlimited"; 66 67 public interface OnItemSelectedListener { onItemClicked(Bundle item, int position)68 void onItemClicked(Bundle item, int position); onItemLongClicked(Bundle item)69 boolean onItemLongClicked(Bundle item); 70 } 71 72 private final Map<String, Integer> mIdToPosMap = new HashMap<>(); 73 74 private final Object mItemsLock = new Object(); 75 private List<Bundle> mItems; 76 private boolean mIsCapped; 77 private OnItemSelectedListener mListener; 78 private int mMaxItems; 79 private boolean mUseSmallHolder; 80 private boolean mNoLeftIcon; 81 private boolean mIsEmptyPlaceholder; 82 private int mFirstItemIndex = 0; 83 84 private final Handler mHandler = new Handler(); 85 DrawerApiAdapter()86 public DrawerApiAdapter() { 87 setHasStableIds(true); 88 } 89 90 @Override getItemViewType(int position)91 public int getItemViewType(int position) { 92 Bundle item; 93 try { 94 item = mItems.get(position); 95 } catch (IndexOutOfBoundsException e) { 96 Log.w(TAG, INDEX_OUT_OF_BOUNDS_MESSAGE, e); 97 return 0; 98 } 99 100 if (KEY_ID_UNAVAILABLE_CATEGORY.equals(item.getString(KEY_ID))) { 101 return R.layout.car_unavailable_category; 102 } 103 104 if (item.containsKey(KEY_EMPTY_PLACEHOLDER) && item.getBoolean(KEY_EMPTY_PLACEHOLDER)) { 105 return R.layout.car_list_item_empty; 106 } 107 108 int flags = item.getInt(KEY_FLAGS); 109 if ((flags & FLAG_BROWSABLE) != 0 || item.containsKey(KEY_RIGHTICON)) { 110 return R.layout.car_imageview; 111 } 112 113 if (!item.containsKey(KEY_WIDGET)) { 114 return 0; 115 } 116 117 switch (item.getInt(KEY_WIDGET)) { 118 case WIDGET_CHECKBOX: 119 return R.layout.car_menu_checkbox; 120 case WIDGET_TEXT_VIEW: 121 return R.layout.car_textview; 122 default: 123 return 0; 124 } 125 } 126 127 @Override onCreateViewHolder(ViewGroup parent, int viewType)128 public CarListItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 129 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 130 View view; 131 if (viewType == R.layout.car_unavailable_category || 132 viewType == R.layout.car_list_item_empty) { 133 view = inflater.inflate(viewType, parent, false); 134 } else { 135 view = inflater.inflate(R.layout.car_menu_list_item, parent, false); 136 } 137 return new CarListItemViewHolder(view, viewType); 138 } 139 140 @Override setMaxItems(int maxItems)141 public void setMaxItems(int maxItems) { 142 if (SystemProperties.getBoolean(UNLIMITED_MODE_PROPERTY, false)) { 143 mMaxItems = PagedListView.ItemCap.UNLIMITED; 144 } else { 145 mMaxItems = maxItems; 146 } 147 } 148 149 @Override onBindViewHolder(final CarListItemViewHolder holder, final int position)150 public void onBindViewHolder(final CarListItemViewHolder holder, final int position) { 151 if (holder.getItemViewType() == R.layout.car_list_item_empty) { 152 onBindEmptyPlaceHolder(holder, position); 153 } else if (holder.getItemViewType() == R.layout.car_unavailable_category) { 154 onBindUnavailableCategoryView(holder); 155 } else { 156 onBindNormalView(holder, position); 157 if (mIsCapped) { 158 // Disable all menu items if it is under unavailable category case. 159 // TODO(b/24163545): holder.itemView.setAlpha() doesn't work all the time, 160 // which makes some items are gray out, the others are not. 161 setHolderStatus(holder, false, 0.3f); 162 } else { 163 setHolderStatus(holder, true, 1.0f); 164 } 165 } 166 167 holder.itemView.setTag(position); 168 holder.itemView.setOnClickListener(mOnClickListener); 169 holder.itemView.setOnLongClickListener(mOnLongClickListener); 170 171 // Ensure correct day/night mode colors are set and not out of sync. 172 setDayNightModeColors(holder); 173 } 174 175 @Override getItemCount()176 public int getItemCount() { 177 synchronized (mItemsLock) { 178 if (mItems != null) { 179 return mMaxItems != PagedListView.ItemCap.UNLIMITED ? 180 Math.min(mItems.size(), mMaxItems) : mItems.size(); 181 } 182 } 183 return 0; 184 } 185 186 @Override getItemId(int position)187 public long getItemId(int position) { 188 synchronized (mItemsLock) { 189 if (mItems != null) { 190 try { 191 return mItems.get(position).getString(KEY_ID).hashCode(); 192 } catch (IndexOutOfBoundsException e) { 193 Log.w(TAG, "invalid item index", e); 194 return RecyclerView.NO_ID; 195 } 196 } 197 } 198 return super.getItemId(position); 199 } 200 setItems(List<Bundle> items, boolean isCapped)201 public synchronized void setItems(List<Bundle> items, boolean isCapped) { 202 synchronized (mItemsLock) { 203 mItems = items; 204 } 205 mIsCapped = isCapped; 206 mFirstItemIndex = 0; 207 if (mItems != null) { 208 mIdToPosMap.clear(); 209 mUseSmallHolder = true; 210 mNoLeftIcon = true; 211 mIsEmptyPlaceholder = false; 212 int index = 0; 213 for (Bundle bundle : items) { 214 if (bundle.containsKey(KEY_EMPTY_PLACEHOLDER) 215 && bundle.getBoolean(KEY_EMPTY_PLACEHOLDER)) { 216 mIsEmptyPlaceholder = true; 217 if (items.size() != 1) { 218 throw new IllegalStateException("Empty placeholder should be the only" 219 + "item showing in the menu list!"); 220 } 221 } 222 223 if (bundle.containsKey(KEY_TEXT) || bundle.containsKey(KEY_REMOTEVIEWS)) { 224 mUseSmallHolder = false; 225 } 226 if (bundle.containsKey(KEY_LEFTICON)) { 227 mNoLeftIcon = false; 228 } 229 if (bundle.containsKey(KEY_FLAGS) && 230 (bundle.getInt(KEY_FLAGS) & FLAG_FIRSTITEM) != 0) { 231 mFirstItemIndex = index; 232 } 233 mIdToPosMap.put(bundle.getString(KEY_ID), index); 234 index++; 235 } 236 } 237 notifyDataSetChanged(); 238 } 239 getMaxItemsNumber()240 public int getMaxItemsNumber() { 241 return mMaxItems; 242 } 243 setItemSelectedListener(OnItemSelectedListener listener)244 public void setItemSelectedListener(OnItemSelectedListener listener) { 245 mListener = listener; 246 } 247 getFirstItemIndex()248 public int getFirstItemIndex() { 249 return mFirstItemIndex; 250 } 251 isEmptyPlaceholder()252 public boolean isEmptyPlaceholder() { 253 return mIsEmptyPlaceholder; 254 } 255 onChildChanged(RecyclerView.ViewHolder holder, Bundle bundle)256 public void onChildChanged(RecyclerView.ViewHolder holder, Bundle bundle) { 257 synchronized (mItemsLock) { 258 // The holder will be null if the view has not been bound yet 259 if (holder != null) { 260 int position = holder.getAdapterPosition(); 261 if (position >= 0 && mItems != null && position < mItems.size()) { 262 final Bundle oldBundle; 263 try { 264 oldBundle = mItems.get(position); 265 } catch (IndexOutOfBoundsException e) { 266 Log.w(TAG, INDEX_OUT_OF_BOUNDS_MESSAGE, e); 267 return; 268 } 269 oldBundle.putAll(bundle); 270 notifyItemChanged(position); 271 } 272 } else { 273 String id = bundle.getString(KEY_ID); 274 int position = mIdToPosMap.get(id); 275 if (position >= 0 && mItems != null && position < mItems.size()) { 276 final Bundle item; 277 try { 278 item = mItems.get(position); 279 } catch (IndexOutOfBoundsException e) { 280 Log.w(TAG, INDEX_OUT_OF_BOUNDS_MESSAGE, e); 281 return; 282 } 283 if (id.equals(item.getString(KEY_ID))) { 284 item.putAll(bundle); 285 notifyItemChanged(position); 286 } 287 } 288 } 289 } 290 } 291 setDayNightModeColors(RecyclerView.ViewHolder viewHolder)292 public void setDayNightModeColors(RecyclerView.ViewHolder viewHolder) { 293 CarListItemViewHolder holder = (CarListItemViewHolder) viewHolder; 294 Context context = holder.itemView.getContext(); 295 holder.itemView.setBackgroundResource(R.drawable.car_list_item_background); 296 if (holder.getItemViewType() == R.layout.car_unavailable_category) { 297 holder.title.setTextAppearance(context, R.style.CarUnavailableCategory); 298 if (holder.text != null) { 299 holder.text.setTextAppearance(context, R.style.CarUnavailableCategory); 300 } 301 holder.icon.setImageTintList(ColorStateList 302 .valueOf(context.getResources().getColor(R.color.car_unavailable_category))); 303 } else { 304 holder.title.setTextAppearance(context, R.style.CarBody1); 305 if (holder.text != null) { 306 holder.text.setTextAppearance(context, R.style.CarBody2); 307 } 308 if (holder.rightCheckbox != null) { 309 holder.rightCheckbox.setButtonTintList( 310 ColorStateList.valueOf(context.getResources().getColor(R.color.car_tint))); 311 } else if (holder.rightImage != null) { 312 Object tag = holder.rightImage.getTag(); 313 if (tag != null && (int) tag != -1) { 314 holder.rightImage.setImageResource((int) tag); 315 } 316 } 317 } 318 } 319 onBindEmptyPlaceHolder(final CarListItemViewHolder holder, final int position)320 private void onBindEmptyPlaceHolder(final CarListItemViewHolder holder, final int position) { 321 maybeSetText(position, KEY_TITLE, holder.title); 322 if (!mNoLeftIcon) { 323 maybeSetBitmap(position, KEY_LEFTICON, holder.icon); 324 holder.iconContainer.setVisibility(View.VISIBLE); 325 } else { 326 holder.iconContainer.setVisibility(View.GONE); 327 } 328 } 329 onBindUnavailableCategoryView(final CarListItemViewHolder holder)330 private void onBindUnavailableCategoryView(final CarListItemViewHolder holder) { 331 mNoLeftIcon = false; 332 holder.itemView.setEnabled(false); 333 } 334 onBindNormalView(final CarListItemViewHolder holder, final int position)335 private void onBindNormalView(final CarListItemViewHolder holder, final int position) { 336 maybeSetText(position, KEY_TITLE, holder.title); 337 maybeSetText(position, KEY_TEXT, holder.text); 338 final Bundle item; 339 try { 340 item = new Bundle(mItems.get(position)); 341 } catch (IndexOutOfBoundsException e) { 342 Log.w(TAG, INDEX_OUT_OF_BOUNDS_MESSAGE, e); 343 return; 344 } 345 final int flags = item.getInt(KEY_FLAGS); 346 if ((flags & FLAG_BROWSABLE) != 0) { 347 // Set the resource id as the tag so we can reload it on day/night mode change. 348 // If the tag is -1 or not set, then assume the app will send an updated bitmap 349 holder.rightImage.setTag(R.drawable.ic_chevron_right); 350 holder.rightImage.setImageResource(R.drawable.ic_chevron_right); 351 } else if (holder.rightImage != null) { 352 maybeSetBitmap(position, KEY_RIGHTICON, holder.rightImage); 353 } 354 355 if (holder.rightCheckbox != null) { 356 holder.rightCheckbox.setChecked(item.getBoolean( 357 KEY_WIDGET_STATE, false)); 358 holder.rightCheckbox.setOnClickListener(mOnClickListener); 359 holder.rightCheckbox.setTag(position); 360 } 361 if (holder.rightText != null) { 362 maybeSetText(position, KEY_RIGHTTEXT, holder.rightText); 363 } 364 if (!mNoLeftIcon) { 365 maybeSetBitmap(position, KEY_LEFTICON, holder.icon); 366 holder.iconContainer.setVisibility(View.VISIBLE); 367 } else { 368 holder.iconContainer.setVisibility(View.GONE); 369 } 370 if (item.containsKey(KEY_REMOTEVIEWS)) { 371 holder.remoteViewsContainer.setVisibility(View.VISIBLE); 372 RemoteViews views = item.getParcelable(KEY_REMOTEVIEWS); 373 View view = views.apply(holder.remoteViewsContainer.getContext(), 374 holder.remoteViewsContainer); 375 holder.remoteViewsContainer.removeAllViews(); 376 holder.remoteViewsContainer.addView(view); 377 } else { 378 holder.remoteViewsContainer.removeAllViews(); 379 holder.remoteViewsContainer.setVisibility(View.GONE); 380 } 381 382 // Set the view holder size 383 Resources r = holder.itemView.getResources(); 384 ViewGroup.LayoutParams params = holder.itemView.getLayoutParams(); 385 params.height = mUseSmallHolder ? 386 r.getDimensionPixelSize(R.dimen.car_list_item_height_small) : 387 r.getDimensionPixelSize(R.dimen.car_list_item_height); 388 holder.itemView.setLayoutParams(params); 389 390 // Set Icon size 391 params = holder.iconContainer.getLayoutParams(); 392 params.height = params.width = mUseSmallHolder ? 393 r.getDimensionPixelSize(R.dimen.car_list_item_small_icon_size) : 394 r.getDimensionPixelSize(R.dimen.car_list_item_icon_size); 395 396 } 397 maybeSetText(int position, String key, TextView view)398 private void maybeSetText(int position, String key, TextView view) { 399 Bundle item; 400 try { 401 item = mItems.get(position); 402 } catch (IndexOutOfBoundsException e) { 403 Log.w(TAG, INDEX_OUT_OF_BOUNDS_MESSAGE, e); 404 return; 405 } 406 if (item.containsKey(key)) { 407 view.setText(item.getString(key)); 408 view.setVisibility(View.VISIBLE); 409 } else { 410 view.setVisibility(View.GONE); 411 } 412 } 413 maybeSetBitmap(int position, String key, ImageView view)414 private void maybeSetBitmap(int position, String key, ImageView view) { 415 Bundle item; 416 try { 417 item = mItems.get(position); 418 } catch (IndexOutOfBoundsException e) { 419 Log.w(TAG, INDEX_OUT_OF_BOUNDS_MESSAGE, e); 420 return; 421 } 422 if (item.containsKey(key)) { 423 view.setImageBitmap((Bitmap) item.getParcelable(key)); 424 view.setVisibility(View.VISIBLE); 425 view.setTag(-1); 426 } else { 427 view.setVisibility(View.GONE); 428 } 429 } 430 setHolderStatus(final CarListItemViewHolder holder, boolean isEnabled, float alpha)431 private void setHolderStatus(final CarListItemViewHolder holder, 432 boolean isEnabled, float alpha) { 433 holder.itemView.setEnabled(isEnabled); 434 if (holder.icon != null) { 435 holder.icon.setAlpha(alpha); 436 } 437 if (holder.title != null) { 438 holder.title.setAlpha(alpha); 439 } 440 if (holder.text != null) { 441 holder.text.setAlpha(alpha); 442 } 443 if (holder.rightCheckbox != null) { 444 holder.rightCheckbox.setAlpha(alpha); 445 } 446 if (holder.rightImage != null) { 447 holder.rightImage.setAlpha(alpha); 448 } 449 if (holder.rightText != null) { 450 holder.rightText.setAlpha(alpha); 451 } 452 } 453 454 private final View.OnClickListener mOnClickListener = new View.OnClickListener() { 455 @Override 456 public void onClick(View view) { 457 final Bundle item; 458 int position = (int) view.getTag(); 459 try { 460 item = mItems.get(position); 461 } catch (IndexOutOfBoundsException e) { 462 Log.w(TAG, INDEX_OUT_OF_BOUNDS_MESSAGE, e); 463 return; 464 } 465 View right = view.findViewById(R.id.right_item); 466 if (right != null && view != right && right instanceof CompoundButton) { 467 ((CompoundButton) right).toggle(); 468 } 469 if (mListener != null) { 470 mListener.onItemClicked(item, position); 471 } 472 } 473 }; 474 475 private final View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() { 476 @Override 477 public boolean onLongClick(View view) { 478 final Bundle item; 479 try { 480 item = mItems.get((int) view.getTag()); 481 } catch (IndexOutOfBoundsException e) { 482 Log.w(TAG, INDEX_OUT_OF_BOUNDS_MESSAGE, e); 483 return true; 484 } 485 final String id = item.getString(KEY_ID); 486 if (mListener != null) { 487 return mListener.onItemLongClicked(item); 488 } 489 return false; 490 } 491 }; 492 } 493