• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.util.Log
21 import android.view.ContextThemeWrapper
22 import android.view.InflateException
23 import androidx.annotation.VisibleForTesting
24 import androidx.annotation.VisibleForTesting.Companion.PROTECTED
25 import androidx.recyclerview.widget.RecyclerView
26 import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
27 import androidx.recyclerview.widget.RecyclerView.ViewHolder
28 import com.android.launcher3.BubbleTextView
29 import com.android.launcher3.BuildConfig
30 import com.android.launcher3.allapps.BaseAllAppsAdapter
31 import com.android.launcher3.config.FeatureFlags
32 import com.android.launcher3.util.CancellableTask
33 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
34 import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
35 import com.android.launcher3.util.Themes
36 import com.android.launcher3.views.ActivityContext
37 
38 const val PREINFLATE_ICONS_ROW_COUNT = 4
39 const val EXTRA_ICONS_COUNT = 2
40 
41 /**
42  * An [RecycledViewPool] that preinflates app icons ([ViewHolder] of [BubbleTextView]) of all apps
43  * [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
44  * will be added to [RecycledViewPool] on main thread.
45  */
46 class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
47 
48     var hasWorkProfile = false
49     @VisibleForTesting(otherwise = PROTECTED)
50     var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
51 
52     companion object {
53         private const val TAG = "AllAppsRecyclerViewPool"
54         private const val NULL_LAYOUT_MANAGER_ERROR_STRING =
55             "activeRv's layoutManager should not be null"
56     }
57 
58     /**
59      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
60      */
61     fun preInflateAllAppsViewHolders(context: T) {
62         val appsView = context.appsView ?: return
63         val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
64         val preInflateCount = getPreinflateCount(context)
65         if (preInflateCount <= 0) {
66             return
67         }
68 
69         if (activeRv.layoutManager == null) {
70             if (BuildConfig.IS_STUDIO_BUILD) {
71                 throw IllegalStateException(NULL_LAYOUT_MANAGER_ERROR_STRING)
72             } else {
73                 Log.e(TAG, NULL_LAYOUT_MANAGER_ERROR_STRING)
74             }
75             return
76         }
77 
78         // Create a separate context dedicated for all apps preinflation thread. The goal is to
79         // create a separate AssetManager obj internally to avoid lock contention with
80         // AssetManager obj that is associated with the launcher context on the main thread.
81         val allAppsPreInflationContext =
82             ContextThemeWrapper(context, Themes.getActivityThemeRes(context)).apply {
83                 applyOverrideConfiguration(context.resources.configuration)
84             }
85 
86         // Because we perform onCreateViewHolder() on worker thread, we need a separate
87         // adapter/inflator object as they are not thread-safe. Note that the adapter
88         // just need to perform onCreateViewHolder(parent, VIEW_TYPE_ICON) so it doesn't need
89         // data source information.
90         val adapter: RecyclerView.Adapter<BaseAllAppsAdapter.ViewHolder> =
91             object :
92                 BaseAllAppsAdapter<T>(
93                     context,
94                     context.appsView.layoutInflater.cloneInContext(allAppsPreInflationContext),
95                     null,
96                     null,
97                 ) {
98                 override fun setAppsPerRow(appsPerRow: Int) = Unit
99 
100                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
101             }
102 
103         preInflateAllAppsViewHolders(
104             adapter,
105             BaseAllAppsAdapter.VIEW_TYPE_ICON,
106             activeRv,
107             preInflateCount,
108         ) {
109             getPreinflateCount(context)
110         }
111     }
112 
113     @VisibleForTesting(otherwise = PROTECTED)
114     fun preInflateAllAppsViewHolders(
115         adapter: RecyclerView.Adapter<*>,
116         viewType: Int,
117         activeRv: RecyclerView,
118         preInflationCount: Int,
119         preInflationCountProvider: () -> Int,
120     ) {
121         if (preInflationCount <= 0) {
122             return
123         }
124         mCancellableTask?.cancel()
125         var task: CancellableTask<List<ViewHolder>>? = null
126         task =
127             CancellableTask(
128                 {
129                     val list: ArrayList<ViewHolder> = ArrayList()
130                     for (i in 0 until preInflationCount) {
131                         if (task?.canceled == true) {
132                             break
133                         }
134                         // If activeRv's layout manager has been reset to null on main thread, skip
135                         // the preinflation as we cannot generate correct LayoutParams
136                         if (activeRv.layoutManager == null) {
137                             list.clear()
138                             break
139                         }
140                         try {
141                             list.add(adapter.createViewHolder(activeRv, viewType))
142                         } catch (e: InflateException) {
143                             list.clear()
144                             // It's still possible for UI thread to set activeRv's layout manager to
145                             // null and we should break the loop and cancel the preinflation.
146                             break
147                         }
148                     }
149                     list
150                 },
151                 MAIN_EXECUTOR,
152                 { viewHolders ->
153                     // Run preInflationCountProvider again as the needed VH might have changed
154                     val newPreInflationCount = preInflationCountProvider.invoke()
155                     for (i in 0 until minOf(viewHolders.size, newPreInflationCount)) {
156                         putRecycledView(viewHolders[i])
157                     }
158                 },
159             )
160         mCancellableTask = task
161         VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
162     }
163 
164     /**
165      * When clearing [RecycledViewPool], we should also abort pre-inflation tasks. This will make
166      * sure we don't inflate app icons after DeviceProfile has changed.
167      */
168     override fun clear() {
169         super.clear()
170         mCancellableTask?.cancel()
171     }
172 
173     /**
174      * After testing on phone, foldable and tablet, we found [PREINFLATE_ICONS_ROW_COUNT] rows of
175      * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
176      * suffice fast scrolling.
177      *
178      * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
179      * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
180      * icons.
181      */
182     fun getPreinflateCount(context: T): Int {
183         var targetPreinflateCount =
184             PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
185                 EXTRA_ICONS_COUNT
186         val grid = ActivityContext.lookupContext<T>(context).deviceProfile
187         targetPreinflateCount += grid.maxAllAppsRowCount * grid.numShownAllAppsColumns
188         if (hasWorkProfile) {
189             targetPreinflateCount *= 2
190         }
191         val existingPreinflateCount = getRecycledViewCount(BaseAllAppsAdapter.VIEW_TYPE_ICON)
192         return targetPreinflateCount - existingPreinflateCount
193     }
194 }
195