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