blob: 5c687a4083d9618bc686109e4551d62157b1e4fd [file] [log] [blame]
/*
* Copyright (C) 2016 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.pm;
import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME;
import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID;
import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO;
import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.app.IActivityManager;
import android.car.Car;
import android.car.content.pm.CarPackageManager;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.TextView;
import com.android.car.CarLog;
import com.android.car.R;
import com.android.car.pm.blurredbackground.BlurredSurfaceRenderer;
import java.util.List;
/**
* Default activity that will be launched when the current foreground activity is not allowed.
* Additional information on blocked Activity should be passed as intent extras.
*/
public class ActivityBlockingActivity extends Activity {
private static final int EGL_CONTEXT_VERSION = 3;
private static final int EGL_CONFIG_SIZE = 8;
private static final int INVALID_TASK_ID = -1;
private final Object mLock = new Object();
private GLSurfaceView mGLSurfaceView;
private BlurredSurfaceRenderer mSurfaceRenderer;
private boolean mIsGLSurfaceSetup = false;
private Car mCar;
private CarUxRestrictionsManager mUxRManager;
private CarPackageManager mCarPackageManager;
private Button mExitButton;
private Button mToggleDebug;
private int mBlockedTaskId;
private IActivityManager mAm;
private final View.OnClickListener mOnExitButtonClickedListener =
v -> {
if (isExitOptionCloseApplication()) {
handleCloseApplication();
} else {
handleRestartingTask();
}
};
private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener(this);
updateButtonWidths();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_blocking);
mExitButton = findViewById(R.id.exit_button);
mAm = ActivityManager.getService();
// Listen to the CarUxRestrictions so this blocking activity can be dismissed when the
// restrictions are lifted.
// This Activity should be launched only after car service is initialized. Currently this
// Activity is only launched from CPMS. So this is safe to do.
mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
(car, ready) -> {
if (!ready) {
return;
}
mCarPackageManager = (CarPackageManager) car.getCarManager(
Car.PACKAGE_SERVICE);
mUxRManager = (CarUxRestrictionsManager) car.getCarManager(
Car.CAR_UX_RESTRICTION_SERVICE);
// This activity would have been launched only in a restricted state.
// But ensuring when the service connection is established, that we are still
// in a restricted state.
handleUxRChange(mUxRManager.getCurrentCarUxRestrictions());
mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange);
});
setupGLSurface();
}
@Override
protected void onStart() {
super.onStart();
if (mIsGLSurfaceSetup) {
mGLSurfaceView.onResume();
}
}
@Override
protected void onResume() {
super.onResume();
// Display info about the current blocked activity, and optionally show an exit button
// to restart the blocked task (stack of activities) if its root activity is DO.
mBlockedTaskId = getIntent().getIntExtra(BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID,
INVALID_TASK_ID);
// blockedActivity is expected to be always passed in as the topmost activity of task.
String blockedActivity = getIntent().getStringExtra(
BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME);
if (!TextUtils.isEmpty(blockedActivity)) {
if (isTopActivityBehindAbaDistractionOptimized()) {
Slog.e(CarLog.TAG_AM, "Top activity is already DO, so finishing");
finish();
return;
}
if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
Slog.d(CarLog.TAG_AM, "Blocking activity " + blockedActivity);
}
}
displayExitButton();
// Show more debug info for non-user build.
if (Build.IS_ENG || Build.IS_USERDEBUG) {
displayDebugInfo();
}
}
@Override
protected void onStop() {
super.onStop();
if (mIsGLSurfaceSetup) {
// We queue this event so that it runs on the Rendering thread
mGLSurfaceView.queueEvent(() -> mSurfaceRenderer.onPause());
mGLSurfaceView.onPause();
}
// Finish when blocking activity goes invisible to avoid it accidentally re-surfaces with
// stale string regarding blocked activity.
finish();
}
private void setupGLSurface() {
DisplayManager displayManager = (DisplayManager) getApplicationContext().getSystemService(
Context.DISPLAY_SERVICE);
DisplayInfo displayInfo = new DisplayInfo();
int displayId = getDisplayId();
displayManager.getDisplay(displayId).getDisplayInfo(displayInfo);
Rect windowRect = getAppWindowRect();
// We currently don't support blur for secondary display
// (because it is hard to take a screenshot of a secondary display)
// So for secondary displays, the GLSurfaceView will not appear blurred
boolean shouldRenderBlurred = getDisplayId() == Display.DEFAULT_DISPLAY;
mSurfaceRenderer = new BlurredSurfaceRenderer(this, windowRect, shouldRenderBlurred);
mGLSurfaceView = findViewById(R.id.blurred_surface_view);
mGLSurfaceView.setEGLContextClientVersion(EGL_CONTEXT_VERSION);
// Sets up the surface so that we can make it translucent if needed
mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
mGLSurfaceView.setEGLConfigChooser(EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE,
EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE);
mGLSurfaceView.setRenderer(mSurfaceRenderer);
// We only want to render the screen once
mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
mIsGLSurfaceSetup = true;
}
/**
* Computes a Rect that represents the portion of the screen that
* contains the activity that is being blocked.
*
* @return Rect that represents the application window
*/
private Rect getAppWindowRect() {
Insets systemBarInsets = getWindowManager()
.getCurrentWindowMetrics()
.getWindowInsets()
.getInsets(WindowInsets.Type.systemBars());
Rect displayBounds = getWindowManager().getCurrentWindowMetrics().getBounds();
int leftX = systemBarInsets.left;
int rightX = displayBounds.width() - systemBarInsets.right;
int topY = systemBarInsets.top;
int bottomY = displayBounds.height() - systemBarInsets.bottom;
return new Rect(leftX, topY, rightX, bottomY);
}
private void displayExitButton() {
String exitButtonText = getExitButtonText();
mExitButton.setText(exitButtonText);
mExitButton.setOnClickListener(mOnExitButtonClickedListener);
}
// If the root activity is DO, the user will have the option to go back to that activity,
// otherwise, the user will have the option to close the blocked application
private boolean isExitOptionCloseApplication() {
boolean isRootDO = getIntent().getBooleanExtra(
BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO, false);
return mBlockedTaskId == INVALID_TASK_ID || !isRootDO;
}
private String getExitButtonText() {
return isExitOptionCloseApplication() ? getString(R.string.exit_button_close_application)
: getString(R.string.exit_button_go_back);
}
/**
* It is possible that the stack info has changed between when the intent to launch this
* activity was initiated and when this activity is started. Check whether the activity behind
* the ABA is distraction optimized.
*/
private boolean isTopActivityBehindAbaDistractionOptimized() {
List<RootTaskInfo> taskInfos;
try {
taskInfos = mAm.getAllRootTaskInfos();
} catch (RemoteException e) {
Slog.e(CarLog.TAG_AM, "Unable to get stack info from ActivityManager");
// assume that the state is still correct, the activity behind is not DO
return false;
}
RootTaskInfo topStackBehindAba = null;
for (RootTaskInfo taskInfo : taskInfos) {
if (taskInfo.displayId != getDisplayId()) {
// ignore stacks on other displays
continue;
}
if (getComponentName().equals(taskInfo.topActivity)) {
// ignore stack with the blocking activity
continue;
}
if (!taskInfo.visible) {
// ignore stacks that aren't visible
continue;
}
if (topStackBehindAba == null || topStackBehindAba.position < taskInfo.position) {
topStackBehindAba = taskInfo;
}
}
if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
Slog.d(CarLog.TAG_AM, String.format("Top stack behind ABA is: %s", topStackBehindAba));
}
if (topStackBehindAba != null && topStackBehindAba.topActivity != null) {
boolean isDo = mCarPackageManager.isActivityDistractionOptimized(
topStackBehindAba.topActivity.getPackageName(),
topStackBehindAba.topActivity.getClassName());
if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
Slog.d(CarLog.TAG_AM,
String.format("Top activity (%s) is DO: %s", topStackBehindAba.topActivity,
isDo));
}
return isDo;
}
// unknown top stack / activity, default to considering it non-DO
return false;
}
private void displayDebugInfo() {
String blockedActivity = getIntent().getStringExtra(
BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME);
String rootActivity = getIntent().getStringExtra(BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME);
TextView debugInfo = findViewById(R.id.debug_info);
debugInfo.setText(getDebugInfo(blockedActivity, rootActivity));
// We still want to ensure driving safety for non-user build;
// toggle visibility of debug info with this button.
mToggleDebug = findViewById(R.id.toggle_debug_info);
mToggleDebug.setVisibility(View.VISIBLE);
mToggleDebug.setOnClickListener(v -> {
boolean isDebugVisible = debugInfo.getVisibility() == View.VISIBLE;
debugInfo.setVisibility(isDebugVisible ? View.GONE : View.VISIBLE);
});
mToggleDebug.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
}
// When the Debug button is visible, we set both of the visible buttons to have the width
// of whichever button is wider
private void updateButtonWidths() {
Button debugButton = findViewById(R.id.toggle_debug_info);
int exitButtonWidth = mExitButton.getWidth();
int debugButtonWidth = debugButton.getWidth();
if (exitButtonWidth > debugButtonWidth) {
debugButton.setWidth(exitButtonWidth);
} else {
mExitButton.setWidth(debugButtonWidth);
}
}
private String getDebugInfo(String blockedActivity, String rootActivity) {
StringBuilder debug = new StringBuilder();
ComponentName blocked = ComponentName.unflattenFromString(blockedActivity);
debug.append("Blocked activity is ")
.append(blocked.getShortClassName())
.append("\nBlocked activity package is ")
.append(blocked.getPackageName());
if (rootActivity != null) {
ComponentName root = ComponentName.unflattenFromString(rootActivity);
// Optionally show root activity info if it differs from the blocked activity.
if (!root.equals(blocked)) {
debug.append("\n\nRoot activity is ").append(root.getShortClassName());
}
if (!root.getPackageName().equals(blocked.getPackageName())) {
debug.append("\nRoot activity package is ").append(root.getPackageName());
}
}
return debug.toString();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
mCar.disconnect();
mUxRManager.unregisterListener();
if (mToggleDebug != null) {
mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener(
mOnGlobalLayoutListener);
}
mCar.disconnect();
}
// If no distraction optimization is required in the new restrictions, then dismiss the
// blocking activity (self).
private void handleUxRChange(CarUxRestrictions restrictions) {
if (restrictions == null) {
return;
}
if (!restrictions.isRequiresDistractionOptimization()) {
finish();
}
}
private void handleCloseApplication() {
if (isFinishing()) {
return;
}
Intent startMain = new Intent(Intent.ACTION_MAIN);
startMain.addCategory(Intent.CATEGORY_HOME);
startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(startMain);
finish();
}
private void handleRestartingTask() {
// Lock on self to avoid restarting the same task twice.
synchronized (mLock) {
if (isFinishing()) {
return;
}
if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) {
Slog.i(CarLog.TAG_AM, "Restarting task " + mBlockedTaskId);
}
mCarPackageManager.restartTask(mBlockedTaskId);
finish();
}
}
}