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

Commit 64dc0466 authored by Ats Jenk's avatar Ats Jenk
Browse files

Use executors for loading bubble view info

Remove deprecated async task when inflating bubbles.
Use shell background and main executors instead.

Bug: 353894869
Test: atest WMShellRobolectricTests:BubbleViewInfoTaskTest
Test: atest WMShellMultivalentTestsOnDevice:BubbleViewInfoTaskTest
Test: atest BubbleViewInfoTest
Flag: com.android.wm.shell.bubble_view_info_executors

Change-Id: I75cc882f5f5c489f7eb947a27646bc2fbba438fa
parent fcd43343
Loading
Loading
Loading
Loading
+349 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.wm.shell.bubbles

import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.content.res.Resources
import android.graphics.Color
import android.os.Handler
import android.os.UserManager
import android.view.IWindowManager
import android.view.WindowManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.R
import com.android.internal.protolog.ProtoLog
import com.android.internal.statusbar.IStatusBarService
import com.android.launcher3.icons.BubbleIconFactory
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.WindowManagerShellWrapper
import com.android.wm.shell.bubbles.properties.BubbleProperties
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.common.TaskStackListenerImpl
import com.android.wm.shell.shared.TransactionPool
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.taskview.TaskView
import com.android.wm.shell.taskview.TaskViewTransitions
import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

