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.car.app.menu.CarMenuCallbacks;
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.os.Bundle;
     22 import android.support.car.ui.PagedListView;
     23 import android.support.car.ui.R;
     24 import android.support.v7.widget.CardView;
     25 import android.support.v7.widget.RecyclerView;
     26 import android.text.TextUtils;
     27 import android.util.Log;
     28 import android.view.View;
     29 import android.view.animation.Animation;
     30 import android.view.animation.AnimationUtils;
     31 import android.widget.ProgressBar;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Arrays;
     35 import java.util.LinkedList;
     36 import java.util.List;
     37 import java.util.Queue;
     38 import java.util.Stack;
     39 
     40 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.FLAG_BROWSABLE;
     41 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_FLAGS;
     42 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_ID;
     43 import static android.car.app.menu.CarMenuConstants.MenuItemConstants.KEY_TITLE;
     44 
     45 /**
     46  * Controls the drawer for SDK app
     47  */
     48 public class DrawerController
     49         implements CarDrawerLayout.DrawerListener, DrawerApiAdapter.OnItemSelectedListener,
     50         CarDrawerLayout.DrawerControllerListener {
     51     private static final String TAG = "CAR.UI.DrawerController";
     52     // Qualify with full package name to make it less likely there will be a collision
     53     private static final String KEY_IDS = "android.support.car.ui.drawer.sdk.IDS";
     54     private static final String KEY_DRAWERSTATE =
     55             "android.support.car.ui.drawer.sdk.DRAWER_STATE";
     56     private static final String KEY_TITLES = "android.support.car.ui.drawer.sdk.TITLES";
     57     private static final String KEY_ROOT = "android.support.car.ui.drawer.sdk.ROOT";
     58     private static final String KEY_ID_UNAVAILABLE_CATEGORY = "UNAVAILABLE_CATEGORY";
     59     private static final String KEY_CLICK_STACK =
     60             "android.support.car.ui.drawer.sdk.CLICK_STACK";
     61     private static final String KEY_MAX_PAGES =
     62             "android.support.car.ui.drawer.sdk.MAX_PAGES";
     63     private static final String KEY_IS_CAPPED =
     64             "android.support.car.ui.drawer.sdk.IS_CAPPED";
     65 
     66     /** Drawer is in Auto dark/light mode */
     67     private static final int MODE_AUTO = 0;
     68     /** Drawer is in Light mode */
     69     private static final int MODE_LIGHT = 1;
     70     /** Drawer is in Dark mode */
     71     private static final int MODE_DARK = 2;
     72 
     73     private final Stack<String> mSubscriptionIds = new Stack<>();
     74     private final Stack<CharSequence> mTitles = new Stack<>();
     75     private final SubscriptionCallbacks mSubscriptionCallbacks = new SubscriptionCallbacks();
     76     // Named to be consistent with CarDrawerFragment to make copying code easier and less error
     77     // prone
     78     private final CarDrawerLayout mContainer;
     79     private final PagedListView mListView;
     80 //    private final CardView mTruncatedListCardView;
     81     private final ProgressBar mProgressBar;
     82     private final Context mContext;
     83     private final ViewAnimationController mPlvAnimationController;
     84     private final CardView mTruncatedListCardView;
     85     private final Stack<Integer> mClickCountStack = new Stack<>();
     86 
     87     private CarMenuCallbacks mCarMenuCallbacks;
     88     private DrawerApiAdapter mAdapter;
     89     private int mScrimColor = CarDrawerLayout.DEFAULT_SCRIM_COLOR;
     90     private boolean mIsDrawerOpen;
     91     private boolean mIsDrawerAnimating;
     92     private boolean mIsCapped;
     93     private int mItemsNumber;
     94     private int mDrawerMode;
     95     private CharSequence mContentTitle;
     96     private String mRootId;
     97     private boolean mRestartedFromDayNightMode;
     98     private CarUiEntry mUiEntry;
     99 
    100     public DrawerController(CarUiEntry uiEntry, View menuButton, CarDrawerLayout drawerLayout,
    101                             PagedListView listView, CardView cardView) {
    102         //mCarAppLayout = appLayout;
    103         menuButton.setOnClickListener(mMenuClickListener);
    104         mContainer = drawerLayout;
    105         mListView = listView;
    106         mUiEntry = uiEntry;
    107         mTruncatedListCardView = cardView;
    108         mListView.setDefaultItemDecoration(new DrawerMenuListDecoration(mListView.getContext()));
    109         mProgressBar = (ProgressBar) mContainer.findViewById(R.id.progress);
    110         mContext = mListView.getContext();
    111         mPlvAnimationController = new ViewAnimationController(
    112                 mListView, R.anim.car_list_in, R.anim.sdk_list_out, R.anim.car_list_pop_out);
    113         mRootId = null;
    114 
    115         mContainer.setDrawerListener(this);
    116         mContainer.setDrawerControllerListener(this);
    117         setAutoLightDarkMode();
    118     }
    119 
    120 
    121     @Override
    122     public void onDrawerOpened(View drawerView) {
    123         mIsDrawerOpen = true;
    124         mIsDrawerAnimating = false;
    125         mUiEntry.setMenuProgress(1.0f);
    126         // This can be null on day/night mode changes
    127         if (mCarMenuCallbacks != null) {
    128             mCarMenuCallbacks.onCarMenuOpened();
    129         }
    130     }
    131 
    132     @Override
    133     public void onDrawerClosed(View drawerView) {
    134         mIsDrawerOpen = false;
    135         mIsDrawerAnimating = false;
    136         clearMenu();
    137         mUiEntry.setMenuProgress(0);
    138         mUiEntry.setTitle(mContentTitle);
    139         // This can be null on day/night mode changes
    140         if (mCarMenuCallbacks != null) {
    141             mCarMenuCallbacks.onCarMenuClosed();
    142         }
    143     }
    144 
    145     @Override
    146     public void onDrawerStateChanged(int newState) {
    147     }
    148 
    149     @Override
    150     public void onDrawerOpening(View drawerView) {
    151         mIsDrawerAnimating = true;
    152         // This can be null on day/night mode changes
    153         if (mCarMenuCallbacks != null) {
    154             mCarMenuCallbacks.onCarMenuOpening();
    155         }
    156     }
    157 
    158     @Override
    159     public void onDrawerSlide(View drawerView, float slideOffset) {
    160         mUiEntry.setMenuProgress(slideOffset);
    161     }
    162 
    163     @Override
    164     public void onDrawerClosing(View drawerView) {
    165         mIsDrawerAnimating = true;
    166         // This can be null on day/night mode changes
    167         if (mCarMenuCallbacks != null) {
    168             mCarMenuCallbacks.onCarMenuClosing();
    169         }
    170     }
    171 
    172     @Override
    173     public void onItemClicked(Bundle item, int position) {
    174         // Don't allow selection while animating
    175         if (mPlvAnimationController.isAnimating()) {
    176             return;
    177         }
    178         int flags = item.getInt(KEY_FLAGS);
    179         String id = item.getString(KEY_ID);
    180 
    181         // Page number is 0 index, + 1 for the actual click.
    182         int clicksUsed = mListView.getPage(position) + 1;
    183         mClickCountStack.push(clicksUsed);
    184         mListView.setMaxPages(mListView.getMaxPages() - clicksUsed);
    185         mCarMenuCallbacks.onItemClicked(id);
    186         if ((flags & FLAG_BROWSABLE) != 0) {
    187             if (mListView.getMaxPages() == 0) {
    188                 mIsCapped = true;
    189             }
    190             CharSequence title = item.getString(KEY_TITLE);
    191             if (TextUtils.isEmpty(title)) {
    192                 title = mContentTitle;
    193             }
    194             mUiEntry.setTitleText(title);
    195             mTitles.push(title);
    196             if (!mSubscriptionIds.isEmpty()) {
    197                 mPlvAnimationController.enqueueExitAnimation(mClearAdapterRunnable);
    198             }
    199             mProgressBar.setVisibility(View.VISIBLE);
    200             if (!mSubscriptionIds.isEmpty()) {
    201                 mCarMenuCallbacks.unsubscribe(mSubscriptionIds.peek(), mSubscriptionCallbacks);
    202             }
    203             mSubscriptionIds.push(id);
    204             subscribe(id);
    205         } else {
    206             closeDrawer();
    207         }
    208     }
    209 
    210     @Override
    211     public boolean onItemLongClicked(Bundle item) {
    212         return mCarMenuCallbacks.onItemLongClicked(item.getString(KEY_ID));
    213     }
    214 
    215     @Override
    216     public void onBack() {
    217         backOrClose();
    218     }
    219 
    220     @Override
    221     public boolean onScroll() {
    222         // Consume scroll event if we are animating.
    223         return mPlvAnimationController.isAnimating();
    224     }
    225 
    226     public void setTitle(CharSequence title) {
    227         Log.d(TAG, "setTitle in drawer" + title);
    228         if (!TextUtils.isEmpty(title)) {
    229             mContentTitle = title;
    230             mUiEntry.showTitle();
    231             mUiEntry.setTitleText(title);
    232         } else {
    233             mUiEntry.hideTitle();
    234         }
    235     }
    236 
    237     public void setRootAndCallbacks(String rootId, CarMenuCallbacks callbacks) {
    238         mAdapter = new DrawerApiAdapter();
    239         mAdapter.setItemSelectedListener(this);
    240         mListView.setAdapter(mAdapter);
    241         mCarMenuCallbacks = callbacks;
    242         // HACK: Due to the handler, setRootId will be called after onRestoreState.
    243         // If onRestoreState has been called, the root id will already be set. So nothing to do.
    244         if (mSubscriptionIds.isEmpty()) {
    245             setRootId(rootId);
    246         } else {
    247             subscribe(mSubscriptionIds.peek());
    248             openDrawer();
    249         }
    250     }
    251 
    252     public void saveState(Bundle out) {
    253         out.putStringArray(KEY_IDS, mSubscriptionIds.toArray(new String[mSubscriptionIds.size()]));
    254         out.putStringArray(KEY_TITLES, mTitles.toArray(new String[mTitles.size()]));
    255         out.putString(KEY_ROOT, mRootId);
    256         out.putBoolean(KEY_DRAWERSTATE, mIsDrawerOpen);
    257         out.putIntegerArrayList(KEY_CLICK_STACK, new ArrayList<Integer>(mClickCountStack));
    258         out.putBoolean(KEY_IS_CAPPED, mIsCapped);
    259         out.putInt(KEY_MAX_PAGES, mListView.getMaxPages());
    260     }
    261 
    262     public void restoreState(Bundle in) {
    263         if (in != null) {
    264             // Restore subscribed CarMenu ids
    265             String[] ids = in.getStringArray(KEY_IDS);
    266             mSubscriptionIds.clear();
    267             if (ids != null) {
    268                 mSubscriptionIds.addAll(Arrays.asList(ids));
    269             }
    270             // Restore drawer titles if there are any
    271             String[] titles = in.getStringArray(KEY_TITLES);
    272             mTitles.clear();
    273             if (titles != null) {
    274                 mTitles.addAll(Arrays.asList(titles));
    275             }
    276             if (!mTitles.isEmpty()) {
    277                 mUiEntry.setTitleText(mTitles.peek());
    278             }
    279             mRootId = in.getString(KEY_ROOT);
    280             mIsDrawerOpen = in.getBoolean(KEY_DRAWERSTATE);
    281             ArrayList<Integer> clickCount = in.getIntegerArrayList(KEY_CLICK_STACK);
    282             mClickCountStack.clear();
    283             if (clickCount != null) {
    284                 mClickCountStack.addAll(clickCount);
    285             }
    286             mIsCapped = in.getBoolean(KEY_IS_CAPPED);
    287             mListView.setMaxPages(in.getInt(KEY_MAX_PAGES));
    288             if (!mRestartedFromDayNightMode && mIsDrawerOpen) {
    289                 closeDrawer();
    290             }
    291         }
    292     }
    293 
    294     public void setScrimColor(int color) {
    295         mScrimColor = color;
    296         mContainer.setScrimColor(color);
    297         updateViewFaders();
    298     }
    299 
    300     public void setAutoLightDarkMode() {
    301         mDrawerMode = MODE_AUTO;
    302         mContainer.setAutoDayNightMode();
    303         updateViewFaders();
    304     }
    305 
    306     public void setLightMode() {
    307         mDrawerMode = MODE_LIGHT;
    308         mContainer.setLightMode();
    309         updateViewFaders();
    310     }
    311 
    312     public void setDarkMode() {
    313         mDrawerMode = MODE_DARK;
    314         mContainer.setDarkMode();
    315         updateViewFaders();
    316     }
    317 
    318     public void openDrawer() {
    319         // If we have no root, then we can't open the drawer.
    320         if (mRootId == null) {
    321             return;
    322         }
    323         mContainer.openDrawer();
    324     }
    325 
    326     public void closeDrawer() {
    327         if (mRootId == null) {
    328             return;
    329         }
    330         mTruncatedListCardView.setVisibility(View.GONE);
    331         mPlvAnimationController.stopAndClearAnimations();
    332         mContainer.closeDrawer();
    333         mUiEntry.setTitle(mContentTitle);
    334     }
    335 
    336     public void setDrawerEnabled(boolean enabled) {
    337         if (enabled) {
    338             mContainer.setDrawerLockMode(CarDrawerLayout.LOCK_MODE_UNLOCKED);
    339         } else {
    340             mContainer.setDrawerLockMode(CarDrawerLayout.LOCK_MODE_LOCKED_CLOSED);
    341         }
    342     }
    343 
    344     public void showMenu(String id, String title) {
    345         // The app wants to show the menu associated with the given id. Create a fake item using the
    346         // given inputs and then pretend as if the user clicked on the item, so that the drawer
    347         // will subscribe to that menu id, set the title appropriately, and properly handle the
    348         // subscription stack.
    349         Bundle bundle = new Bundle();
    350         bundle.putString(KEY_ID, id);
    351         bundle.putString(KEY_TITLE, title);
    352         bundle.putInt(KEY_FLAGS, FLAG_BROWSABLE);
    353         onItemClicked(bundle, 0 /* position */);
    354     }
    355 
    356     public void setRootId(String rootId) {
    357         mRootId = rootId;
    358     }
    359 
    360     public void setRestartedFromDayNightMode(boolean restarted) {
    361         mRestartedFromDayNightMode = restarted;
    362     }
    363 
    364     public boolean isTruncatedList() {
    365         int maxItems = mAdapter.getMaxItemsNumber();
    366         return maxItems != PagedListView.ItemCap.UNLIMITED && mItemsNumber > maxItems;
    367     }
    368 
    369     private void clearMenu() {
    370         if (!mSubscriptionIds.isEmpty()) {
    371             mCarMenuCallbacks.unsubscribe(mSubscriptionIds.peek(), mSubscriptionCallbacks);
    372             mSubscriptionIds.clear();
    373             mTitles.clear();
    374         }
    375         mListView.setVisibility(View.GONE);
    376         mListView.resetMaxPages();
    377         mClickCountStack.clear();
    378         mIsCapped = false;
    379     }
    380 
    381     /**
    382      * Check if the drawer is inside of a CarAppLayout and add the relevant views if it is,
    383      * automagically add view faders for the correct views
    384      */
    385     private void updateViewFaders() {
    386         mContainer.removeViewFader(mStatusViewViewFader);
    387         mContainer.addViewFader(mStatusViewViewFader);
    388     }
    389 
    390     private void subscribe(String id) {
    391         mProgressBar.setVisibility(View.VISIBLE);
    392         mCarMenuCallbacks.subscribe(id, mSubscriptionCallbacks);
    393     }
    394 
    395     private final CarDrawerLayout.ViewFader mStatusViewViewFader = new CarDrawerLayout.ViewFader() {
    396         @Override
    397         public void setColor(int color) {
    398             mUiEntry.setMenuButtonColor(color);
    399         }
    400     };
    401 
    402     private void backOrClose() {
    403         if (mSubscriptionIds.size() > 1) {
    404             mPlvAnimationController.enqueueBackAnimation(mClearAdapterRunnable);
    405             mProgressBar.setVisibility(View.VISIBLE);
    406             mCarMenuCallbacks.unsubscribe(mSubscriptionIds.pop(),
    407                     mSubscriptionCallbacks);
    408             subscribe(mSubscriptionIds.peek());
    409             // Restore the title for this menu level.
    410             mTitles.pop();
    411             CharSequence title = mTitles.peek();
    412             if (TextUtils.isEmpty(title)) {
    413                 title = mContentTitle;
    414             }
    415             mUiEntry.setTitleText(title);
    416         } else {
    417             closeDrawer();
    418         }
    419     }
    420 
    421     private final View.OnClickListener mMenuClickListener = new View.OnClickListener() {
    422         @Override
    423         public void onClick(View view) {
    424             if (mIsDrawerAnimating || mCarMenuCallbacks.onMenuClicked()) {
    425                 return;
    426             }
    427             // Check if drawer has root set.
    428             if (mRootId == null) {
    429                 return;
    430             }
    431             mTruncatedListCardView.setVisibility(View.GONE);
    432             if (mIsDrawerOpen) {
    433                 if (!mClickCountStack.isEmpty()) {
    434                     mListView.setMaxPages(mListView.getMaxPages() + mClickCountStack.pop());
    435                 }
    436                 mIsCapped = false;
    437                 backOrClose();
    438             } else {
    439                 mSubscriptionIds.push(mRootId);
    440                 mTitles.push(mContentTitle);
    441                 subscribe(mRootId);
    442                 openDrawer();
    443             }
    444         }
    445     };
    446 
    447     private final Runnable mClearAdapterRunnable = new Runnable() {
    448         @Override
    449         public void run() {
    450             mListView.setVisibility(View.GONE);
    451         }
    452     };
    453 
    454     public void updateDayNightMode() {
    455         mContainer.findViewById(R.id.drawer).setBackgroundColor(
    456                 mContext.getResources().getColor(R.color.car_card));
    457         mListView.setAutoDayNightMode();
    458         switch (mDrawerMode) {
    459             case MODE_AUTO:
    460                 setAutoLightDarkMode();
    461                 break;
    462             case MODE_LIGHT:
    463                 setLightMode();
    464                 break;
    465             case MODE_DARK:
    466                 setDarkMode();
    467                 break;
    468         }
    469         updateViewFaders();
    470         RecyclerView rv = mListView.getRecyclerView();
    471         for (int i = 0; i < mAdapter.getItemCount(); ++i) {
    472             mAdapter.setDayNightModeColors(rv.findViewHolderForAdapterPosition(i));
    473         }
    474     }
    475 
    476     private static class ViewAnimationController implements Animation.AnimationListener {
    477         private final Animation mExitAnim;
    478         private final Animation mEnterAnim;
    479         private final Animation mBackAnim;
    480         private final View mView;
    481         private final Context mContext;
    482         private final Queue<Animation> mQueue = new LinkedList<>();
    483 
    484         private Runnable mOnEnterAnimStartRunnable;
    485         private Runnable mOnExitAnimCompleteRunnable;
    486 
    487         private Animation mCurrentAnimation;
    488 
    489         public ViewAnimationController(View view, int enter, int exit, int back) {
    490             mView = view;
    491             mContext = view.getContext();
    492 
    493             mEnterAnim = AnimationUtils.loadAnimation(mContext, enter);
    494             mExitAnim = AnimationUtils.loadAnimation(mContext, exit);
    495             mBackAnim = AnimationUtils.loadAnimation(mContext, back);
    496 
    497             mExitAnim.setAnimationListener(this);
    498             mEnterAnim.setAnimationListener(this);
    499             mBackAnim.setAnimationListener(this);
    500         }
    501 
    502         @Override
    503         public void onAnimationStart(Animation animation) {
    504             if (animation == mEnterAnim && mOnEnterAnimStartRunnable != null) {
    505                 mOnEnterAnimStartRunnable.run();
    506                 mOnEnterAnimStartRunnable = null;
    507             }
    508         }
    509 
    510         @Override
    511         public  void onAnimationEnd(Animation animation) {
    512             if ((animation == mExitAnim || animation == mBackAnim)
    513                     && mOnExitAnimCompleteRunnable != null) {
    514                 mOnExitAnimCompleteRunnable.run();
    515                 mOnExitAnimCompleteRunnable = null;
    516             }
    517             Animation nextAnimation = mQueue.poll();
    518             if (nextAnimation != null) {
    519                 mCurrentAnimation = animation;
    520                 mView.startAnimation(nextAnimation);
    521             } else {
    522                 mCurrentAnimation = null;
    523             }
    524        }
    525 
    526         @Override
    527         public void onAnimationRepeat(Animation animation) {
    528 
    529         }
    530 
    531         public void enqueueEnterAnimation(Runnable r) {
    532             if (r != null) {
    533                 mOnEnterAnimStartRunnable = r;
    534             }
    535             enqueueAnimation(mEnterAnim);
    536         }
    537 
    538         public void enqueueExitAnimation(Runnable r) {
    539             // If the view isn't visible, don't play the exit animation.
    540             // It will cause flicker.
    541             if (mView.getVisibility() != View.VISIBLE) {
    542                 return;
    543             }
    544             if (r != null) {
    545                 mOnExitAnimCompleteRunnable = r;
    546             }
    547             enqueueAnimation(mExitAnim);
    548         }
    549 
    550         public void enqueueBackAnimation(Runnable r) {
    551             // If the view isn't visible, don't play the back animation.
    552             if (mView.getVisibility() != View.VISIBLE) {
    553                 return;
    554             }
    555             if (r != null) {
    556                 mOnExitAnimCompleteRunnable = r;
    557             }
    558             enqueueAnimation(mBackAnim);
    559         }
    560 
    561         public synchronized void stopAndClearAnimations() {
    562             if (mExitAnim.hasStarted()) {
    563                 mExitAnim.cancel();
    564             }
    565 
    566             if (mEnterAnim.hasStarted()) {
    567                 mEnterAnim.cancel();
    568             }
    569 
    570             mQueue.clear();
    571             mCurrentAnimation = null;
    572         }
    573 
    574         public boolean isAnimating() {
    575             return mCurrentAnimation != null;
    576         }
    577 
    578         private synchronized void enqueueAnimation(final Animation animation) {
    579             if (mQueue.contains(animation)) {
    580                 return;
    581             }
    582             if (mCurrentAnimation != null) {
    583                 mQueue.add(animation);
    584             } else {
    585                 mCurrentAnimation = animation;
    586                 mView.startAnimation(animation);
    587             }
    588         }
    589     }
    590 
    591     private class SubscriptionCallbacks extends android.car.app.menu.SubscriptionCallbacks {
    592         private final Object mItemLock = new Object();
    593         private volatile List<Bundle> mItems;
    594 
    595         @Override
    596         public void onChildrenLoaded(String parentId, final List<Bundle> items) {
    597             if (mSubscriptionIds.isEmpty() || parentId.equals(mSubscriptionIds.peek())) {
    598                 // Add unavailable category explanation at the first item of menu.
    599                 if (mIsCapped) {
    600                     Bundle extra = new Bundle();
    601                     extra.putString(KEY_ID, KEY_ID_UNAVAILABLE_CATEGORY);
    602                     items.add(0, extra);
    603                 }
    604                 mItems = items;
    605                 mItemsNumber = mItems.size();
    606                 mProgressBar.setVisibility(View.GONE);
    607                 mPlvAnimationController.enqueueEnterAnimation(new Runnable() {
    608                     @Override
    609                     public void run() {
    610                         synchronized (mItemLock) {
    611                             mAdapter.setItems(mItems, mIsCapped);
    612                             mListView.setVisibility(View.VISIBLE);
    613                             mItems = null;
    614                         }
    615                         mListView.scrollToPosition(mAdapter.getFirstItemIndex());
    616                     }
    617                 });
    618             }
    619         }
    620 
    621         @Override
    622         public void onError(String id) {
    623             // TODO: do something useful here.
    624         }
    625 
    626         @Override
    627         public void onChildChanged(String parentId, Bundle bundle) {
    628             if (!mSubscriptionIds.isEmpty() && parentId.equals(mSubscriptionIds.peek())) {
    629                 // List is still animating, so adapter hasn't been updated. Update the list that
    630                 // needs to be set.
    631                 String id = bundle.getString(KEY_ID);
    632                 synchronized (mItemLock) {
    633                     if (mItems != null) {
    634                         for (Bundle item : mItems) {
    635                             if (item.getString(KEY_ID).equals(id)) {
    636                                 item.putAll(bundle);
    637                                 break;
    638                             }
    639                         }
    640                         return;
    641                     }
    642                 }
    643                 RecyclerView rv = mListView.getRecyclerView();
    644                 RecyclerView.ViewHolder holder = rv.findViewHolderForItemId(id.hashCode());
    645                 mAdapter.onChildChanged(holder, bundle);
    646             }
    647         }
    648     }
    649 
    650     private class DrawerMenuListDecoration extends PagedListView.Decoration {
    651 
    652         public DrawerMenuListDecoration(Context context) {
    653             super(context);
    654         }
    655 
    656         @Override
    657         public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    658             if (mAdapter != null && mAdapter.isEmptyPlaceholder()) {
    659                 return;
    660             }
    661             super.onDrawOver(c, parent, state);
    662         }
    663     }
    664 }
    665