/*
* 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 android.car.app;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.car.Car;
import android.car.builtin.util.Slogf;
import android.car.builtin.view.ViewHelper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Binder;
import android.os.UserManager;
import android.util.Slog;
import android.view.Display;
import android.view.SurfaceControl;
import java.util.concurrent.Executor;
/**
* A {@link ControlledRemoteCarTaskView} should be used when the launch intent of the task is known
* before hand.
*
* The underlying task will be restarted if it is crashed depending on the
* {@link ControlledRemoteCarTaskViewConfig#shouldAutoRestartOnCrash()}.
*
*
It should be preferred when:
*
* - The underlying task is meant to be started by the host and be there forever.
*
*
* @hide
*/
@SystemApi
public final class ControlledRemoteCarTaskView extends RemoteCarTaskView {
private static final String TAG = ControlledRemoteCarTaskView.class.getSimpleName();
private final Executor mCallbackExecutor;
private final ControlledRemoteCarTaskViewCallback mCallback;
private final UserManager mUserManager;
private final CarTaskViewController mCarTaskViewController;
private final Context mContext;
private final ControlledRemoteCarTaskViewConfig mConfig;
private final Rect mTmpRect = new Rect();
private ActivityManager.RunningTaskInfo mTaskInfo;
@Nullable private RunnerWithBackoff mStartActivityWithBackoff;
final ICarTaskViewClient mICarTaskViewClient = new ICarTaskViewClient.Stub() {
@Override
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
long identity = Binder.clearCallingIdentity();
try {
mTaskInfo = taskInfo;
updateWindowBounds();
if (taskInfo.taskDescription != null) {
ViewHelper.seResizeBackgroundColor(ControlledRemoteCarTaskView.this,
taskInfo.taskDescription.getBackgroundColor());
}
ControlledRemoteCarTaskView.this.onTaskAppeared(taskInfo, leash);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
long identity = Binder.clearCallingIdentity();
try {
if (taskInfo.taskDescription != null) {
ViewHelper.seResizeBackgroundColor(ControlledRemoteCarTaskView.this,
taskInfo.taskDescription.getBackgroundColor());
}
ControlledRemoteCarTaskView.this.onTaskInfoChanged(taskInfo);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
long identity = Binder.clearCallingIdentity();
try {
mTaskInfo = null;
ControlledRemoteCarTaskView.this.onTaskVanished(taskInfo);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void setResizeBackgroundColor(SurfaceControl.Transaction t, int color) {
ViewHelper.seResizeBackgroundColor(ControlledRemoteCarTaskView.this, color);
}
@Override
public Rect getCurrentBoundsOnScreen() {
ViewHelper.getBoundsOnScreen(ControlledRemoteCarTaskView.this, mTmpRect);
return mTmpRect;
}
};
ControlledRemoteCarTaskView(
@NonNull Context context,
ControlledRemoteCarTaskViewConfig config,
@NonNull Executor callbackExecutor,
@NonNull ControlledRemoteCarTaskViewCallback callback,
CarTaskViewController carTaskViewController,
@NonNull UserManager userManager) {
super(context);
mContext = context;
mConfig = config;
mCallbackExecutor = callbackExecutor;
mCallback = callback;
mCarTaskViewController = carTaskViewController;
mUserManager = userManager;
mCallbackExecutor.execute(() -> mCallback.onTaskViewCreated(this));
if (mConfig.mShouldAutoRestartOnTaskRemoval) {
mStartActivityWithBackoff = new RunnerWithBackoff(this::startActivityInternal);
}
}
/**
* Starts the underlying activity, specified as part of
* {@link CarTaskViewController#createControlledRemoteCarTaskView(Executor, Intent, boolean,
* ControlledRemoteCarTaskViewCallback)}.
*/
@RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
@MainThread
public void startActivity() {
if (mStartActivityWithBackoff == null) {
startActivityInternal();
return;
}
mStartActivityWithBackoff.stop();
mStartActivityWithBackoff.start();
}
private void stopTheStartActivityBackoffIfExists() {
if (mStartActivityWithBackoff == null) {
if (CarTaskViewController.DBG) {
Slog.d(TAG, "mStartActivityWithBackoff is not present.");
}
return;
}
mStartActivityWithBackoff.stop();
}
private void startActivityInternal() {
if (!mUserManager.isUserUnlocked()) {
if (CarTaskViewController.DBG) {
Slogf.d(TAG, "Can't start activity due to user is isn't unlocked");
}
return;
}
// Use mContext.getDisplay() instead of View#getDisplay() since
// ControlledRemoteCarTaskViews can be created using window context. Using
// View#getDisplay() would return null.
Display display = mContext.getDisplay();
// Don't start activity when the display is off. This can happen when the taskview is not
// attached to a window.
if (display == null) {
Slogf.w(TAG, "Can't start activity because display is not available in "
+ "taskview yet.");
return;
}
// Don't start activity when the display is off for ActivityVisibilityTests.
if (display.getState() != Display.STATE_ON) {
Slogf.w(TAG, "Can't start activity due to the display is off");
return;
}
ActivityOptions options = ActivityOptions
.makeCustomAnimation(mContext, /* enterResId= */ 0, /* exitResId= */ 0)
.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
Rect launchBounds = new Rect();
ViewHelper.getBoundsOnScreen(this, launchBounds);
launchBounds.set(launchBounds);
if (CarTaskViewController.DBG) {
Slogf.d(TAG, "Starting (" + mConfig.mActivityIntent.getComponent() + ") on "
+ launchBounds);
}
Intent fillInIntent = null;
if ((mConfig.mActivityIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) {
fillInIntent = new Intent().addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
}
startActivity(
PendingIntent.getActivity(mContext, /* requestCode= */ 0,
mConfig.mActivityIntent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT),
fillInIntent, options, launchBounds);
}
@Override
void onInitialized() {
mContext.getMainExecutor().execute(() -> {
// Check for isReleased() because the car task view might have already been
// released but this code path is executed later because the executor was busy.
if (isReleased()) {
Slogf.w(TAG, "car task view has already been released");
return;
}
startActivity();
});
mCallbackExecutor.execute(() -> mCallback.onTaskViewInitialized());
}
@Override
void onReleased() {
mTaskInfo = null;
mCallbackExecutor.execute(() -> mCallback.onTaskViewReleased());
mCarTaskViewController.onRemoteCarTaskViewReleased(this);
}
@Override
void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
super.onTaskAppeared(taskInfo, leash);
// Stop the start activity backoff because a task has already appeared.
stopTheStartActivityBackoffIfExists();
mCallbackExecutor.execute(() -> {
if (isReleased()) {
Slogf.w(TAG, "car task view has already been released");
return;
}
mCallback.onTaskAppeared(taskInfo);
});
}
@Override
void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
super.onTaskInfoChanged(taskInfo);
mCallbackExecutor.execute(() -> {
if (isReleased()) {
Slogf.w(TAG, "car task view has already been released");
return;
}
mCallback.onTaskInfoChanged(taskInfo);
});
}
@RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
@Override
@MainThread
public void showEmbeddedTask() {
super.showEmbeddedTask();
if (getTaskInfo() == null) {
if (CarTaskViewController.DBG) {
Slogf.d(TAG, "Embedded task not available, starting it now.");
}
startActivity();
}
}
@Override
void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
super.onTaskVanished(taskInfo);
if (mConfig.mShouldAutoRestartOnTaskRemoval && mCarTaskViewController.isHostVisible()) {
// onTaskVanished can be called when the host is in the background. In this case
// embedded activity should not be started.
Slogf.i(TAG, "Restarting task " + taskInfo.baseActivity
+ " in ControlledRemoteCarTaskView");
startActivity();
}
mCallbackExecutor.execute(() -> {
if (isReleased()) {
Slogf.w(TAG, "car task view has already been released");
return;
}
mCallback.onTaskVanished(taskInfo);
});
}
/**
* @return the {@link android.app.ActivityManager.RunningTaskInfo} of the task currently
* running in the TaskView.
*/
@MainThread
@Override
@Nullable
public ActivityManager.RunningTaskInfo getTaskInfo() {
return mTaskInfo;
}
ControlledRemoteCarTaskViewConfig getConfig() {
return mConfig;
}
@Override
public String toString() {
return toString(/* withBounds= */ false);
}
String toString(boolean withBounds) {
if (withBounds) {
ViewHelper.getBoundsOnScreen(this, mTmpRect);
}
return TAG + " {\n"
+ " config=" + mConfig + "\n"
+ " taskId=" + (getTaskInfo() == null ? "null" : getTaskInfo().taskId) + "\n"
+ (withBounds ? (" boundsOnScreen=" + mTmpRect) : "")
+ "}\n";
}
// Since SurfaceView is public, these methods need to be overridden. Details in b/296680464.
@Override
@RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
@MainThread
public void addInsets(int index, int type, @NonNull Rect frame) {
super.addInsets(index, type, frame);
}
@Override
@RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
public void removeInsets(int index, int type) {
super.removeInsets(index, type);
}
@Override
@MainThread
public void release() {
super.release();
stopTheStartActivityBackoffIfExists();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
@Override
@MainThread
public boolean isInitialized() {
return super.isInitialized();
}
@Override
@MainThread
public void setObscuredTouchRegion(@NonNull Region obscuredRegion) {
super.setObscuredTouchRegion(obscuredRegion);
}
@Override
@MainThread
public void setObscuredTouchRect(@NonNull Rect obscuredRect) {
super.setObscuredTouchRect(obscuredRect);
}
@Override
@RequiresPermission(Car.PERMISSION_REGISTER_CAR_SYSTEM_UI_PROXY)
@MainThread
public void updateWindowBounds() {
super.updateWindowBounds();
}
}