Cancel all apps icons preinflation when device profile has changed
Bug: 312816372
Test: perinflate large number of App Icons then rotate screen. Verified 1) preinflation runnable is cancelled and 2) no ViewHolder created from this cancelled runnable is added to RecyclerViewPool
Flag: LEGACY ENABLE_ALL_APPS_RV_PREINFLATION ENABLED
Change-Id: I1a6110278e1af2b32387ab27273106d30513886f
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 45174a7..cbc6f44 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -23,10 +23,10 @@
import com.android.launcher3.BubbleTextView
import com.android.launcher3.allapps.BaseAllAppsAdapter
import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.util.ExecutorRunnable
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
import com.android.launcher3.views.ActivityContext
-import java.util.concurrent.Future
const val PREINFLATE_ICONS_ROW_COUNT = 4
const val EXTRA_ICONS_COUNT = 2
@@ -38,9 +38,8 @@
*/
class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
- private var future: Future<Void>? = null
-
var hasWorkProfile = false
+ var executorRunnable: ExecutorRunnable<List<ViewHolder>>? = null
/**
* Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
@@ -63,21 +62,38 @@
override fun getLayoutManager(): RecyclerView.LayoutManager? = null
}
- // Inflate view holders on background thread, and added to view pool on main thread.
- future?.cancel(true)
- future =
- VIEW_PREINFLATION_EXECUTOR.submit<Void> {
- val viewHolders =
- Array(preInflateCount) {
- adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
+ executorRunnable?.cancel(/* interrupt= */ true)
+ executorRunnable =
+ ExecutorRunnable.createAndExecute(
+ VIEW_PREINFLATION_EXECUTOR,
+ {
+ val list: ArrayList<ViewHolder> = ArrayList()
+ for (i in 0 until preInflateCount) {
+ if (Thread.interrupted()) {
+ break
+ }
+ list.add(
+ adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
+ )
}
- MAIN_EXECUTOR.execute {
+ list
+ },
+ MAIN_EXECUTOR,
+ { viewHolders ->
for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
putRecycledView(viewHolders[i])
}
}
- null
- }
+ )
+ }
+
+ /**
+ * When clearing [RecycledViewPool], we should also abort pre-inflation tasks. This will make
+ * sure we don't inflate app icons after DeviceProfile has changed.
+ */
+ override fun clear() {
+ super.clear()
+ executorRunnable?.cancel(/* interrupt= */ true)
}
/**
diff --git a/src/com/android/launcher3/util/ExecutorRunnable.kt b/src/com/android/launcher3/util/ExecutorRunnable.kt
new file mode 100644
index 0000000..49cf592
--- /dev/null
+++ b/src/com/android/launcher3/util/ExecutorRunnable.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Future
+import java.util.function.Consumer
+import java.util.function.Supplier
+
+/** A [Runnable] that can be posted to a [Executor] that can be cancelled. */
+class ExecutorRunnable<T>
+private constructor(
+ private val task: Supplier<T>,
+ // Executor where consumer needs to be executed on. Typically UI executor.
+ private val callbackExecutor: Executor,
+ // Consumer that needs to be accepted upon completion of the task. Typically work that needs to
+ // be done in UI thread after task completes.
+ private val callback: Consumer<T>
+) : Runnable {
+
+ // future of this runnable that will used for cancellation.
+ lateinit var future: Future<*>
+
+ // flag to cancel the callback
+ var canceled = false
+
+ override fun run() {
+ val value: T = task.get()
+ callbackExecutor.execute {
+ if (!canceled) {
+ callback.accept(value)
+ }
+ }
+ }
+
+ /**
+ * Cancel the [ExecutorRunnable] if not scheduled. If [ExecutorRunnable] has started execution
+ * at this time, we will try to cancel the callback if not executed yet.
+ */
+ fun cancel(interrupt: Boolean) {
+ future.cancel(interrupt)
+ canceled = true
+ }
+
+ companion object {
+ /**
+ * Create [ExecutorRunnable] and execute it on task [Executor]. It will also save the
+ * [Future] into this [ExecutorRunnable] to be used for cancellation.
+ */
+ fun <T> createAndExecute(
+ // Executor where task will be executed, typically an Executor running on background
+ // thread.
+ taskExecutor: ExecutorService,
+ task: Supplier<T>,
+ callbackExecutor: Executor,
+ callback: Consumer<T>
+ ): ExecutorRunnable<T> {
+ val executorRunnable = ExecutorRunnable(task, callbackExecutor, callback)
+ executorRunnable.future = taskExecutor.submit(executorRunnable)
+ return executorRunnable
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/ExecutorRunnableTest.kt b/tests/src/com/android/launcher3/util/ExecutorRunnableTest.kt
new file mode 100644
index 0000000..b4591ba
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/ExecutorRunnableTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import java.util.concurrent.ExecutorService
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit test for [ExecutorRunnable] */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ExecutorRunnableTest {
+
+ private lateinit var underTest: ExecutorRunnable<Int>
+
+ private var result: Int = -1
+ private var isTaskExecuted = false
+ private var isCallbackExecuted = false
+
+ @Before
+ fun setup() {
+ reset()
+ underTest =
+ ExecutorRunnable.createAndExecute(
+ Executors.UI_HELPER_EXECUTOR,
+ {
+ isTaskExecuted = true
+ 1
+ },
+ Executors.VIEW_PREINFLATION_EXECUTOR,
+ {
+ isCallbackExecuted = true
+ result = it + 1
+ }
+ )
+ }
+
+ @Test
+ fun run_and_complete() {
+ awaitAllExecutorCompleted()
+
+ assertTrue(isTaskExecuted)
+ assertTrue(isCallbackExecuted)
+ assertEquals(2, result)
+ }
+
+ @Test
+ fun run_and_cancel_cancelCallback() {
+ underTest.cancel(true)
+ awaitAllExecutorCompleted()
+
+ assertFalse(isCallbackExecuted)
+ assertEquals(0, result)
+ }
+
+ @Test
+ fun run_and_cancelAfterCompletion_executeAll() {
+ awaitAllExecutorCompleted()
+
+ underTest.cancel(true)
+
+ assertTrue(isTaskExecuted)
+ assertTrue(isCallbackExecuted)
+ assertEquals(2, result)
+ }
+
+ private fun awaitExecutorCompleted(executor: ExecutorService) {
+ executor.submit<Any> { null }.get()
+ }
+
+ private fun awaitAllExecutorCompleted() {
+ awaitExecutorCompleted(Executors.UI_HELPER_EXECUTOR)
+ awaitExecutorCompleted(Executors.VIEW_PREINFLATION_EXECUTOR)
+ }
+
+ private fun reset() {
+ result = 0
+ isTaskExecuted = false
+ isCallbackExecuted = false
+ }
+}