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

Commit a6c15388 authored by Mady Mellor's avatar Mady Mellor
Browse files

Don't set bubble launch flags on non-chat shortcut bubbles

Previously the isShortcutBubble path was only for chat bubbles using
shortcuts, however, with bubble anything we allow bubbling shortcuts
on launcher. These were still being tagged as "launch from bubble"
when they shouldn't be.

This CL only sets those flags if it's a chat bubble. If it's a
shortcut bubble from bubble anything, only set the multiple task flag
which is consistent with intent/pendingIntent app bubbles.

This CL also adds a bunch of verification in the BubbleTaskViewListener
test for the flags set on intents / activity options.

It seems like there are some issues with robolectric and PendingIntents.
I couldn't figure out a way to capture PendingIntents in robolectric, so
I modified the code to always use a fillInIntent & the flags are checked
on that instead.

Flag: EXEMPT bug fix
Test: atest BubbleTaskViewListenerTest
Bug: 395145961
Bug: 395178844
Change-Id: Ia01f740ab80d46f8b6533d9f87ba319a259bd5e0
parent 567e8a79
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@
    package="com.android.wm.shell.multivalenttests">

    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
    <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT"/>

    <application android:debuggable="true" android:supportsRtl="true" >
        <uses-library android:name="android.test.runner" />
+153 −8
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.wm.shell.bubbles

