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