1 package com.android.systemui.qs;
2 
3 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE;
4 
5 import android.animation.Animator;
6 import android.animation.AnimatorListenerAdapter;
7 import android.animation.AnimatorSet;
8 import android.animation.ObjectAnimator;
9 import android.animation.PropertyValuesHolder;
10 import android.app.ActivityManager;
11 import android.content.Context;
12 import android.content.res.Configuration;
13 import android.os.Bundle;
14 import android.util.AttributeSet;
15 import android.view.LayoutInflater;
16 import android.view.View;
17 import android.view.ViewGroup;
18 import android.view.accessibility.AccessibilityEvent;
19 import android.view.accessibility.AccessibilityNodeInfo;
20 import android.view.animation.Interpolator;
21 import android.view.animation.OvershootInterpolator;
22 import android.widget.Scroller;
23 
24 import androidx.annotation.Nullable;
25 import androidx.viewpager.widget.PagerAdapter;
26 import androidx.viewpager.widget.ViewPager;
27 
28 import com.android.internal.jank.InteractionJankMonitor;
29 import com.android.internal.logging.UiEventLogger;
30 import com.android.systemui.R;
31 import com.android.systemui.plugins.qs.QSTile;
32 import com.android.systemui.qs.QSPanel.QSTileLayout;
33 import com.android.systemui.qs.QSPanelControllerBase.TileRecord;
34 import com.android.systemui.qs.logging.QSLogger;
35 
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Set;
39 
40 public class PagedTileLayout extends ViewPager implements QSTileLayout {
41 
42     private static final String CURRENT_PAGE = "current_page";
43     private static final int NO_PAGE = -1;
44 
45     private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
46     private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
47     private static final long BOUNCE_ANIMATION_DURATION = 450L;
48     private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
49     private static final Interpolator SCROLL_CUBIC = (t) -> {
50         t -= 1.0f;
51         return t * t * t + 1.0f;
52     };
53 
54     private final ArrayList<TileRecord> mTiles = new ArrayList<>();
55     private final ArrayList<TileLayout> mPages = new ArrayList<>();
56 
57     private QSLogger mLogger;
58     @Nullable
59     private PageIndicator mPageIndicator;
60     private float mPageIndicatorPosition;
61 
62     @Nullable
63     private PageListener mPageListener;
64 
65     private boolean mListening;
66     private Scroller mScroller;
67 
68     @Nullable
69     private AnimatorSet mBounceAnimatorSet;
70     private float mLastExpansion;
71     private boolean mDistributeTiles = false;
72     private int mPageToRestore = -1;
73     private int mLayoutOrientation;
74     private int mLayoutDirection;
75     private final UiEventLogger mUiEventLogger = QSEvents.INSTANCE.getQsUiEventsLogger();
76     private int mExcessHeight;
77     private int mLastExcessHeight;
78     private int mMinRows = 1;
79     private int mMaxColumns = TileLayout.NO_MAX_COLUMNS;
80 
PagedTileLayout(Context context, AttributeSet attrs)81     public PagedTileLayout(Context context, AttributeSet attrs) {
82         super(context, attrs);
83         mScroller = new Scroller(context, SCROLL_CUBIC);
84         setAdapter(mAdapter);
85         setOnPageChangeListener(mOnPageChangeListener);
86         setCurrentItem(0, false);
87         mLayoutOrientation = getResources().getConfiguration().orientation;
88         mLayoutDirection = getLayoutDirection();
89     }
90     private int mLastMaxHeight = -1;
91 
92     @Override
setPageMargin(int marginPixels)93     public void setPageMargin(int marginPixels) {
94         // Using page margins creates some rounding issues that interfere with the correct position
95         // in the onPageChangedListener and therefore present bad positions to the PageIndicator.
96         // Instead, we use negative margins in the container and positive padding in the pages,
97         // matching the margin set from QSContainerImpl (note that new pages will always be inflated
98         // with the correct value.
99         // QSContainerImpl resources are set onAttachedView, so this view will always have the right
100         // values when attached.
101         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
102         lp.setMarginStart(-marginPixels);
103         lp.setMarginEnd(-marginPixels);
104         setLayoutParams(lp);
105 
106         int nPages = mPages.size();
107         for (int i = 0; i < nPages; i++) {
108             View v = mPages.get(i);
109             v.setPadding(marginPixels, v.getPaddingTop(), marginPixels, v.getPaddingBottom());
110         }
111     }
112 
saveInstanceState(Bundle outState)113     public void saveInstanceState(Bundle outState) {
114         int resolvedPage = mPageToRestore != NO_PAGE ? mPageToRestore : getCurrentPageNumber();
115         outState.putInt(CURRENT_PAGE, resolvedPage);
116     }
117 
restoreInstanceState(Bundle savedInstanceState)118     public void restoreInstanceState(Bundle savedInstanceState) {
119         // There's only 1 page at this point. We want to restore the correct page once the
120         // pages have been inflated
121         mPageToRestore = savedInstanceState.getInt(CURRENT_PAGE, NO_PAGE);
122     }
123 
124     @Override
getTilesHeight()125     public int getTilesHeight() {
126         // Use the first page as that is the maximum height we need to show.
127         TileLayout tileLayout = mPages.get(0);
128         if (tileLayout == null) {
129             return 0;
130         }
131         return tileLayout.getTilesHeight();
132     }
133 
134     @Override
onConfigurationChanged(Configuration newConfig)135     protected void onConfigurationChanged(Configuration newConfig) {
136         super.onConfigurationChanged(newConfig);
137         // Pass configuration change to non-attached pages as well. Some config changes will cause
138         // QS to recreate itself (as determined in FragmentHostManager), but in order to minimize
139         // those, make sure that all get passed to all pages.
140         int numPages = mPages.size();
141         for (int i = 0; i < numPages; i++) {
142             View page = mPages.get(i);
143             if (page.getParent() == null) {
144                 page.dispatchConfigurationChanged(newConfig);
145             }
146         }
147         if (mLayoutOrientation != newConfig.orientation) {
148             mLayoutOrientation = newConfig.orientation;
149             forceTilesRedistribution("orientation changed to " + mLayoutOrientation);
150             setCurrentItem(0, false);
151             mPageToRestore = 0;
152         } else {
153             // logging in case we missed redistribution because orientation was not changed
154             // while configuration changed, can be removed after b/255208946 is fixed
155             mLogger.d(
156                     "Orientation didn't change, tiles might be not redistributed, new config",
157                     newConfig);
158         }
159     }
160 
161     @Override
onRtlPropertiesChanged(int layoutDirection)162     public void onRtlPropertiesChanged(int layoutDirection) {
163         // The configuration change will change the flag in the view (that's returned in
164         // isLayoutRtl). As we detect the change, we use the cached direction to store the page
165         // before setting it.
166         final int page = getPageNumberForDirection(mLayoutDirection == LAYOUT_DIRECTION_RTL);
167         super.onRtlPropertiesChanged(layoutDirection);
168         if (mLayoutDirection != layoutDirection) {
169             mLayoutDirection = layoutDirection;
170             setAdapter(mAdapter);
171             setCurrentItem(page, false);
172         }
173     }
174 
175     @Override
setCurrentItem(int item, boolean smoothScroll)176     public void setCurrentItem(int item, boolean smoothScroll) {
177         if (isLayoutRtl()) {
178             item = mPages.size() - 1 - item;
179         }
180         super.setCurrentItem(item, smoothScroll);
181     }
182 
183     /**
184      * Obtains the current page number respecting RTL
185      */
getCurrentPageNumber()186     private int getCurrentPageNumber() {
187         return getPageNumberForDirection(isLayoutRtl());
188     }
189 
getPageNumberForDirection(boolean isLayoutRTL)190     private int getPageNumberForDirection(boolean isLayoutRTL) {
191         int page = getCurrentItem();
192         if (isLayoutRTL) {
193             page = mPages.size() - 1 - page;
194         }
195         return page;
196     }
197 
198     // This will dump to the ui log all the tiles that are visible in this page
logVisibleTiles(TileLayout page)199     private void logVisibleTiles(TileLayout page) {
200         for (int i = 0; i < page.mRecords.size(); i++) {
201             QSTile t = page.mRecords.get(i).tile;
202             mUiEventLogger.logWithInstanceId(QSEvent.QS_TILE_VISIBLE, 0, t.getMetricsSpec(),
203                     t.getInstanceId());
204         }
205     }
206 
207     @Override
setListening(boolean listening, UiEventLogger uiEventLogger)208     public void setListening(boolean listening, UiEventLogger uiEventLogger) {
209         if (mListening == listening) return;
210         mListening = listening;
211         updateListening();
212     }
213 
214     @Override
setSquishinessFraction(float squishinessFraction)215     public void setSquishinessFraction(float squishinessFraction) {
216         int nPages = mPages.size();
217         for (int i = 0; i < nPages; i++) {
218             mPages.get(i).setSquishinessFraction(squishinessFraction);
219         }
220     }
221 
updateListening()222     private void updateListening() {
223         for (TileLayout tilePage : mPages) {
224             tilePage.setListening(tilePage.getParent() != null && mListening);
225         }
226     }
227 
228     @Override
fakeDragBy(float xOffset)229     public void fakeDragBy(float xOffset) {
230         try {
231             super.fakeDragBy(xOffset);
232             // Keep on drawing until the animation has finished.
233             postInvalidateOnAnimation();
234         } catch (NullPointerException e) {
235             mLogger.logException("FakeDragBy called before begin", e);
236             // If we were trying to fake drag, it means we just added a new tile to the last
237             // page, so animate there.
238             final int lastPageNumber = mPages.size() - 1;
239             post(() -> {
240                 setCurrentItem(lastPageNumber, true);
241                 if (mBounceAnimatorSet != null) {
242                     mBounceAnimatorSet.start();
243                 }
244                 setOffscreenPageLimit(1);
245             });
246         }
247     }
248 
249     @Override
endFakeDrag()250     public void endFakeDrag() {
251         try {
252             super.endFakeDrag();
253         } catch (NullPointerException e) {
254             // Not sure what's going on. Let's log it
255             mLogger.logException("endFakeDrag called without velocityTracker", e);
256         }
257     }
258 
259     @Override
computeScroll()260     public void computeScroll() {
261         if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
262             if (!isFakeDragging()) {
263                 beginFakeDrag();
264             }
265             fakeDragBy(getScrollX() - mScroller.getCurrX());
266         } else if (isFakeDragging()) {
267             endFakeDrag();
268             if (mBounceAnimatorSet != null) {
269                 mBounceAnimatorSet.start();
270             }
271             setOffscreenPageLimit(1);
272         }
273         super.computeScroll();
274     }
275 
276     @Override
hasOverlappingRendering()277     public boolean hasOverlappingRendering() {
278         return false;
279     }
280 
281     @Override
onFinishInflate()282     protected void onFinishInflate() {
283         super.onFinishInflate();
284         mPages.add(createTileLayout());
285         mAdapter.notifyDataSetChanged();
286     }
287 
createTileLayout()288     private TileLayout createTileLayout() {
289         TileLayout page = (TileLayout) LayoutInflater.from(getContext())
290                 .inflate(R.layout.qs_paged_page, this, false);
291         page.setMinRows(mMinRows);
292         page.setMaxColumns(mMaxColumns);
293         page.setSelected(false);
294         return page;
295     }
296 
setPageIndicator(PageIndicator indicator)297     public void setPageIndicator(PageIndicator indicator) {
298         mPageIndicator = indicator;
299         mPageIndicator.setNumPages(mPages.size());
300         mPageIndicator.setLocation(mPageIndicatorPosition);
301     }
302 
303     @Override
getOffsetTop(TileRecord tile)304     public int getOffsetTop(TileRecord tile) {
305         final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
306         if (parent == null) return 0;
307         return parent.getTop() + getTop();
308     }
309 
310     @Override
addTile(TileRecord tile)311     public void addTile(TileRecord tile) {
312         mTiles.add(tile);
313         forceTilesRedistribution("adding new tile");
314         requestLayout();
315     }
316 
317     @Override
removeTile(TileRecord tile)318     public void removeTile(TileRecord tile) {
319         if (mTiles.remove(tile)) {
320             forceTilesRedistribution("removing tile");
321             requestLayout();
322         }
323     }
324 
325     @Override
setExpansion(float expansion, float proposedTranslation)326     public void setExpansion(float expansion, float proposedTranslation) {
327         mLastExpansion = expansion;
328         updateSelected();
329     }
330 
updateSelected()331     private void updateSelected() {
332         // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
333         // other expansion ratios since there is no way way to pause the marquee.
334         if (mLastExpansion > 0f && mLastExpansion < 1f) {
335             return;
336         }
337         boolean selected = mLastExpansion == 1f;
338 
339         // Disable accessibility temporarily while we update selected state purely for the
340         // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED
341         // event on any of the children.
342         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
343         int currentItem = getCurrentPageNumber();
344         for (int i = 0; i < mPages.size(); i++) {
345             TileLayout page = mPages.get(i);
346             page.setSelected(i == currentItem ? selected : false);
347             if (page.isSelected()) {
348                 logVisibleTiles(page);
349             }
350         }
351         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
352     }
353 
setPageListener(PageListener listener)354     public void setPageListener(PageListener listener) {
355         mPageListener = listener;
356     }
357 
getSpecsForPage(int page)358     public List<String> getSpecsForPage(int page) {
359         ArrayList<String> out = new ArrayList<>();
360         if (page < 0) return out;
361         int perPage = mPages.get(0).maxTiles();
362         int startOfPage = page * perPage;
363         int endOfPage = (page + 1) * perPage;
364         for (int i = startOfPage; i < endOfPage && i < mTiles.size(); i++) {
365             out.add(mTiles.get(i).tile.getTileSpec());
366         }
367         return out;
368     }
369 
distributeTiles()370     private void distributeTiles() {
371         emptyAndInflateOrRemovePages();
372 
373         final int tilesPerPageCount = mPages.get(0).maxTiles();
374         int index = 0;
375         final int totalTilesCount = mTiles.size();
376         mLogger.logTileDistributionInProgress(tilesPerPageCount, totalTilesCount);
377         for (int i = 0; i < totalTilesCount; i++) {
378             TileRecord tile = mTiles.get(i);
379             if (mPages.get(index).mRecords.size() == tilesPerPageCount) index++;
380             mLogger.logTileDistributed(tile.tile.getClass().getSimpleName(), index);
381             mPages.get(index).addTile(tile);
382         }
383     }
384 
emptyAndInflateOrRemovePages()385     private void emptyAndInflateOrRemovePages() {
386         final int numPages = getNumPages();
387         final int NP = mPages.size();
388         for (int i = 0; i < NP; i++) {
389             mPages.get(i).removeAllViews();
390         }
391         if (NP == numPages) {
392             return;
393         }
394         while (mPages.size() < numPages) {
395             mLogger.d("Adding new page");
396             mPages.add(createTileLayout());
397         }
398         while (mPages.size() > numPages) {
399             mLogger.d("Removing page");
400             mPages.remove(mPages.size() - 1);
401         }
402         mPageIndicator.setNumPages(mPages.size());
403         setAdapter(mAdapter);
404         mAdapter.notifyDataSetChanged();
405         if (mPageToRestore != NO_PAGE) {
406             setCurrentItem(mPageToRestore, false);
407             mPageToRestore = NO_PAGE;
408         }
409     }
410 
411     @Override
updateResources()412     public boolean updateResources() {
413         boolean changed = false;
414         for (int i = 0; i < mPages.size(); i++) {
415             changed |= mPages.get(i).updateResources();
416         }
417         if (changed) {
418             forceTilesRedistribution("resources in pages changed");
419             requestLayout();
420         } else {
421             // logging in case we missed redistribution because number of column in updateResources
422             // was not changed, can be removed after b/255208946 is fixed
423             mLogger.d("resource in pages didn't change, tiles might be not redistributed");
424         }
425         return changed;
426     }
427 
428     @Override
setMinRows(int minRows)429     public boolean setMinRows(int minRows) {
430         mMinRows = minRows;
431         boolean changed = false;
432         for (int i = 0; i < mPages.size(); i++) {
433             if (mPages.get(i).setMinRows(minRows)) {
434                 changed = true;
435                 forceTilesRedistribution("minRows changed in page");
436             }
437         }
438         return changed;
439     }
440 
441     @Override
setMaxColumns(int maxColumns)442     public boolean setMaxColumns(int maxColumns) {
443         mMaxColumns = maxColumns;
444         boolean changed = false;
445         for (int i = 0; i < mPages.size(); i++) {
446             if (mPages.get(i).setMaxColumns(maxColumns)) {
447                 changed = true;
448                 forceTilesRedistribution("maxColumns in pages changed");
449             }
450         }
451         return changed;
452     }
453 
454     /**
455      * Set the amount of excess space that we gave this view compared to the actual available
456      * height. This is because this view is in a scrollview.
457      */
setExcessHeight(int excessHeight)458     public void setExcessHeight(int excessHeight) {
459         mExcessHeight = excessHeight;
460     }
461 
462     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)463     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
464 
465         final int nTiles = mTiles.size();
466         // If we have no reason to recalculate the number of rows, skip this step. In particular,
467         // if the height passed by its parent is the same as the last time, we try not to remeasure.
468         if (mDistributeTiles || mLastMaxHeight != MeasureSpec.getSize(heightMeasureSpec)
469                 || mLastExcessHeight != mExcessHeight) {
470 
471             mLastMaxHeight = MeasureSpec.getSize(heightMeasureSpec);
472             mLastExcessHeight = mExcessHeight;
473             // Only change the pages if the number of rows or columns (from updateResources) has
474             // changed or the tiles have changed
475             int availableHeight = mLastMaxHeight - mExcessHeight;
476             if (mPages.get(0).updateMaxRows(availableHeight, nTiles) || mDistributeTiles) {
477                 mDistributeTiles = false;
478                 distributeTiles();
479             }
480 
481             final int nRows = mPages.get(0).mRows;
482             for (int i = 0; i < mPages.size(); i++) {
483                 TileLayout t = mPages.get(i);
484                 t.mRows = nRows;
485             }
486         }
487 
488         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
489 
490         // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
491         // of the pages.
492         int maxHeight = 0;
493         final int N = getChildCount();
494         for (int i = 0; i < N; i++) {
495             int height = getChildAt(i).getMeasuredHeight();
496             if (height > maxHeight) {
497                 maxHeight = height;
498             }
499         }
500         if (mPages.get(0).getParent() == null) {
501             // Measure page 0 so we know how tall it is if it's not attached to the pager.
502             mPages.get(0).measure(widthMeasureSpec, heightMeasureSpec);
503             int height = mPages.get(0).getMeasuredHeight();
504             if (height > maxHeight) {
505                 maxHeight = height;
506             }
507         }
508         setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
509     }
510 
511     @Override
onLayout(boolean changed, int l, int t, int r, int b)512     protected void onLayout(boolean changed, int l, int t, int r, int b) {
513         super.onLayout(changed, l, t, r, b);
514         if (mPages.get(0).getParent() == null) {
515             // Layout page 0, so we can get the bottom of the tiles. We only do this if the page
516             // is not attached.
517             mPages.get(0).layout(l, t, r, b);
518         }
519     }
520 
getColumnCount()521     public int getColumnCount() {
522         if (mPages.size() == 0) return 0;
523         return mPages.get(0).mColumns;
524     }
525 
526     /**
527      * Gets the number of pages in this paged tile layout
528      */
getNumPages()529     public int getNumPages() {
530         final int nTiles = mTiles.size();
531         // We should always have at least one page, even if it's empty.
532         int numPages = Math.max(nTiles / mPages.get(0).maxTiles(), 1);
533 
534         // Add one more not full page if needed
535         if (nTiles > numPages * mPages.get(0).maxTiles()) {
536             numPages++;
537         }
538 
539         return numPages;
540     }
541 
getNumVisibleTiles()542     public int getNumVisibleTiles() {
543         if (mPages.size() == 0) return 0;
544         TileLayout currentPage = mPages.get(getCurrentPageNumber());
545         return currentPage.mRecords.size();
546     }
547 
getNumTilesFirstPage()548     public int getNumTilesFirstPage() {
549         if (mPages.size() == 0) return 0;
550         return mPages.get(0).mRecords.size();
551     }
552 
startTileReveal(Set<String> tilesToReveal, final Runnable postAnimation)553     public void startTileReveal(Set<String> tilesToReveal, final Runnable postAnimation) {
554         if (shouldNotRunAnimation(tilesToReveal)) {
555             return;
556         }
557         // This method has side effects (beings the fake drag, if it returns true). If we have
558         // decided that we want to do a tile reveal, we do a last check to verify that we can
559         // actually perform a fake drag.
560         if (!beginFakeDrag()) {
561             return;
562         }
563 
564         final int lastPageNumber = mPages.size() - 1;
565         final TileLayout lastPage = mPages.get(lastPageNumber);
566         final ArrayList<Animator> bounceAnims = new ArrayList<>();
567         for (TileRecord tr : lastPage.mRecords) {
568             if (tilesToReveal.contains(tr.tile.getTileSpec())) {
569                 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
570             }
571         }
572 
573         if (bounceAnims.isEmpty()) {
574             // All tilesToReveal are on the first page. Nothing to do.
575             // TODO: potentially show a bounce animation for first page QS tiles
576             endFakeDrag();
577             return;
578         }
579 
580         mBounceAnimatorSet = new AnimatorSet();
581         mBounceAnimatorSet.playTogether(bounceAnims);
582         mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
583             @Override
584             public void onAnimationEnd(Animator animation) {
585                 mBounceAnimatorSet = null;
586                 postAnimation.run();
587             }
588         });
589         setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
590         int dx = getWidth() * lastPageNumber;
591         mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
592                 REVEAL_SCROLL_DURATION_MILLIS);
593         postInvalidateOnAnimation();
594     }
595 
shouldNotRunAnimation(Set<String> tilesToReveal)596     private boolean shouldNotRunAnimation(Set<String> tilesToReveal) {
597         // None of these have side effects. That way, we don't need to rely on short-circuiting
598         // behavior
599         boolean noAnimationNeeded = tilesToReveal.isEmpty() || mPages.size() < 2;
600         boolean scrollingInProgress = getScrollX() != 0 || !isFakeDragging();
601         // isRunningInTestHarness() to disable animation in functional testing as it caused
602         // flakiness and is not needed there. Alternative solutions were more complex and would
603         // still be either potentially flaky or modify internal data.
604         // For more info see b/253493927 and b/293234595
605         return noAnimationNeeded || scrollingInProgress || ActivityManager.isRunningInTestHarness();
606     }
607 
608     private int sanitizePageAction(int action) {
609         int pageLeftId = AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT.getId();
610         int pageRightId = AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT.getId();
611         if (action == pageLeftId || action == pageRightId) {
612             if (!isLayoutRtl()) {
613                 if (action == pageLeftId) {
614                     return AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
615                 } else {
616                     return AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
617                 }
618             } else {
619                 if (action == pageLeftId) {
620                     return AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
621                 } else {
622                     return AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
623                 }
624             }
625         }
626         return action;
627     }
628 
629     @Override
630     public boolean performAccessibilityAction(int action, Bundle arguments) {
631         action = sanitizePageAction(action);
632         boolean performed = super.performAccessibilityAction(action, arguments);
633         if (performed && (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
634                 || action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) {
635             requestAccessibilityFocus();
636         }
637         return performed;
638     }
639 
640     @Override
641     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
642         super.onInitializeAccessibilityNodeInfoInternal(info);
643         // getCurrentItem does not respect RTL, so it works well together with page actions that
644         // use left/right positioning.
645         if (getCurrentItem() != 0) {
646             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);
647         }
648         if (getCurrentItem() != mPages.size() - 1) {
649             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);
650         }
651     }
652 
653     @Override
654     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
655         super.onInitializeAccessibilityEvent(event);
656         if (mAdapter != null && mAdapter.getCount() > 0) {
657             event.setItemCount(mAdapter.getCount());
658             event.setFromIndex(getCurrentPageNumber());
659             event.setToIndex(getCurrentPageNumber());
660         }
661     }
662 
663     private static Animator setupBounceAnimator(View view, int ordinal) {
664         view.setAlpha(0f);
665         view.setScaleX(0f);
666         view.setScaleY(0f);
667         ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
668                 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
669                 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
670                 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
671         animator.setDuration(BOUNCE_ANIMATION_DURATION);
672         animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
673         animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
674         return animator;
675     }
676 
677     private final ViewPager.OnPageChangeListener mOnPageChangeListener =
678             new ViewPager.SimpleOnPageChangeListener() {
679 
680                 private int mCurrentScrollState = SCROLL_STATE_IDLE;
681                 // Flag to avoid redundant call InteractionJankMonitor::begin()
682                 private boolean mIsScrollJankTraceBegin = false;
683 
684                 @Override
685                 public void onPageSelected(int position) {
686                     updateSelected();
687                     if (mPageIndicator == null) return;
688                     if (mPageListener != null) {
689                         int pageNumber = isLayoutRtl() ? mPages.size() - 1 - position : position;
690                         mPageListener.onPageChanged(pageNumber == 0, pageNumber);
691                     }
692                 }
693 
694                 @Override
695                 public void onPageScrolled(int position, float positionOffset,
696                         int positionOffsetPixels) {
697 
698                     if (!mIsScrollJankTraceBegin && mCurrentScrollState == SCROLL_STATE_DRAGGING) {
699                         InteractionJankMonitor.getInstance().begin(PagedTileLayout.this,
700                                 CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE);
701                         mIsScrollJankTraceBegin = true;
702                     }
703 
704                     if (mPageIndicator == null) return;
705                     mPageIndicatorPosition = position + positionOffset;
706                     mPageIndicator.setLocation(mPageIndicatorPosition);
707                     if (mPageListener != null) {
708                         int pageNumber = isLayoutRtl() ? mPages.size() - 1 - position : position;
709                         mPageListener.onPageChanged(
710                                 positionOffsetPixels == 0 && pageNumber == 0,
711                                 // Send only valid page number on integer pages
712                                 positionOffsetPixels == 0 ? pageNumber : PageListener.INVALID_PAGE
713                         );
714                     }
715                 }
716 
717                 @Override
718                 public void onPageScrollStateChanged(int state) {
719                     if (state != mCurrentScrollState && state == SCROLL_STATE_IDLE) {
720                         InteractionJankMonitor.getInstance().end(
721                                 CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE);
722                         mIsScrollJankTraceBegin = false;
723                     }
724                     mCurrentScrollState = state;
725                 }
726             };
727 
728     private final PagerAdapter mAdapter = new PagerAdapter() {
729         @Override
730         public void destroyItem(ViewGroup container, int position, Object object) {
731             mLogger.d("Destantiating page at", position);
732             container.removeView((View) object);
733             updateListening();
734         }
735 
736         @Override
737         public Object instantiateItem(ViewGroup container, int position) {
738             mLogger.d("Instantiating page at", position);
739             if (isLayoutRtl()) {
740                 position = mPages.size() - 1 - position;
741             }
742             ViewGroup view = mPages.get(position);
743             if (view.getParent() != null) {
744                 container.removeView(view);
745             }
746             container.addView(view);
747             updateListening();
748             return view;
749         }
750 
751         @Override
752         public int getCount() {
753             return mPages.size();
754         }
755 
756         @Override
757         public boolean isViewFromObject(View view, Object object) {
758             return view == object;
759         }
760     };
761 
762     /**
763      * Force all tiles to be redistributed across pages.
764      * Should be called when one of the following changes: rows, columns, number of tiles.
765      */
forceTilesRedistribution(String reason)766     public void forceTilesRedistribution(String reason) {
767         mLogger.d("forcing tile redistribution across pages, reason", reason);
768         mDistributeTiles = true;
769     }
770 
setLogger(QSLogger qsLogger)771     public void setLogger(QSLogger qsLogger) {
772         mLogger = qsLogger;
773     }
774 
775     public interface PageListener {
776         int INVALID_PAGE = -1;
777 
778         void onPageChanged(boolean isFirst, int pageNumber);
779     }
780 }
781