1 /*
2  * Copyright (C) 2023 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 package com.android.car.carlauncher;
18 
19 import static androidx.lifecycle.FlowLiveDataConversions.asLiveData;
20 
21 import static com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection;
22 import static com.android.car.carlauncher.AppGridConstants.PageOrientation;
23 import static com.android.car.hidden.apis.HiddenApiAccess.getDragSurface;
24 
25 import static java.lang.annotation.RetentionPolicy.SOURCE;
26 
27 import android.animation.ValueAnimator;
28 import android.car.Car;
29 import android.car.content.pm.CarPackageManager;
30 import android.car.drivingstate.CarUxRestrictionsManager;
31 import android.car.media.CarMediaManager;
32 import android.content.Intent;
33 import android.content.pm.LauncherApps;
34 import android.content.pm.PackageManager;
35 import android.media.session.MediaSessionManager;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.util.Log;
39 import android.view.DragEvent;
40 import android.view.SurfaceControl;
41 import android.view.View;
42 import android.view.ViewTreeObserver;
43 import android.widget.FrameLayout;
44 import android.widget.LinearLayout;
45 
46 import androidx.annotation.IntDef;
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 import androidx.annotation.StringRes;
50 import androidx.annotation.VisibleForTesting;
51 import androidx.appcompat.app.AppCompatActivity;
52 import androidx.lifecycle.ViewModelProvider;
53 import androidx.recyclerview.widget.RecyclerView;
54 
55 import com.android.car.carlauncher.datasources.AppOrderDataSource;
56 import com.android.car.carlauncher.datasources.AppOrderProtoDataSourceImpl;
57 import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSource;
58 import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSourceImpl;
59 import com.android.car.carlauncher.datasources.LauncherActivitiesDataSource;
60 import com.android.car.carlauncher.datasources.LauncherActivitiesDataSourceImpl;
61 import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSource;
62 import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSourceImpl;
63 import com.android.car.carlauncher.datasources.UXRestrictionDataSource;
64 import com.android.car.carlauncher.datasources.UXRestrictionDataSourceImpl;
65 import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSource;
66 import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSourceImpl;
67 import com.android.car.carlauncher.datasources.restricted.TosDataSource;
68 import com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl;
69 import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource;
70 import com.android.car.carlauncher.pagination.PageMeasurementHelper;
71 import com.android.car.carlauncher.pagination.PaginationController;
72 import com.android.car.carlauncher.recyclerview.AppGridAdapter;
73 import com.android.car.carlauncher.recyclerview.AppGridItemAnimator;
74 import com.android.car.carlauncher.recyclerview.AppGridLayoutManager;
75 import com.android.car.carlauncher.recyclerview.AppItemViewHolder;
76 import com.android.car.carlauncher.repositories.AppGridRepository;
77 import com.android.car.carlauncher.repositories.AppGridRepositoryImpl;
78 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory;
79 import com.android.car.carlauncher.repositories.appactions.AppShortcutsFactory;
80 import com.android.car.ui.core.CarUi;
81 import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup;
82 import com.android.car.ui.toolbar.MenuItem;
83 import com.android.car.ui.toolbar.NavButtonMode;
84 import com.android.car.ui.toolbar.ToolbarController;
85 
86 import java.lang.annotation.Retention;
87 import java.util.Collections;
88 
89 import kotlin.Unit;
90 import kotlinx.coroutines.CoroutineDispatcher;
91 import kotlinx.coroutines.Dispatchers;
92 
93 /**
94  * Launcher activity that shows a grid of apps.
95  */
96 public class AppGridActivity extends AppCompatActivity implements
97         AppGridPageSnapper.PageSnapListener, AppItemViewHolder.AppItemDragListener,
98         PaginationController.DimensionUpdateListener,
99         AppGridAdapter.AppGridAdapterListener {
100     private static final String TAG = "AppGridActivity";
101     private static final boolean DEBUG_BUILD = false;
102     private static final String MODE_INTENT_EXTRA = "com.android.car.carlauncher.mode";
103     private static CarUiShortcutsPopup sCarUiShortcutsPopup;
104 
105     private boolean mShowAllApps = true;
106     private boolean mShowToolbar = true;
107     private Car mCar;
108     private Mode mMode;
109     private AppGridAdapter mAdapter;
110     private AppGridRecyclerView mRecyclerView;
111     private PageIndicator mPageIndicator;
112     private AppGridLayoutManager mLayoutManager;
113     private boolean mIsCurrentlyDragging;
114     private long mOffPageHoverBeforeScrollMs;
115     private Banner mBanner;
116 
117     private AppGridDragController mAppGridDragController;
118     private PaginationController mPaginationController;
119 
120     private int mNumOfRows;
121     private int mNumOfCols;
122     private int mAppGridMarginHorizontal;
123     private int mAppGridMarginVertical;
124     private int mAppGridWidth;
125     private int mAppGridHeight;
126     @PageOrientation
127     private int mPageOrientation;
128 
129     private int mCurrentScrollOffset;
130     private int mCurrentScrollState;
131     private int mNextScrollDestination;
132     private AppGridPageSnapper.AppGridPageSnapCallback mSnapCallback;
133     private AppItemViewHolder.AppItemDragCallback mDragCallback;
134     private BackgroundAnimationHelper mBackgroundAnimationHelper;
135 
136     private AppGridViewModel mAppGridViewModel;
137 
138     @Retention(SOURCE)
139     @IntDef({APP_TYPE_LAUNCHABLES, APP_TYPE_MEDIA_SERVICES})
140     @interface AppTypes {}
141     static final int APP_TYPE_LAUNCHABLES = 1;
142     static final int APP_TYPE_MEDIA_SERVICES = 2;
143 
144     public enum Mode {
145         ALL_APPS(R.string.app_launcher_title_all_apps,
146                 APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
147                 true),
148         MEDIA_ONLY(R.string.app_launcher_title_media_only,
149                 APP_TYPE_MEDIA_SERVICES,
150                 true),
151         MEDIA_POPUP(R.string.app_launcher_title_media_only,
152                 APP_TYPE_MEDIA_SERVICES,
153                 false),
154         ;
155         @StringRes
156         public final int mTitleStringId;
157         @AppTypes
158         public final int mAppTypes;
159         public final boolean mOpenMediaCenter;
160 
Mode(@tringRes int titleStringId, @AppTypes int appTypes, boolean openMediaCenter)161         Mode(@StringRes int titleStringId, @AppTypes int appTypes,
162                 boolean openMediaCenter) {
163             mTitleStringId = titleStringId;
164             mAppTypes = appTypes;
165             mOpenMediaCenter = openMediaCenter;
166         }
167     }
168 
169     /**
170      * Updates the state of the app grid components depending on the driving state.
171      */
handleDistractionOptimization(boolean requiresDistractionOptimization)172     private void handleDistractionOptimization(boolean requiresDistractionOptimization) {
173         mAdapter.setIsDistractionOptimizationRequired(requiresDistractionOptimization);
174         if (requiresDistractionOptimization) {
175             // if the user start driving while drag is in action, we cancel existing drag operations
176             if (mIsCurrentlyDragging) {
177                 mIsCurrentlyDragging = false;
178                 mLayoutManager.setShouldLayoutChildren(true);
179                 mRecyclerView.cancelDragAndDrop();
180             }
181             dismissShortcutPopup();
182         }
183     }
184 
185     @Override
onCreate(@ullable Bundle savedInstanceState)186     protected void onCreate(@Nullable Bundle savedInstanceState) {
187         // TODO (b/267548246) deprecate toolbar and find another way to hide debug apps
188         mShowToolbar = false;
189         if (mShowToolbar) {
190             setTheme(R.style.Theme_Launcher_AppGridActivity);
191         } else {
192             setTheme(R.style.Theme_Launcher_AppGridActivity_NoToolbar);
193         }
194         super.onCreate(savedInstanceState);
195 
196         mCar = Car.createCar(this);
197         setContentView(R.layout.app_grid_activity);
198         updateMode();
199         initViewModel();
200 
201         if (mShowToolbar) {
202             ToolbarController toolbar = CarUi.requireToolbar(this);
203 
204             toolbar.setNavButtonMode(NavButtonMode.CLOSE);
205 
206             if (DEBUG_BUILD) {
207                 toolbar.setMenuItems(Collections.singletonList(MenuItem.builder(this)
208                         .setDisplayBehavior(MenuItem.DisplayBehavior.NEVER)
209                         .setTitle(R.string.hide_debug_apps)
210                         .setOnClickListener(i -> {
211                             mShowAllApps = !mShowAllApps;
212                             i.setTitle(mShowAllApps
213                                     ? R.string.hide_debug_apps
214                                     : R.string.show_debug_apps);
215                         })
216                         .build()));
217             }
218         }
219 
220         mSnapCallback = new AppGridPageSnapper.AppGridPageSnapCallback(this);
221         mDragCallback = new AppItemViewHolder.AppItemDragCallback(this);
222 
223         mNumOfCols = getResources().getInteger(R.integer.car_app_selector_column_number);
224         mNumOfRows = getResources().getInteger(R.integer.car_app_selector_row_number);
225         mAppGridDragController = new AppGridDragController();
226         mOffPageHoverBeforeScrollMs = getResources().getInteger(
227                 R.integer.ms_off_page_hover_before_scroll);
228 
229         mPageOrientation = getResources().getBoolean(R.bool.use_vertical_app_grid)
230                 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL;
231 
232         mRecyclerView = requireViewById(R.id.apps_grid);
233         mRecyclerView.setFocusable(false);
234         mLayoutManager = new AppGridLayoutManager(this, mNumOfCols, mNumOfRows, mPageOrientation);
235         mRecyclerView.setLayoutManager(mLayoutManager);
236 
237         AppGridPageSnapper pageSnapper = new AppGridPageSnapper(
238                 this,
239                 mNumOfCols,
240                 mNumOfRows,
241                 mSnapCallback);
242         pageSnapper.attachToRecyclerView(mRecyclerView);
243 
244         mRecyclerView.setItemAnimator(new AppGridItemAnimator());
245 
246         // hide the default scrollbar and replace it with a visual page indicator
247         mRecyclerView.setVerticalScrollBarEnabled(false);
248         mRecyclerView.setHorizontalScrollBarEnabled(false);
249         mRecyclerView.addOnScrollListener(new AppGridOnScrollListener());
250 
251         // TODO: (b/271637411) move this to be contained in a scroll controller
252         mPageIndicator = requireViewById(R.id.page_indicator);
253         FrameLayout pageIndicatorContainer = requireViewById(R.id.page_indicator_container);
254         mPageIndicator.setContainer(pageIndicatorContainer);
255 
256         // recycler view is set to LTR to prevent layout manager from reassigning layout direction.
257         // instead, PageIndexinghelper will determine the grid index based on the system layout
258         // direction and provide LTR mapping at adapter level.
259         mRecyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
260         pageIndicatorContainer.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
261 
262         // we create but do not attach the adapter to recyclerview until view tree layout is
263         // complete and the total size of the app grid is measureable.
264         mAdapter = new AppGridAdapter(this, mNumOfCols, mNumOfRows,
265                 /* dragCallback */ mDragCallback,
266                 /* snapCallback */ mSnapCallback, this, mMode);
267 
268         mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
269             @Override
270             public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
271                 // scroll state will need to be updated after item has been dropped
272                 mNextScrollDestination = mSnapCallback.getSnapPosition();
273                 updateScrollState();
274             }
275         });
276         mRecyclerView.setAdapter(mAdapter);
277 
278         asLiveData(mAppGridViewModel.getAppList()).observe(this,
279                 appItems -> {
280                     mAdapter.setLauncherItems(appItems);
281                     mNextScrollDestination = mSnapCallback.getSnapPosition();
282                     updateScrollState();
283                 });
284 
285         asLiveData(mAppGridViewModel.requiresDistractionOptimization()).observe(this,
286                 uxRestrictions -> {
287                     handleDistractionOptimization(uxRestrictions);
288                 });
289 
290         // set drag listener and global layout listener, which will dynamically adjust app grid
291         // height and width depending on device screen size.
292         if (getResources().getBoolean(R.bool.config_allow_reordering)) {
293             mRecyclerView.setOnDragListener(new AppGridDragListener());
294         }
295 
296         // since some measurements for window size may not be available yet during onCreate or may
297         // later change, we add a listener that redraws the app grid when window size changes.
298         LinearLayout windowBackground = requireViewById(R.id.apps_grid_background);
299         windowBackground.setOrientation(
300                 isHorizontal() ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
301         PaginationController.DimensionUpdateCallback dimensionUpdateCallback =
302                 new PaginationController.DimensionUpdateCallback();
303         dimensionUpdateCallback.addListener(mRecyclerView);
304         dimensionUpdateCallback.addListener(mPageIndicator);
305         dimensionUpdateCallback.addListener(this);
306         mPaginationController = new PaginationController(windowBackground, dimensionUpdateCallback);
307 
308         mBanner = requireViewById(R.id.tos_banner);
309 
310         mBackgroundAnimationHelper = new BackgroundAnimationHelper(windowBackground, mBanner);
311 
312         setupTosBanner();
313     }
314 
initViewModel()315     private void initViewModel() {
316         LauncherActivitiesDataSource launcherActivities = new LauncherActivitiesDataSourceImpl(
317                 getSystemService(LauncherApps.class),
318                 (broadcastReceiver, intentFilter) -> {
319                     registerReceiver(broadcastReceiver, intentFilter);
320                     return Unit.INSTANCE;
321                 }, broadcastReceiver -> {
322             unregisterReceiver(broadcastReceiver);
323             return Unit.INSTANCE;
324         },
325                 android.os.Process.myUserHandle(),
326                 getApplication().getResources(),
327                 Dispatchers.getDefault()
328         );
329         MediaTemplateAppsDataSource mediaTemplateApps = new MediaTemplateAppsDataSourceImpl(
330                 getPackageManager(),
331                 getApplication(),
332                 Dispatchers.getDefault()
333         );
334 
335         DisabledAppsDataSource disabledApps = new DisabledAppsDataSourceImpl(getContentResolver(),
336                 getPackageManager(), Dispatchers.getIO());
337         TosDataSource tosApps = new TosDataSourceImpl(getContentResolver(), getPackageManager(),
338                 Dispatchers.getIO());
339         ControlCenterMirroringDataSource controlCenterMirroringDataSource =
340                 new ControlCenterMirroringDataSourceImpl(getApplication().getResources(),
341                         (intent, serviceConnection, flags) -> {
342                             bindService(intent, serviceConnection, flags);
343                             return Unit.INSTANCE;
344                         },
345                         (serviceConnection) -> {
346                             unbindService(serviceConnection);
347                             return Unit.INSTANCE;
348                         },
349                         getPackageManager(),
350                         Dispatchers.getIO()
351                 );
352         UXRestrictionDataSource uxRestrictionDataSource = new UXRestrictionDataSourceImpl(
353                 (CarUxRestrictionsManager) mCar.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE),
354                 (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE),
355                 getSystemService(MediaSessionManager.class),
356                 getApplication().getResources(),
357                 Dispatchers.getDefault()
358         );
359         AppOrderDataSource appOrderDataSource = new AppOrderProtoDataSourceImpl(
360                 new LauncherItemListSource(getFilesDir(), "order.data"),
361                 Dispatchers.getIO()
362         );
363 
364         PackageManager packageManager = getPackageManager();
365         AppLaunchProviderFactory launchProviderFactory = new AppLaunchProviderFactory(
366                 (CarMediaManager) mCar.getCarManager(Car.CAR_MEDIA_SERVICE),
367                 mMode.mOpenMediaCenter,
368                 () -> {
369                     finish();
370                     return Unit.INSTANCE;
371                 },
372                 getPackageManager());
373         AppShortcutsFactory appShortcutsFactory = new AppShortcutsFactory(
374                 (CarMediaManager) mCar.getCarManager(Car.CAR_MEDIA_SERVICE),
375                 Collections.emptySet(),
376                 this::onShortcutsShow
377         );
378         CoroutineDispatcher bgDispatcher = Dispatchers.getDefault();
379 
380         AppGridRepository repo = new AppGridRepositoryImpl(launcherActivities, mediaTemplateApps,
381                 disabledApps, tosApps, controlCenterMirroringDataSource, uxRestrictionDataSource,
382                 appOrderDataSource, packageManager, launchProviderFactory, appShortcutsFactory,
383                 bgDispatcher);
384 
385         mAppGridViewModel = new ViewModelProvider(this,
386                 AppGridViewModel.Companion.provideFactory(repo, getApplication(), this, null)).get(
387                 AppGridViewModel.class);
388     }
389 
390     @Override
onNewIntent(Intent intent)391     protected void onNewIntent(Intent intent) {
392         super.onNewIntent(intent);
393         setIntent(intent);
394         updateMode();
395     }
396 
397     @Override
onDestroy()398     protected void onDestroy() {
399         if (mCar.isConnected()) {
400             mCar.disconnect();
401             mCar = null;
402         }
403         super.onDestroy();
404     }
405 
updateMode()406     private void updateMode() {
407         mMode = parseMode(getIntent());
408         setTitle(mMode.mTitleStringId);
409         if (mShowToolbar) {
410             CarUi.requireToolbar(this).setTitle(mMode.mTitleStringId);
411         }
412     }
413 
414     @VisibleForTesting
isHorizontal()415     boolean isHorizontal() {
416         return AppGridConstants.isHorizontal(mPageOrientation);
417     }
418 
419     /**
420      * Note: This activity is exported, meaning that it might receive intents from any source.
421      * Intent data parsing must be extra careful.
422      */
423     @NonNull
parseMode(@ullable Intent intent)424     private Mode parseMode(@Nullable Intent intent) {
425         String mode = intent != null ? intent.getStringExtra(MODE_INTENT_EXTRA) : null;
426         try {
427             return mode != null ? Mode.valueOf(mode) : Mode.ALL_APPS;
428         } catch (IllegalArgumentException e) {
429             throw new IllegalArgumentException("Received invalid mode: " + mode, e);
430         }
431     }
432 
433     @Override
onResume()434     protected void onResume() {
435         super.onResume();
436         updateScrollState();
437         mAdapter.setLayoutDirection(getResources().getConfiguration().getLayoutDirection());
438         mAppGridViewModel.updateMode(mMode);
439     }
440 
441     @Override
onDimensionsUpdated(PageMeasurementHelper.PageDimensions pageDimens, PageMeasurementHelper.GridDimensions gridDimens)442     public void onDimensionsUpdated(PageMeasurementHelper.PageDimensions pageDimens,
443             PageMeasurementHelper.GridDimensions gridDimens) {
444         // TODO(b/271637411): move this method into a scroll controller
445         mAppGridMarginHorizontal = pageDimens.marginHorizontalPx;
446         mAppGridMarginVertical = pageDimens.marginVerticalPx;
447         mAppGridWidth = gridDimens.gridWidthPx;
448         mAppGridHeight = gridDimens.gridHeightPx;
449     }
450 
451     /**
452      * Updates the scroll state after receiving data changes, such as new apps being added or
453      * reordered, and when user returns to launcher onResume.
454      *
455      * Additionally, notify page indicator to handle resizing in case new app addition creates a
456      * new page or deleted a page.
457      */
updateScrollState()458     void updateScrollState() {
459         // TODO(b/271637411): move this method into a scroll controller
460         // to calculate how many pages we need to offset, we use the scroll offset anchor position
461         // as item count and map to the page which the anchor is on.
462         int offsetPageCount = mAdapter.getPageCount(mNextScrollDestination + 1) - 1;
463         mRecyclerView.suppressLayout(false);
464         mCurrentScrollOffset = offsetPageCount * (isHorizontal()
465                 ? (mAppGridWidth + 2 * mAppGridMarginHorizontal)
466                 : (mAppGridHeight + 2 * mAppGridMarginVertical));
467         mLayoutManager.scrollToPositionWithOffset(/* position */
468                 offsetPageCount * mNumOfRows * mNumOfCols, /* offset */ 0);
469 
470         mPageIndicator.updateOffset(mCurrentScrollOffset);
471         mPageIndicator.updatePageCount(mAdapter.getPageCount());
472     }
473 
474     @Override
onPause()475     protected void onPause() {
476         dismissShortcutPopup();
477         super.onPause();
478     }
479 
480     @Override
onSnapToPosition(int position)481     public void onSnapToPosition(int position) {
482         mNextScrollDestination = position;
483     }
484 
485     @Override
onItemLongPressed(boolean isLongPressed)486     public void onItemLongPressed(boolean isLongPressed) {
487         // after the user long presses the app icon, scrolling should be disabled until long press
488         // is canceled as to allow MotionEvent to be interpreted as attempt to drag the app icon.
489         mRecyclerView.suppressLayout(isLongPressed);
490     }
491 
492     @Override
onItemSelected(int gridPositionFrom)493     public void onItemSelected(int gridPositionFrom) {
494         mIsCurrentlyDragging = true;
495         mLayoutManager.setShouldLayoutChildren(false);
496         mAdapter.setDragStartPoint(gridPositionFrom);
497         dismissShortcutPopup();
498     }
499 
500     @Override
onItemDragged()501     public void onItemDragged() {
502         mAppGridDragController.cancelDelayedPageFling();
503     }
504 
505     @Override
onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection)506     public void onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection) {
507         if (mAdapter.getOffsetBoundDirection(gridPosition) == exitDirection) {
508             mAppGridDragController.postDelayedPageFling(exitDirection);
509         }
510     }
511 
512     @Override
onItemDropped(int gridPositionFrom, int gridPositionTo)513     public void onItemDropped(int gridPositionFrom, int gridPositionTo) {
514         mLayoutManager.setShouldLayoutChildren(true);
515         mAdapter.moveAppItem(gridPositionFrom, gridPositionTo);
516     }
517 
onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup)518     public void onShortcutsShow(CarUiShortcutsPopup carUiShortcutsPopup) {
519         sCarUiShortcutsPopup = carUiShortcutsPopup;
520     }
521 
dismissShortcutPopup()522     private void dismissShortcutPopup() {
523         // TODO (b/268563442): shortcut popup is set to be static since its
524         // sometimes recreated when taskview is present, find out why
525         if (sCarUiShortcutsPopup != null) {
526             sCarUiShortcutsPopup.dismiss();
527             sCarUiShortcutsPopup = null;
528         }
529     }
530 
531     @Override
onAppPositionChanged(int newPosition, AppItem appItem)532     public void onAppPositionChanged(int newPosition, AppItem appItem) {
533         mAppGridViewModel.saveAppOrder(newPosition, appItem);
534     }
535 
536 
537     private class AppGridOnScrollListener extends RecyclerView.OnScrollListener {
538         @Override
onScrolled(@onNull RecyclerView recyclerView, int dx, int dy)539         public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
540             mCurrentScrollOffset = mCurrentScrollOffset + (isHorizontal() ? dx : dy);
541             mPageIndicator.updateOffset(mCurrentScrollOffset);
542         }
543 
544         @Override
onScrollStateChanged(@onNull RecyclerView recyclerView, int newState)545         public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
546             mCurrentScrollState = newState;
547             mSnapCallback.setScrollState(mCurrentScrollState);
548             switch (newState) {
549                 case RecyclerView.SCROLL_STATE_DRAGGING:
550                     if (!mIsCurrentlyDragging) {
551                         mDragCallback.cancelDragTasks();
552                     }
553                     dismissShortcutPopup();
554                     mPageIndicator.animateAppearance();
555                     break;
556 
557                 case RecyclerView.SCROLL_STATE_SETTLING:
558                     mPageIndicator.animateAppearance();
559                     break;
560 
561                 case RecyclerView.SCROLL_STATE_IDLE:
562                     if (mIsCurrentlyDragging) {
563                         mLayoutManager.setShouldLayoutChildren(false);
564                     }
565                     mPageIndicator.animateFading();
566                     // in case the recyclerview was scrolled by rotary input, we need to handle
567                     // focusing the correct element: either on the first or last element on page
568                     mRecyclerView.maybeHandleRotaryFocus();
569             }
570         }
571     }
572 
573     private class AppGridDragController {
574         // TODO: (b/271320404) move DragController to separate directory called dragndrop and
575         // migrate logic this class and AppItemViewHolder there.
576         private final Handler mHandler;
577 
AppGridDragController()578         AppGridDragController() {
579             mHandler = new Handler(getMainLooper());
580         }
581 
cancelDelayedPageFling()582         void cancelDelayedPageFling() {
583             mHandler.removeCallbacksAndMessages(null);
584         }
585 
postDelayedPageFling(@ppItemBoundDirection int exitDirection)586         void postDelayedPageFling(@AppItemBoundDirection int exitDirection) {
587             boolean scrollToNextPage = isHorizontal()
588                     ? exitDirection == AppItemBoundDirection.RIGHT
589                     : exitDirection == AppItemBoundDirection.BOTTOM;
590             mHandler.removeCallbacksAndMessages(null);
591             mHandler.postDelayed(new Runnable() {
592                 public void run() {
593                     if (mCurrentScrollState == RecyclerView.SCROLL_STATE_IDLE) {
594                         mAdapter.updatePageScrollDestination(scrollToNextPage);
595                         mNextScrollDestination = mSnapCallback.getSnapPosition();
596 
597                         mLayoutManager.setShouldLayoutChildren(true);
598                         mRecyclerView.smoothScrollToPosition(mNextScrollDestination);
599                     }
600                     // another delayed scroll will be queued to enable the user to input multiple
601                     // page scrolls by holding the recyclerview at the app grid margin
602                     postDelayedPageFling(exitDirection);
603                 }
604             }, mOffPageHoverBeforeScrollMs);
605         }
606     }
607 
608     /**
609      * Private onDragListener for handling dispatching off page scroll event when user holds the app
610      * icon at the page margin.
611      */
612     private class AppGridDragListener implements View.OnDragListener {
613         @Override
onDrag(View v, DragEvent event)614         public boolean onDrag(View v, DragEvent event) {
615             int action = event.getAction();
616             if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
617                 mIsCurrentlyDragging = false;
618                 mAppGridDragController.cancelDelayedPageFling();
619                 mDragCallback.resetCallbackState();
620                 mLayoutManager.setShouldLayoutChildren(true);
621                 if (action == DragEvent.ACTION_DROP) {
622                     return false;
623                 } else {
624                     animateDropEnded(getDragSurface(event));
625                 }
626             }
627             return true;
628         }
629     }
630 
animateDropEnded(@ullable SurfaceControl dragSurface)631     private void animateDropEnded(@Nullable SurfaceControl dragSurface) {
632         if (dragSurface == null) {
633             Log.d(TAG, "animateDropEnded, dragSurface unavailable");
634             return;
635         }
636         // update default animation for the drag shadow after user lifts their finger
637         SurfaceControl.Transaction txn = new SurfaceControl.Transaction();
638         // set an animator to animate a delay before clearing the dragSurface
639         ValueAnimator delayedDismissAnimator = ValueAnimator.ofFloat(0f, 1f);
640         delayedDismissAnimator.setStartDelay(
641                 getResources().getInteger(R.integer.ms_drop_animation_delay));
642         delayedDismissAnimator.addUpdateListener(
643                 new ValueAnimator.AnimatorUpdateListener() {
644                     @Override
645                     public void onAnimationUpdate(ValueAnimator animation) {
646                         txn.setAlpha(dragSurface, 0);
647                         txn.apply();
648                     }
649                 });
650         delayedDismissAnimator.start();
651     }
652 
setupTosBanner()653     private void setupTosBanner() {
654         asLiveData(mAppGridViewModel.getShouldShowTosBanner()).observe(AppGridActivity.this,
655                 showBanner -> {
656                     if (showBanner) {
657                         mBanner.setVisibility(View.VISIBLE);
658                         // Pre draw is required for animation to work.
659                         mBanner.getViewTreeObserver().addOnPreDrawListener(
660                                 new ViewTreeObserver.OnPreDrawListener() {
661                                     @Override
662                                     public boolean onPreDraw() {
663                                         mBanner.getViewTreeObserver().removeOnPreDrawListener(this);
664                                         mBackgroundAnimationHelper.showBanner();
665                                         return true;
666                                     }
667                                 });
668                     } else {
669                         mBanner.setVisibility(View.GONE);
670                     }
671                 });
672         mBanner.setFirstButtonOnClickListener(v -> {
673             Intent tosIntent = AppLauncherUtils.getIntentForTosAcceptanceFlow(v.getContext());
674             AppLauncherUtils.launchApp(v.getContext(), tosIntent);
675         });
676         mBanner.setSecondButtonOnClickListener(
677                 v -> {
678                     mBackgroundAnimationHelper.hideBanner();
679                     mAppGridViewModel.saveTosBannerDismissalTime();
680                 });
681     }
682 
683 }
684