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