• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles;
18 
19 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
20 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
21 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
22 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
23 
24 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
25 
26 import android.app.ActivityOptions;
27 import android.app.ActivityTaskManager;
28 import android.app.PendingIntent;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.graphics.Rect;
33 import android.util.Log;
34 import android.view.View;
35 import android.view.ViewGroup;
36 
37 import androidx.annotation.Nullable;
38 
39 import com.android.internal.protolog.ProtoLog;
40 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
41 import com.android.wm.shell.taskview.TaskView;
42 
43 /**
44  * A listener that works with task views for bubbles, manages launching the appropriate
45  * content into the task view from the bubble and sends updates of task view events back to
46  * the parent view via {@link BubbleTaskViewListener.Callback}.
47  */
48 public class BubbleTaskViewListener implements TaskView.Listener {
49     private static final String TAG = BubbleTaskViewListener.class.getSimpleName();
50 
51     /**
52      * Callback to let the view parent of TaskView to be notified of different events.
53      */
54     public interface Callback {
55 
56         /** Called when the task is first created. */
onTaskCreated()57         void onTaskCreated();
58 
59         /** Called when the visibility of the task changes. */
onContentVisibilityChanged(boolean visible)60         void onContentVisibilityChanged(boolean visible);
61 
62         /** Called when back is pressed on the task root. */
onBackPressed()63         void onBackPressed();
64 
65         /** Called when task removal has started. */
onTaskRemovalStarted()66         void onTaskRemovalStarted();
67     }
68 
69     private final Context mContext;
70     private final BubbleExpandedViewManager mExpandedViewManager;
71     private final BubbleTaskViewListener.Callback mCallback;
72     private final View mParentView;
73 
74     private Bubble mBubble;
75     @Nullable
76     private PendingIntent mPendingIntent;
77     private int mTaskId = INVALID_TASK_ID;
78     private TaskView mTaskView;
79 
80     private boolean mInitialized = false;
81     private boolean mDestroyed = false;
82 
BubbleTaskViewListener(Context context, BubbleTaskView bubbleTaskView, View parentView, BubbleExpandedViewManager manager, BubbleTaskViewListener.Callback callback)83     public BubbleTaskViewListener(Context context, BubbleTaskView bubbleTaskView, View parentView,
84             BubbleExpandedViewManager manager, BubbleTaskViewListener.Callback callback) {
85         mContext = context;
86         mTaskView = bubbleTaskView.getTaskView();
87         mParentView = parentView;
88         mExpandedViewManager = manager;
89         mCallback = callback;
90         bubbleTaskView.setDelegateListener(this);
91         if (bubbleTaskView.isCreated()) {
92             mTaskId = bubbleTaskView.getTaskId();
93             callback.onTaskCreated();
94         }
95     }
96 
97     @Override
onInitialized()98     public void onInitialized() {
99         ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s",
100                 mDestroyed, mInitialized, getBubbleKey());
101 
102         if (mDestroyed || mInitialized) {
103             return;
104         }
105 
106         // Custom options so there is no activity transition animation
107         ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext,
108                 0 /* enterResId */, 0 /* exitResId */);
109 
110         Rect launchBounds = new Rect();
111         mTaskView.getBoundsOnScreen(launchBounds);
112 
113         // TODO: I notice inconsistencies in lifecycle
114         // Post to keep the lifecycle normal
115         // TODO - currently based on type, really it's what the "launch item" is.
116         mParentView.post(() -> {
117             ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s",
118                     getBubbleKey());
119             try {
120                 options.setTaskAlwaysOnTop(true);
121                 options.setPendingIntentBackgroundActivityStartMode(
122                         MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
123                 final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId()
124                         || (mBubble.isShortcut()
125                         && BubbleAnythingFlagHelper.enableCreateAnyBubble()));
126                 if (mBubble.getPreparingTransition() != null) {
127                     mBubble.getPreparingTransition().surfaceCreated();
128                 } else if (mBubble.isApp() || mBubble.isNote()) {
129                     Context context =
130                             mContext.createContextAsUser(
131                                     mBubble.getUser(), Context.CONTEXT_RESTRICTED);
132                     Intent fillInIntent = new Intent();
133                     // First try get pending intent from the bubble
134                     PendingIntent pi = mBubble.getPendingIntent();
135                     if (pi == null) {
136                         // If null - create new one based on the bubble intent
137                         pi = PendingIntent.getActivity(
138                                 context,
139                                 /* requestCode= */ 0,
140                                 mBubble.getIntent(),
141                                 // Needs to be mutable for the fillInIntent
142                                 PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
143                                 /* options= */ null);
144                     }
145                     mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
146                 } else if (isShortcutBubble) {
147                     if (mBubble.isChat()) {
148                         options.setLaunchedFromBubble(true);
149                         options.setApplyActivityFlagsForBubbles(true);
150                     } else {
151                         options.setApplyMultipleTaskFlagForShortcut(true);
152                     }
153                     mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
154                             options, launchBounds);
155                 } else {
156                     options.setLaunchedFromBubble(true);
157                     if (mBubble != null) {
158                         mBubble.setPendingIntentActive();
159                     }
160                     final Intent fillInIntent = new Intent();
161                     // Apply flags to make behaviour match documentLaunchMode=always.
162                     fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
163                     fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
164                     mTaskView.startActivity(mPendingIntent, fillInIntent, options,
165                             launchBounds);
166                 }
167             } catch (RuntimeException e) {
168                 // If there's a runtime exception here then there's something
169                 // wrong with the intent, we can't really recover / try to populate
170                 // the bubble again so we'll just remove it.
171                 Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
172                         + ", " + e.getMessage() + "; removing bubble");
173                 mExpandedViewManager.removeBubble(
174                         getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT);
175             }
176             mInitialized = true;
177         });
178     }
179 
180     @Override
onReleased()181     public void onReleased() {
182         mDestroyed = true;
183     }
184 
185     @Override
onTaskCreated(int taskId, ComponentName name)186     public void onTaskCreated(int taskId, ComponentName name) {
187         ProtoLog.d(WM_SHELL_BUBBLES, "onTaskCreated: taskId=%d bubble=%s",
188                 taskId, getBubbleKey());
189         // The taskId is saved to use for removeTask, preventing appearance in recent tasks.
190         mTaskId = taskId;
191 
192         if (mBubble != null && mBubble.isNote()) {
193             // Let the controller know sooner what the taskId is.
194             mExpandedViewManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId);
195         }
196 
197         // With the task org, the taskAppeared callback will only happen once the task has
198         // already drawn
199         mCallback.onTaskCreated();
200     }
201 
202     @Override
onTaskVisibilityChanged(int taskId, boolean visible)203     public void onTaskVisibilityChanged(int taskId, boolean visible) {
204         mCallback.onContentVisibilityChanged(visible);
205     }
206 
207     @Override
onTaskRemovalStarted(int taskId)208     public void onTaskRemovalStarted(int taskId) {
209         ProtoLog.d(WM_SHELL_BUBBLES, "onTaskRemovalStarted: taskId=%d bubble=%s",
210                 taskId, getBubbleKey());
211         if (mBubble != null) {
212             mExpandedViewManager.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
213         }
214         if (mTaskView != null) {
215             mTaskView.release();
216             ((ViewGroup) mParentView).removeView(mTaskView);
217             mTaskView = null;
218         }
219         mCallback.onTaskRemovalStarted();
220     }
221 
222     @Override
onBackPressedOnTaskRoot(int taskId)223     public void onBackPressedOnTaskRoot(int taskId) {
224         if (mTaskId == taskId && mExpandedViewManager.isStackExpanded()) {
225             mCallback.onBackPressed();
226         }
227     }
228 
229     /**
230      * Sets the bubble or updates the bubble used to populate the view.
231      *
232      * @return true if the bubble is new or if the launch content of the bubble changed from the
233      * previous bubble.
234      */
setBubble(Bubble bubble)235     public boolean setBubble(Bubble bubble) {
236         boolean isNew = mBubble == null || didBackingContentChange(bubble);
237         mBubble = bubble;
238         if (isNew) {
239             mPendingIntent = mBubble.getPendingIntent();
240         }
241         return isNew;
242     }
243 
244     /** Returns the TaskView associated with this view. */
245     @Nullable
getTaskView()246     public TaskView getTaskView() {
247         return mTaskView;
248     }
249 
250     /**
251      * Returns the task id associated with the task in this view. If the task doesn't exist then
252      * {@link ActivityTaskManager#INVALID_TASK_ID}.
253      */
getTaskId()254     public int getTaskId() {
255         return mTaskId;
256     }
257 
getBubbleKey()258     private String getBubbleKey() {
259         return mBubble != null ? mBubble.getKey() : "";
260     }
261 
262     // TODO (b/274980695): Is this still relevant?
263     /**
264      * Bubbles are backed by a pending intent or a shortcut, once the activity is
265      * started we never change it / restart it on notification updates -- unless the bubble's
266      * backing data switches.
267      *
268      * This indicates if the new bubble is backed by a different data source than what was
269      * previously shown here (e.g. previously a pending intent & now a shortcut).
270      *
271      * @param newBubble the bubble this view is being updated with.
272      * @return true if the backing content has changed.
273      */
didBackingContentChange(Bubble newBubble)274     private boolean didBackingContentChange(Bubble newBubble) {
275         boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
276         boolean newIsIntentBased = newBubble.getPendingIntent() != null;
277         return prevWasIntentBased != newIsIntentBased;
278     }
279 }
280