1 /*
2  * Copyright (C) 2014 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 
17 
18 package com.android.internal.widget;
19 
20 import static android.content.res.Resources.ID_NULL;
21 
22 import android.annotation.IdRes;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.TypedArray;
26 import android.graphics.Canvas;
27 import android.graphics.Rect;
28 import android.graphics.drawable.Drawable;
29 import android.metrics.LogMaker;
30 import android.os.Bundle;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.util.AttributeSet;
34 import android.util.Log;
35 import android.view.MotionEvent;
36 import android.view.VelocityTracker;
37 import android.view.View;
38 import android.view.ViewConfiguration;
39 import android.view.ViewGroup;
40 import android.view.ViewParent;
41 import android.view.ViewTreeObserver;
42 import android.view.accessibility.AccessibilityEvent;
43 import android.view.accessibility.AccessibilityNodeInfo;
44 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
45 import android.view.animation.AnimationUtils;
46 import android.widget.AbsListView;
47 import android.widget.OverScroller;
48 
49 import com.android.internal.R;
50 import com.android.internal.logging.MetricsLogger;
51 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
52 
53 public class ResolverDrawerLayout extends ViewGroup {
54     private static final String TAG = "ResolverDrawerLayout";
55     private MetricsLogger mMetricsLogger;
56 
57 
58 
59     /**
60      * Max width of the whole drawer layout and its res id
61      */
62     private int mMaxWidthResId;
63     private int mMaxWidth;
64 
65     /**
66      * Max total visible height of views not marked always-show when in the closed/initial state
67      */
68     private int mMaxCollapsedHeight;
69 
70     /**
71      * Max total visible height of views not marked always-show when in the closed/initial state
72      * when a default option is present
73      */
74     private int mMaxCollapsedHeightSmall;
75 
76     /**
77      * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
78      * inferred by {@code mMaxCollapsedHeight}.
79      */
80     private final boolean mIsMaxCollapsedHeightSmallExplicit;
81 
82     private boolean mSmallCollapsed;
83 
84     /**
85      * Move views down from the top by this much in px
86      */
87     private float mCollapseOffset;
88 
89     /**
90       * Track fractions of pixels from drag calculations. Without this, the view offsets get
91       * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
92       */
93     private float mDragRemainder = 0.0f;
94     private int mCollapsibleHeight;
95     private int mUncollapsibleHeight;
96     private int mAlwaysShowHeight;
97 
98     /**
99      * The height in pixels of reserved space added to the top of the collapsed UI;
100      * e.g. chooser targets
101      */
102     private int mCollapsibleHeightReserved;
103 
104     private int mTopOffset;
105     private boolean mShowAtTop;
106     @IdRes
107     private int mIgnoreOffsetTopLimitViewId = ID_NULL;
108 
109     private boolean mIsDragging;
110     private boolean mOpenOnClick;
111     private boolean mOpenOnLayout;
112     private boolean mDismissOnScrollerFinished;
113     private final int mTouchSlop;
114     private final float mMinFlingVelocity;
115     private final OverScroller mScroller;
116     private final VelocityTracker mVelocityTracker;
117 
118     private Drawable mScrollIndicatorDrawable;
119 
120     private OnDismissedListener mOnDismissedListener;
121     private RunOnDismissedListener mRunOnDismissedListener;
122     private OnCollapsedChangedListener mOnCollapsedChangedListener;
123 
124     private boolean mDismissLocked;
125 
126     private float mInitialTouchX;
127     private float mInitialTouchY;
128     private float mLastTouchY;
129     private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
130 
131     private final Rect mTempRect = new Rect();
132 
133     private AbsListView mNestedListChild;
134     private RecyclerView mNestedRecyclerChild;
135 
136     private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
137             new ViewTreeObserver.OnTouchModeChangeListener() {
138                 @Override
139                 public void onTouchModeChanged(boolean isInTouchMode) {
140                     if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
141                         smoothScrollTo(0, 0);
142                     }
143                 }
144             };
145 
ResolverDrawerLayout(Context context)146     public ResolverDrawerLayout(Context context) {
147         this(context, null);
148     }
149 
ResolverDrawerLayout(Context context, AttributeSet attrs)150     public ResolverDrawerLayout(Context context, AttributeSet attrs) {
151         this(context, attrs, 0);
152     }
153 
ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr)154     public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
155         super(context, attrs, defStyleAttr);
156 
157         final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
158                 defStyleAttr, 0);
159         mMaxWidthResId = a.getResourceId(R.styleable.ResolverDrawerLayout_maxWidth, -1);
160         mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1);
161         mMaxCollapsedHeight = a.getDimensionPixelSize(
162                 R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
163         mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
164                 R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
165                 mMaxCollapsedHeight);
166         mIsMaxCollapsedHeightSmallExplicit =
167                 a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
168         mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
169         if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) {
170             mIgnoreOffsetTopLimitViewId = a.getResourceId(
171                     R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
172         }
173         a.recycle();
174 
175         mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material);
176 
177         mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
178                 android.R.interpolator.decelerate_quint));
179         mVelocityTracker = VelocityTracker.obtain();
180 
181         final ViewConfiguration vc = ViewConfiguration.get(context);
182         mTouchSlop = vc.getScaledTouchSlop();
183         mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
184 
185         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
186     }
187 
188     /**
189      * Dynamically set the max collapsed height. Note this also updates the small collapsed
190      * height if it wasn't specified explicitly.
191      */
setMaxCollapsedHeight(int heightInPixels)192     public void setMaxCollapsedHeight(int heightInPixels) {
193         if (heightInPixels == mMaxCollapsedHeight) {
194             return;
195         }
196         mMaxCollapsedHeight = heightInPixels;
197         if (!mIsMaxCollapsedHeightSmallExplicit) {
198             mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
199         }
200         requestLayout();
201     }
202 
setSmallCollapsed(boolean smallCollapsed)203     public void setSmallCollapsed(boolean smallCollapsed) {
204         if (mSmallCollapsed != smallCollapsed) {
205             mSmallCollapsed = smallCollapsed;
206             requestLayout();
207         }
208     }
209 
isSmallCollapsed()210     public boolean isSmallCollapsed() {
211         return mSmallCollapsed;
212     }
213 
isCollapsed()214     public boolean isCollapsed() {
215         return mCollapseOffset > 0;
216     }
217 
setShowAtTop(boolean showOnTop)218     public void setShowAtTop(boolean showOnTop) {
219         if (mShowAtTop != showOnTop) {
220             mShowAtTop = showOnTop;
221             requestLayout();
222         }
223     }
224 
getShowAtTop()225     public boolean getShowAtTop() {
226         return mShowAtTop;
227     }
228 
setCollapsed(boolean collapsed)229     public void setCollapsed(boolean collapsed) {
230         if (!isLaidOut()) {
231             mOpenOnLayout = !collapsed;
232         } else {
233             smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
234         }
235     }
236 
setCollapsibleHeightReserved(int heightPixels)237     public void setCollapsibleHeightReserved(int heightPixels) {
238         final int oldReserved = mCollapsibleHeightReserved;
239         mCollapsibleHeightReserved = heightPixels;
240         if (oldReserved != mCollapsibleHeightReserved) {
241             requestLayout();
242         }
243 
244         final int dReserved = mCollapsibleHeightReserved - oldReserved;
245         if (dReserved != 0 && mIsDragging) {
246             mLastTouchY -= dReserved;
247         }
248 
249         final int oldCollapsibleHeight = mCollapsibleHeight;
250         mCollapsibleHeight = Math.min(mCollapsibleHeight, getMaxCollapsedHeight());
251 
252         if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
253             return;
254         }
255 
256         invalidate();
257     }
258 
setDismissLocked(boolean locked)259     public void setDismissLocked(boolean locked) {
260         mDismissLocked = locked;
261     }
262 
isMoving()263     private boolean isMoving() {
264         return mIsDragging || !mScroller.isFinished();
265     }
266 
isDragging()267     private boolean isDragging() {
268         return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
269     }
270 
updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed)271     private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
272         if (oldCollapsibleHeight == mCollapsibleHeight) {
273             return false;
274         }
275 
276         if (getShowAtTop()) {
277             // Keep the drawer fully open.
278             setCollapseOffset(0);
279             return false;
280         }
281 
282         if (isLaidOut()) {
283             final boolean isCollapsedOld = mCollapseOffset != 0;
284             if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
285                     && mCollapseOffset == oldCollapsibleHeight)) {
286                 // Stay closed even at the new height.
287                 setCollapseOffset(mCollapsibleHeight);
288             } else {
289                 setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight));
290             }
291             final boolean isCollapsedNew = mCollapseOffset != 0;
292             if (isCollapsedOld != isCollapsedNew) {
293                 onCollapsedChanged(isCollapsedNew);
294             }
295         } else {
296             // Start out collapsed at first unless we restored state for otherwise
297             setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight);
298         }
299         return true;
300     }
301 
setCollapseOffset(float collapseOffset)302     private void setCollapseOffset(float collapseOffset) {
303         if (mCollapseOffset != collapseOffset) {
304             mCollapseOffset = collapseOffset;
305             requestLayout();
306         }
307     }
308 
getMaxCollapsedHeight()309     private int getMaxCollapsedHeight() {
310         return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
311                 + mCollapsibleHeightReserved;
312     }
313 
setOnDismissedListener(OnDismissedListener listener)314     public void setOnDismissedListener(OnDismissedListener listener) {
315         mOnDismissedListener = listener;
316     }
317 
isDismissable()318     private boolean isDismissable() {
319         return mOnDismissedListener != null && !mDismissLocked;
320     }
321 
setOnCollapsedChangedListener(OnCollapsedChangedListener listener)322     public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
323         mOnCollapsedChangedListener = listener;
324     }
325 
326     @Override
onInterceptTouchEvent(MotionEvent ev)327     public boolean onInterceptTouchEvent(MotionEvent ev) {
328         final int action = ev.getActionMasked();
329 
330         if (action == MotionEvent.ACTION_DOWN) {
331             mVelocityTracker.clear();
332         }
333 
334         mVelocityTracker.addMovement(ev);
335 
336         switch (action) {
337             case MotionEvent.ACTION_DOWN: {
338                 final float x = ev.getX();
339                 final float y = ev.getY();
340                 mInitialTouchX = x;
341                 mInitialTouchY = mLastTouchY = y;
342                 mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
343             }
344             break;
345 
346             case MotionEvent.ACTION_MOVE: {
347                 final float x = ev.getX();
348                 final float y = ev.getY();
349                 final float dy = y - mInitialTouchY;
350                 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
351                         (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
352                     mActivePointerId = ev.getPointerId(0);
353                     mIsDragging = true;
354                     mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
355                             Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
356                 }
357             }
358             break;
359 
360             case MotionEvent.ACTION_POINTER_UP: {
361                 onSecondaryPointerUp(ev);
362             }
363             break;
364 
365             case MotionEvent.ACTION_CANCEL:
366             case MotionEvent.ACTION_UP: {
367                 resetTouch();
368             }
369             break;
370         }
371 
372         if (mIsDragging) {
373             abortAnimation();
374         }
375         return mIsDragging || mOpenOnClick;
376     }
377 
isNestedListChildScrolled()378     private boolean isNestedListChildScrolled() {
379         return  mNestedListChild != null
380                 && mNestedListChild.getChildCount() > 0
381                 && (mNestedListChild.getFirstVisiblePosition() > 0
382                         || mNestedListChild.getChildAt(0).getTop() < 0);
383     }
384 
isNestedRecyclerChildScrolled()385     private boolean isNestedRecyclerChildScrolled() {
386         if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
387             final RecyclerView.ViewHolder vh =
388                     mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
389             return vh == null || vh.itemView.getTop() < 0;
390         }
391         return false;
392     }
393 
394     @Override
onTouchEvent(MotionEvent ev)395     public boolean onTouchEvent(MotionEvent ev) {
396         final int action = ev.getActionMasked();
397 
398         mVelocityTracker.addMovement(ev);
399 
400         boolean handled = false;
401         switch (action) {
402             case MotionEvent.ACTION_DOWN: {
403                 final float x = ev.getX();
404                 final float y = ev.getY();
405                 mInitialTouchX = x;
406                 mInitialTouchY = mLastTouchY = y;
407                 mActivePointerId = ev.getPointerId(0);
408                 final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
409                 handled = isDismissable() || mCollapsibleHeight > 0;
410                 mIsDragging = hitView && handled;
411                 abortAnimation();
412             }
413             break;
414 
415             case MotionEvent.ACTION_MOVE: {
416                 int index = ev.findPointerIndex(mActivePointerId);
417                 if (index < 0) {
418                     Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
419                     index = 0;
420                     mActivePointerId = ev.getPointerId(0);
421                     mInitialTouchX = ev.getX();
422                     mInitialTouchY = mLastTouchY = ev.getY();
423                 }
424                 final float x = ev.getX(index);
425                 final float y = ev.getY(index);
426                 if (!mIsDragging) {
427                     final float dy = y - mInitialTouchY;
428                     if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
429                         handled = mIsDragging = true;
430                         mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
431                                 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
432                     }
433                 }
434                 if (mIsDragging) {
435                     final float dy = y - mLastTouchY;
436                     if (dy > 0 && isNestedListChildScrolled()) {
437                         mNestedListChild.smoothScrollBy((int) -dy, 0);
438                     } else if (dy > 0 && isNestedRecyclerChildScrolled()) {
439                         mNestedRecyclerChild.scrollBy(0, (int) -dy);
440                     } else {
441                         performDrag(dy);
442                     }
443                 }
444                 mLastTouchY = y;
445             }
446             break;
447 
448             case MotionEvent.ACTION_POINTER_DOWN: {
449                 final int pointerIndex = ev.getActionIndex();
450                 mActivePointerId = ev.getPointerId(pointerIndex);
451                 mInitialTouchX = ev.getX(pointerIndex);
452                 mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
453             }
454             break;
455 
456             case MotionEvent.ACTION_POINTER_UP: {
457                 onSecondaryPointerUp(ev);
458             }
459             break;
460 
461             case MotionEvent.ACTION_UP: {
462                 final boolean wasDragging = mIsDragging;
463                 mIsDragging = false;
464                 if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
465                         findChildUnder(ev.getX(), ev.getY()) == null) {
466                     if (isDismissable()) {
467                         dispatchOnDismissed();
468                         resetTouch();
469                         return true;
470                     }
471                 }
472                 if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
473                         Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
474                     smoothScrollTo(0, 0);
475                     return true;
476                 }
477                 mVelocityTracker.computeCurrentVelocity(1000);
478                 final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
479                 if (Math.abs(yvel) > mMinFlingVelocity) {
480                     if (getShowAtTop()) {
481                         if (isDismissable() && yvel < 0) {
482                             abortAnimation();
483                             dismiss();
484                         } else {
485                             smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
486                         }
487                     } else {
488                         if (isDismissable()
489                                 && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
490                             smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
491                             mDismissOnScrollerFinished = true;
492                         } else {
493                             scrollNestedScrollableChildBackToTop();
494                             smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
495                         }
496                     }
497                 }else {
498                     smoothScrollTo(
499                             mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
500                 }
501                 resetTouch();
502             }
503             break;
504 
505             case MotionEvent.ACTION_CANCEL: {
506                 if (mIsDragging) {
507                     smoothScrollTo(
508                             mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
509                 }
510                 resetTouch();
511                 return true;
512             }
513         }
514 
515         return handled;
516     }
517 
518     /**
519      * Scroll nested scrollable child back to top if it has been scrolled.
520      */
521     public void scrollNestedScrollableChildBackToTop() {
522         if (isNestedListChildScrolled()) {
523             mNestedListChild.smoothScrollToPosition(0);
524         } else if (isNestedRecyclerChildScrolled()) {
525             mNestedRecyclerChild.smoothScrollToPosition(0);
526         }
527     }
528 
529     private void onSecondaryPointerUp(MotionEvent ev) {
530         final int pointerIndex = ev.getActionIndex();
531         final int pointerId = ev.getPointerId(pointerIndex);
532         if (pointerId == mActivePointerId) {
533             // This was our active pointer going up. Choose a new
534             // active pointer and adjust accordingly.
535             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
536             mInitialTouchX = ev.getX(newPointerIndex);
537             mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
538             mActivePointerId = ev.getPointerId(newPointerIndex);
539         }
540     }
541 
542     private void resetTouch() {
543         mActivePointerId = MotionEvent.INVALID_POINTER_ID;
544         mIsDragging = false;
545         mOpenOnClick = false;
546         mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
547         mVelocityTracker.clear();
548     }
549 
550     private void dismiss() {
551         mRunOnDismissedListener = new RunOnDismissedListener();
552         post(mRunOnDismissedListener);
553     }
554 
555     @Override
556     public void computeScroll() {
557         super.computeScroll();
558         if (mScroller.computeScrollOffset()) {
559             final boolean keepGoing = !mScroller.isFinished();
560             performDrag(mScroller.getCurrY() - mCollapseOffset);
561             if (keepGoing) {
562                 postInvalidateOnAnimation();
563             } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
564                 dismiss();
565             }
566         }
567     }
568 
569     private void abortAnimation() {
570         mScroller.abortAnimation();
571         mRunOnDismissedListener = null;
572         mDismissOnScrollerFinished = false;
573     }
574 
575     private float performDrag(float dy) {
576         if (getShowAtTop()) {
577             return 0;
578         }
579 
580         final float newPos = Math.max(0, Math.min(mCollapseOffset + dy,
581                 mCollapsibleHeight + mUncollapsibleHeight));
582         if (newPos != mCollapseOffset) {
583             dy = newPos - mCollapseOffset;
584 
585             mDragRemainder += dy - (int) dy;
586             if (mDragRemainder >= 1.0f) {
587                 mDragRemainder -= 1.0f;
588                 dy += 1.0f;
589             } else if (mDragRemainder <= -1.0f) {
590                 mDragRemainder += 1.0f;
591                 dy -= 1.0f;
592             }
593 
594             boolean isIgnoreOffsetLimitSet = false;
595             int ignoreOffsetLimit = 0;
596             View ignoreOffsetLimitView = findIgnoreOffsetLimitView();
597             if (ignoreOffsetLimitView != null) {
598                 LayoutParams lp = (LayoutParams) ignoreOffsetLimitView.getLayoutParams();
599                 ignoreOffsetLimit = ignoreOffsetLimitView.getBottom() + lp.bottomMargin;
600                 isIgnoreOffsetLimitSet = true;
601             }
602             final int childCount = getChildCount();
603             for (int i = 0; i < childCount; i++) {
604                 final View child = getChildAt(i);
605                 if (child.getVisibility() == View.GONE) {
606                     continue;
607                 }
608                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
609                 if (!lp.ignoreOffset) {
610                     child.offsetTopAndBottom((int) dy);
611                 } else if (isIgnoreOffsetLimitSet) {
612                     int top = child.getTop();
613                     int targetTop = Math.max(
614                             (int) (ignoreOffsetLimit + lp.topMargin + dy),
615                             lp.mFixedTop);
616                     if (top != targetTop) {
617                         child.offsetTopAndBottom(targetTop - top);
618                     }
619                     ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
620                 }
621             }
622             final boolean isCollapsedOld = mCollapseOffset != 0;
623             mCollapseOffset = newPos;
624             mTopOffset += dy;
625             final boolean isCollapsedNew = newPos != 0;
626             if (isCollapsedOld != isCollapsedNew) {
627                 onCollapsedChanged(isCollapsedNew);
628                 getMetricsLogger().write(
629                         new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED)
630                         .setSubtype(isCollapsedNew ? 1 : 0));
631             }
632             onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
633             postInvalidateOnAnimation();
634             return dy;
635         }
636         return 0;
637     }
638 
639     private void onCollapsedChanged(boolean isCollapsed) {
640         notifyViewAccessibilityStateChangedIfNeeded(
641                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
642 
643         if (mScrollIndicatorDrawable != null) {
644             setWillNotDraw(!isCollapsed);
645         }
646 
647         if (mOnCollapsedChangedListener != null) {
648             mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
649         }
650     }
651 
652     void dispatchOnDismissed() {
653         if (mOnDismissedListener != null) {
654             mOnDismissedListener.onDismissed();
655         }
656         if (mRunOnDismissedListener != null) {
657             removeCallbacks(mRunOnDismissedListener);
658             mRunOnDismissedListener = null;
659         }
660     }
661 
662     private void smoothScrollTo(int yOffset, float velocity) {
663         abortAnimation();
664         final int sy = (int) mCollapseOffset;
665         int dy = yOffset - sy;
666         if (dy == 0) {
667             return;
668         }
669 
670         final int height = getHeight();
671         final int halfHeight = height / 2;
672         final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
673         final float distance = halfHeight + halfHeight *
674                 distanceInfluenceForSnapDuration(distanceRatio);
675 
676         int duration = 0;
677         velocity = Math.abs(velocity);
678         if (velocity > 0) {
679             duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
680         } else {
681             final float pageDelta = (float) Math.abs(dy) / height;
682             duration = (int) ((pageDelta + 1) * 100);
683         }
684         duration = Math.min(duration, 300);
685 
686         mScroller.startScroll(0, sy, 0, dy, duration);
687         postInvalidateOnAnimation();
688     }
689 
690     private float distanceInfluenceForSnapDuration(float f) {
691         f -= 0.5f; // center the values about 0.
692         f *= 0.3f * Math.PI / 2.0f;
693         return (float) Math.sin(f);
694     }
695 
696     /**
697      * Note: this method doesn't take Z into account for overlapping views
698      * since it is only used in contexts where this doesn't affect the outcome.
699      */
700     private View findChildUnder(float x, float y) {
701         return findChildUnder(this, x, y);
702     }
703 
704     private static View findChildUnder(ViewGroup parent, float x, float y) {
705         final int childCount = parent.getChildCount();
706         for (int i = childCount - 1; i >= 0; i--) {
707             final View child = parent.getChildAt(i);
708             if (isChildUnder(child, x, y)) {
709                 return child;
710             }
711         }
712         return null;
713     }
714 
715     private View findListChildUnder(float x, float y) {
716         View v = findChildUnder(x, y);
717         while (v != null) {
718             x -= v.getX();
719             y -= v.getY();
720             if (v instanceof AbsListView) {
721                 // One more after this.
722                 return findChildUnder((ViewGroup) v, x, y);
723             }
724             v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
725         }
726         return v;
727     }
728 
729     /**
730      * This only checks clipping along the bottom edge.
731      */
732     private boolean isListChildUnderClipped(float x, float y) {
733         final View listChild = findListChildUnder(x, y);
734         return listChild != null && isDescendantClipped(listChild);
735     }
736 
737     private boolean isDescendantClipped(View child) {
738         mTempRect.set(0, 0, child.getWidth(), child.getHeight());
739         offsetDescendantRectToMyCoords(child, mTempRect);
740         View directChild;
741         if (child.getParent() == this) {
742             directChild = child;
743         } else {
744             View v = child;
745             ViewParent p = child.getParent();
746             while (p != this) {
747                 v = (View) p;
748                 p = v.getParent();
749             }
750             directChild = v;
751         }
752 
753         // ResolverDrawerLayout lays out vertically in child order;
754         // the next view and forward is what to check against.
755         int clipEdge = getHeight() - getPaddingBottom();
756         final int childCount = getChildCount();
757         for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
758             final View nextChild = getChildAt(i);
759             if (nextChild.getVisibility() == GONE) {
760                 continue;
761             }
762             clipEdge = Math.min(clipEdge, nextChild.getTop());
763         }
764         return mTempRect.bottom > clipEdge;
765     }
766 
767     private static boolean isChildUnder(View child, float x, float y) {
768         final float left = child.getX();
769         final float top = child.getY();
770         final float right = left + child.getWidth();
771         final float bottom = top + child.getHeight();
772         return x >= left && y >= top && x < right && y < bottom;
773     }
774 
775     @Override
776     public void requestChildFocus(View child, View focused) {
777         super.requestChildFocus(child, focused);
778         if (!isInTouchMode() && isDescendantClipped(focused)) {
779             smoothScrollTo(0, 0);
780         }
781     }
782 
783     @Override
784     protected void onAttachedToWindow() {
785         super.onAttachedToWindow();
786         getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
787     }
788 
789     @Override
790     protected void onDetachedFromWindow() {
791         super.onDetachedFromWindow();
792         getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
793         abortAnimation();
794     }
795 
796     @Override
797     public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
798         if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
799             if (target instanceof AbsListView) {
800                 mNestedListChild = (AbsListView) target;
801             }
802             if (target instanceof RecyclerView) {
803                 mNestedRecyclerChild = (RecyclerView) target;
804             }
805             return true;
806         }
807         return false;
808     }
809 
810     @Override
811     public void onNestedScrollAccepted(View child, View target, int axes) {
812         super.onNestedScrollAccepted(child, target, axes);
813     }
814 
815     @Override
816     public void onStopNestedScroll(View child) {
817         super.onStopNestedScroll(child);
818         if (mScroller.isFinished()) {
819             smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
820         }
821     }
822 
823     @Override
824     public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
825             int dxUnconsumed, int dyUnconsumed) {
826         if (dyUnconsumed < 0) {
827             performDrag(-dyUnconsumed);
828         }
829     }
830 
831     @Override
832     public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
833         if (dy > 0) {
834             consumed[1] = (int) -performDrag(-dy);
835         }
836     }
837 
838     @Override
839     public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
840         if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
841             smoothScrollTo(0, velocityY);
842             return true;
843         }
844         return false;
845     }
846 
847     @Override
848     public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
849         if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
850             if (getShowAtTop()) {
851                 if (isDismissable() && velocityY > 0) {
852                     abortAnimation();
853                     dismiss();
854                 } else {
855                     smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
856                 }
857             } else {
858                 if (isDismissable()
859                         && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
860                     smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
861                     mDismissOnScrollerFinished = true;
862                 } else {
863                     smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
864                 }
865             }
866             return true;
867         }
868         return false;
869     }
870 
871     private boolean performAccessibilityActionCommon(int action) {
872         switch (action) {
873             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
874             case AccessibilityNodeInfo.ACTION_EXPAND:
875             case R.id.accessibilityActionScrollDown:
876                 if (mCollapseOffset != 0) {
877                     smoothScrollTo(0, 0);
878                     return true;
879                 }
880                 break;
881             case AccessibilityNodeInfo.ACTION_COLLAPSE:
882                 if (mCollapseOffset < mCollapsibleHeight) {
883                     smoothScrollTo(mCollapsibleHeight, 0);
884                     return true;
885                 }
886                 break;
887             case AccessibilityNodeInfo.ACTION_DISMISS:
888                 if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
889                         && isDismissable()) {
890                     smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0);
891                     mDismissOnScrollerFinished = true;
892                     return true;
893                 }
894                 break;
895         }
896 
897         return false;
898     }
899 
900     @Override
901     public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
902         if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
903             return true;
904         }
905 
906         return performAccessibilityActionCommon(action);
907     }
908 
909     @Override
910     public CharSequence getAccessibilityClassName() {
911         // Since we support scrolling, make this ViewGroup look like a
912         // ScrollView. This is kind of a hack until we have support for
913         // specifying auto-scroll behavior.
914         return android.widget.ScrollView.class.getName();
915     }
916 
917     @Override
918     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
919         super.onInitializeAccessibilityNodeInfoInternal(info);
920 
921         if (isEnabled()) {
922             if (mCollapseOffset != 0) {
923                 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
924                 info.addAction(AccessibilityAction.ACTION_EXPAND);
925                 info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
926                 info.setScrollable(true);
927             }
928             if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
929                     && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
930                 info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
931                 info.setScrollable(true);
932             }
933             if (mCollapseOffset < mCollapsibleHeight) {
934                 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
935             }
936             if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && isDismissable()) {
937                 info.addAction(AccessibilityAction.ACTION_DISMISS);
938             }
939         }
940 
941         // This view should never get accessibility focus, but it's interactive
942         // via nested scrolling, so we can't hide it completely.
943         info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
944     }
945 
946     @Override
947     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
948         if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
949             // This view should never get accessibility focus.
950             return false;
951         }
952 
953         if (super.performAccessibilityActionInternal(action, arguments)) {
954             return true;
955         }
956 
957         return performAccessibilityActionCommon(action);
958     }
959 
960     @Override
961     public void onDrawForeground(Canvas canvas) {
962         if (mScrollIndicatorDrawable != null) {
963             mScrollIndicatorDrawable.draw(canvas);
964         }
965 
966         super.onDrawForeground(canvas);
967     }
968 
969     @Override
970     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
971         final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
972         int widthSize = sourceWidth;
973         final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
974 
975         // Single-use layout; just ignore the mode and use available space.
976         // Clamp to maxWidth.
977         if (mMaxWidth >= 0) {
978             widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight());
979         }
980 
981         final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
982         final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
983 
984         // Currently we allot more height than is really needed so that the entirety of the
985         // sheet may be pulled up.
986         // TODO: Restrict the height here to be the right value.
987         int heightUsed = 0;
988 
989         // Measure always-show children first.
990         final int childCount = getChildCount();
991         for (int i = 0; i < childCount; i++) {
992             final View child = getChildAt(i);
993             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
994             if (lp.alwaysShow && child.getVisibility() != GONE) {
995                 if (lp.maxHeight != -1) {
996                     final int remainingHeight = heightSize - heightUsed;
997                     measureChildWithMargins(child, widthSpec, 0,
998                             MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
999                             lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
1000                 } else {
1001                     measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
1002                 }
1003                 heightUsed += child.getMeasuredHeight();
1004             }
1005         }
1006 
1007         mAlwaysShowHeight = heightUsed;
1008 
1009         // And now the rest.
1010         for (int i = 0; i < childCount; i++) {
1011             final View child = getChildAt(i);
1012 
1013             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1014             if (!lp.alwaysShow && child.getVisibility() != GONE) {
1015                 if (lp.maxHeight != -1) {
1016                     final int remainingHeight = heightSize - heightUsed;
1017                     measureChildWithMargins(child, widthSpec, 0,
1018                             MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
1019                             lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
1020                 } else {
1021                     measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
1022                 }
1023                 heightUsed += child.getMeasuredHeight();
1024             }
1025         }
1026 
1027         final int oldCollapsibleHeight = mCollapsibleHeight;
1028         mCollapsibleHeight = Math.max(0,
1029                 heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
1030         mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
1031 
1032         updateCollapseOffset(oldCollapsibleHeight, !isDragging());
1033 
1034         if (getShowAtTop()) {
1035             mTopOffset = 0;
1036         } else {
1037             mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
1038         }
1039 
1040         setMeasuredDimension(sourceWidth, heightSize);
1041     }
1042 
1043     /**
1044       * @return The space reserved by views with 'alwaysShow=true'
1045       */
1046     public int getAlwaysShowHeight() {
1047         return mAlwaysShowHeight;
1048     }
1049 
1050     /**
1051      * Max width of the drawer needs to be updated after the configuration is changed.
1052      * For example, foldables have different layout width when the device is folded and unfolded.
1053      */
1054     @Override
1055     protected void onConfigurationChanged(Configuration newConfig) {
1056         super.onConfigurationChanged(newConfig);
1057         if (mMaxWidthResId > 0) {
1058             mMaxWidth = getResources().getDimensionPixelSize(mMaxWidthResId);
1059         }
1060     }
1061 
1062     @Override
1063     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1064         final int width = getWidth();
1065 
1066         View indicatorHost = null;
1067 
1068         int ypos = mTopOffset;
1069         final int leftEdge = getPaddingLeft();
1070         final int rightEdge = width - getPaddingRight();
1071         final int widthAvailable = rightEdge - leftEdge;
1072 
1073         boolean isIgnoreOffsetLimitSet = false;
1074         int ignoreOffsetLimit = 0;
1075         final int childCount = getChildCount();
1076         for (int i = 0; i < childCount; i++) {
1077             final View child = getChildAt(i);
1078             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1079             if (lp.hasNestedScrollIndicator) {
1080                 indicatorHost = child;
1081             }
1082 
1083             if (child.getVisibility() == GONE) {
1084                 continue;
1085             }
1086 
1087             if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) {
1088                 if (mIgnoreOffsetTopLimitViewId == child.getId()) {
1089                     ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
1090                     isIgnoreOffsetLimitSet = true;
1091                 }
1092             }
1093 
1094             int top = ypos + lp.topMargin;
1095             if (lp.ignoreOffset) {
1096                 if (!isDragging()) {
1097                     lp.mFixedTop = (int) (top - mCollapseOffset);
1098                 }
1099                 if (isIgnoreOffsetLimitSet) {
1100                     top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset));
1101                     ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin;
1102                 } else {
1103                     top -= mCollapseOffset;
1104                 }
1105             }
1106             final int bottom = top + child.getMeasuredHeight();
1107 
1108             final int childWidth = child.getMeasuredWidth();
1109             final int left = leftEdge + (widthAvailable - childWidth) / 2;
1110             final int right = left + childWidth;
1111 
1112             child.layout(left, top, right, bottom);
1113 
1114             ypos = bottom + lp.bottomMargin;
1115         }
1116 
1117         if (mScrollIndicatorDrawable != null) {
1118             if (indicatorHost != null) {
1119                 final int left = indicatorHost.getLeft();
1120                 final int right = indicatorHost.getRight();
1121                 final int bottom = indicatorHost.getTop();
1122                 final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
1123                 mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
1124                 setWillNotDraw(!isCollapsed());
1125             } else {
1126                 mScrollIndicatorDrawable = null;
1127                 setWillNotDraw(true);
1128             }
1129         }
1130     }
1131 
1132     @Override
1133     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1134         return new LayoutParams(getContext(), attrs);
1135     }
1136 
1137     @Override
1138     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1139         if (p instanceof LayoutParams) {
1140             return new LayoutParams((LayoutParams) p);
1141         } else if (p instanceof MarginLayoutParams) {
1142             return new LayoutParams((MarginLayoutParams) p);
1143         }
1144         return new LayoutParams(p);
1145     }
1146 
1147     @Override
1148     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
1149         return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
1150     }
1151 
1152     @Override
1153     protected Parcelable onSaveInstanceState() {
1154         final SavedState ss = new SavedState(super.onSaveInstanceState());
1155         ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
1156         ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
1157         return ss;
1158     }
1159 
1160     @Override
1161     protected void onRestoreInstanceState(Parcelable state) {
1162         final SavedState ss = (SavedState) state;
1163         super.onRestoreInstanceState(ss.getSuperState());
1164         mOpenOnLayout = ss.open;
1165         mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
1166     }
1167 
1168     private View findIgnoreOffsetLimitView() {
1169         if (mIgnoreOffsetTopLimitViewId == ID_NULL) {
1170             return null;
1171         }
1172         View v = findViewById(mIgnoreOffsetTopLimitViewId);
1173         if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) {
1174             return v;
1175         }
1176         return null;
1177     }
1178 
1179     public static class LayoutParams extends MarginLayoutParams {
1180         public boolean alwaysShow;
1181         public boolean ignoreOffset;
1182         public boolean hasNestedScrollIndicator;
1183         public int maxHeight;
1184         int mFixedTop;
1185 
1186         public LayoutParams(Context c, AttributeSet attrs) {
1187             super(c, attrs);
1188 
1189             final TypedArray a = c.obtainStyledAttributes(attrs,
1190                     R.styleable.ResolverDrawerLayout_LayoutParams);
1191             alwaysShow = a.getBoolean(
1192                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
1193                     false);
1194             ignoreOffset = a.getBoolean(
1195                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
1196                     false);
1197             hasNestedScrollIndicator = a.getBoolean(
1198                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
1199                     false);
1200             maxHeight = a.getDimensionPixelSize(
1201                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1);
1202             a.recycle();
1203         }
1204 
1205         public LayoutParams(int width, int height) {
1206             super(width, height);
1207         }
1208 
1209         public LayoutParams(LayoutParams source) {
1210             super(source);
1211             this.alwaysShow = source.alwaysShow;
1212             this.ignoreOffset = source.ignoreOffset;
1213             this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
1214             this.maxHeight = source.maxHeight;
1215         }
1216 
1217         public LayoutParams(MarginLayoutParams source) {
1218             super(source);
1219         }
1220 
1221         public LayoutParams(ViewGroup.LayoutParams source) {
1222             super(source);
1223         }
1224     }
1225 
1226     static class SavedState extends BaseSavedState {
1227         boolean open;
1228         private int mCollapsibleHeightReserved;
1229 
1230         SavedState(Parcelable superState) {
1231             super(superState);
1232         }
1233 
1234         private SavedState(Parcel in) {
1235             super(in);
1236             open = in.readInt() != 0;
1237             mCollapsibleHeightReserved = in.readInt();
1238         }
1239 
1240         @Override
1241         public void writeToParcel(Parcel out, int flags) {
1242             super.writeToParcel(out, flags);
1243             out.writeInt(open ? 1 : 0);
1244             out.writeInt(mCollapsibleHeightReserved);
1245         }
1246 
1247         public static final Parcelable.Creator<SavedState> CREATOR =
1248                 new Parcelable.Creator<SavedState>() {
1249             @Override
1250             public SavedState createFromParcel(Parcel in) {
1251                 return new SavedState(in);
1252             }
1253 
1254             @Override
1255             public SavedState[] newArray(int size) {
1256                 return new SavedState[size];
1257             }
1258         };
1259     }
1260 
1261     /**
1262      * Listener for sheet dismissed events.
1263      */
1264     public interface OnDismissedListener {
1265         /**
1266          * Callback when the sheet is dismissed by the user.
1267          */
1268         void onDismissed();
1269     }
1270 
1271     /**
1272      * Listener for sheet collapsed / expanded events.
1273      */
1274     public interface OnCollapsedChangedListener {
1275         /**
1276          * Callback when the sheet is either fully expanded or collapsed.
1277          * @param isCollapsed true when collapsed, false when expanded.
1278          */
1279         void onCollapsedChanged(boolean isCollapsed);
1280     }
1281 
1282     private class RunOnDismissedListener implements Runnable {
1283         @Override
1284         public void run() {
1285             dispatchOnDismissed();
1286         }
1287     }
1288 
1289     private MetricsLogger getMetricsLogger() {
1290         if (mMetricsLogger == null) {
1291             mMetricsLogger = new MetricsLogger();
1292         }
1293         return mMetricsLogger;
1294     }
1295 }
1296