Loading src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt +29 −13 Original line number Diff line number Diff line Loading @@ -23,10 +23,10 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder 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 Loading @@ -38,9 +38,8 @@ const val EXTRA_ICONS_COUNT = 2 */ 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. Loading @@ -63,21 +62,38 @@ class AllAppsRecyclerViewPool<T> : RecycledViewPool() { 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) { 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) } /** Loading src/com/android/launcher3/util/ExecutorRunnable.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line /* * 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 } } } tests/src/com/android/launcher3/util/ExecutorRunnableTest.kt 0 → 100644 +101 −0 Original line number Diff line number Diff line /* * 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 } } Loading
src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt +29 −13 Original line number Diff line number Diff line Loading @@ -23,10 +23,10 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder 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 Loading @@ -38,9 +38,8 @@ const val EXTRA_ICONS_COUNT = 2 */ 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. Loading @@ -63,21 +62,38 @@ class AllAppsRecyclerViewPool<T> : RecycledViewPool() { 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) { 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) } /** Loading
src/com/android/launcher3/util/ExecutorRunnable.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line /* * 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 } } }
tests/src/com/android/launcher3/util/ExecutorRunnableTest.kt 0 → 100644 +101 −0 Original line number Diff line number Diff line /* * 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 } }