1 /* 2 * Copyright (C) 2023 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 package com.android.wm.shell.bubbles; 17 18 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; 19 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; 21 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; 22 23 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; 24 25 import android.app.ActivityOptions; 26 import android.app.ActivityTaskManager; 27 import android.app.PendingIntent; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.graphics.Rect; 32 import android.os.RemoteException; 33 import android.util.Log; 34 import android.view.View; 35 36 import androidx.annotation.Nullable; 37 38 import com.android.wm.shell.common.ShellExecutor; 39 import com.android.wm.shell.common.annotations.ShellMainThread; 40 import com.android.wm.shell.taskview.TaskView; 41 import com.android.wm.shell.taskview.TaskViewTaskController; 42 43 /** 44 * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. 45 */ 46 public class BubbleTaskViewHelper { 47 48 private static final String TAG = BubbleTaskViewHelper.class.getSimpleName(); 49 50 /** 51 * Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events 52 * on the task. 53 */ 54 public interface Listener { 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 66 private final Context mContext; 67 private final BubbleController mController; 68 private final @ShellMainThread ShellExecutor mMainExecutor; 69 private final BubbleTaskViewHelper.Listener mListener; 70 private final View mParentView; 71 72 @Nullable 73 private Bubble mBubble; 74 @Nullable 75 private PendingIntent mPendingIntent; 76 private TaskViewTaskController mTaskViewTaskController; 77 @Nullable 78 private TaskView mTaskView; 79 private int mTaskId = INVALID_TASK_ID; 80 81 private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { 82 private boolean mInitialized = false; 83 private boolean mDestroyed = false; 84 85 @Override 86 public void onInitialized() { 87 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 88 Log.d(TAG, "onInitialized: destroyed=" + mDestroyed 89 + " initialized=" + mInitialized 90 + " bubble=" + getBubbleKey()); 91 } 92 93 if (mDestroyed || mInitialized) { 94 return; 95 } 96 97 // Custom options so there is no activity transition animation 98 ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 99 0 /* enterResId */, 0 /* exitResId */); 100 101 Rect launchBounds = new Rect(); 102 mTaskView.getBoundsOnScreen(launchBounds); 103 104 // TODO: I notice inconsistencies in lifecycle 105 // Post to keep the lifecycle normal 106 mParentView.post(() -> { 107 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 108 Log.d(TAG, "onInitialized: calling startActivity, bubble=" 109 + getBubbleKey()); 110 } 111 try { 112 options.setTaskAlwaysOnTop(true); 113 options.setLaunchedFromBubble(true); 114 options.setPendingIntentBackgroundActivityStartMode( 115 MODE_BACKGROUND_ACTIVITY_START_ALLOWED); 116 options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); 117 118 Intent fillInIntent = new Intent(); 119 // Apply flags to make behaviour match documentLaunchMode=always. 120 fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); 121 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); 122 123 if (mBubble.isAppBubble()) { 124 Context context = 125 mContext.createContextAsUser( 126 mBubble.getUser(), Context.CONTEXT_RESTRICTED); 127 PendingIntent pi = PendingIntent.getActivity( 128 context, 129 /* requestCode= */ 0, 130 mBubble.getAppBubbleIntent() 131 .addFlags(FLAG_ACTIVITY_NEW_DOCUMENT) 132 .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), 133 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, 134 /* options= */ null); 135 mTaskView.startActivity(pi, /* fillInIntent= */ null, options, 136 launchBounds); 137 } else if (mBubble.hasMetadataShortcutId()) { 138 options.setApplyActivityFlagsForBubbles(true); 139 mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), 140 options, launchBounds); 141 } else { 142 if (mBubble != null) { 143 mBubble.setIntentActive(); 144 } 145 mTaskView.startActivity(mPendingIntent, fillInIntent, options, 146 launchBounds); 147 } 148 } catch (RuntimeException e) { 149 // If there's a runtime exception here then there's something 150 // wrong with the intent, we can't really recover / try to populate 151 // the bubble again so we'll just remove it. 152 Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() 153 + ", " + e.getMessage() + "; removing bubble"); 154 mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); 155 } 156 mInitialized = true; 157 }); 158 } 159 160 @Override 161 public void onReleased() { 162 mDestroyed = true; 163 } 164 165 @Override 166 public void onTaskCreated(int taskId, ComponentName name) { 167 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 168 Log.d(TAG, "onTaskCreated: taskId=" + taskId 169 + " bubble=" + getBubbleKey()); 170 } 171 // The taskId is saved to use for removeTask, preventing appearance in recent tasks. 172 mTaskId = taskId; 173 174 // With the task org, the taskAppeared callback will only happen once the task has 175 // already drawn 176 mListener.onTaskCreated(); 177 } 178 179 @Override 180 public void onTaskVisibilityChanged(int taskId, boolean visible) { 181 mListener.onContentVisibilityChanged(visible); 182 } 183 184 @Override 185 public void onTaskRemovalStarted(int taskId) { 186 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 187 Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId 188 + " bubble=" + getBubbleKey()); 189 } 190 if (mBubble != null) { 191 mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); 192 } 193 } 194 195 @Override 196 public void onBackPressedOnTaskRoot(int taskId) { 197 if (mTaskId == taskId && mController.isStackExpanded()) { 198 mListener.onBackPressed(); 199 } 200 } 201 }; 202 BubbleTaskViewHelper(Context context, BubbleController controller, BubbleTaskViewHelper.Listener listener, View parent)203 public BubbleTaskViewHelper(Context context, 204 BubbleController controller, 205 BubbleTaskViewHelper.Listener listener, 206 View parent) { 207 mContext = context; 208 mController = controller; 209 mMainExecutor = mController.getMainExecutor(); 210 mListener = listener; 211 mParentView = parent; 212 mTaskViewTaskController = new TaskViewTaskController(mContext, 213 mController.getTaskOrganizer(), 214 mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); 215 mTaskView = new TaskView(mContext, mTaskViewTaskController); 216 mTaskView.setListener(mMainExecutor, mTaskViewListener); 217 } 218 219 /** 220 * Sets the bubble or updates the bubble used to populate the view. 221 * 222 * @return true if the bubble is new, false if it was an update to the same bubble. 223 */ update(Bubble bubble)224 public boolean update(Bubble bubble) { 225 boolean isNew = mBubble == null || didBackingContentChange(bubble); 226 mBubble = bubble; 227 if (isNew) { 228 mPendingIntent = mBubble.getBubbleIntent(); 229 return true; 230 } 231 return false; 232 } 233 234 /** Cleans up anything related to the task and {@code TaskView}. */ cleanUpTaskView()235 public void cleanUpTaskView() { 236 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 237 Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); 238 } 239 if (mTaskId != INVALID_TASK_ID) { 240 try { 241 ActivityTaskManager.getService().removeTask(mTaskId); 242 } catch (RemoteException e) { 243 Log.w(TAG, e.getMessage()); 244 } 245 } 246 if (mTaskView != null) { 247 mTaskView.release(); 248 mTaskView = null; 249 } 250 } 251 252 /** Returns the bubble key associated with this view. */ 253 @Nullable getBubbleKey()254 public String getBubbleKey() { 255 return mBubble != null ? mBubble.getKey() : null; 256 } 257 258 /** Returns the TaskView associated with this view. */ 259 @Nullable getTaskView()260 public TaskView getTaskView() { 261 return mTaskView; 262 } 263 264 /** 265 * Returns the task id associated with the task in this view. If the task doesn't exist then 266 * {@link ActivityTaskManager#INVALID_TASK_ID}. 267 */ getTaskId()268 public int getTaskId() { 269 return mTaskId; 270 } 271 272 /** Returns whether the bubble set on the helper is valid to populate the task view. */ isValidBubble()273 public boolean isValidBubble() { 274 return mBubble != null && (mPendingIntent != null || mBubble.hasMetadataShortcutId()); 275 } 276 277 // TODO (b/274980695): Is this still relevant? 278 /** 279 * Bubbles are backed by a pending intent or a shortcut, once the activity is 280 * started we never change it / restart it on notification updates -- unless the bubble's 281 * backing data switches. 282 * 283 * This indicates if the new bubble is backed by a different data source than what was 284 * previously shown here (e.g. previously a pending intent & now a shortcut). 285 * 286 * @param newBubble the bubble this view is being updated with. 287 * @return true if the backing content has changed. 288 */ didBackingContentChange(Bubble newBubble)289 private boolean didBackingContentChange(Bubble newBubble) { 290 boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; 291 boolean newIsIntentBased = newBubble.getBubbleIntent() != null; 292 return prevWasIntentBased != newIsIntentBased; 293 } 294 } 295