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

Commit 9a9ba473 authored by Mady Mellor's avatar Mady Mellor Committed by Android (Google) Code Review
Browse files

Merge changes I05ad1f20,Ied8d8952,I92de8b18 into main

* changes:
  Move the constructor of BubbleTaskViewListener above the listener impl
  Rename the helper to listener
  Change BubbleTaskViewHelper to implement the listener
parents 961797ca 19cae5d0
Loading
Loading
Loading
Loading
+491 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.app.Notification
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Icon
import android.os.UserHandle
import android.service.notification.NotificationListenerService.Ranking
import android.service.notification.StatusBarNotification
import android.view.View
import android.widget.FrameLayout
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener
import com.android.wm.shell.common.TestShellExecutor
import com.android.wm.shell.taskview.TaskView
import com.android.wm.shell.taskview.TaskViewController
import com.android.wm.shell.taskview.TaskViewTaskController
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

/**
 * Tests for [BubbleTaskViewListener].
 */
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleTaskViewListenerTest {

    private val context = ApplicationProvider.getApplicationContext<Context>()

    private var taskViewController = mock<TaskViewController>()
    private var listenerCallback = mock<BubbleTaskViewListener.Callback>()
    private var expandedViewManager = mock<BubbleExpandedViewManager>()

    private lateinit var bubbleTaskViewListener: BubbleTaskViewListener
    private lateinit var taskView: TaskView
    private lateinit var bubbleTaskView: BubbleTaskView
    private lateinit var parentView: ViewPoster
    private lateinit var mainExecutor: TestShellExecutor
    private lateinit var bgExecutor: TestShellExecutor

    @Before
    fun setUp() {
        ProtoLog.REQUIRE_PROTOLOGTOOL = false
        ProtoLog.init()

        parentView = ViewPoster(context)
        mainExecutor = TestShellExecutor()
        bgExecutor = TestShellExecutor()

        taskView = TaskView(context, taskViewController, mock<TaskViewTaskController>())
        bubbleTaskView = BubbleTaskView(taskView, mainExecutor)

        bubbleTaskViewListener =
            BubbleTaskViewListener(
                context,
                bubbleTaskView,
                parentView,
                expandedViewManager,
                listenerCallback
            )
    }

    @Test
    fun createBubbleTaskViewListener_withCreatedTaskView() {
        // Make the bubbleTaskView look like it's been created
        val taskId = 123
        bubbleTaskView.listener.onTaskCreated(taskId, mock<ComponentName>())
        reset(listenerCallback)

        bubbleTaskViewListener =
            BubbleTaskViewListener(
                context,
                bubbleTaskView,
                parentView,
                expandedViewManager,
                listenerCallback
            )

        assertThat(bubbleTaskView.delegateListener).isEqualTo(bubbleTaskViewListener)
        assertThat(bubbleTaskViewListener.taskView).isEqualTo(bubbleTaskView.taskView)

        verify(listenerCallback).onTaskCreated()
        assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId)
    }

    @Test
    fun createBubbleTaskViewListener() {
        bubbleTaskViewListener =
            BubbleTaskViewListener(
                context,
                bubbleTaskView,
                parentView,
                expandedViewManager,
                listenerCallback
            )

        assertThat(bubbleTaskView.delegateListener).isEqualTo(bubbleTaskViewListener)
        assertThat(bubbleTaskViewListener.taskView).isEqualTo(bubbleTaskView.taskView)
        verify(listenerCallback, never()).onTaskCreated()
    }

    @Test
    fun onInitialized_pendingIntentChatBubble() {
        val target = Intent(context, TestActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(context, 0, target,
            PendingIntent.FLAG_MUTABLE)

        val b = createChatBubble("key", pendingIntent)
        bubbleTaskViewListener.setBubble(b)

        assertThat(b.isChat).isTrue()
        // Has shortcut info
        assertThat(b.shortcutInfo).isNotNull()
        // But it didn't use that on bubble metadata
        assertThat(b.metadataShortcutId).isNull()

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()

        // ..so it's pending intent-based, and launches that
        assertThat(b.isPendingIntentActive).isTrue()
        verify(taskViewController).startActivity(any(), eq(pendingIntent), any(), any(), any())
    }

    @Test
    fun onInitialized_shortcutChatBubble() {
        val shortcutInfo = ShortcutInfo.Builder(context)
            .setId("mockShortcutId")
            .build()
        val b = createChatBubble("key", shortcutInfo)
        bubbleTaskViewListener.setBubble(b)

        assertThat(b.isChat).isTrue()
        assertThat(b.shortcutInfo).isNotNull()
        // Chat bubble using a shortcut
        assertThat(b.metadataShortcutId).isNotNull()

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()

        assertThat(b.isPendingIntentActive).isFalse()
        verify(taskViewController).startShortcutActivity(any(), eq(shortcutInfo), any(), any())
    }

    @Test
    fun onInitialized_appBubble() {
        val b = createAppBubble()
        bubbleTaskViewListener.setBubble(b)

        assertThat(b.isApp).isTrue()

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()

        assertThat(b.isPendingIntentActive).isFalse()
        verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any())
    }

    @Test
    fun onInitialized_preparingTransition() {
        val b = createAppBubble()
        bubbleTaskViewListener.setBubble(b)
        taskView = Mockito.spy(taskView)
        val preparingTransition = mock<BubbleTransitions.BubbleTransition>()
        b.preparingTransition = preparingTransition

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()

        verify(preparingTransition).surfaceCreated()
    }

    @Test
    fun onInitialized_destroyed() {
        val b = createAppBubble()
        bubbleTaskViewListener.setBubble(b)

        assertThat(b.isApp).isTrue()

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onReleased()
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()

        verify(taskViewController, never()).startActivity(any(), any(), anyOrNull(), any(), any())
    }

    @Test
    fun onInitialized_initialized() {
        val b = createAppBubble()
        bubbleTaskViewListener.setBubble(b)

        assertThat(b.isApp).isTrue()

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()

        reset(taskViewController)

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        // Already initialized, so no activity should be started.
        verify(taskViewController, never()).startActivity(any(), any(), anyOrNull(), any(), any())
    }

    @Test
    fun onTaskCreated() {
        val b = createAppBubble()
        bubbleTaskViewListener.setBubble(b)

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()
        verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any())

        val taskId = 123
        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>())
        }
        getInstrumentation().waitForIdleSync()

        verify(listenerCallback).onTaskCreated()
        verify(expandedViewManager, never()).setNoteBubbleTaskId(any(), any())
        assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId)
    }

    @Test
    fun onTaskCreated_noteBubble() {
        val b = createNoteBubble()
        bubbleTaskViewListener.setBubble(b)
        assertThat(b.isNote).isTrue()

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()
        verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any())

        val taskId = 123
        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>())
        }
        getInstrumentation().waitForIdleSync()

        verify(listenerCallback).onTaskCreated()
        verify(expandedViewManager).setNoteBubbleTaskId(eq(b.key), eq(taskId))
        assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId)
    }

    @Test
    fun onTaskVisibilityChanged_true() {
        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onTaskVisibilityChanged(1, true)
        }
        verify(listenerCallback).onContentVisibilityChanged(eq(true))
    }

    @Test
    fun onTaskVisibilityChanged_false() {
        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onTaskVisibilityChanged(1, false)
        }
        verify(listenerCallback).onContentVisibilityChanged(eq(false))
    }

    @Test
    fun onTaskRemovalStarted() {
        val mockTaskView = mock<TaskView>()
        bubbleTaskView = BubbleTaskView(mockTaskView, mainExecutor)

        bubbleTaskViewListener =
            BubbleTaskViewListener(
                context,
                bubbleTaskView,
                parentView,
                expandedViewManager,
                listenerCallback
            )

        val b = createAppBubble()
        bubbleTaskViewListener.setBubble(b)

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onInitialized()
        }
        getInstrumentation().waitForIdleSync()
        verify(mockTaskView).startActivity(any(), anyOrNull(), any(), any())

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onTaskRemovalStarted(1)
        }

        verify(expandedViewManager).removeBubble(eq(b.key), eq(Bubbles.DISMISS_TASK_FINISHED))
        verify(mockTaskView).release()
        assertThat(parentView.lastRemovedView).isEqualTo(mockTaskView)
        assertThat(bubbleTaskViewListener.taskView).isNull()
        verify(listenerCallback).onTaskRemovalStarted()
    }

    @Test
    fun onBackPressedOnTaskRoot_expanded() {
        val taskId = 123
        whenever(expandedViewManager.isStackExpanded()).doReturn(true)

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>())
            bubbleTaskViewListener.onBackPressedOnTaskRoot(taskId)
        }
        verify(listenerCallback).onBackPressed()
    }

    @Test
    fun onBackPressedOnTaskRoot_notExpanded() {
        val taskId = 123
        whenever(expandedViewManager.isStackExpanded()).doReturn(false)

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>())
            bubbleTaskViewListener.onBackPressedOnTaskRoot(taskId)
        }
        verify(listenerCallback, never()).onBackPressed()
    }

    @Test
    fun onBackPressedOnTaskRoot_taskIdMissMatch() {
        val taskId = 123
        whenever(expandedViewManager.isStackExpanded()).doReturn(true)

        getInstrumentation().runOnMainSync {
            bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>())
            bubbleTaskViewListener.onBackPressedOnTaskRoot(42)
        }
        verify(listenerCallback, never()).onBackPressed()
    }

    @Test
    fun setBubble_isNew() {
        val b = createAppBubble()
        val isNew = bubbleTaskViewListener.setBubble(b)
        assertThat(isNew).isTrue()
    }

    @Test
    fun setBubble_launchContentChanged() {
        val target = Intent(context, TestActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(
            context, 0, target,
            PendingIntent.FLAG_MUTABLE
        )

        val b = createChatBubble("key", pendingIntent)
        var isNew = bubbleTaskViewListener.setBubble(b)
        // First time bubble is set, so it is "new"
        assertThat(isNew).isTrue()

        val b2 = createChatBubble("key", pendingIntent)
        isNew = bubbleTaskViewListener.setBubble(b2)
        // Second time bubble is set & it uses same type of launch content, not "new"
        assertThat(isNew).isFalse()

        val shortcutInfo = ShortcutInfo.Builder(context)
            .setId("mockShortcutId")
            .build()
        val b3 = createChatBubble("key", shortcutInfo)
        // bubble is using different content, so it is "new"
        isNew = bubbleTaskViewListener.setBubble(b3)
        assertThat(isNew).isTrue()
    }

    private fun createAppBubble(): Bubble {
        val target = Intent(context, TestActivity::class.java)
        target.setPackage(context.packageName)
        return Bubble.createAppBubble(target, mock<UserHandle>(), mock<Icon>(),
            mainExecutor, bgExecutor)
    }

    private fun createNoteBubble(): Bubble {
        val target = Intent(context, TestActivity::class.java)
        target.setPackage(context.packageName)
        return Bubble.createNotesBubble(target, mock<UserHandle>(), mock<Icon>(),
            mainExecutor, bgExecutor)
    }

    private fun createChatBubble(key: String, shortcutInfo: ShortcutInfo): Bubble {
        return Bubble(
            key,
            shortcutInfo,
            0 /* desiredHeight */,
            0 /* desiredHeightResId */,
            "title",
            -1 /*taskId */,
            null /* locusId */, true /* isdismissabel */,
            mainExecutor, bgExecutor, mock<BubbleMetadataFlagListener>()
        )
    }

    private fun createChatBubble(key: String, pendingIntent: PendingIntent): Bubble {
        val metadata = Notification.BubbleMetadata.Builder(
            pendingIntent,
            Icon.createWithResource(context, R.drawable.bubble_ic_create_bubble)
        ).build()
        val shortcutInfo = ShortcutInfo.Builder(context)
            .setId("shortcutId")
            .build()
        val notification: Notification =
            Notification.Builder(context, key)
                .setSmallIcon(mock<Icon>())
                .setWhen(System.currentTimeMillis())
                .setContentTitle("title")
                .setContentText("content")
                .setBubbleMetadata(metadata)
                .build()
        val sbn = mock<StatusBarNotification>()
        val ranking = mock<Ranking>()
        whenever(sbn.getNotification()).thenReturn(notification)
        whenever(sbn.getKey()).thenReturn(key)
        whenever(ranking.getConversationShortcutInfo()).thenReturn(shortcutInfo)
        val entry = BubbleEntry(sbn, ranking, true, false, false, false)
        return Bubble(
            entry, mock<BubbleMetadataFlagListener>(), null, mainExecutor,
            bgExecutor
        )
    }

    /**
     * FrameLayout that immediately runs any runnables posted to it and tracks view removals.
     */
    class ViewPoster(context: Context) : FrameLayout(context) {

        lateinit var lastRemovedView: View

        override fun post(r: Runnable): Boolean {
            r.run()
            return true
        }

        override fun removeView(v: View) {
            super.removeView(v)
            lastRemovedView = v
        }
    }
}
 No newline at end of file
