1 /*
<lambda>null2  * Copyright (C) 2024 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 android.animation.ValueAnimator
20 import android.car.Car
21 import android.car.content.pm.CarPackageManager
22 import android.car.drivingstate.CarUxRestrictionsManager
23 import android.car.media.CarMediaManager
24 import android.content.BroadcastReceiver
25 import android.content.Intent
26 import android.content.IntentFilter
27 import android.content.pm.LauncherApps
28 import android.content.pm.PackageManager
29 import android.media.session.MediaSessionManager
30 import android.os.Bundle
31 import android.os.Handler
32 import android.os.Looper.getMainLooper
33 import android.os.Process
34 import android.os.UserManager
35 import android.util.Log
36 import android.view.DragEvent
37 import android.view.LayoutInflater
38 import android.view.SurfaceControl
39 import android.view.View
40 import android.view.ViewGroup
41 import android.view.ViewTreeObserver
42 import android.widget.FrameLayout
43 import android.widget.LinearLayout
44 import androidx.annotation.StringRes
45 import androidx.fragment.app.Fragment
46 import androidx.lifecycle.ViewModelProvider
47 import androidx.lifecycle.asLiveData
48 import androidx.recyclerview.widget.RecyclerView
49 import com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection
50 import com.android.car.carlauncher.AppGridConstants.PageOrientation
51 import com.android.car.carlauncher.AppGridConstants.isHorizontal
52 import com.android.car.carlauncher.AppGridFragment.AppTypes.Companion.APP_TYPE_LAUNCHABLES
53 import com.android.car.carlauncher.AppGridFragment.AppTypes.Companion.APP_TYPE_MEDIA_SERVICES
54 import com.android.car.carlauncher.AppGridPageSnapper.AppGridPageSnapCallback
55 import com.android.car.carlauncher.AppGridPageSnapper.PageSnapListener
56 import com.android.car.carlauncher.AppGridViewModel.Companion.provideFactory
57 import com.android.car.carlauncher.datasources.AppOrderDataSource
58 import com.android.car.carlauncher.datasources.AppOrderProtoDataSourceImpl
59 import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSource
60 import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSourceImpl
61 import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSourceImpl.MirroringServiceConnection
62 import com.android.car.carlauncher.datasources.LauncherActivitiesDataSource
63 import com.android.car.carlauncher.datasources.LauncherActivitiesDataSourceImpl
64 import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSource
65 import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSourceImpl
66 import com.android.car.carlauncher.datasources.UXRestrictionDataSource
67 import com.android.car.carlauncher.datasources.UXRestrictionDataSourceImpl
68 import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSource
69 import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSourceImpl
70 import com.android.car.carlauncher.datasources.restricted.TosDataSource
71 import com.android.car.carlauncher.datasources.restricted.TosDataSourceImpl
72 import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource
73 import com.android.car.carlauncher.pagination.PageMeasurementHelper
74 import com.android.car.carlauncher.pagination.PaginationController
75 import com.android.car.carlauncher.pagination.PaginationController.DimensionUpdateCallback
76 import com.android.car.carlauncher.pagination.PaginationController.DimensionUpdateListener
77 import com.android.car.carlauncher.recyclerview.AppGridAdapter
78 import com.android.car.carlauncher.recyclerview.AppGridAdapter.AppGridAdapterListener
79 import com.android.car.carlauncher.recyclerview.AppGridItemAnimator
80 import com.android.car.carlauncher.recyclerview.AppGridLayoutManager
81 import com.android.car.carlauncher.recyclerview.AppItemViewHolder.AppItemDragCallback
82 import com.android.car.carlauncher.recyclerview.AppItemViewHolder.AppItemDragListener
83 import com.android.car.carlauncher.repositories.AppGridRepository
84 import com.android.car.carlauncher.repositories.AppGridRepositoryImpl
85 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory
86 import com.android.car.carlauncher.repositories.appactions.AppShortcutsFactory
87 import com.android.car.carlauncher.repositories.appactions.AppShortcutsFactory.ShortcutsListener
88 import com.android.car.hidden.apis.HiddenApiAccess
89 import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup
90 import kotlinx.coroutines.Dispatchers.Default
91 import kotlinx.coroutines.Dispatchers.IO
92 
93 /**
94  * Fragment which renders the Apps based on the [Mode] provided in the [setArguments]
95  *
96  * To create an instance of this Fragment use [newInstance]
97  */
98 class AppGridFragment : Fragment(), PageSnapListener, AppItemDragListener, DimensionUpdateListener,
99     AppGridAdapterListener {
100 
101     private lateinit var car: Car
102     private lateinit var mode: Mode
103     private lateinit var snapCallback: AppGridPageSnapCallback
104     private lateinit var dragCallback: AppItemDragCallback
105     private lateinit var appGridDragController: AppGridDragController
106     private lateinit var appGridRecyclerView: AppGridRecyclerView
107     private lateinit var layoutManager: AppGridLayoutManager
108     private lateinit var pageIndicator: PageIndicator
109     private lateinit var adapter: AppGridAdapter
110     private lateinit var paginationController: PaginationController
111     private lateinit var backgroundAnimationHelper: BackgroundAnimationHelper
112     private lateinit var appGridViewModel: AppGridViewModel
113     private lateinit var banner: Banner
114 
115     private var appGridMarginHorizontal = 0
116     private var appGridMarginVertical = 0
117     private var appGridWidth = 0
118     private var appGridHeight = 0
119     private var offPageHoverBeforeScrollMs = 0L
120     private var nextScrollDestination = 0
121     private var currentScrollOffset = 0
122     private var currentScrollState = 0
123     private var isCurrentlyDragging = false
124     private var carUiShortcutsPopup: CarUiShortcutsPopup? = null
125 
126     @PageOrientation
127     private var pageOrientation = 0
128 
129     override fun onCreateView(
130         inflater: LayoutInflater,
131         container: ViewGroup?,
132         savedInstanceState: Bundle?
133     ): View? {
134         super.onCreateView(inflater, container, savedInstanceState)
135         return inflater.inflate(R.layout.app_grid_fragment, container, false)
136     }
137 
138     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
139         super.onViewCreated(view, savedInstanceState)
140         car = Car.createCar(requireContext()) ?: throw IllegalStateException("Car not initialized")
141         mode = Mode.valueOf(requireArguments().getString(MODE_INTENT_EXTRA, Mode.ALL_APPS.name))
142         initViewModel()
143         updateMode(mode)
144 
145         snapCallback = AppGridPageSnapCallback(this)
146         dragCallback = AppItemDragCallback(this)
147 
148         appGridDragController = AppGridDragController()
149         offPageHoverBeforeScrollMs = resources.getInteger(
150             R.integer.ms_off_page_hover_before_scroll
151         ).toLong()
152 
153         pageOrientation =
154             if (resources.getBoolean(R.bool.use_vertical_app_grid)) {
155                 PageOrientation.VERTICAL
156             } else {
157                 PageOrientation.HORIZONTAL
158             }
159 
160         appGridRecyclerView = view.requireViewById(R.id.apps_grid)
161         appGridRecyclerView.isFocusable = false
162         layoutManager =
163             AppGridLayoutManager(requireContext(), pageOrientation)
164         appGridRecyclerView.layoutManager = layoutManager
165 
166         val pageSnapper = AppGridPageSnapper(
167             requireContext(),
168             snapCallback
169         )
170         pageSnapper.attachToRecyclerView(appGridRecyclerView)
171 
172         appGridRecyclerView.itemAnimator = AppGridItemAnimator()
173 
174         // hide the default scrollbar and replace it with a visual page indicator
175         appGridRecyclerView.isVerticalScrollBarEnabled = false
176         appGridRecyclerView.isHorizontalScrollBarEnabled = false
177         appGridRecyclerView.addOnScrollListener(AppGridOnScrollListener())
178 
179         // TODO: (b/271637411) move this to be contained in a scroll controller
180         pageIndicator = view.requireViewById(R.id.page_indicator)
181         val pageIndicatorContainer: FrameLayout =
182             view.requireViewById(R.id.page_indicator_container)
183         pageIndicator.setContainer(pageIndicatorContainer)
184 
185         // recycler view is set to LTR to prevent layout manager from reassigning layout direction.
186         // instead, PageIndexinghelper will determine the grid index based on the system layout
187         // direction and provide LTR mapping at adapter level.
188         appGridRecyclerView.layoutDirection = View.LAYOUT_DIRECTION_LTR
189         pageIndicatorContainer.layoutDirection = View.LAYOUT_DIRECTION_LTR
190 
191         adapter = AppGridAdapter(
192             requireContext(), dragCallback, snapCallback, this, mode
193         )
194 
195         adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
196             override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
197                 // scroll state will need to be updated after item has been dropped
198                 nextScrollDestination = snapCallback.snapPosition
199                 updateScrollState()
200             }
201         })
202         appGridRecyclerView.adapter = adapter
203 
204         appGridViewModel.getAppList().asLiveData().observe(
205             viewLifecycleOwner
206         ) { appItems: List<AppItem?>? ->
207             adapter.setLauncherItems(appItems)
208             nextScrollDestination = snapCallback.snapPosition
209             updateScrollState()
210         }
211 
212         appGridViewModel.requiresDistractionOptimization().asLiveData().observe(
213             viewLifecycleOwner
214         ) { uxRestrictions: Boolean ->
215             handleDistractionOptimization(
216                 uxRestrictions
217             )
218         }
219 
220         // set drag listener and global layout listener, which will dynamically adjust app grid
221         // height and width depending on device screen size. ize.
222         if (resources.getBoolean(R.bool.config_allow_reordering)) {
223             appGridRecyclerView.setOnDragListener(AppGridDragListener())
224         }
225 
226         // since some measurements for window size may not be available yet during onCreate or may
227         // later change, we add a listener that redraws the app grid when window size changes.
228         val windowBackground: LinearLayout = view.requireViewById(R.id.apps_grid_background)
229         windowBackground.orientation =
230             if (isHorizontal(pageOrientation)) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
231         val dimensionUpdateCallback = DimensionUpdateCallback()
232         dimensionUpdateCallback.addListener(appGridRecyclerView)
233         dimensionUpdateCallback.addListener(pageIndicator)
234         dimensionUpdateCallback.addListener(this)
235         paginationController =
236             PaginationController(windowBackground, dimensionUpdateCallback)
237 
238         banner = view.requireViewById(R.id.tos_banner)
239 
240         backgroundAnimationHelper = BackgroundAnimationHelper(windowBackground, banner)
241 
242         setupTosBanner()
243     }
244 
245     /**
246      * Updates the state of the app grid components depending on the driving state.
247      */
248     private fun handleDistractionOptimization(requiresDistractionOptimization: Boolean) {
249         adapter.setIsDistractionOptimizationRequired(requiresDistractionOptimization)
250         if (requiresDistractionOptimization) {
251             // if the user start driving while drag is in action, we cancel existing drag operations
252             if (isCurrentlyDragging) {
253                 isCurrentlyDragging = false
254                 layoutManager.setShouldLayoutChildren(true)
255                 appGridRecyclerView.cancelDragAndDrop()
256             }
257             dismissShortcutPopup()
258         }
259     }
260 
261     private fun initViewModel() {
262         val launcherActivities: LauncherActivitiesDataSource = LauncherActivitiesDataSourceImpl(
263             requireContext().getSystemService(LauncherApps::class.java),
264             { broadcastReceiver: BroadcastReceiver?, intentFilter: IntentFilter? ->
265                 requireContext().registerReceiver(broadcastReceiver, intentFilter)
266             },
267             { broadcastReceiver: BroadcastReceiver? ->
268                 requireContext().unregisterReceiver(broadcastReceiver)
269             },
270             Process.myUserHandle(),
271             requireContext().applicationContext.resources,
272             Default
273         )
274         val mediaTemplateApps: MediaTemplateAppsDataSource = MediaTemplateAppsDataSourceImpl(
275             requireContext().packageManager,
276             requireContext().applicationContext,
277             Default
278         )
279         val disabledApps: DisabledAppsDataSource = DisabledAppsDataSourceImpl(
280             requireContext().contentResolver,
281             requireContext().packageManager,
282             IO
283         )
284         val tosApps: TosDataSource = TosDataSourceImpl(
285             requireContext().contentResolver,
286             requireContext().packageManager,
287             IO
288         )
289         val controlCenterMirroringDataSource: ControlCenterMirroringDataSource =
290             ControlCenterMirroringDataSourceImpl(
291                 requireContext().applicationContext.resources,
292                 { intent: Intent, serviceConnection: MirroringServiceConnection, flags: Int ->
293                     requireContext().bindService(intent, serviceConnection, flags)
294                 },
295                 { serviceConnection: MirroringServiceConnection ->
296                     requireContext().unbindService(serviceConnection)
297                 },
298                 requireContext().packageManager,
299                 IO
300             )
301         val uxRestrictionDataSource: UXRestrictionDataSource = UXRestrictionDataSourceImpl(
302             requireContext(),
303             requireNotNull(car.getCarManager(CarUxRestrictionsManager::class.java)),
304             requireNotNull(car.getCarManager(CarPackageManager::class.java)),
305             requireContext().getSystemService(MediaSessionManager::class.java),
306             requireContext().applicationContext.resources,
307             Default
308         )
309         val appOrderDataSource: AppOrderDataSource = AppOrderProtoDataSourceImpl(
310             LauncherItemListSource(requireContext().filesDir, "order.data"),
311             IO
312         )
313         val packageManager: PackageManager = requireContext().packageManager
314         val launchProviderFactory = AppLaunchProviderFactory(
315             requireNotNull(car.getCarManager(CarMediaManager::class.java)),
316             mode.openMediaCenter,
317             {
318                 activity?.finish()
319             },
320             requireContext().packageManager
321         )
322 
323         val appShortcutsFactory = AppShortcutsFactory(
324             requireNotNull(car.getCarManager(CarMediaManager::class.java)),
325             emptySet(),
326             object : ShortcutsListener {
327                 override fun onShortcutsShow(carUiShortcutsPopup: CarUiShortcutsPopup) {
328                     this@AppGridFragment.carUiShortcutsPopup = carUiShortcutsPopup
329                 }
330             }
331         )
332         val bgDispatcher = Default
333         val repo: AppGridRepository = AppGridRepositoryImpl(
334             launcherActivities, mediaTemplateApps,
335             disabledApps, tosApps, controlCenterMirroringDataSource, uxRestrictionDataSource,
336             appOrderDataSource, packageManager, launchProviderFactory, appShortcutsFactory,
337             requireContext().getSystemService(UserManager::class.java), bgDispatcher
338         )
339 
340         appGridViewModel = ViewModelProvider(
341             this,
342             provideFactory(repo, requireActivity().application, this, null)
343         )[AppGridViewModel::class.java]
344     }
345 
346     private fun animateDropEnded(dragSurface: SurfaceControl?) {
347         if (dragSurface == null) {
348             if (DEBUG_BUILD) {
349                 Log.d(TAG, "animateDropEnded, dragSurface unavailable")
350             }
351             return
352         }
353         // update default animation for the drag shadow after user lifts their finger
354         val txn = SurfaceControl.Transaction()
355         // set an animator to animate a delay before clearing the dragSurface
356         val delayedDismissAnimator = ValueAnimator.ofFloat(0f, 1f)
357         delayedDismissAnimator.startDelay =
358             resources.getInteger(R.integer.ms_drop_animation_delay).toLong()
359         delayedDismissAnimator.addUpdateListener {
360             txn.setAlpha(dragSurface, 0f)
361             txn.apply()
362         }
363         delayedDismissAnimator.start()
364     }
365 
366     private fun setupTosBanner() {
367         appGridViewModel.getShouldShowTosBanner().asLiveData()
368             .observe(
369                 viewLifecycleOwner
370             ) { showBanner: Boolean ->
371                 if (showBanner) {
372                     banner.visibility = View.VISIBLE
373                     // Pre draw is required for animation to work.
374                     banner.viewTreeObserver.addOnPreDrawListener(
375                         object : ViewTreeObserver.OnPreDrawListener {
376                             override fun onPreDraw(): Boolean {
377                                 banner.viewTreeObserver.removeOnPreDrawListener(this)
378                                 backgroundAnimationHelper.showBanner()
379                                 return true
380                             }
381                         }
382                     )
383                 } else {
384                     banner.visibility = View.GONE
385                 }
386             }
387         banner.setFirstButtonOnClickListener { v: View ->
388             val tosIntent =
389                 AppLauncherUtils.getIntentForTosAcceptanceFlow(v.context)
390             AppLauncherUtils.launchApp(v.context, tosIntent)
391         }
392         banner.setSecondButtonOnClickListener { _ ->
393             backgroundAnimationHelper.hideBanner()
394             appGridViewModel.saveTosBannerDismissalTime()
395         }
396     }
397 
398     /**
399      * Updates the scroll state after receiving data changes, such as new apps being added or
400      * reordered, and when user returns to launcher onResume.
401      *
402      * Additionally, notify page indicator to handle resizing in case new app addition creates a
403      * new page or deleted a page.
404      */
405     fun updateScrollState() {
406         // TODO(b/271637411): move this method into a scroll controller
407         // to calculate how many pages we need to offset, we use the scroll offset anchor position
408         // as item count and map to the page which the anchor is on.
409         val offsetPageCount = adapter.getPageCount(nextScrollDestination + 1) - 1
410         appGridRecyclerView.suppressLayout(false)
411         currentScrollOffset =
412             offsetPageCount * if (isHorizontal(pageOrientation)) {
413                 appGridWidth + 2 * appGridMarginHorizontal
414             } else {
415                 appGridHeight + 2 * appGridMarginVertical
416             }
417         layoutManager.scrollToPositionWithOffset(
418             offsetPageCount * appGridRecyclerView.numOfRows * appGridRecyclerView.numOfCols,
419             0
420         )
421         pageIndicator.updateOffset(currentScrollOffset)
422         pageIndicator.updatePageCount(adapter.pageCount)
423     }
424 
425     /**
426      * Change the mode of the apps shown in the AppGrid
427      * @see [Mode]
428      */
429     fun updateMode(mode: Mode) {
430         this.mode = mode
431         appGridViewModel.updateMode(mode)
432     }
433 
434     private inner class AppGridOnScrollListener : RecyclerView.OnScrollListener() {
435         override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
436             currentScrollOffset += if (isHorizontal(pageOrientation)) dx else dy
437             pageIndicator.updateOffset(currentScrollOffset)
438         }
439 
440         override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
441             currentScrollState = newState
442             snapCallback.scrollState = currentScrollState
443             when (newState) {
444                 RecyclerView.SCROLL_STATE_DRAGGING -> {
445                     if (!isCurrentlyDragging) {
446                         dragCallback.cancelDragTasks()
447                     }
448                     dismissShortcutPopup()
449                     pageIndicator.animateAppearance()
450                 }
451 
452                 RecyclerView.SCROLL_STATE_SETTLING -> pageIndicator.animateAppearance()
453                 RecyclerView.SCROLL_STATE_IDLE -> {
454                     if (isCurrentlyDragging) {
455                         layoutManager.setShouldLayoutChildren(false)
456                     }
457                     pageIndicator.animateFading()
458                     // in case the recyclerview was scrolled by rotary input, we need to handle
459                     // focusing the correct element: either on the first or last element on page
460                     appGridRecyclerView.maybeHandleRotaryFocus()
461                 }
462             }
463         }
464     }
465 
466     private fun dismissShortcutPopup() {
467         carUiShortcutsPopup?.let {
468             it.dismiss()
469             carUiShortcutsPopup = null
470         }
471     }
472 
473     override fun onPause() {
474         dismissShortcutPopup()
475         super.onPause()
476     }
477 
478     override fun onDestroy() {
479         if (car.isConnected) {
480             car.disconnect()
481         }
482         super.onDestroy()
483     }
484 
485     override fun onSnapToPosition(gridPosition: Int) {
486         nextScrollDestination = gridPosition
487     }
488 
489     override fun onDimensionsUpdated(
490         pageDimens: PageMeasurementHelper.PageDimensions,
491         gridDimens: PageMeasurementHelper.GridDimensions
492     ) {
493         // TODO(b/271637411): move this method into a scroll controller
494         appGridMarginHorizontal = pageDimens.marginHorizontalPx
495         appGridMarginVertical = pageDimens.marginVerticalPx
496         appGridWidth = gridDimens.gridWidthPx
497         appGridHeight = gridDimens.gridHeightPx
498     }
499 
500     override fun onAppPositionChanged(newPosition: Int, appItem: AppItem) {
501         appGridViewModel.saveAppOrder(newPosition, appItem)
502     }
503 
504     override fun onItemLongPressed(longPressed: Boolean) {
505         // after the user long presses the app icon, scrolling should be disabled until long press
506         // is canceled as to allow MotionEvent to be interpreted as attempt to drag the app icon.
507         appGridRecyclerView.suppressLayout(longPressed)
508     }
509 
510     override fun onItemSelected(gridPositionFrom: Int) {
511         isCurrentlyDragging = true
512         layoutManager.setShouldLayoutChildren(false)
513         adapter.setDragStartPoint(gridPositionFrom)
514         dismissShortcutPopup()
515     }
516 
517     override fun onItemDragged() {
518         appGridDragController.cancelDelayedPageFling()
519     }
520 
521     override fun onDragExited(gridPosition: Int, exitDirection: Int) {
522         if (adapter.getOffsetBoundDirection(gridPosition) == exitDirection) {
523             appGridDragController.postDelayedPageFling(exitDirection)
524         }
525     }
526 
527     override fun onItemDropped(gridPositionFrom: Int, gridPositionTo: Int) {
528         layoutManager.setShouldLayoutChildren(true)
529         adapter.moveAppItem(gridPositionFrom, gridPositionTo)
530     }
531 
532     private inner class AppGridDragController() {
533         // TODO: (b/271320404) move DragController to separate directory called dragndrop and
534         // migrate logic this class and AppItemViewHolder there.
535         private val handler: Handler = Handler(getMainLooper())
536 
537         fun cancelDelayedPageFling() {
538             handler.removeCallbacksAndMessages(null)
539         }
540 
541         fun postDelayedPageFling(@AppItemBoundDirection exitDirection: Int) {
542             val scrollToNextPage =
543                 if (isHorizontal(pageOrientation)) {
544                     exitDirection == AppItemBoundDirection.RIGHT
545                 } else {
546                     exitDirection == AppItemBoundDirection.BOTTOM
547                 }
548             handler.removeCallbacksAndMessages(null)
549             handler.postDelayed({
550                 if (currentScrollState == RecyclerView.SCROLL_STATE_IDLE) {
551                     adapter.updatePageScrollDestination(scrollToNextPage)
552                     nextScrollDestination = snapCallback.snapPosition
553                     layoutManager.setShouldLayoutChildren(true)
554                     appGridRecyclerView.smoothScrollToPosition(nextScrollDestination)
555                 }
556                 // another delayed scroll will be queued to enable the user to input multiple
557                 // page scrolls by holding the recyclerview at the app grid margin
558                 postDelayedPageFling(exitDirection)
559             }, offPageHoverBeforeScrollMs)
560         }
561     }
562 
563     /**
564      * Private onDragListener for handling dispatching off page scroll event when user holds the app
565      * icon at the page margin.
566      */
567     private inner class AppGridDragListener : View.OnDragListener {
568         override fun onDrag(v: View, event: DragEvent): Boolean {
569             val action = event.action
570             if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
571                 isCurrentlyDragging = false
572                 appGridDragController.cancelDelayedPageFling()
573                 dragCallback.resetCallbackState()
574                 layoutManager.setShouldLayoutChildren(true)
575                 if (action == DragEvent.ACTION_DROP) {
576                     return false
577                 } else {
578                     animateDropEnded(HiddenApiAccess.getDragSurface(event))
579                 }
580             }
581             return true
582         }
583     }
584 
585     annotation class AppTypes {
586         companion object {
587             const val APP_TYPE_LAUNCHABLES = 1
588             const val APP_TYPE_MEDIA_SERVICES = 2
589         }
590     }
591 
592     enum class Mode(
593         @field:StringRes @param:StringRes val titleStringId: Int,
594         @field:AppTypes @param:AppTypes val appTypes: Int,
595         val openMediaCenter: Boolean
596     ) {
597         ALL_APPS(
598             R.string.app_launcher_title_all_apps,
599             APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
600             true
601         ),
602         MEDIA_ONLY(
603             R.string.app_launcher_title_media_only,
604             APP_TYPE_MEDIA_SERVICES,
605             true
606         ),
607         MEDIA_POPUP(
608             R.string.app_launcher_title_media_only,
609             APP_TYPE_MEDIA_SERVICES,
610             false
611         )
612     }
613 
614     companion object {
615         const val TAG = "AppGridFragment"
616         const val DEBUG_BUILD = false
617         const val MODE_INTENT_EXTRA = "com.android.car.carlauncher.mode"
618 
619         @JvmStatic
620         fun newInstance(mode: Mode): AppGridFragment {
621             return AppGridFragment().apply {
622                 arguments = Bundle().apply {
623                     putString(MODE_INTENT_EXTRA, mode.name)
624                 }
625             }
626         }
627     }
628 }
629