1 /* 2 * Copyright (C) 2021 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.systemui.car.activity; 17 18 import android.app.Activity; 19 import android.app.ActivityManager; 20 import android.car.Car; 21 import android.car.CarOccupantZoneManager; 22 import android.car.app.CarActivityManager; 23 import android.car.content.pm.CarPackageManager; 24 import android.car.drivingstate.CarUxRestrictions; 25 import android.car.drivingstate.CarUxRestrictionsManager; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.graphics.Insets; 30 import android.graphics.PixelFormat; 31 import android.graphics.Rect; 32 import android.hardware.display.DisplayManager; 33 import android.opengl.GLSurfaceView; 34 import android.os.Build; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.UserHandle; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.util.Slog; 41 import android.view.DisplayInfo; 42 import android.view.View; 43 import android.view.ViewTreeObserver; 44 import android.view.WindowInsets; 45 import android.widget.Button; 46 import android.widget.TextView; 47 48 import com.android.systemui.R; 49 import com.android.systemui.car.activity.blurredbackground.BlurredSurfaceRenderer; 50 51 import java.util.List; 52 53 /** 54 * Default activity that will be launched when the current foreground activity is not allowed. 55 * Additional information on blocked Activity should be passed as intent extras. 56 */ 57 public class ActivityBlockingActivity extends Activity { 58 private static final int ACTIVITY_MONITORING_DELAY_MS = 1000; 59 private static final String TAG = "BlockingActivity"; 60 private static final int EGL_CONTEXT_VERSION = 2; 61 private static final int EGL_CONFIG_SIZE = 8; 62 private static final int INVALID_TASK_ID = -1; 63 private final Object mLock = new Object(); 64 65 private GLSurfaceView mGLSurfaceView; 66 private BlurredSurfaceRenderer mSurfaceRenderer; 67 private boolean mIsGLSurfaceSetup = false; 68 69 private Car mCar; 70 private CarUxRestrictionsManager mUxRManager; 71 private CarPackageManager mCarPackageManager; 72 private CarActivityManager mCarActivityManager; 73 private CarOccupantZoneManager mCarOccupantZoneManager; 74 75 private Button mExitButton; 76 private Button mToggleDebug; 77 78 private int mBlockedTaskId; 79 private final Handler mHandler = new Handler(); 80 81 private final View.OnClickListener mOnExitButtonClickedListener = 82 v -> { 83 if (isExitOptionCloseApplication()) { 84 handleCloseApplication(); 85 } else { 86 handleRestartingTask(); 87 } 88 }; 89 90 private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = 91 new ViewTreeObserver.OnGlobalLayoutListener() { 92 @Override 93 public void onGlobalLayout() { 94 mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener(this); 95 updateButtonWidths(); 96 } 97 }; 98 99 @Override onCreate(Bundle savedInstanceState)100 protected void onCreate(Bundle savedInstanceState) { 101 super.onCreate(savedInstanceState); 102 setContentView(R.layout.activity_blocking); 103 104 mExitButton = findViewById(R.id.exit_button); 105 106 // Listen to the CarUxRestrictions so this blocking activity can be dismissed when the 107 // restrictions are lifted. 108 // This Activity should be launched only after car service is initialized. Currently this 109 // Activity is only launched from CPMS. So this is safe to do. 110 mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 111 (car, ready) -> { 112 if (!ready) { 113 return; 114 } 115 mCarPackageManager = (CarPackageManager) car.getCarManager( 116 Car.PACKAGE_SERVICE); 117 mCarActivityManager = (CarActivityManager) car.getCarManager( 118 Car.CAR_ACTIVITY_SERVICE); 119 mUxRManager = (CarUxRestrictionsManager) car.getCarManager( 120 Car.CAR_UX_RESTRICTION_SERVICE); 121 mCarOccupantZoneManager = car.getCarManager(CarOccupantZoneManager.class); 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( 147 CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID, 148 INVALID_TASK_ID); 149 150 // blockedActivity is expected to be always passed in as the topmost activity of task. 151 String blockedActivity = getIntent().getStringExtra( 152 CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 153 if (!TextUtils.isEmpty(blockedActivity)) { 154 boolean finished = finishIfActivitiesAreDistractionOptimised(); 155 if (finished) { 156 return; 157 } 158 159 if (Log.isLoggable(TAG, Log.DEBUG)) { 160 Slog.d(TAG, "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 finishIfActivitiesAreDistractionOptimised()188 private boolean finishIfActivitiesAreDistractionOptimised() { 189 if (areAllVisibleActivitiesDistractionOptimised()) { 190 Slog.i(TAG, "All visible activities are already DO, so finishing"); 191 finish(); 192 return true; 193 } 194 mHandler.postDelayed(() -> finishIfActivitiesAreDistractionOptimised(), 195 ACTIVITY_MONITORING_DELAY_MS); 196 return false; 197 } 198 setupGLSurface()199 private void setupGLSurface() { 200 DisplayManager displayManager = (DisplayManager) getApplicationContext().getSystemService( 201 Context.DISPLAY_SERVICE); 202 DisplayInfo displayInfo = new DisplayInfo(); 203 204 int displayId = getDisplayId(); 205 displayManager.getDisplay(displayId).getDisplayInfo(displayInfo); 206 207 Rect windowRect = getAppWindowRect(); 208 209 mSurfaceRenderer = new BlurredSurfaceRenderer(this, windowRect, getDisplayId()); 210 211 mGLSurfaceView = findViewById(R.id.blurred_surface_view); 212 mGLSurfaceView.setEGLContextClientVersion(EGL_CONTEXT_VERSION); 213 214 // Sets up the surface so that we can make it translucent if needed 215 mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); 216 mGLSurfaceView.setEGLConfigChooser(EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, 217 EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE); 218 219 mGLSurfaceView.setRenderer(mSurfaceRenderer); 220 221 // We only want to render the screen once 222 mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 223 224 mIsGLSurfaceSetup = true; 225 } 226 227 /** 228 * Computes a Rect that represents the portion of the screen that contains the activity that is 229 * being blocked. 230 * 231 * @return Rect that represents the application window 232 */ getAppWindowRect()233 private Rect getAppWindowRect() { 234 Insets systemBarInsets = getWindowManager() 235 .getCurrentWindowMetrics() 236 .getWindowInsets() 237 .getInsets(WindowInsets.Type.systemBars()); 238 239 Rect displayBounds = getWindowManager().getCurrentWindowMetrics().getBounds(); 240 241 int leftX = systemBarInsets.left; 242 int rightX = displayBounds.width() - systemBarInsets.right; 243 int topY = systemBarInsets.top; 244 int bottomY = displayBounds.height() - systemBarInsets.bottom; 245 246 return new Rect(leftX, topY, rightX, bottomY); 247 } 248 displayExitButton()249 private void displayExitButton() { 250 String exitButtonText = getExitButtonText(); 251 252 mExitButton.setText(exitButtonText); 253 mExitButton.setOnClickListener(mOnExitButtonClickedListener); 254 } 255 256 // If the root activity is DO, the user will have the option to go back to that activity, 257 // otherwise, the user will have the option to close the blocked application isExitOptionCloseApplication()258 private boolean isExitOptionCloseApplication() { 259 boolean isRootDO = getIntent().getBooleanExtra( 260 CarPackageManager.BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO, false); 261 return mBlockedTaskId == INVALID_TASK_ID || !isRootDO; 262 } 263 getExitButtonText()264 private String getExitButtonText() { 265 return isExitOptionCloseApplication() ? getString(R.string.exit_button_close_application) 266 : getString(R.string.exit_button_go_back); 267 } 268 269 /** 270 * It is possible that the stack info has changed between when the intent to launch this 271 * activity was initiated and when this activity is started. Check whether all the visible 272 * activities are distraction optimized. 273 */ areAllVisibleActivitiesDistractionOptimised()274 private boolean areAllVisibleActivitiesDistractionOptimised() { 275 List<ActivityManager.RunningTaskInfo> visibleTasks; 276 visibleTasks = mCarActivityManager.getVisibleTasks(); 277 for (int i = visibleTasks.size() - 1; i >= 0; i--) { 278 ActivityManager.RunningTaskInfo taskInfo = visibleTasks.get(i); 279 if (taskInfo.displayId != getDisplayId()) { 280 // ignore stacks on other displays 281 continue; 282 } 283 284 if (getComponentName().equals(taskInfo.topActivity)) { 285 // skip the ActivityBlockingActivity itself 286 continue; 287 } 288 289 if (taskInfo.topActivity != null) { 290 boolean isDo = mCarPackageManager.isActivityDistractionOptimized( 291 taskInfo.topActivity.getPackageName(), 292 taskInfo.topActivity.getClassName()); 293 if (Log.isLoggable(TAG, Log.DEBUG)) { 294 Slog.d(TAG, 295 String.format("Activity (%s) is DO: %s", taskInfo.topActivity, isDo)); 296 } 297 if (!isDo) { 298 return false; 299 } 300 } 301 } 302 303 // No visible non-DO activity found. 304 return true; 305 } 306 displayDebugInfo()307 private void displayDebugInfo() { 308 String blockedActivity = getIntent().getStringExtra( 309 CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 310 String rootActivity = getIntent().getStringExtra( 311 CarPackageManager.BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME); 312 313 TextView debugInfo = findViewById(R.id.debug_info); 314 debugInfo.setText(getDebugInfo(blockedActivity, rootActivity)); 315 316 // We still want to ensure driving safety for non-user build; 317 // toggle visibility of debug info with this button. 318 mToggleDebug = findViewById(R.id.toggle_debug_info); 319 mToggleDebug.setVisibility(View.VISIBLE); 320 mToggleDebug.setOnClickListener(v -> { 321 boolean isDebugVisible = debugInfo.getVisibility() == View.VISIBLE; 322 debugInfo.setVisibility(isDebugVisible ? View.GONE : View.VISIBLE); 323 }); 324 325 mToggleDebug.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); 326 } 327 328 // When the Debug button is visible, we set both of the visible buttons to have the width 329 // of whichever button is wider updateButtonWidths()330 private void updateButtonWidths() { 331 Button debugButton = findViewById(R.id.toggle_debug_info); 332 333 int exitButtonWidth = mExitButton.getWidth(); 334 int debugButtonWidth = debugButton.getWidth(); 335 336 if (exitButtonWidth > debugButtonWidth) { 337 debugButton.setWidth(exitButtonWidth); 338 } else { 339 mExitButton.setWidth(debugButtonWidth); 340 } 341 } 342 getDebugInfo(String blockedActivity, String rootActivity)343 private String getDebugInfo(String blockedActivity, String rootActivity) { 344 StringBuilder debug = new StringBuilder(); 345 346 ComponentName blocked = ComponentName.unflattenFromString(blockedActivity); 347 debug.append("Blocked activity is ") 348 .append(blocked.getShortClassName()) 349 .append("\nBlocked activity package is ") 350 .append(blocked.getPackageName()); 351 352 if (rootActivity != null) { 353 ComponentName root = ComponentName.unflattenFromString(rootActivity); 354 // Optionally show root activity info if it differs from the blocked activity. 355 if (!root.equals(blocked)) { 356 debug.append("\n\nRoot activity is ").append(root.getShortClassName()); 357 } 358 if (!root.getPackageName().equals(blocked.getPackageName())) { 359 debug.append("\nRoot activity package is ").append(root.getPackageName()); 360 } 361 } 362 return debug.toString(); 363 } 364 365 @Override onNewIntent(Intent intent)366 protected void onNewIntent(Intent intent) { 367 super.onNewIntent(intent); 368 setIntent(intent); 369 } 370 371 @Override onDestroy()372 protected void onDestroy() { 373 super.onDestroy(); 374 mCar.disconnect(); 375 mUxRManager.unregisterListener(); 376 if (mToggleDebug != null) { 377 mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener( 378 mOnGlobalLayoutListener); 379 } 380 mHandler.removeCallbacksAndMessages(null); 381 mCar.disconnect(); 382 } 383 384 // If no distraction optimization is required in the new restrictions, then dismiss the 385 // blocking activity (self). handleUxRChange(CarUxRestrictions restrictions)386 private void handleUxRChange(CarUxRestrictions restrictions) { 387 if (restrictions == null) { 388 return; 389 } 390 if (!restrictions.isRequiresDistractionOptimization()) { 391 finish(); 392 } 393 } 394 handleCloseApplication()395 private void handleCloseApplication() { 396 if (isFinishing()) { 397 return; 398 } 399 400 int displayId = getDisplayId(); 401 int userOnDisplay = mCarOccupantZoneManager.getUserForDisplayId(displayId); 402 if (userOnDisplay == CarOccupantZoneManager.INVALID_USER_ID) { 403 Slog.e(TAG, "can not find user on display " + displayId 404 + " to start Home"); 405 finish(); 406 } 407 408 Intent startMain = new Intent(Intent.ACTION_MAIN); 409 410 int driverDisplayId = mCarOccupantZoneManager.getDisplayIdForDriver( 411 CarOccupantZoneManager.DISPLAY_TYPE_MAIN); 412 if (Log.isLoggable(TAG, Log.DEBUG)) { 413 Slog.d(TAG, String.format("display id: %d, driver display id: %d", 414 displayId, driverDisplayId)); 415 } 416 startMain.addCategory(Intent.CATEGORY_HOME); 417 startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 418 startActivityAsUser(startMain, UserHandle.of(userOnDisplay)); 419 finish(); 420 } 421 handleRestartingTask()422 private void handleRestartingTask() { 423 // Lock on self to avoid restarting the same task twice. 424 synchronized (mLock) { 425 if (isFinishing()) { 426 return; 427 } 428 429 if (Log.isLoggable(TAG, Log.INFO)) { 430 Slog.i(TAG, "Restarting task " + mBlockedTaskId); 431 } 432 mCarPackageManager.restartTask(mBlockedTaskId); 433 finish(); 434 } 435 } 436 } 437