+279 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 * Copyright (C) 2025 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.
@@ -13,6 +13,7 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wm.shell.bubbles;

import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
@@ -40,17 +41,17 @@ import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.taskview.TaskView;

/**
 * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}.
 * A listener that works with task views for bubbles, manages launching the appropriate
 * content into the task view from the bubble and sends updates of task view events back to
 * the parent view via {@link BubbleTaskViewListener.Callback}.
 */
public class BubbleTaskViewHelper {

    private static final String TAG = BubbleTaskViewHelper.class.getSimpleName();
public class BubbleTaskViewListener implements TaskView.Listener {
    private static final String TAG = BubbleTaskViewListener.class.getSimpleName();

    /**
     * Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events
     * on the task.
     * Callback to let the view parent of TaskView to be notified of different events.
     */
    public interface Listener {
    public interface Callback {

        /** Called when the task is first created. */
        void onTaskCreated();
@@ -67,21 +68,32 @@ public class BubbleTaskViewHelper {

    private final Context mContext;
    private final BubbleExpandedViewManager mExpandedViewManager;
    private final BubbleTaskViewHelper.Listener mListener;
    private final BubbleTaskViewListener.Callback mCallback;
    private final View mParentView;

    @Nullable
    private Bubble mBubble;
    @Nullable
    private PendingIntent mPendingIntent;
    @Nullable
    private TaskView mTaskView;
    private int mTaskId = INVALID_TASK_ID;
    private TaskView mTaskView;

    private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
    private boolean mInitialized = false;
    private boolean mDestroyed = false;

    public BubbleTaskViewListener(Context context, BubbleTaskView bubbleTaskView, View parentView,
            BubbleExpandedViewManager manager, BubbleTaskViewListener.Callback callback) {
        mContext = context;
        mTaskView = bubbleTaskView.getTaskView();
        mParentView = parentView;
        mExpandedViewManager = manager;
        mCallback = callback;
        bubbleTaskView.setDelegateListener(this);
        if (bubbleTaskView.isCreated()) {
            mTaskId = bubbleTaskView.getTaskId();
            callback.onTaskCreated();
        }
    }

    @Override
    public void onInitialized() {
        ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s",
@@ -118,10 +130,10 @@ public class BubbleTaskViewHelper {
                            mContext.createContextAsUser(
                                    mBubble.getUser(), Context.CONTEXT_RESTRICTED);
                    Intent fillInIntent = null;
                        //first try get pending intent from the bubble
                    // First try get pending intent from the bubble
                    PendingIntent pi = mBubble.getPendingIntent();
                    if (pi == null) {
                            // if null - create new one
                        // If null - create new one
                        pi = PendingIntent.getActivity(
                                context,
                                /* requestCode= */ 0,
@@ -184,12 +196,12 @@ public class BubbleTaskViewHelper {

        // With the task org, the taskAppeared callback will only happen once the task has
        // already drawn
            mListener.onTaskCreated();
        mCallback.onTaskCreated();
    }

    @Override
    public void onTaskVisibilityChanged(int taskId, boolean visible) {
            mListener.onContentVisibilityChanged(visible);
        mCallback.onContentVisibilityChanged(visible);
    }

    @Override
@@ -204,53 +216,29 @@ public class BubbleTaskViewHelper {
            ((ViewGroup) mParentView).removeView(mTaskView);
            mTaskView = null;
        }
            mListener.onTaskRemovalStarted();
        mCallback.onTaskRemovalStarted();
    }

    @Override
    public void onBackPressedOnTaskRoot(int taskId) {
        if (mTaskId == taskId && mExpandedViewManager.isStackExpanded()) {
                mListener.onBackPressed();
            }
        }
    };

    public BubbleTaskViewHelper(Context context,
            BubbleExpandedViewManager expandedViewManager,
            BubbleTaskViewHelper.Listener listener,
            BubbleTaskView bubbleTaskView,
            View parent) {
        mContext = context;
        mExpandedViewManager = expandedViewManager;
        mListener = listener;
        mParentView = parent;
        mTaskView = bubbleTaskView.getTaskView();
        bubbleTaskView.setDelegateListener(mTaskViewListener);
        if (bubbleTaskView.isCreated()) {
            mTaskId = bubbleTaskView.getTaskId();
            mListener.onTaskCreated();
            mCallback.onBackPressed();
        }
    }

    /**
     * Sets the bubble or updates the bubble used to populate the view.
     *
     * @return true if the bubble is new, false if it was an update to the same bubble.
     * @return true if the bubble is new or if the launch content of the bubble changed from the
     * previous bubble.
     */
    public boolean update(Bubble bubble) {
    public boolean setBubble(Bubble bubble) {
        boolean isNew = mBubble == null || didBackingContentChange(bubble);
        mBubble = bubble;
        if (isNew) {
            mPendingIntent = mBubble.getPendingIntent();
            return true;
        }
        return false;
        }

    /** Returns the bubble key associated with this view. */
    @Nullable
    public String getBubbleKey() {
        return mBubble != null ? mBubble.getKey() : null;
        return isNew;
    }

    /** Returns the TaskView associated with this view. */
@@ -267,9 +255,8 @@ public class BubbleTaskViewHelper {
        return mTaskId;
    }

    /** Returns whether the bubble set on the helper is valid to populate the task view. */
    public boolean isValidBubble() {
        return mBubble != null && (mPendingIntent != null || mBubble.hasMetadataShortcutId());
    private String getBubbleKey() {
        return mBubble != null ? mBubble.getKey() : "";
    }

    // TODO (b/274980695): Is this still relevant?
+11 −8

File changed.

Preview size limit exceeded, changes collapsed.