Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 939ca48b authored by Fengjiang Li's avatar Fengjiang Li
Browse files

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
parent 7808efbb
Loading
Loading
Loading
Loading
+29 −13
Original line number Diff line number Diff line
@@ -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
@@ -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.
@@ -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)
    }

    /**
+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
        }
    }
}
+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
    }
}