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 
DrawerController(CarUiEntry uiEntry, View menuButton, CarDrawerLayout drawerLayout, PagedListView listView, CardView cardView)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
onDrawerOpened(View drawerView)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
onDrawerClosed(View drawerView)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
onDrawerStateChanged(int newState)146     public void onDrawerStateChanged(int newState) {
147     }
148 
149     @Override
onDrawerOpening(View drawerView)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
onDrawerSlide(View drawerView, float slideOffset)159     public void onDrawerSlide(View drawerView, float slideOffset) {
160         mUiEntry.setMenuProgress(slideOffset);
161     }
162 
163     @Override
onDrawerClosing(View drawerView)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
onItemClicked(Bundle item, int position)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
onItemLongClicked(Bundle item)211     public boolean onItemLongClicked(Bundle item) {
212         return mCarMenuCallbacks.onItemLongClicked(item.getString(KEY_ID));
213     }
214 
215     @Override
onBack()216     public void onBack() {
217         backOrClose();
218     }
219 
220     @Override
onScroll()221     public boolean onScroll() {
222         // Consume scroll event if we are animating.
223         return mPlvAnimationController.isAnimating();
224     }
225 
setTitle(CharSequence title)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 
setRootAndCallbacks(String rootId, CarMenuCallbacks callbacks)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 
saveState(Bundle out)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 
restoreState(Bundle in)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 
setScrimColor(int color)294     public void setScrimColor(int color) {
295         mScrimColor = color;
296         mContainer.setScrimColor(color);
297         updateViewFaders();
298     }
299 
setAutoLightDarkMode()300     public void setAutoLightDarkMode() {
301         mDrawerMode = MODE_AUTO;
302         mContainer.setAutoDayNightMode();
303         updateViewFaders();
304     }
305 
setLightMode()306     public void setLightMode() {
307         mDrawerMode = MODE_LIGHT;
308         mContainer.setLightMode();
309         updateViewFaders();
310     }
311 
setDarkMode()312     public void setDarkMode() {
313         mDrawerMode = MODE_DARK;
314         mContainer.setDarkMode();
315         updateViewFaders();
316     }
317 
openDrawer()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 
closeDrawer()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 
setDrawerEnabled(boolean enabled)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 
showMenu(String id, String title)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 
setRootId(String rootId)356     public void setRootId(String rootId) {
357         mRootId = rootId;
358     }
359 
setRestartedFromDayNightMode(boolean restarted)360     public void setRestartedFromDayNightMode(boolean restarted) {
361         mRestartedFromDayNightMode = restarted;
362     }
363 
isTruncatedList()364     public boolean isTruncatedList() {
365         int maxItems = mAdapter.getMaxItemsNumber();
366         return maxItems != PagedListView.ItemCap.UNLIMITED && mItemsNumber > maxItems;
367     }
368 
clearMenu()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      */
updateViewFaders()385     private void updateViewFaders() {
386         mContainer.removeViewFader(mStatusViewViewFader);
387         mContainer.addViewFader(mStatusViewViewFader);
388     }
389 
subscribe(String id)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 
backOrClose()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 
updateDayNightMode()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 
ViewAnimationController(View view, int enter, int exit, int back)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
onAnimationStart(Animation animation)503         public void onAnimationStart(Animation animation) {
504             if (animation == mEnterAnim && mOnEnterAnimStartRunnable != null) {
505                 mOnEnterAnimStartRunnable.run();
506                 mOnEnterAnimStartRunnable = null;
507             }
508         }
509 
510         @Override
onAnimationEnd(Animation animation)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
onAnimationRepeat(Animation animation)527         public void onAnimationRepeat(Animation animation) {
528 
529         }
530 
enqueueEnterAnimation(Runnable r)531         public void enqueueEnterAnimation(Runnable r) {
532             if (r != null) {
533                 mOnEnterAnimStartRunnable = r;
534             }
535             enqueueAnimation(mEnterAnim);
536         }
537 
enqueueExitAnimation(Runnable r)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 
enqueueBackAnimation(Runnable r)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 
stopAndClearAnimations()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 
isAnimating()574         public boolean isAnimating() {
575             return mCurrentAnimation != null;
576         }
577 
enqueueAnimation(final Animation animation)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
onChildrenLoaded(String parentId, final List<Bundle> items)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
onError(String id)622         public void onError(String id) {
623             // TODO: do something useful here.
624         }
625 
626         @Override
onChildChanged(String parentId, Bundle bundle)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 
DrawerMenuListDecoration(Context context)652         public DrawerMenuListDecoration(Context context) {
653             super(context);
654         }
655 
656         @Override
onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)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