blob: 1861e48482b8775411718a002b7f8d23c8733e11 [file] [log] [blame]
/*
* Copyright (C) 2020 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;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Binder;
import android.util.CloseGuard;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewTreeObserver;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import java.io.PrintWriter;
import java.util.concurrent.Executor;
/**
* View that can display a task.
*/
public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
ShellTaskOrganizer.TaskListener, ViewTreeObserver.OnComputeInternalInsetsListener {
/** Callback for listening task state. */
public interface Listener {
/** Called when the container is ready for launching activities. */
default void onInitialized() {}
/** Called when the container can no longer launch activities. */
default void onReleased() {}
/** Called when a task is created inside the container. */
default void onTaskCreated(int taskId, ComponentName name) {}
/** Called when a task visibility changes. */
default void onTaskVisibilityChanged(int taskId, boolean visible) {}
/** Called when a task is about to be removed from the stack inside the container. */
default void onTaskRemovalStarted(int taskId) {}
/** Called when a task is created inside the container. */
default void onBackPressedOnTaskRoot(int taskId) {}
}
private final CloseGuard mGuard = new CloseGuard();
private final ShellTaskOrganizer mTaskOrganizer;
private final Executor mShellExecutor;
private ActivityManager.RunningTaskInfo mTaskInfo;
private WindowContainerToken mTaskToken;
private SurfaceControl mTaskLeash;
private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
private boolean mSurfaceCreated;
private boolean mIsInitialized;
private Listener mListener;
private Executor mListenerExecutor;
private Rect mObscuredTouchRect;
private final Rect mTmpRect = new Rect();
private final Rect mTmpRootRect = new Rect();
private final int[] mTmpLocation = new int[2];
public TaskView(Context context, ShellTaskOrganizer organizer) {
super(context, null, 0, 0, true /* disableBackgroundLayer */);
mTaskOrganizer = organizer;
mShellExecutor = organizer.getExecutor();
setUseAlpha();
getHolder().addCallback(this);
mGuard.open("release");
}
/**
* Only one listener may be set on the view, throws an exception otherwise.
*/
public void setListener(@NonNull Executor executor, Listener listener) {
if (mListener != null) {
throw new IllegalStateException(
"Trying to set a listener when one has already been set");
}
mListener = listener;
mListenerExecutor = executor;
}
/**
* Launch an activity represented by {@link ShortcutInfo}.
* <p>The owner of this container must be allowed to access the shortcut information,
* as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method.
*
* @param shortcut the shortcut used to launch the activity.
* @param options options for the activity.
* @param launchBounds the bounds (window size and position) that the activity should be
* launched in, in pixels and in screen coordinates.
*/
public void startShortcutActivity(@NonNull ShortcutInfo shortcut,
@NonNull ActivityOptions options, @Nullable Rect launchBounds) {
prepareActivityOptions(options, launchBounds);
LauncherApps service = mContext.getSystemService(LauncherApps.class);
try {
service.startShortcut(shortcut, null /* sourceBounds */, options.toBundle());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Launch a new activity.
*
* @param pendingIntent Intent used to launch an activity.
* @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()}
* @param options options for the activity.
* @param launchBounds the bounds (window size and position) that the activity should be
* launched in, in pixels and in screen coordinates.
*/
public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent,
@NonNull ActivityOptions options, @Nullable Rect launchBounds) {
prepareActivityOptions(options, launchBounds);
try {
pendingIntent.send(mContext, 0 /* code */, fillInIntent,
null /* onFinished */, null /* handler */, null /* requiredPermission */,
options.toBundle());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void prepareActivityOptions(ActivityOptions options, Rect launchBounds) {
final Binder launchCookie = new Binder();
mShellExecutor.execute(() -> {
mTaskOrganizer.setPendingLaunchCookieListener(launchCookie, this);
});
options.setLaunchBounds(launchBounds);
options.setLaunchCookie(launchCookie);
options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
options.setRemoveWithTaskOrganizer(true);
}
/**
* Indicates a region of the view that is not touchable.
*
* @param obscuredRect the obscured region of the view.
*/
public void setObscuredTouchRect(Rect obscuredRect) {
mObscuredTouchRect = obscuredRect;
}
/**
* Call when view position or size has changed. Do not call when animating.
*/
public void onLocationChanged() {
if (mTaskToken == null) {
return;
}
// Update based on the screen bounds
getBoundsOnScreen(mTmpRect);
getRootView().getBoundsOnScreen(mTmpRootRect);
if (!mTmpRootRect.contains(mTmpRect)) {
mTmpRect.offsetTo(0, 0);
}
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(mTaskToken, mTmpRect);
// TODO(b/151449487): Enable synchronization
mTaskOrganizer.applyTransaction(wct);
}
/**
* Release this container if it is initialized.
*/
public void release() {
performRelease();
}
@Override
protected void finalize() throws Throwable {
try {
if (mGuard != null) {
mGuard.warnIfOpen();
performRelease();
}
} finally {
super.finalize();
}
}
private void performRelease() {
getHolder().removeCallback(this);
mShellExecutor.execute(() -> {
mTaskOrganizer.removeListener(this);
resetTaskInfo();
});
mGuard.close();
if (mListener != null && mIsInitialized) {
mListenerExecutor.execute(() -> {
mListener.onReleased();
});
mIsInitialized = false;
}
}
private void resetTaskInfo() {
mTaskInfo = null;
mTaskToken = null;
mTaskLeash = null;
}
private void updateTaskVisibility() {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */);
mTaskOrganizer.applyTransaction(wct);
// TODO(b/151449487): Only call callback once we enable synchronization
if (mListener != null) {
final int taskId = mTaskInfo.taskId;
mListenerExecutor.execute(() -> {
mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated);
});
}
}
@Override
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl leash) {
mTaskInfo = taskInfo;
mTaskToken = taskInfo.token;
mTaskLeash = leash;
if (mSurfaceCreated) {
// Surface is ready, so just reparent the task to this surface control
mTransaction.reparent(mTaskLeash, getSurfaceControl())
.show(mTaskLeash)
.apply();
} else {
// The surface has already been destroyed before the task has appeared,
// so go ahead and hide the task entirely
updateTaskVisibility();
}
mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true);
// TODO: Synchronize show with the resize
onLocationChanged();
if (taskInfo.taskDescription != null) {
setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
}
if (mListener != null) {
final int taskId = taskInfo.taskId;
final ComponentName baseActivity = taskInfo.baseActivity;
mListenerExecutor.execute(() -> {
mListener.onTaskCreated(taskId, baseActivity);
});
}
}
@Override
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return;
if (mListener != null) {
final int taskId = taskInfo.taskId;
mListenerExecutor.execute(() -> {
mListener.onTaskRemovalStarted(taskId);
});
}
mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false);
// Unparent the task when this surface is destroyed
mTransaction.reparent(mTaskLeash, null).apply();
resetTaskInfo();
}
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
if (taskInfo.taskDescription != null) {
setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
}
}
@Override
public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) {
if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return;
if (mListener != null) {
final int taskId = taskInfo.taskId;
mListenerExecutor.execute(() -> {
mListener.onBackPressedOnTaskRoot(taskId);
});
}
}
@Override
public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
if (mTaskInfo.taskId != taskId) {
throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
}
b.setParent(mTaskLeash);
}
@Override
public void dump(@androidx.annotation.NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
final String childPrefix = innerPrefix + " ";
pw.println(prefix + this);
}
@Override
public String toString() {
return "TaskView" + ":" + (mTaskInfo != null ? mTaskInfo.taskId : "null");
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceCreated = true;
if (mListener != null && !mIsInitialized) {
mIsInitialized = true;
mListenerExecutor.execute(() -> {
mListener.onInitialized();
});
}
mShellExecutor.execute(() -> {
if (mTaskToken == null) {
// Nothing to update, task is not yet available
return;
}
// Reparent the task when this surface is created
mTransaction.reparent(mTaskLeash, getSurfaceControl())
.show(mTaskLeash)
.apply();
updateTaskVisibility();
});
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mTaskToken == null) {
return;
}
onLocationChanged();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mSurfaceCreated = false;
mShellExecutor.execute(() -> {
if (mTaskToken == null) {
// Nothing to update, task is not yet available
return;
}
// Unparent the task when this surface is destroyed
mTransaction.reparent(mTaskLeash, null).apply();
updateTaskVisibility();
});
}
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
// TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this
// is dependent on the order of listener.
// If there are multiple TaskViews, we'll set the touchable area as the root-view, then
// subtract each TaskView from it.
if (inoutInfo.touchableRegion.isEmpty()) {
inoutInfo.setTouchableInsets(
ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
View root = getRootView();
root.getLocationInWindow(mTmpLocation);
mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight());
inoutInfo.touchableRegion.set(mTmpRootRect);
}
getLocationInWindow(mTmpLocation);
mTmpRect.set(mTmpLocation[0], mTmpLocation[1],
mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight());
inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE);
if (mObscuredTouchRect != null) {
inoutInfo.touchableRegion.union(mObscuredTouchRect);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
}
}