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