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.launcher3.model 18 19 import android.os.Looper 20 import android.platform.test.flag.junit.SetFlagsRule 21 import android.util.Pair 22 import android.util.SparseArray 23 import android.view.View 24 import androidx.test.ext.junit.runners.AndroidJUnit4 25 import androidx.test.filters.SmallTest 26 import com.android.launcher3.Flags 27 import com.android.launcher3.model.BgDataModel.Callbacks 28 import com.android.launcher3.model.data.ItemInfo 29 import com.android.launcher3.util.Executors.MAIN_EXECUTOR 30 import com.android.launcher3.util.Executors.MODEL_EXECUTOR 31 import com.android.launcher3.util.IntArray 32 import com.android.launcher3.util.IntSet 33 import com.android.launcher3.util.ItemInflater 34 import com.android.launcher3.util.LauncherLayoutBuilder 35 import com.android.launcher3.util.LauncherModelHelper 36 import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE 37 import com.android.launcher3.util.RunnableList 38 import org.junit.After 39 import org.junit.Assert.assertEquals 40 import org.junit.Assert.assertFalse 41 import org.junit.Assert.assertTrue 42 import org.junit.Before 43 import org.junit.Rule 44 import org.junit.Test 45 import org.junit.runner.RunWith 46 import org.mockito.Mock 47 import org.mockito.MockitoAnnotations 48 import org.mockito.Spy 49 import org.mockito.kotlin.any 50 import org.mockito.kotlin.argThat 51 import org.mockito.kotlin.atLeastOnce 52 import org.mockito.kotlin.doAnswer 53 import org.mockito.kotlin.isNull 54 import org.mockito.kotlin.never 55 import org.mockito.kotlin.reset 56 import org.mockito.kotlin.times 57 import org.mockito.kotlin.verify 58 import org.mockito.kotlin.whenever 59 60 /** Tests to verify async binding of model views */ 61 @SmallTest 62 @RunWith(AndroidJUnit4::class) 63 class AsyncBindingTest { 64 65 @get:Rule val setFlagsRule = SetFlagsRule() 66 67 @Spy private var callbacks = MyCallbacks() 68 @Mock private lateinit var itemInflater: ItemInflater<*> 69 70 private val inflationLooper = SparseArray<Looper>() 71 72 private lateinit var modelHelper: LauncherModelHelper 73 74 @Before 75 fun setUp() { 76 setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WORKSPACE_INFLATION) 77 MockitoAnnotations.initMocks(this) 78 modelHelper = LauncherModelHelper() 79 80 doAnswer { i -> 81 inflationLooper[(i.arguments[0] as ItemInfo).id] = Looper.myLooper() 82 View(modelHelper.sandboxContext) 83 } 84 .whenever(itemInflater) 85 .inflateItem(any(), any(), isNull()) 86 87 // Set up the workspace with 3 pages of apps 88 modelHelper.setupDefaultLayoutProvider( 89 LauncherLayoutBuilder() 90 .atWorkspace(0, 1, 0) 91 .putApp(TEST_PACKAGE, TEST_PACKAGE) 92 .atWorkspace(1, 1, 0) 93 .putApp(TEST_PACKAGE, TEST_PACKAGE) 94 .atWorkspace(0, 1, 1) 95 .putApp(TEST_PACKAGE, TEST_PACKAGE) 96 .atWorkspace(1, 1, 1) 97 .putApp(TEST_PACKAGE, TEST_PACKAGE) 98 .atWorkspace(0, 1, 2) 99 .putApp(TEST_PACKAGE, TEST_PACKAGE) 100 ) 101 } 102 103 @After 104 fun tearDown() { 105 modelHelper.destroy() 106 } 107 108 @Test 109 fun test_bind_normally_without_itemInflater() { 110 MAIN_EXECUTOR.execute { modelHelper.model.addCallbacksAndLoad(callbacks) } 111 waitForLoaderAndTempMainThread() 112 113 verify(callbacks, never()).bindInflatedItems(any()) 114 verify(callbacks, atLeastOnce()).bindItems(any(), any()) 115 } 116 117 @Test 118 fun test_bind_inflates_item_on_background() { 119 callbacks.inflater = itemInflater 120 MAIN_EXECUTOR.execute { modelHelper.model.addCallbacksAndLoad(callbacks) } 121 waitForLoaderAndTempMainThread() 122 123 verify(callbacks, never()).bindItems(any(), any()) 124 verify(callbacks, times(1)).bindInflatedItems(argThat { t -> t.size == 2 }) 125 126 // Verify remaining items are bound using pendingTasks 127 reset(callbacks) 128 MAIN_EXECUTOR.submit(callbacks.pendingTasks!!::executeAllAndDestroy).get() 129 verify(callbacks, times(1)).bindInflatedItems(argThat { t -> t.size == 3 }) 130 131 // Verify that all items were inflated on the background thread 132 assertEquals(5, inflationLooper.size()) 133 for (i in 0..4) assertEquals(MODEL_EXECUTOR.looper, inflationLooper.valueAt(i)) 134 } 135 136 @Test 137 fun test_bind_sync_partially_inflates_on_background() { 138 modelHelper.loadModelSync() 139 assertTrue(modelHelper.model.isModelLoaded) 140 callbacks.inflater = itemInflater 141 142 val firstPageBindIds = IntSet() 143 144 MAIN_EXECUTOR.submit { 145 modelHelper.model.addCallbacksAndLoad(callbacks) 146 verify(callbacks, never()).bindItems(any(), any()) 147 verify(callbacks, times(1)) 148 .bindInflatedItems( 149 argThat { t -> 150 t.forEach { firstPageBindIds.add(it.first.id) } 151 t.size == 2 152 } 153 ) 154 155 // Verify that onInitialBindComplete is called and the binding is not yet complete 156 assertFalse(callbacks.onCompleteSignal!!.isDestroyed) 157 } 158 .get() 159 160 waitForLoaderAndTempMainThread() 161 assertTrue(callbacks.onCompleteSignal!!.isDestroyed) 162 163 // Verify that firstPageBindIds are loaded on the main thread and remaining 164 // on the background thread. 165 assertEquals(5, inflationLooper.size()) 166 for (i in 0..4) { 167 if (firstPageBindIds.contains(inflationLooper.keyAt(i))) 168 assertEquals(MAIN_EXECUTOR.looper, inflationLooper.valueAt(i)) 169 else assertEquals(MODEL_EXECUTOR.looper, inflationLooper.valueAt(i)) 170 } 171 172 MAIN_EXECUTOR.submit { 173 reset(callbacks) 174 callbacks.pendingTasks!!.executeAllAndDestroy() 175 // Verify remaining 3 times are bound using pending tasks 176 verify(callbacks, times(1)).bindInflatedItems(argThat { t -> t.size == 3 }) 177 } 178 .get() 179 } 180 181 private fun waitForLoaderAndTempMainThread() { 182 MAIN_EXECUTOR.submit {}.get() 183 MODEL_EXECUTOR.submit {}.get() 184 MAIN_EXECUTOR.submit {}.get() 185 } 186 187 class MyCallbacks : Callbacks { 188 189 var inflater: ItemInflater<*>? = null 190 var pendingTasks: RunnableList? = null 191 var onCompleteSignal: RunnableList? = null 192 193 override fun bindItems(shortcuts: MutableList<ItemInfo>, forceAnimateIcons: Boolean) {} 194 195 override fun bindInflatedItems(items: MutableList<Pair<ItemInfo, View>>) {} 196 197 override fun getPagesToBindSynchronously(orderedScreenIds: IntArray?) = IntSet.wrap(0) 198 199 override fun onInitialBindComplete( 200 boundPages: IntSet, 201 pendingTasks: RunnableList, 202 onCompleteSignal: RunnableList, 203 workspaceItemCount: Int, 204 isBindSync: Boolean 205 ) { 206 this.pendingTasks = pendingTasks 207 this.onCompleteSignal = onCompleteSignal 208 } 209 210 override fun getItemInflater() = inflater 211 } 212 } 213