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

Commit e228112c authored by Fengjiang Li's avatar Fengjiang Li Committed by Android (Google) Code Review
Browse files

Merge "Cancel all apps icons preinflation when device profile has changed" into main

parents d93bb98e 939ca48b
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
    }
}