Home | History | Annotate | Download | only in provider
      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 {
     66         void onItemClicked(Bundle item, int position);
     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 
     84     public DrawerApiAdapter() {
     85         setHasStableIds(true);
     86     }
     87 
     88     @Override
     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
    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
    139     public void setMaxItems(int maxItems) {
    140         mMaxItems = maxItems;
    141     }
    142 
    143     @Override
    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
    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
    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 
    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 
    233     public int getMaxItemsNumber() {
    234         return mMaxItems;
    235     }
    236 
    237     public void setItemSelectedListener(OnItemSelectedListener listener) {
    238         mListener = listener;
    239     }
    240 
    241     public int getFirstItemIndex() {
    242         return mFirstItemIndex;
    243     }
    244 
    245     public boolean isEmptyPlaceholder() {
    246         return mIsEmptyPlaceholder;
    247     }
    248 
    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 
    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 
    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 
    323     private void onBindUnavailableCategoryView(final CarListItemViewHolder holder) {
    324         mNoLeftIcon = false;
    325         holder.itemView.setEnabled(false);
    326     }
    327 
    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 
    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 
    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 
    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