import android.app.ActivityOptions
import android.app.Notification
import android.app.PendingIntent
import android.content.ComponentName
@@ -24,6 +25,8 @@ import android.content.Intent
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Icon
import android.os.UserHandle
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.service.notification.NotificationListenerService.Ranking
import android.service.notification.StatusBarNotification
import android.view.View
@@ -33,6 +36,7 @@ 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.Flags.FLAG_ENABLE_BUBBLE_ANYTHING
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener
import com.android.wm.shell.common.TestShellExecutor
@@ -41,6 +45,7 @@ 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.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
@@ -48,6 +53,7 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
@@ -61,6 +67,9 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class BubbleTaskViewListenerTest {

    @get:Rule
    val setFlagsRule = SetFlagsRule()

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

    private var taskViewController = mock<TaskViewController>()
@@ -155,9 +164,22 @@ class BubbleTaskViewListenerTest {
        }
        getInstrumentation().waitForIdleSync()

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

        val intentCaptor = argumentCaptor<Intent>()
        val optionsCaptor = argumentCaptor<ActivityOptions>()

        verify(taskViewController).startActivity(any(),
            eq(pendingIntent),
            intentCaptor.capture(),
            optionsCaptor.capture(),
            any())
        val intentFlags = intentCaptor.lastValue.flags
        assertThat((intentFlags and Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0).isTrue()
        assertThat((intentFlags and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
        assertThat(optionsCaptor.lastValue.launchedFromBubble).isTrue()
        assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
    }

    @Test
@@ -178,12 +200,52 @@ class BubbleTaskViewListenerTest {
        }
        getInstrumentation().waitForIdleSync()

        assertThat(b.isPendingIntentActive).isFalse()
        verify(taskViewController).startShortcutActivity(any(), eq(shortcutInfo), any(), any())
        val optionsCaptor = argumentCaptor<ActivityOptions>()

        assertThat(b.isPendingIntentActive).isFalse() // not triggered for shortcut chats
        verify(taskViewController).startShortcutActivity(any(),
            eq(shortcutInfo),
            optionsCaptor.capture(),
            any())
        assertThat(optionsCaptor.lastValue.launchedFromBubble).isTrue()
        assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isTrue()
        assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_ANYTHING)
    @Test
    fun onInitialized_appBubble() {
    fun onInitialized_shortcutBubble() {
        val shortcutInfo = ShortcutInfo.Builder(context)
            .setId("mockShortcutId")
            .build()

        val b = createShortcutBubble(shortcutInfo)
        bubbleTaskViewListener.setBubble(b)

        assertThat(b.isChat).isFalse()
        assertThat(b.isShortcut).isTrue()
        assertThat(b.shortcutInfo).isNotNull()

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

        val optionsCaptor = argumentCaptor<ActivityOptions>()

        assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active
        verify(taskViewController).startShortcutActivity(any(),
            eq(shortcutInfo),
            optionsCaptor.capture(),
            any())
        assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
        assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
        assertThat(optionsCaptor.lastValue.isApplyMultipleTaskFlagForShortcut).isTrue()
        assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
    }

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

@@ -194,10 +256,82 @@ class BubbleTaskViewListenerTest {
        }
        getInstrumentation().waitForIdleSync()

        assertThat(b.isPendingIntentActive).isFalse()
        verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any())
        val intentCaptor = argumentCaptor<Intent>()
        val optionsCaptor = argumentCaptor<ActivityOptions>()

        assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active
        verify(taskViewController).startActivity(any(),
            any(),
            intentCaptor.capture(),
            optionsCaptor.capture(),
            any())

        assertThat((intentCaptor.lastValue.flags
                and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
        assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
        assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
        assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
    }

    @Test
    fun onInitialized_appBubble_pendingIntent() {
        val b = createAppBubble(usePendingIntent = true)
        bubbleTaskViewListener.setBubble(b)

        assertThat(b.isApp).isTrue()

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

        val intentCaptor = argumentCaptor<Intent>()
        val optionsCaptor = argumentCaptor<ActivityOptions>()

        assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active
        verify(taskViewController).startActivity(any(),
            any(),
            intentCaptor.capture(),
            optionsCaptor.capture(),
            any())

        assertThat((intentCaptor.lastValue.flags
                and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
        assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
        assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
        assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
    }

    @Test
    fun onInitialized_noteBubble() {
        val b = createNoteBubble()
        bubbleTaskViewListener.setBubble(b)

        assertThat(b.isNote).isTrue()

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

        val intentCaptor = argumentCaptor<Intent>()
        val optionsCaptor = argumentCaptor<ActivityOptions>()

        assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active
        verify(taskViewController).startActivity(any(),
            any(),
            intentCaptor.capture(),
            optionsCaptor.capture(),
            any())

        assertThat((intentCaptor.lastValue.flags
                and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
        assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
        assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
        assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
    }


    @Test
    fun onInitialized_preparingTransition() {
        val b = createAppBubble()
@@ -416,13 +550,24 @@ class BubbleTaskViewListenerTest {
        assertThat(isNew).isTrue()
    }

    private fun createAppBubble(): Bubble {
    private fun createAppBubble(usePendingIntent: Boolean = false): Bubble {
        val target = Intent(context, TestActivity::class.java)
        target.setPackage(context.packageName)
        if (usePendingIntent) {
            // Robolectric doesn't seem to play nice with PendingIntents, have to mock it.
            val pendingIntent = mock<PendingIntent>()
            whenever(pendingIntent.intent).thenReturn(target)
            return Bubble.createAppBubble(pendingIntent, mock<UserHandle>(),
                mainExecutor, bgExecutor)
        }
        return Bubble.createAppBubble(target, mock<UserHandle>(), mock<Icon>(),
            mainExecutor, bgExecutor)
    }

    private fun createShortcutBubble(shortcutInfo: ShortcutInfo): Bubble {
        return Bubble.createShortcutBubble(shortcutInfo, mainExecutor, bgExecutor)
    }

    private fun createNoteBubble(): Bubble {
        val target = Intent(context, TestActivity::class.java)
        target.setPackage(context.packageName)
+1 −1
Original line number Diff line number Diff line
@@ -364,7 +364,7 @@ public class Bubble implements BubbleViewProvider {
            @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
        return new Bubble(intent,
                user,
                /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
                /* key= */ getAppBubbleKeyForApp(intent.getIntent().getPackage(), user),
                mainExecutor, bgExecutor);
    }

+12 −6
Original line number Diff line number Diff line
@@ -237,18 +237,24 @@ public class BubbleExpandedView extends LinearLayout {
                        Context context =
                                mContext.createContextAsUser(
                                        mBubble.getUser(), Context.CONTEXT_RESTRICTED);
                        Intent fillInIntent = new Intent();
                        fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                        PendingIntent pi = PendingIntent.getActivity(
                                context,
                                /* requestCode= */ 0,
                                mBubble.getIntent().addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
                                PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
                                mBubble.getIntent(),
                                // Needs to be mutable for the fillInIntent
                                PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
                                /* options= */ null);
                        mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
                                launchBounds);
                        mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
                    } else if (!mIsOverflow && isShortcutBubble) {
                        ProtoLog.v(WM_SHELL_BUBBLES, "startingShortcutBubble=%s", getBubbleKey());
                        if (mBubble.isChat()) {
                            options.setLaunchedFromBubble(true);
                            options.setApplyActivityFlagsForBubbles(true);
                        } else {
                            options.setApplyMultipleTaskFlagForShortcut(true);
                        }
                        mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
                                options, launchBounds);
                    } else {
+12 −11
Original line number Diff line number Diff line
@@ -129,27 +129,28 @@ public class BubbleTaskViewListener implements TaskView.Listener {
                    Context context =
                            mContext.createContextAsUser(
                                    mBubble.getUser(), Context.CONTEXT_RESTRICTED);
                    Intent fillInIntent = null;
                    Intent fillInIntent = new Intent();
                    fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                    // 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 based on the bubble intent
                        pi = PendingIntent.getActivity(
                                context,
                                /* requestCode= */ 0,
                                mBubble.getIntent()
                                        .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
                                PendingIntent.FLAG_IMMUTABLE
                                        | PendingIntent.FLAG_UPDATE_CURRENT,
                                mBubble.getIntent(),
                                // Needs to be mutable for the fillInIntent
                                PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
                                /* options= */ null);
                    } else {
                        fillInIntent = new Intent(pi.getIntent());
                        fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                    }
                    mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
                } else if (isShortcutBubble) {
                    if (mBubble.isChat()) {
                        options.setLaunchedFromBubble(true);
                        options.setApplyActivityFlagsForBubbles(true);
                    } else {
                        options.setApplyMultipleTaskFlagForShortcut(true);
                    }
                    mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
                            options, launchBounds);
                } else {