1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car.pm; 17 18 import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME; 19 import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID; 20 import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO; 21 import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME; 22 23 import android.app.Activity; 24 import android.app.ActivityManager; 25 import android.app.ActivityTaskManager.RootTaskInfo; 26 import android.app.IActivityManager; 27 import android.car.Car; 28 import android.car.content.pm.CarPackageManager; 29 import android.car.drivingstate.CarUxRestrictions; 30 import android.car.drivingstate.CarUxRestrictionsManager; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.graphics.Insets; 35 import android.graphics.PixelFormat; 36 import android.graphics.Rect; 37 import android.hardware.display.DisplayManager; 38 import android.opengl.GLSurfaceView; 39 import android.os.Build; 40 import android.os.Bundle; 41 import android.os.RemoteException; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.util.Slog; 45 import android.view.Display; 46 import android.view.DisplayInfo; 47 import android.view.View; 48 import android.view.ViewTreeObserver; 49 import android.view.WindowInsets; 50 import android.widget.Button; 51 import android.widget.TextView; 52 53 import com.android.car.CarLog; 54 import com.android.car.R; 55 import com.android.car.pm.blurredbackground.BlurredSurfaceRenderer; 56 57 import java.util.List; 58 59 /** 60 * Default activity that will be launched when the current foreground activity is not allowed. 61 * Additional information on blocked Activity should be passed as intent extras. 62 */ 63 public class ActivityBlockingActivity extends Activity { 64 private static final int EGL_CONTEXT_VERSION = 3; 65 private static final int EGL_CONFIG_SIZE = 8; 66 private static final int INVALID_TASK_ID = -1; 67 private final Object mLock = new Object(); 68 69 private GLSurfaceView mGLSurfaceView; 70 private BlurredSurfaceRenderer mSurfaceRenderer; 71 private boolean mIsGLSurfaceSetup = false; 72 73 private Car mCar; 74 private CarUxRestrictionsManager mUxRManager; 75 private CarPackageManager mCarPackageManager; 76 77 private Button mExitButton; 78 private Button mToggleDebug; 79 80 private int mBlockedTaskId; 81 private IActivityManager mAm; 82 83 private final View.OnClickListener mOnExitButtonClickedListener = 84 v -> { 85 if (isExitOptionCloseApplication()) { 86 handleCloseApplication(); 87 } else { 88 handleRestartingTask(); 89 } 90 }; 91 92 private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = 93 new ViewTreeObserver.OnGlobalLayoutListener() { 94 @Override 95 public void onGlobalLayout() { 96 mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener(this); 97 updateButtonWidths(); 98 } 99 }; 100 101 @Override onCreate(Bundle savedInstanceState)102 protected void onCreate(Bundle savedInstanceState) { 103 super.onCreate(savedInstanceState); 104 setContentView(R.layout.activity_blocking); 105 106 mExitButton = findViewById(R.id.exit_button); 107 mAm = ActivityManager.getService(); 108 109 // Listen to the CarUxRestrictions so this blocking activity can be dismissed when the 110 // restrictions are lifted. 111 // This Activity should be launched only after car service is initialized. Currently this 112 // Activity is only launched from CPMS. So this is safe to do. 113 mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 114 (car, ready) -> { 115 if (!ready) { 116 return; 117 } 118 mCarPackageManager = (CarPackageManager) car.getCarManager( 119 Car.PACKAGE_SERVICE); 120 mUxRManager = (CarUxRestrictionsManager) car.getCarManager( 121 Car.CAR_UX_RESTRICTION_SERVICE); 122 // This activity would have been launched only in a restricted state. 123 // But ensuring when the service connection is established, that we are still 124 // in a restricted state. 125 handleUxRChange(mUxRManager.getCurrentCarUxRestrictions()); 126 mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange); 127 }); 128 129 setupGLSurface(); 130 } 131 132 @Override onStart()133 protected void onStart() { 134 super.onStart(); 135 if (mIsGLSurfaceSetup) { 136 mGLSurfaceView.onResume(); 137 } 138 } 139 140 @Override onResume()141 protected void onResume() { 142 super.onResume(); 143 144 // Display info about the current blocked activity, and optionally show an exit button 145 // to restart the blocked task (stack of activities) if its root activity is DO. 146 mBlockedTaskId = getIntent().getIntExtra(BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID, 147 INVALID_TASK_ID); 148 149 // blockedActivity is expected to be always passed in as the topmost activity of task. 150 String blockedActivity = getIntent().getStringExtra( 151 BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 152 if (!TextUtils.isEmpty(blockedActivity)) { 153 if (isTopActivityBehindAbaDistractionOptimized()) { 154 Slog.e(CarLog.TAG_AM, "Top activity is already DO, so finishing"); 155 finish(); 156 return; 157 } 158 159 if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) { 160 Slog.d(CarLog.TAG_AM, "Blocking activity " + blockedActivity); 161 } 162 } 163 164 displayExitButton(); 165 166 // Show more debug info for non-user build. 167 if (Build.IS_ENG || Build.IS_USERDEBUG) { 168 displayDebugInfo(); 169 } 170 } 171 172 @Override onStop()173 protected void onStop() { 174 super.onStop(); 175 176 if (mIsGLSurfaceSetup) { 177 // We queue this event so that it runs on the Rendering thread 178 mGLSurfaceView.queueEvent(() -> mSurfaceRenderer.onPause()); 179 180 mGLSurfaceView.onPause(); 181 } 182 183 // Finish when blocking activity goes invisible to avoid it accidentally re-surfaces with 184 // stale string regarding blocked activity. 185 finish(); 186 } 187 setupGLSurface()188 private void setupGLSurface() { 189 DisplayManager displayManager = (DisplayManager) getApplicationContext().getSystemService( 190 Context.DISPLAY_SERVICE); 191 DisplayInfo displayInfo = new DisplayInfo(); 192 193 int displayId = getDisplayId(); 194 displayManager.getDisplay(displayId).getDisplayInfo(displayInfo); 195 196 Rect windowRect = getAppWindowRect(); 197 198 // We currently don't support blur for secondary display 199 // (because it is hard to take a screenshot of a secondary display) 200 // So for secondary displays, the GLSurfaceView will not appear blurred 201 boolean shouldRenderBlurred = getDisplayId() == Display.DEFAULT_DISPLAY; 202 203 mSurfaceRenderer = new BlurredSurfaceRenderer(this, windowRect, shouldRenderBlurred); 204 205 mGLSurfaceView = findViewById(R.id.blurred_surface_view); 206 mGLSurfaceView.setEGLContextClientVersion(EGL_CONTEXT_VERSION); 207 208 // Sets up the surface so that we can make it translucent if needed 209 mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); 210 mGLSurfaceView.setEGLConfigChooser(EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, 211 EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE); 212 213 mGLSurfaceView.setRenderer(mSurfaceRenderer); 214 215 // We only want to render the screen once 216 mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 217 218 mIsGLSurfaceSetup = true; 219 } 220 221 /** 222 * Computes a Rect that represents the portion of the screen that 223 * contains the activity that is being blocked. 224 * 225 * @return Rect that represents the application window 226 */ getAppWindowRect()227 private Rect getAppWindowRect() { 228 Insets systemBarInsets = getWindowManager() 229 .getCurrentWindowMetrics() 230 .getWindowInsets() 231 .getInsets(WindowInsets.Type.systemBars()); 232 233 Rect displayBounds = getWindowManager().getCurrentWindowMetrics().getBounds(); 234 235 int leftX = systemBarInsets.left; 236 int rightX = displayBounds.width() - systemBarInsets.right; 237 int topY = systemBarInsets.top; 238 int bottomY = displayBounds.height() - systemBarInsets.bottom; 239 240 return new Rect(leftX, topY, rightX, bottomY); 241 } 242 displayExitButton()243 private void displayExitButton() { 244 String exitButtonText = getExitButtonText(); 245 246 mExitButton.setText(exitButtonText); 247 mExitButton.setOnClickListener(mOnExitButtonClickedListener); 248 } 249 250 // If the root activity is DO, the user will have the option to go back to that activity, 251 // otherwise, the user will have the option to close the blocked application isExitOptionCloseApplication()252 private boolean isExitOptionCloseApplication() { 253 boolean isRootDO = getIntent().getBooleanExtra( 254 BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO, false); 255 return mBlockedTaskId == INVALID_TASK_ID || !isRootDO; 256 } 257 getExitButtonText()258 private String getExitButtonText() { 259 return isExitOptionCloseApplication() ? getString(R.string.exit_button_close_application) 260 : getString(R.string.exit_button_go_back); 261 } 262 263 /** 264 * It is possible that the stack info has changed between when the intent to launch this 265 * activity was initiated and when this activity is started. Check whether the activity behind 266 * the ABA is distraction optimized. 267 */ isTopActivityBehindAbaDistractionOptimized()268 private boolean isTopActivityBehindAbaDistractionOptimized() { 269 List<RootTaskInfo> taskInfos; 270 try { 271 taskInfos = mAm.getAllRootTaskInfos(); 272 } catch (RemoteException e) { 273 Slog.e(CarLog.TAG_AM, "Unable to get stack info from ActivityManager"); 274 // assume that the state is still correct, the activity behind is not DO 275 return false; 276 } 277 278 RootTaskInfo topStackBehindAba = null; 279 for (RootTaskInfo taskInfo : taskInfos) { 280 if (taskInfo.displayId != getDisplayId()) { 281 // ignore stacks on other displays 282 continue; 283 } 284 285 if (getComponentName().equals(taskInfo.topActivity)) { 286 // ignore stack with the blocking activity 287 continue; 288 } 289 290 if (!taskInfo.visible) { 291 // ignore stacks that aren't visible 292 continue; 293 } 294 295 if (topStackBehindAba == null || topStackBehindAba.position < taskInfo.position) { 296 topStackBehindAba = taskInfo; 297 } 298 } 299 300 if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) { 301 Slog.d(CarLog.TAG_AM, String.format("Top stack behind ABA is: %s", topStackBehindAba)); 302 } 303 304 if (topStackBehindAba != null && topStackBehindAba.topActivity != null) { 305 boolean isDo = mCarPackageManager.isActivityDistractionOptimized( 306 topStackBehindAba.topActivity.getPackageName(), 307 topStackBehindAba.topActivity.getClassName()); 308 if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) { 309 Slog.d(CarLog.TAG_AM, 310 String.format("Top activity (%s) is DO: %s", topStackBehindAba.topActivity, 311 isDo)); 312 } 313 return isDo; 314 } 315 316 // unknown top stack / activity, default to considering it non-DO 317 return false; 318 } 319 displayDebugInfo()320 private void displayDebugInfo() { 321 String blockedActivity = getIntent().getStringExtra( 322 BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 323 String rootActivity = getIntent().getStringExtra(BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME); 324 325 TextView debugInfo = findViewById(R.id.debug_info); 326 debugInfo.setText(getDebugInfo(blockedActivity, rootActivity)); 327 328 // We still want to ensure driving safety for non-user build; 329 // toggle visibility of debug info with this button. 330 mToggleDebug = findViewById(R.id.toggle_debug_info); 331 mToggleDebug.setVisibility(View.VISIBLE); 332 mToggleDebug.setOnClickListener(v -> { 333 boolean isDebugVisible = debugInfo.getVisibility() == View.VISIBLE; 334 debugInfo.setVisibility(isDebugVisible ? View.GONE : View.VISIBLE); 335 }); 336 337 mToggleDebug.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); 338 } 339 340 // When the Debug button is visible, we set both of the visible buttons to have the width 341 // of whichever button is wider updateButtonWidths()342 private void updateButtonWidths() { 343 Button debugButton = findViewById(R.id.toggle_debug_info); 344 345 int exitButtonWidth = mExitButton.getWidth(); 346 int debugButtonWidth = debugButton.getWidth(); 347 348 if (exitButtonWidth > debugButtonWidth) { 349 debugButton.setWidth(exitButtonWidth); 350 } else { 351 mExitButton.setWidth(debugButtonWidth); 352 } 353 } 354 getDebugInfo(String blockedActivity, String rootActivity)355 private String getDebugInfo(String blockedActivity, String rootActivity) { 356 StringBuilder debug = new StringBuilder(); 357 358 ComponentName blocked = ComponentName.unflattenFromString(blockedActivity); 359 debug.append("Blocked activity is ") 360 .append(blocked.getShortClassName()) 361 .append("\nBlocked activity package is ") 362 .append(blocked.getPackageName()); 363 364 if (rootActivity != null) { 365 ComponentName root = ComponentName.unflattenFromString(rootActivity); 366 // Optionally show root activity info if it differs from the blocked activity. 367 if (!root.equals(blocked)) { 368 debug.append("\n\nRoot activity is ").append(root.getShortClassName()); 369 } 370 if (!root.getPackageName().equals(blocked.getPackageName())) { 371 debug.append("\nRoot activity package is ").append(root.getPackageName()); 372 } 373 } 374 return debug.toString(); 375 } 376 377 @Override onNewIntent(Intent intent)378 protected void onNewIntent(Intent intent) { 379 super.onNewIntent(intent); 380 setIntent(intent); 381 } 382 383 @Override onDestroy()384 protected void onDestroy() { 385 super.onDestroy(); 386 mCar.disconnect(); 387 mUxRManager.unregisterListener(); 388 if (mToggleDebug != null) { 389 mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener( 390 mOnGlobalLayoutListener); 391 } 392 mCar.disconnect(); 393 } 394 395 // If no distraction optimization is required in the new restrictions, then dismiss the 396 // blocking activity (self). handleUxRChange(CarUxRestrictions restrictions)397 private void handleUxRChange(CarUxRestrictions restrictions) { 398 if (restrictions == null) { 399 return; 400 } 401 if (!restrictions.isRequiresDistractionOptimization()) { 402 finish(); 403 } 404 } 405 handleCloseApplication()406 private void handleCloseApplication() { 407 if (isFinishing()) { 408 return; 409 } 410 411 Intent startMain = new Intent(Intent.ACTION_MAIN); 412 startMain.addCategory(Intent.CATEGORY_HOME); 413 startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 414 startActivity(startMain); 415 finish(); 416 } 417 handleRestartingTask()418 private void handleRestartingTask() { 419 // Lock on self to avoid restarting the same task twice. 420 synchronized (mLock) { 421 if (isFinishing()) { 422 return; 423 } 424 425 if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) { 426 Slog.i(CarLog.TAG_AM, "Restarting task " + mBlockedTaskId); 427 } 428 mCarPackageManager.restartTask(mBlockedTaskId); 429 finish(); 430 } 431 } 432 } 433