blob: c3f73f973817e2f8128d88dab8b004f13393ebe9 [file] [log] [blame]
/*
* Copyright (C) 2021 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.car.carlauncher.displayarea;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.car.carlauncher.CarLauncher.TAG;
import static com.android.car.carlauncher.displayarea.CarDisplayAreaController.BACKGROUND_LAYER_INDEX;
import static com.android.car.carlauncher.displayarea.CarDisplayAreaController.FOREGROUND_LAYER_INDEX;
import android.app.ActivityOptions;
import android.car.Car;
import android.car.app.CarActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.SurfaceControl;
import android.window.DisplayAreaAppearedInfo;
import android.window.DisplayAreaInfo;
import android.window.DisplayAreaOrganizer;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.car.carlauncher.AppGridActivity;
import com.android.car.carlauncher.R;
import com.android.wm.shell.common.SyncTransactionQueue;
import java.util.List;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
/**
* Organizer for controlling the policies defined in
* {@link com.android.server.wm.CarDisplayAreaPolicyProvider}
*/
public class CarDisplayAreaOrganizer extends DisplayAreaOrganizer {
/**
* The display partition to launch applications by default.
*/
public static final int FOREGROUND_DISPLAY_AREA_ROOT = FEATURE_VENDOR_FIRST + 1;
/**
* Background applications task container.
*/
public static final int BACKGROUND_TASK_CONTAINER = FEATURE_VENDOR_FIRST + 2;
private static final int FEATURE_TASKDISPLAYAREA_PARENT = FEATURE_VENDOR_FIRST + 3;
/**
* Control bar task container.
*/
public static final int CONTROL_BAR_DISPLAY_AREA = FEATURE_VENDOR_FIRST + 4;
public static final int FEATURE_TITLE_BAR = FEATURE_VENDOR_FIRST + 5;
private static final int FEATURE_VOICE_PLATE = FEATURE_VENDOR_FIRST + 7;
@Nullable
private final ComponentName mAssistantVoicePlateActivityName;
private static CarDisplayAreaOrganizer sCarDisplayAreaOrganizer;
private final Context mContext;
private final Intent mMapsIntent;
private final SyncTransactionQueue mTransactionQueue;
private final Rect mBackgroundApplicationDisplayBounds = new Rect();
private final Intent mAudioControlIntent;
private boolean mIsShowingBackgroundDisplay;
private boolean mIsShowingControlBarDisplay;
private final CarLauncherDisplayAreaAnimationController mAnimationController;
private final Handler mHandlerForAnimation;
private final Rect mLastVisualDisplayBounds = new Rect();
private final ArrayMap<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap =
new ArrayMap();
private WindowContainerToken mBackgroundDisplayToken;
private WindowContainerToken mForegroundDisplayToken;
private WindowContainerToken mControbarDisplayToken;
private WindowContainerToken mTitleBarDisplayToken;
private int mDpiDensity = -1;
private DisplayAreaAppearedInfo mBackgroundApplicationDisplay;
private DisplayAreaAppearedInfo mForegroundApplicationDisplay;
private DisplayAreaAppearedInfo mTitleBarDisplay;
private DisplayAreaAppearedInfo mControlBarDisplay;
private boolean mIsRegistered = false;
private boolean mIsDisplayAreaAnimating = false;
private AppGridActivity.CAR_LAUNCHER_STATE mToState;
DisplayAreaAnimationRunnable mDisplayAreaAnimationRunnable = null;
@VisibleForTesting
CarLauncherDisplayAreaAnimationCallback mDisplayAreaAnimationCallback =
new CarLauncherDisplayAreaAnimationCallback() {
@Override
public void onAnimationStart(
CarLauncherDisplayAreaAnimationController
.CarLauncherDisplayAreaTransitionAnimator animator) {
mIsDisplayAreaAnimating = true;
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
// Update the foreground panel layer index to animate on top of the
// background DA.
tx.setLayer(mForegroundApplicationDisplay.getLeash(),
BACKGROUND_LAYER_INDEX + 1);
tx.apply(true);
}
@Override
public void onAnimationEnd(SurfaceControl.Transaction tx,
CarLauncherDisplayAreaAnimationController
.CarLauncherDisplayAreaTransitionAnimator animator) {
mIsDisplayAreaAnimating = false;
mAnimationController.removeAnimator(animator.getToken());
if (mAnimationController.isAnimatorsConsumed()) {
WindowContainerTransaction wct = new WindowContainerTransaction();
if (mToState == AppGridActivity.CAR_LAUNCHER_STATE.DEFAULT) {
// Foreground DA opens to default height.
updateBackgroundDisplayBounds(wct);
}
}
}
@Override
public void onAnimationCancel(
CarLauncherDisplayAreaAnimationController
.CarLauncherDisplayAreaTransitionAnimator animator) {
mIsDisplayAreaAnimating = false;
mAnimationController.removeAnimator(animator.getToken());
}
};
/**
* Gets the instance of {@link CarDisplayAreaOrganizer}.
*/
public static CarDisplayAreaOrganizer getInstance(Executor executor,
Context context, Intent mapsIntent, Intent audioControlIntent,
SyncTransactionQueue tx) {
if (sCarDisplayAreaOrganizer == null) {
sCarDisplayAreaOrganizer = new CarDisplayAreaOrganizer(executor,
context, mapsIntent, audioControlIntent, tx);
}
return sCarDisplayAreaOrganizer;
}
private CarDisplayAreaOrganizer(Executor executor, Context context, Intent mapsIntent,
Intent audioControlIntent, SyncTransactionQueue tx) {
super(executor);
mContext = context;
mMapsIntent = mapsIntent;
mAudioControlIntent = audioControlIntent;
mTransactionQueue = tx;
// TODO(b/201712747): Gets the Assistant Activity by resolving the indirect Intent.
mAssistantVoicePlateActivityName = ComponentName.unflattenFromString(
context.getResources().getString(R.string.config_assistantVoicePlateActivity));
mAnimationController = new CarLauncherDisplayAreaAnimationController(mContext);
mHandlerForAnimation = mContext.getMainThreadHandler();
Car.createCar(context, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
mCarServiceLifecycleListener);
}
private final Car.CarServiceLifecycleListener mCarServiceLifecycleListener =
new Car.CarServiceLifecycleListener() {
@Override
public void onLifecycleChanged(@NonNull Car car, boolean ready) {
if (ready) {
CarActivityManager carAm = (CarActivityManager) car.getCarManager(
Car.CAR_ACTIVITY_SERVICE);
setPersistentActivity(carAm, mMapsIntent.getComponent(),
BACKGROUND_TASK_CONTAINER, "Background");
// The following code will be enabled after FEATURE_VOICE_PLATE is landed.
//setPersistentActivity(carAm, mAssistantVoicePlateActivityName,
// FEATURE_VOICE_PLATE, "VoicePlate");
}
}
};
private static void setPersistentActivity(CarActivityManager am,
@Nullable ComponentName activity, int featureId, String featureName) {
if (activity == null) {
Log.e(TAG, "Empty activity for " + featureName + " (" + featureId + ")");
return;
}
int ret = am.setPersistentActivity(activity, DEFAULT_DISPLAY, featureId);
if (ret != CarActivityManager.RESULT_SUCCESS) {
Log.e(TAG, "Failed to set PersistentActivity: activity=" + activity
+ ", ret=" + ret);
return;
}
}
int getDpiDensity() {
if (mDpiDensity != -1) {
return mDpiDensity;
}
DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
Resources displayResources = mContext.createDisplayContext(display).getResources();
mDpiDensity = displayResources.getConfiguration().densityDpi;
return mDpiDensity;
}
boolean isDisplayAreaAnimating() {
return mIsDisplayAreaAnimating;
}
private void updateBackgroundDisplayBounds(WindowContainerTransaction wct) {
Rect backgroundApplicationDisplayBound = mBackgroundApplicationDisplayBounds;
WindowContainerToken backgroundDisplayToken =
mBackgroundApplicationDisplay.getDisplayAreaInfo().token;
int backgroundDisplayWidthDp =
backgroundApplicationDisplayBound.width() * DisplayMetrics.DENSITY_DEFAULT
/ getDpiDensity();
int backgroundDisplayHeightDp =
backgroundApplicationDisplayBound.height() * DisplayMetrics.DENSITY_DEFAULT
/ getDpiDensity();
wct.setBounds(backgroundDisplayToken, backgroundApplicationDisplayBound);
wct.setScreenSizeDp(backgroundDisplayToken, backgroundDisplayWidthDp,
backgroundDisplayHeightDp);
wct.setSmallestScreenWidthDp(backgroundDisplayToken,
Math.min(backgroundDisplayWidthDp, backgroundDisplayHeightDp));
mTransactionQueue.runInSync(t -> {
t.setPosition(mBackgroundApplicationDisplay.getLeash(),
backgroundApplicationDisplayBound.left,
backgroundApplicationDisplayBound.top);
});
applyTransaction(wct);
}
void resetWindowsOffset() {
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
mDisplayAreaTokenMap.forEach(
(token, leash) -> {
CarLauncherDisplayAreaAnimationController
.CarLauncherDisplayAreaTransitionAnimator animator =
mAnimationController.getAnimatorMap().remove(token);
if (animator != null && animator.isRunning()) {
animator.cancel();
}
tx.setPosition(leash, /* x= */ 0, /* y= */ 0)
.setWindowCrop(leash, /* width= */ -1, /* height= */ -1)
.setCornerRadius(leash, /* cornerRadius= */ -1);
});
tx.apply();
}
/**
* Offsets the windows by a given offset on Y-axis, triggered also from screen rotation.
* Directly perform manipulation/offset on the leash.
*/
void scheduleOffset(int fromPos, int toPos, Rect finalBackgroundBounds,
DisplayAreaAppearedInfo backgroundApplicationDisplay,
DisplayAreaAppearedInfo foregroundDisplay,
DisplayAreaAppearedInfo titleBarDisplay,
DisplayAreaAppearedInfo controlBarDisplay,
AppGridActivity.CAR_LAUNCHER_STATE toState,
int durationMs) {
mToState = toState;
mBackgroundApplicationDisplay = backgroundApplicationDisplay;
mForegroundApplicationDisplay = foregroundDisplay;
mControlBarDisplay = controlBarDisplay;
mTitleBarDisplay = titleBarDisplay;
mDisplayAreaTokenMap.forEach(
(token, leash) -> {
if (token == mBackgroundDisplayToken) {
mBackgroundApplicationDisplayBounds.set(finalBackgroundBounds);
} else if (token == mForegroundDisplayToken) {
animateWindows(token, leash, fromPos, toPos, durationMs);
}
});
if (mToState == AppGridActivity.CAR_LAUNCHER_STATE.CONTROL_BAR) {
WindowContainerTransaction wct = new WindowContainerTransaction();
updateBackgroundDisplayBounds(wct);
}
}
void animateWindows(WindowContainerToken token, SurfaceControl leash, float fromPos,
float toPos, int durationMs) {
CarLauncherDisplayAreaAnimationController.CarLauncherDisplayAreaTransitionAnimator
animator =
mAnimationController.getAnimator(token, leash, fromPos, toPos,
mLastVisualDisplayBounds);
if (animator != null) {
if (mDisplayAreaAnimationRunnable != null) {
mDisplayAreaAnimationRunnable.stopAnimation();
mHandlerForAnimation.removeCallbacks(mDisplayAreaAnimationRunnable);
}
mDisplayAreaAnimationRunnable = new DisplayAreaAnimationRunnable(animator, durationMs);
mHandlerForAnimation.post(mDisplayAreaAnimationRunnable);
}
}
/**
* A custom runnable with a flag to stop running the code within the {@link #run()} method when
* the runnable is in the message queue. In such cases calling
* {@link #removeCallbacksAndMessages(null)} won't work it only stops pending messages
* (Runnables) not currently running runnable.
*/
private class DisplayAreaAnimationRunnable implements Runnable {
private boolean mStopAnimation = false;
private final CarLauncherDisplayAreaAnimationController
.CarLauncherDisplayAreaTransitionAnimator mAnimator;
private final int mDurationMs;
DisplayAreaAnimationRunnable(
CarLauncherDisplayAreaAnimationController
.CarLauncherDisplayAreaTransitionAnimator animator,
int durationMs) {
mAnimator = animator;
mDurationMs = durationMs;
}
@Override
public void run() {
if (mStopAnimation) {
return;
}
mAnimator.addDisplayAreaAnimationCallback(mDisplayAreaAnimationCallback)
.setDuration(mDurationMs)
.start();
}
public void stopAnimation() {
// we don't call animator.cancel() here because if there is only one animation call
// such as just to open the DA then it will get canceled here.
mStopAnimation = true;
}
}
@Override
public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
@NonNull SurfaceControl leash) {
if (displayAreaInfo.featureId == BACKGROUND_TASK_CONTAINER) {
mBackgroundDisplayToken = displayAreaInfo.token;
mIsShowingBackgroundDisplay = true;
} else if (displayAreaInfo.featureId == CONTROL_BAR_DISPLAY_AREA) {
mControbarDisplayToken = displayAreaInfo.token;
mIsShowingControlBarDisplay = true;
} else if (displayAreaInfo.featureId == FOREGROUND_DISPLAY_AREA_ROOT) {
mForegroundDisplayToken = displayAreaInfo.token;
} else if (displayAreaInfo.featureId == FEATURE_TITLE_BAR) {
mTitleBarDisplayToken = displayAreaInfo.token;
}
mDisplayAreaTokenMap.put(displayAreaInfo.token, leash);
}
/**
* Launches the map in the background DA.
*/
public void startMapsInBackGroundDisplayArea() {
ActivityOptions options = ActivityOptions
.makeCustomAnimation(mContext,
/* enterResId= */ 0, /* exitResId= */ 0);
mContext.startActivity(mMapsIntent, options.toBundle());
}
/**
* Launches the control bar in the control bar DA.
*/
public void startControlBarInDisplayArea() {
ActivityOptions options = ActivityOptions
.makeCustomAnimation(mContext,
/* enterResId= */ 0, /* exitResId= */ 0);
options.setLaunchTaskDisplayArea(mControbarDisplayToken);
mContext.startActivity(mAudioControlIntent, options.toBundle());
}
@Override
public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) {
if (displayAreaInfo.featureId == BACKGROUND_TASK_CONTAINER) {
mIsShowingBackgroundDisplay = false;
} else if (displayAreaInfo.featureId == CONTROL_BAR_DISPLAY_AREA) {
mIsShowingControlBarDisplay = false;
}
if (!mIsRegistered) {
mDisplayAreaTokenMap.remove(displayAreaInfo.token);
}
}
@Override
public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) {
super.onDisplayAreaInfoChanged(displayAreaInfo);
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
tx.setLayer(mForegroundApplicationDisplay.getLeash(), FOREGROUND_LAYER_INDEX);
tx.apply(true);
}
@Override
public List<DisplayAreaAppearedInfo> registerOrganizer(int displayAreaFeature) {
List<DisplayAreaAppearedInfo> displayAreaInfos =
super.registerOrganizer(displayAreaFeature);
for (DisplayAreaAppearedInfo info : displayAreaInfos) {
onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash());
}
mIsRegistered = true;
return displayAreaInfos;
}
@Override
public void unregisterOrganizer() {
super.unregisterOrganizer();
mIsRegistered = false;
}
}