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