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