/** Test inflating bubbles with [BubbleViewInfoTask]. */
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleViewInfoTaskTest {

    private val context = ApplicationProvider.getApplicationContext<Context>()
    private lateinit var metadataFlagListener: Bubbles.BubbleMetadataFlagListener
    private lateinit var iconFactory: BubbleIconFactory
    private lateinit var bubbleController: BubbleController
    private lateinit var mainExecutor: TestExecutor
    private lateinit var bgExecutor: TestExecutor
    private lateinit var bubbleStackView: BubbleStackView
    private lateinit var bubblePositioner: BubblePositioner
    private lateinit var expandedViewManager: BubbleExpandedViewManager

    private val bubbleTaskViewFactory = BubbleTaskViewFactory {
        BubbleTaskView(mock<TaskView>(), directExecutor())
    }

    @Before
    fun setUp() {
        ProtoLog.REQUIRE_PROTOLOGTOOL = false
        metadataFlagListener = Bubbles.BubbleMetadataFlagListener {}
        iconFactory =
            BubbleIconFactory(
                context,
                60,
                30,
                Color.RED,
                context.resources.getDimensionPixelSize(R.dimen.importance_ring_stroke_width)
            )

        mainExecutor = TestExecutor()
        bgExecutor = TestExecutor()
        val windowManager = context.getSystemService(WindowManager::class.java)
        val shellInit = ShellInit(mainExecutor)
        val shellCommandHandler = ShellCommandHandler()
        val shellController =
            ShellController(
                context,
                shellInit,
                shellCommandHandler,
                mock<DisplayInsetsController>(),
                mainExecutor
            )
        bubblePositioner = BubblePositioner(context, windowManager)
        val bubbleData =
            BubbleData(
                context,
                mock<BubbleLogger>(),
                bubblePositioner,
                BubbleEducationController(context),
                mainExecutor,
                bgExecutor
            )

        val surfaceSynchronizer = { obj: Runnable -> obj.run() }

        val bubbleDataRepository =
            BubbleDataRepository(
                mock<LauncherApps>(),
                mainExecutor,
                bgExecutor,
                BubblePersistentRepository(context)
            )

        bubbleController =
            BubbleController(
                context,
                shellInit,
                shellCommandHandler,
                shellController,
                bubbleData,
                surfaceSynchronizer,
                FloatingContentCoordinator(),
                bubbleDataRepository,
                mock<IStatusBarService>(),
                windowManager,
                WindowManagerShellWrapper(mainExecutor),
                mock<UserManager>(),
                mock<LauncherApps>(),
                mock<BubbleLogger>(),
                mock<TaskStackListenerImpl>(),
                mock<ShellTaskOrganizer>(),
                bubblePositioner,
                mock<DisplayController>(),
                null,
                null,
                mainExecutor,
                mock<Handler>(),
                bgExecutor,
                mock<TaskViewTransitions>(),
                mock<Transitions>(),
                SyncTransactionQueue(TransactionPool(), mainExecutor),
                mock<IWindowManager>(),
                mock<BubbleProperties>()
            )

        val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController)
        bubbleStackView =
            BubbleStackView(
                context,
                bubbleStackViewManager,
                bubblePositioner,
                bubbleData,
                surfaceSynchronizer,
                FloatingContentCoordinator(),
                bubbleController,
                mainExecutor
            )
        expandedViewManager = BubbleExpandedViewManager.fromBubbleController(bubbleController)
    }

    @Test
    fun start_runsOnExecutors() {
        val bubble = createBubbleWithShortcut()
        val task = createBubbleViewInfoTask(bubble)

        task.start()

        assertThat(bubble.isInflated).isFalse()
        assertThat(bubble.expandedView).isNull()
        assertThat(task.isFinished).isFalse()

        bgExecutor.flushAll()
        assertThat(bubble.isInflated).isFalse()
        assertThat(bubble.expandedView).isNull()
        assertThat(task.isFinished).isFalse()

        mainExecutor.flushAll()
        assertThat(bubble.isInflated).isTrue()
        assertThat(bubble.expandedView).isNotNull()
        assertThat(task.isFinished).isTrue()
    }

    @Test
    fun startSync_runsImmediately() {
        val bubble = createBubbleWithShortcut()
        val task = createBubbleViewInfoTask(bubble)

        task.startSync()
        assertThat(bubble.isInflated).isTrue()
        assertThat(bubble.expandedView).isNotNull()
        assertThat(task.isFinished).isTrue()
    }

    @Test
    fun start_calledTwice_throwsIllegalStateException() {
        val bubble = createBubbleWithShortcut()
        val task = createBubbleViewInfoTask(bubble)
        task.start()
        Assert.assertThrows(IllegalStateException::class.java) { task.start() }
    }

    @Test
    fun startSync_calledTwice_throwsIllegalStateException() {
        val bubble = createBubbleWithShortcut()
        val task = createBubbleViewInfoTask(bubble)
        task.startSync()
        Assert.assertThrows(IllegalStateException::class.java) { task.startSync() }
    }

    @Test
    fun start_callbackNotified() {
        val bubble = createBubbleWithShortcut()
        var bubbleFromCallback: Bubble? = null
        val callback = BubbleViewInfoTask.Callback { b: Bubble? -> bubbleFromCallback = b }
        val task = createBubbleViewInfoTask(bubble, callback)
        task.start()
        bgExecutor.flushAll()
        mainExecutor.flushAll()
        assertThat(bubbleFromCallback).isSameInstanceAs(bubble)
    }

    @Test
    fun startSync_callbackNotified() {
        val bubble = createBubbleWithShortcut()
        var bubbleFromCallback: Bubble? = null
        val callback = BubbleViewInfoTask.Callback { b: Bubble? -> bubbleFromCallback = b }
        val task = createBubbleViewInfoTask(bubble, callback)
        task.startSync()
        assertThat(bubbleFromCallback).isSameInstanceAs(bubble)
    }

    @Test
    fun cancel_beforeBackgroundWorkStarts_bubbleNotInflated() {
        val bubble = createBubbleWithShortcut()
        val task = createBubbleViewInfoTask(bubble)
        task.start()

        // Cancel before allowing background or main executor to run
        task.cancel()
        bgExecutor.flushAll()
        mainExecutor.flushAll()

        assertThat(bubble.isInflated).isFalse()
        assertThat(bubble.expandedView).isNull()
        assertThat(task.isFinished).isTrue()
    }

    @Test
    fun cancel_afterBackgroundWorkBeforeMainThreadWork_bubbleNotInflated() {
        val bubble = createBubbleWithShortcut()
        val task = createBubbleViewInfoTask(bubble)
        task.start()

        // Cancel after background executor runs, but before main executor runs
        bgExecutor.flushAll()
        task.cancel()
        mainExecutor.flushAll()

        assertThat(bubble.isInflated).isFalse()
        assertThat(bubble.expandedView).isNull()
        assertThat(task.isFinished).isTrue()
    }

    @Test
    fun cancel_beforeStart_bubbleNotInflated() {
        val bubble = createBubbleWithShortcut()
        val task = createBubbleViewInfoTask(bubble)
        task.cancel()
        task.start()
        bgExecutor.flushAll()
        mainExecutor.flushAll()

        assertThat(task.isFinished).isTrue()
        assertThat(bubble.isInflated).isFalse()
        assertThat(bubble.expandedView).isNull()
    }

    private fun createBubbleWithShortcut(): Bubble {
        val shortcutInfo = ShortcutInfo.Builder(context, "mockShortcutId").build()
        return Bubble(
            "mockKey",
            shortcutInfo,
            1000,
            Resources.ID_NULL,
            "mockTitle",
            0 /* taskId */,
            "mockLocus",
            true /* isDismissible */,
            mainExecutor,
            bgExecutor,
            metadataFlagListener
        )
    }

    private fun createBubbleViewInfoTask(
        bubble: Bubble,
        callback: BubbleViewInfoTask.Callback? = null
    ): BubbleViewInfoTask {
        return BubbleViewInfoTask(
            bubble,
            context,
            expandedViewManager,
            bubbleTaskViewFactory,
            bubblePositioner,
            bubbleStackView,
            null /* layerView */,
            iconFactory,
            false /* skipInflation */,
            callback,
            mainExecutor,
            bgExecutor
        )
    }

    private class TestExecutor : ShellExecutor {

        private val runnables: MutableList<Runnable> = mutableListOf()

        override fun execute(runnable: Runnable) {
            runnables.add(runnable)
        }

        override fun executeDelayed(runnable: Runnable, delayMillis: Long) {
            execute(runnable)
        }

        override fun removeCallbacks(runnable: Runnable?) {}

        override fun hasCallback(runnable: Runnable?): Boolean = false

        fun flushAll() {
            while (runnables.isNotEmpty()) {
                runnables.removeAt(0).run()
            }
        }
    }
}
+7 −7
Original line number Diff line number Diff line
@@ -569,10 +569,9 @@ public class Bubble implements BubbleViewProvider {
            BubbleIconFactory iconFactory,
            boolean skipInflation) {
        if (Flags.bubbleViewInfoExecutors()) {
            if (mInflationTask != null && mInflationTask.getStatus() != FINISHED) {
                mInflationTask.cancel(true /* mayInterruptIfRunning */);
            if (mInflationTask != null && !mInflationTask.isFinished()) {
                mInflationTask.cancel();
            }
            // TODO(b/353894869): switch to executors
            mInflationTask = new BubbleViewInfoTask(this,
                    context,
                    expandedViewManager,
@@ -583,11 +582,12 @@ public class Bubble implements BubbleViewProvider {
                    iconFactory,
                    skipInflation,
                    callback,
                    mMainExecutor);
                    mMainExecutor,
                    mBgExecutor);
            if (mInflateSynchronously) {
                mInflationTask.onPostExecute(mInflationTask.doInBackground());
                mInflationTask.startSync();
            } else {
                mInflationTask.execute();
                mInflationTask.start();
            }
        } else {
            if (mInflationTaskLegacy != null && mInflationTaskLegacy.getStatus() != FINISHED) {
@@ -625,7 +625,7 @@ public class Bubble implements BubbleViewProvider {
            if (mInflationTask == null) {
                return;
            }
            mInflationTask.cancel(true /* mayInterruptIfRunning */);
            mInflationTask.cancel();
        } else {
            if (mInflationTaskLegacy == null) {
                return;
+104 −31
Original line number Diff line number Diff line
@@ -34,7 +34,6 @@ import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.AsyncTask;
import android.util.Log;
import android.util.PathParser;
import android.view.LayoutInflater;
@@ -50,15 +49,14 @@ import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Simple task to inflate views & load necessary info to display a bubble.
 */
// TODO(b/353894869): switch to executors
public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> {
public class BubbleViewInfoTask {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES;


    /**
     * Callback to find out when the bubble has been inflated & necessary data loaded.
     */
@@ -69,17 +67,22 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
        void onBubbleViewsReady(Bubble bubble);
    }

    private Bubble mBubble;
    private WeakReference<Context> mContext;
    private WeakReference<BubbleExpandedViewManager> mExpandedViewManager;
    private WeakReference<BubbleTaskViewFactory> mTaskViewFactory;
    private WeakReference<BubblePositioner> mPositioner;
    private WeakReference<BubbleStackView> mStackView;
    private WeakReference<BubbleBarLayerView> mLayerView;
    private BubbleIconFactory mIconFactory;
    private boolean mSkipInflation;
    private Callback mCallback;
    private Executor mMainExecutor;
    private final Bubble mBubble;
    private final WeakReference<Context> mContext;
    private final WeakReference<BubbleExpandedViewManager> mExpandedViewManager;
    private final WeakReference<BubbleTaskViewFactory> mTaskViewFactory;
    private final WeakReference<BubblePositioner> mPositioner;
    private final WeakReference<BubbleStackView> mStackView;
    private final WeakReference<BubbleBarLayerView> mLayerView;
    private final BubbleIconFactory mIconFactory;
    private final boolean mSkipInflation;
    private final Callback mCallback;
    private final Executor mMainExecutor;
    private final Executor mBgExecutor;

    private final AtomicBoolean mStarted = new AtomicBoolean();
    private final AtomicBoolean mCancelled = new AtomicBoolean();
    private final AtomicBoolean mFinished = new AtomicBoolean();

    /**
     * Creates a task to load information for the provided {@link Bubble}. Once all info
@@ -95,7 +98,8 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
            BubbleIconFactory factory,
            boolean skipInflation,
            Callback c,
            Executor mainExecutor) {
            Executor mainExecutor,
            Executor bgExecutor) {
        mBubble = b;
        mContext = new WeakReference<>(context);
        mExpandedViewManager = new WeakReference<>(expandedViewManager);
@@ -107,10 +111,86 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
        mSkipInflation = skipInflation;
        mCallback = c;
        mMainExecutor = mainExecutor;
        mBgExecutor = bgExecutor;
    }

    @Override
    protected BubbleViewInfo doInBackground(Void... voids) {
    /**
     * Load bubble view info in background using {@code bgExecutor} specified in constructor.
     * <br>
     * Use {@link #cancel()} to stop the task.
     *
     * @throws IllegalStateException if the task is already started
     */
    public void start() {
        verifyCanStart();
        if (mCancelled.get()) {
            // We got cancelled even before start was called. Exit early
            mFinished.set(true);
            return;
        }
        mBgExecutor.execute(() -> {
            if (mCancelled.get()) {
                // We got cancelled while background executor was busy and this was waiting
                mFinished.set(true);
                return;
            }
            BubbleViewInfo viewInfo = loadViewInfo();
            if (mCancelled.get()) {
                // Do not schedule anything on main executor if we got cancelled.
                // Loading view info involves inflating views and it is possible we get cancelled
                // during it.
                mFinished.set(true);
                return;
            }
            mMainExecutor.execute(() -> {
                // Before updating view info check that we did not get cancelled while waiting
                // main executor to pick up the work
                if (!mCancelled.get()) {
                    updateViewInfo(viewInfo);
                }
                mFinished.set(true);
            });
        });
    }

    private void verifyCanStart() {
        if (mStarted.getAndSet(true)) {
            throw new IllegalStateException("Task already started");
        }
    }

    /**
     * Load bubble view info synchronously.
     *
     * @throws IllegalStateException if the task is already started
     */
    public void startSync() {
        verifyCanStart();
        if (mCancelled.get()) {
            mFinished.set(true);
            return;
        }
        updateViewInfo(loadViewInfo());
        mFinished.set(true);
    }

    /**
     * Cancel the task. Stops the task from running if called before {@link #start()} or
     * {@link #startSync()}
     */
    public void cancel() {
        mCancelled.set(true);
    }

    /**
     * Return {@code true} when the task has completed loading the view info.
     */
    public boolean isFinished() {
        return mFinished.get();
    }

    @Nullable
    private BubbleViewInfo loadViewInfo() {
        if (!verifyState()) {
            // If we're in an inconsistent state, then switched modes and should just bail now.
            return null;
@@ -126,21 +206,14 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
        }
    }

    @Override
    protected void onPostExecute(BubbleViewInfo viewInfo) {
        if (isCancelled() || viewInfo == null) {
            return;
        }

        mMainExecutor.execute(() -> {
            if (!verifyState()) {
    private void updateViewInfo(@Nullable BubbleViewInfo viewInfo) {
        if (viewInfo == null || !verifyState()) {
            return;
        }
        mBubble.setViewInfo(viewInfo);
        if (mCallback != null) {
            mCallback.onBubbleViewsReady(mBubble);
        }
        });
    }

    private boolean verifyState() {