1 /*
<lambda>null2  * 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.launcher3.recyclerview
18 
19 import android.content.Context
20 import androidx.recyclerview.widget.RecyclerView
21 import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
22 import androidx.recyclerview.widget.RecyclerView.ViewHolder
23 import com.android.launcher3.BubbleTextView
24 import com.android.launcher3.allapps.BaseAllAppsAdapter
25 import com.android.launcher3.config.FeatureFlags
26 import com.android.launcher3.util.CancellableTask
27 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
28 import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
29 import com.android.launcher3.util.Themes
30 import com.android.launcher3.views.ActivityContext
31 import com.android.launcher3.views.ActivityContext.ActivityContextDelegate
32 
33 const val PREINFLATE_ICONS_ROW_COUNT = 4
34 const val EXTRA_ICONS_COUNT = 2
35 
36 /**
37  * An [RecycledViewPool] that preinflates app icons ([ViewHolder] of [BubbleTextView]) of all apps
38  * [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
39  * will be added to [RecycledViewPool] on main thread.
40  */
41 class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
42 
43     var hasWorkProfile = false
44     private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
45 
46     /**
47      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
48      */
49     fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
50         val appsView = context.appsView ?: return
51         val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
52         val preInflateCount = getPreinflateCount(context)
53         if (preInflateCount <= 0) {
54             return
55         }
56 
57         // Create a separate context dedicated for all apps preinflation thread. The goal is to
58         // create a separate AssetManager obj internally to avoid lock contention with
59         // AssetManager obj that is associated with the launcher context on the main thread.
60         val allAppsPreInflationContext =
61             ActivityContextDelegate(
62                 context.createConfigurationContext(context.resources.configuration),
63                 Themes.getActivityThemeRes(context),
64                 context
65             )
66 
67         // Because we perform onCreateViewHolder() on worker thread, we need a separate
68         // adapter/inflator object as they are not thread-safe. Note that the adapter
69         // just need to perform onCreateViewHolder(parent, VIEW_TYPE_ICON) so it doesn't need
70         // data source information.
71         val adapter: RecyclerView.Adapter<BaseAllAppsAdapter.ViewHolder> =
72             object :
73                 BaseAllAppsAdapter<T>(
74                     context,
75                     context.appsView.layoutInflater.cloneInContext(allAppsPreInflationContext),
76                     null,
77                     null
78                 ) {
79                 override fun setAppsPerRow(appsPerRow: Int) = Unit
80                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
81             }
82 
83         mCancellableTask?.cancel()
84         var task: CancellableTask<List<ViewHolder>>? = null
85         task =
86             CancellableTask(
87                 {
88                     val list: ArrayList<ViewHolder> = ArrayList()
89                     for (i in 0 until preInflateCount) {
90                         if (task?.canceled == true) {
91                             break
92                         }
93                         list.add(
94                             adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
95                         )
96                     }
97                     list
98                 },
99                 MAIN_EXECUTOR,
100                 { viewHolders ->
101                     for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
102                         putRecycledView(viewHolders[i])
103                     }
104                 }
105             )
106         mCancellableTask = task
107         VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
108     }
109 
110     /**
111      * When clearing [RecycledViewPool], we should also abort pre-inflation tasks. This will make
112      * sure we don't inflate app icons after DeviceProfile has changed.
113      */
114     override fun clear() {
115         super.clear()
116         mCancellableTask?.cancel()
117     }
118 
119     /**
120      * After testing on phone, foldable and tablet, we found [PREINFLATE_ICONS_ROW_COUNT] rows of
121      * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
122      * suffice fast scrolling.
123      *
124      * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
125      * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
126      * icons.
127      */
128     fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
129         var targetPreinflateCount =
130             PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
131                 EXTRA_ICONS_COUNT
132         if (FeatureFlags.ALL_APPS_GONE_VISIBILITY.get()) {
133             val grid = ActivityContext.lookupContext<T>(context).deviceProfile
134             targetPreinflateCount += grid.maxAllAppsRowCount * grid.numShownAllAppsColumns
135         }
136         if (hasWorkProfile) {
137             targetPreinflateCount *= 2
138         }
139         val existingPreinflateCount = getRecycledViewCount(BaseAllAppsAdapter.VIEW_TYPE_ICON)
140         return targetPreinflateCount - existingPreinflateCount
141     }
142 }
143