blob: 7a5815994dd0f9f17bc7e8a3a3d13fbab4116d42 [file] [log] [blame]
/*
* Copyright (C) 2023 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 static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.taskview.TaskView;
import com.android.wm.shell.taskview.TaskViewTaskController;
/**
* Handles creating and updating the {@link TaskView} associated with a {@link Bubble}.
*/
public class BubbleTaskViewHelper {
private static final String TAG = BubbleTaskViewHelper.class.getSimpleName();
/**
* Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events
* on the task.
*/
public interface Listener {
/** Called when the task is first created. */
void onTaskCreated();
/** Called when the visibility of the task changes. */
void onContentVisibilityChanged(boolean visible);
/** Called when back is pressed on the task root. */
void onBackPressed();
}
private final Context mContext;
private final BubbleController mController;
private final @ShellMainThread ShellExecutor mMainExecutor;
private final BubbleTaskViewHelper.Listener mListener;
private final View mParentView;
@Nullable
private Bubble mBubble;
@Nullable
private PendingIntent mPendingIntent;
private TaskViewTaskController mTaskViewTaskController;
@Nullable
private TaskView mTaskView;
private int mTaskId = INVALID_TASK_ID;
private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
private boolean mInitialized = false;
private boolean mDestroyed = false;
@Override
public void onInitialized() {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onInitialized: destroyed=" + mDestroyed
+ " initialized=" + mInitialized
+ " bubble=" + getBubbleKey());
}
if (mDestroyed || mInitialized) {
return;
}
// Custom options so there is no activity transition animation
ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext,
0 /* enterResId */, 0 /* exitResId */);
Rect launchBounds = new Rect();
mTaskView.getBoundsOnScreen(launchBounds);
// TODO: I notice inconsistencies in lifecycle
// Post to keep the lifecycle normal
mParentView.post(() -> {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onInitialized: calling startActivity, bubble="
+ getBubbleKey());
}
try {
options.setTaskAlwaysOnTop(true);
options.setLaunchedFromBubble(true);
Intent fillInIntent = new Intent();
// Apply flags to make behaviour match documentLaunchMode=always.
fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
if (mBubble.isAppBubble()) {
PendingIntent pi = PendingIntent.getActivity(mContext, 0,
mBubble.getAppBubbleIntent(),
PendingIntent.FLAG_MUTABLE,
null);
mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
} else if (mBubble.hasMetadataShortcutId()) {
options.setApplyActivityFlagsForBubbles(true);
mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
options, launchBounds);
} else {
if (mBubble != null) {
mBubble.setIntentActive();
}
mTaskView.startActivity(mPendingIntent, fillInIntent, options,
launchBounds);
}
} catch (RuntimeException e) {
// If there's a runtime exception here then there's something
// wrong with the intent, we can't really recover / try to populate
// the bubble again so we'll just remove it.
Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
+ ", " + e.getMessage() + "; removing bubble");
mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT);
}
mInitialized = true;
});
}
@Override
public void onReleased() {
mDestroyed = true;
}
@Override
public void onTaskCreated(int taskId, ComponentName name) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onTaskCreated: taskId=" + taskId
+ " bubble=" + getBubbleKey());
}
// The taskId is saved to use for removeTask, preventing appearance in recent tasks.
mTaskId = taskId;
// With the task org, the taskAppeared callback will only happen once the task has
// already drawn
mListener.onTaskCreated();
}
@Override
public void onTaskVisibilityChanged(int taskId, boolean visible) {
mListener.onContentVisibilityChanged(visible);
}
@Override
public void onTaskRemovalStarted(int taskId) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
+ " bubble=" + getBubbleKey());
}
if (mBubble != null) {
mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
}
}
@Override
public void onBackPressedOnTaskRoot(int taskId) {
if (mTaskId == taskId && mController.isStackExpanded()) {
mListener.onBackPressed();
}
}
};
public BubbleTaskViewHelper(Context context,
BubbleController controller,
BubbleTaskViewHelper.Listener listener,
View parent) {
mContext = context;
mController = controller;
mMainExecutor = mController.getMainExecutor();
mListener = listener;
mParentView = parent;
mTaskViewTaskController = new TaskViewTaskController(mContext,
mController.getTaskOrganizer(),
mController.getTaskViewTransitions(), mController.getSyncTransactionQueue());
mTaskView = new TaskView(mContext, mTaskViewTaskController);
mTaskView.setListener(mMainExecutor, mTaskViewListener);
}
/**
* 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.
*/
public boolean update(Bubble bubble) {
boolean isNew = mBubble == null || didBackingContentChange(bubble);
mBubble = bubble;
if (isNew) {
mPendingIntent = mBubble.getBubbleIntent();
return true;
}
return false;
}
/** Cleans up anything related to the task and {@code TaskView}. */
public void cleanUpTaskView() {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
}
if (mTaskId != INVALID_TASK_ID) {
try {
ActivityTaskManager.getService().removeTask(mTaskId);
} catch (RemoteException e) {
Log.w(TAG, e.getMessage());
}
}
if (mTaskView != null) {
mTaskView.release();
mTaskView = null;
}
}
/** Returns the bubble key associated with this view. */
@Nullable
public String getBubbleKey() {
return mBubble != null ? mBubble.getKey() : null;
}
/** Returns the TaskView associated with this view. */
@Nullable
public TaskView getTaskView() {
return mTaskView;
}
/**
* Returns the task id associated with the task in this view. If the task doesn't exist then
* {@link ActivityTaskManager#INVALID_TASK_ID}.
*/
public int getTaskId() {
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());
}
// TODO (b/274980695): Is this still relevant?
/**
* Bubbles are backed by a pending intent or a shortcut, once the activity is
* started we never change it / restart it on notification updates -- unless the bubble's
* backing data switches.
*
* This indicates if the new bubble is backed by a different data source than what was
* previously shown here (e.g. previously a pending intent & now a shortcut).
*
* @param newBubble the bubble this view is being updated with.
* @return true if the backing content has changed.
*/
private boolean didBackingContentChange(Bubble newBubble) {
boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
return prevWasIntentBased != newIsIntentBased;
}
}