/* * 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 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(); } } }