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.wm.activity; 17 18 import static com.android.systemui.car.Flags.configAppBlockingActivities; 19 20 import android.app.ActivityManager; 21 import android.car.Car; 22 import android.car.CarOccupantZoneManager; 23 import android.car.app.CarActivityManager; 24 import android.car.content.pm.CarPackageManager; 25 import android.car.drivingstate.CarUxRestrictions; 26 import android.car.drivingstate.CarUxRestrictionsManager; 27 import android.content.ActivityNotFoundException; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.graphics.Insets; 32 import android.graphics.Rect; 33 import android.hardware.display.DisplayManager; 34 import android.opengl.GLSurfaceView; 35 import android.os.Build; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.Looper; 39 import android.os.UserHandle; 40 import android.text.TextUtils; 41 import android.util.Log; 42 import android.util.Slog; 43 import android.view.DisplayInfo; 44 import android.view.View; 45 import android.view.ViewTreeObserver; 46 import android.view.WindowInsets; 47 import android.widget.Button; 48 import android.widget.TextView; 49 50 import androidx.fragment.app.FragmentActivity; 51 import androidx.lifecycle.ViewModelProvider; 52 53 import com.android.systemui.R; 54 import com.android.systemui.car.ndo.BlockerViewModel; 55 import com.android.systemui.car.ndo.NdoViewModelFactory; 56 import com.android.systemui.car.wm.activity.blurredbackground.BlurredSurfaceRenderer; 57 58 import java.util.List; 59 import java.util.concurrent.Executor; 60 61 import javax.inject.Inject; 62 63 /** 64 * Default activity that will be launched when the current foreground activity is not allowed. 65 * Additional information on blocked Activity should be passed as intent extras. 66 */ 67 public class ActivityBlockingActivity extends FragmentActivity { 68 private static final int ACTIVITY_MONITORING_DELAY_MS = 1000; 69 private static final String TAG = "BlockingActivity"; 70 private static final int EGL_CONTEXT_VERSION = 2; 71 private static final int EGL_CONFIG_SIZE = 8; 72 private static final int INVALID_TASK_ID = -1; 73 private final Object mLock = new Object(); 74 75 private GLSurfaceView mGLSurfaceView; 76 private BlurredSurfaceRenderer mSurfaceRenderer; 77 private boolean mIsGLSurfaceSetup = false; 78 79 private Car mCar; 80 private CarUxRestrictionsManager mUxRManager; 81 private CarPackageManager mCarPackageManager; 82 private CarActivityManager mCarActivityManager; 83 private CarOccupantZoneManager mCarOccupantZoneManager; 84 85 private Button mExitButton; 86 private Button mToggleDebug; 87 88 private int mBlockedTaskId; 89 private final Handler mHandler = new Handler(); 90 private String mBlockedActivityName; 91 private final NdoViewModelFactory mViewModelFactory; 92 93 private final View.OnClickListener mOnExitButtonClickedListener = 94 v -> { 95 if (isExitOptionCloseApplication()) { 96 handleCloseApplication(); 97 } else { 98 handleRestartingTask(); 99 } 100 }; 101 102 private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = 103 new ViewTreeObserver.OnGlobalLayoutListener() { 104 @Override 105 public void onGlobalLayout() { 106 mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener(this); 107 updateButtonWidths(); 108 } 109 }; 110 111 private final CarPackageManager.BlockingUiCommandListener mBlockingUiCommandListener = () -> { 112 if (Log.isLoggable(TAG, Log.DEBUG)) { 113 Slog.d(TAG, "Finishing ABA due to task stack change"); 114 } 115 finish(); 116 }; 117 118 @Inject ActivityBlockingActivity(NdoViewModelFactory viewModelFactory)119 public ActivityBlockingActivity(NdoViewModelFactory viewModelFactory) { 120 mViewModelFactory = viewModelFactory; 121 } 122 123 @Override onCreate(Bundle savedInstanceState)124 protected void onCreate(Bundle savedInstanceState) { 125 super.onCreate(savedInstanceState); 126 setContentView(R.layout.activity_blocking); 127 mExitButton = findViewById(R.id.exit_button); 128 // Listen to the CarUxRestrictions so this blocking activity can be dismissed when the 129 // restrictions are lifted. 130 // This Activity should be launched only after car service is initialized. Currently this 131 // Activity is only launched from CPMS. So this is safe to do. 132 mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 133 (car, ready) -> { 134 if (!ready) { 135 return; 136 } 137 mCarPackageManager = (CarPackageManager) car.getCarManager( 138 Car.PACKAGE_SERVICE); 139 mCarActivityManager = (CarActivityManager) car.getCarManager( 140 Car.CAR_ACTIVITY_SERVICE); 141 mUxRManager = (CarUxRestrictionsManager) car.getCarManager( 142 Car.CAR_UX_RESTRICTION_SERVICE); 143 mCarOccupantZoneManager = car.getCarManager(CarOccupantZoneManager.class); 144 // This activity would have been launched only in a restricted state. 145 // But ensuring when the service connection is established, that we are still 146 // in a restricted state. 147 handleUxRChange(mUxRManager.getCurrentCarUxRestrictions()); 148 mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange); 149 Executor executor = new Handler(Looper.getMainLooper())::post; 150 mCarPackageManager.registerBlockingUiCommandListener(getDisplayId(), executor, 151 mBlockingUiCommandListener); 152 }); 153 154 setupGLSurface(); 155 156 if (!configAppBlockingActivities()) { 157 Slog.d(TAG, "Ignoring app blocking activity feature"); 158 } else if (getResources().getBoolean(R.bool.config_enableAppBlockingActivities)) { 159 mBlockedActivityName = getIntent().getStringExtra( 160 CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 161 BlockerViewModel blockerViewModel = new ViewModelProvider(this, mViewModelFactory) 162 .get(BlockerViewModel.class); 163 int userOnDisplay = getUserForCurrentDisplay(); 164 if (userOnDisplay == CarOccupantZoneManager.INVALID_USER_ID) { 165 Slog.w(TAG, "Can't find user on display " + getDisplayId() 166 + " defaulting to current user"); 167 userOnDisplay = UserHandle.USER_CURRENT; 168 } 169 blockerViewModel.initialize(mBlockedActivityName, UserHandle.of(userOnDisplay)); 170 blockerViewModel.getBlockingTypeLiveData().observe(this, blockingType -> { 171 switch (blockingType) { 172 case DIALER -> startBlockingActivity( 173 getString(R.string.config_dialerBlockingActivity)); 174 case MEDIA -> startBlockingActivity( 175 getString(R.string.config_mediaBlockingActivity)); 176 case NONE -> { /* no-op */ } 177 } 178 }); 179 } 180 } 181 182 @Override onStart()183 protected void onStart() { 184 super.onStart(); 185 if (mIsGLSurfaceSetup) { 186 mGLSurfaceView.onResume(); 187 } 188 } 189 190 @Override onResume()191 protected void onResume() { 192 super.onResume(); 193 194 // Display info about the current blocked activity, and optionally show an exit button 195 // to restart the blocked task (stack of activities) if its root activity is DO. 196 mBlockedTaskId = getIntent().getIntExtra( 197 CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID, 198 INVALID_TASK_ID); 199 200 // blockedActivity is expected to be always passed in as the topmost activity of task. 201 String blockedActivity = getIntent().getStringExtra( 202 CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 203 if (!TextUtils.isEmpty(blockedActivity)) { 204 if (isTopActivityBehindAbaDistractionOptimized()) { 205 Slog.w(TAG, "Top activity is already DO, so finishing"); 206 finish(); 207 return; 208 } 209 210 if (Log.isLoggable(TAG, Log.DEBUG)) { 211 Slog.d(TAG, "Blocking activity " + blockedActivity); 212 } 213 } 214 215 displayExitButton(); 216 217 // Show more debug info for non-user build. 218 if (Build.IS_ENG || Build.IS_USERDEBUG) { 219 displayDebugInfo(); 220 } 221 } 222 223 @Override onStop()224 protected void onStop() { 225 super.onStop(); 226 227 if (mIsGLSurfaceSetup) { 228 // We queue this event so that it runs on the Rendering thread 229 mGLSurfaceView.queueEvent(() -> mSurfaceRenderer.onPause()); 230 231 mGLSurfaceView.onPause(); 232 } 233 234 // Finish when blocking activity goes invisible to avoid it accidentally re-surfaces with 235 // stale string regarding blocked activity. 236 finish(); 237 } 238 setupGLSurface()239 private void setupGLSurface() { 240 DisplayManager displayManager = (DisplayManager) getApplicationContext().getSystemService( 241 Context.DISPLAY_SERVICE); 242 DisplayInfo displayInfo = new DisplayInfo(); 243 244 int displayId = getDisplayId(); 245 displayManager.getDisplay(displayId).getDisplayInfo(displayInfo); 246 247 Rect windowRect = getAppWindowRect(); 248 249 mSurfaceRenderer = new BlurredSurfaceRenderer(this, windowRect, getDisplayId()); 250 251 mGLSurfaceView = findViewById(R.id.blurred_surface_view); 252 mGLSurfaceView.setEGLContextClientVersion(EGL_CONTEXT_VERSION); 253 254 mGLSurfaceView.setEGLConfigChooser(EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, 255 EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE); 256 257 mGLSurfaceView.setRenderer(mSurfaceRenderer); 258 259 // We only want to render the screen once 260 mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 261 262 mIsGLSurfaceSetup = true; 263 } 264 265 /** 266 * Computes a Rect that represents the portion of the screen that contains the activity that is 267 * being blocked. 268 * 269 * @return Rect that represents the application window 270 */ getAppWindowRect()271 private Rect getAppWindowRect() { 272 Insets systemBarInsets = getWindowManager() 273 .getCurrentWindowMetrics() 274 .getWindowInsets() 275 .getInsets(WindowInsets.Type.systemBars()); 276 277 Rect displayBounds = getWindowManager().getCurrentWindowMetrics().getBounds(); 278 279 int leftX = systemBarInsets.left; 280 int rightX = displayBounds.width() - systemBarInsets.right; 281 int topY = systemBarInsets.top; 282 int bottomY = displayBounds.height() - systemBarInsets.bottom; 283 284 return new Rect(leftX, topY, rightX, bottomY); 285 } 286 displayExitButton()287 private void displayExitButton() { 288 String exitButtonText = getExitButtonText(); 289 290 mExitButton.setText(exitButtonText); 291 mExitButton.setOnClickListener(mOnExitButtonClickedListener); 292 } 293 294 // If the root activity is DO, the user will have the option to go back to that activity, 295 // otherwise, the user will have the option to close the blocked application isExitOptionCloseApplication()296 private boolean isExitOptionCloseApplication() { 297 boolean isRootDO = getIntent().getBooleanExtra( 298 CarPackageManager.BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO, false); 299 return mBlockedTaskId == INVALID_TASK_ID || !isRootDO; 300 } 301 getExitButtonText()302 private String getExitButtonText() { 303 return isExitOptionCloseApplication() ? getString(R.string.exit_button_close_application) 304 : getString(R.string.exit_button_go_back); 305 } 306 307 /** 308 * It is possible that the stack info has changed between when the intent to launch this 309 * activity was initiated and when this activity is started. Check whether the activity behind 310 * the ABA is distraction optimized. 311 * 312 * @return {@code true} if the activity is distraction optimized, {@code false} if the top task 313 * behind the ABA is null or the top task's top activity is null or if the top activity is 314 * non-distraction optimized. 315 */ isTopActivityBehindAbaDistractionOptimized()316 private boolean isTopActivityBehindAbaDistractionOptimized() { 317 List<ActivityManager.RunningTaskInfo> taskInfosTopToBottom; 318 taskInfosTopToBottom = mCarActivityManager.getVisibleTasks(); 319 ActivityManager.RunningTaskInfo topStackBehindAba = null; 320 321 // Iterate in bottom to top manner 322 for (int i = taskInfosTopToBottom.size() - 1; i >= 0; i--) { 323 ActivityManager.RunningTaskInfo taskInfo = taskInfosTopToBottom.get(i); 324 if (taskInfo.displayId != getDisplayId()) { 325 // ignore stacks on other displays 326 continue; 327 } 328 329 if (getComponentName().equals(taskInfo.topActivity)) { 330 // quit when stack with the blocking activity is encountered because the last seen 331 // task will be the topStackBehindAba. 332 break; 333 } 334 335 topStackBehindAba = taskInfo; 336 } 337 338 if (Log.isLoggable(TAG, Log.DEBUG)) { 339 Slog.d(TAG, String.format("Top stack behind ABA is: %s", topStackBehindAba)); 340 } 341 342 if (topStackBehindAba != null && topStackBehindAba.topActivity != null) { 343 boolean isDo = mCarPackageManager.isActivityDistractionOptimized( 344 topStackBehindAba.topActivity.getPackageName(), 345 topStackBehindAba.topActivity.getClassName()); 346 if (Log.isLoggable(TAG, Log.DEBUG)) { 347 Slog.d(TAG, 348 String.format("Top activity (%s) is DO: %s", topStackBehindAba.topActivity, 349 isDo)); 350 } 351 return isDo; 352 } 353 354 // unknown top stack / activity, default to considering it non-DO 355 return false; 356 } 357 displayDebugInfo()358 private void displayDebugInfo() { 359 String blockedActivity = getIntent().getStringExtra( 360 CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 361 String rootActivity = getIntent().getStringExtra( 362 CarPackageManager.BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME); 363 364 TextView debugInfo = findViewById(R.id.debug_info); 365 debugInfo.setText(getDebugInfo(blockedActivity, rootActivity)); 366 367 // We still want to ensure driving safety for non-user build; 368 // toggle visibility of debug info with this button. 369 mToggleDebug = findViewById(R.id.toggle_debug_info); 370 mToggleDebug.setVisibility(View.VISIBLE); 371 mToggleDebug.setOnClickListener(v -> { 372 boolean isDebugVisible = debugInfo.getVisibility() == View.VISIBLE; 373 debugInfo.setVisibility(isDebugVisible ? View.GONE : View.VISIBLE); 374 }); 375 376 mToggleDebug.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); 377 } 378 379 // When the Debug button is visible, we set both of the visible buttons to have the width 380 // of whichever button is wider updateButtonWidths()381 private void updateButtonWidths() { 382 Button debugButton = findViewById(R.id.toggle_debug_info); 383 384 int exitButtonWidth = mExitButton.getWidth(); 385 int debugButtonWidth = debugButton.getWidth(); 386 387 if (exitButtonWidth > debugButtonWidth) { 388 debugButton.setWidth(exitButtonWidth); 389 } else { 390 mExitButton.setWidth(debugButtonWidth); 391 } 392 } 393 getDebugInfo(String blockedActivity, String rootActivity)394 private String getDebugInfo(String blockedActivity, String rootActivity) { 395 StringBuilder debug = new StringBuilder(); 396 397 ComponentName blocked = ComponentName.unflattenFromString(blockedActivity); 398 debug.append("Blocked activity is ") 399 .append(blocked.getShortClassName()) 400 .append("\nBlocked activity package is ") 401 .append(blocked.getPackageName()); 402 403 if (rootActivity != null) { 404 ComponentName root = ComponentName.unflattenFromString(rootActivity); 405 // Optionally show root activity info if it differs from the blocked activity. 406 if (!root.equals(blocked)) { 407 debug.append("\n\nRoot activity is ").append(root.getShortClassName()); 408 } 409 if (!root.getPackageName().equals(blocked.getPackageName())) { 410 debug.append("\nRoot activity package is ").append(root.getPackageName()); 411 } 412 } 413 return debug.toString(); 414 } 415 416 @Override onNewIntent(Intent intent)417 protected void onNewIntent(Intent intent) { 418 super.onNewIntent(intent); 419 setIntent(intent); 420 } 421 422 @Override onDestroy()423 protected void onDestroy() { 424 super.onDestroy(); 425 mCar.disconnect(); 426 mUxRManager.unregisterListener(); 427 mCarPackageManager.unregisterBlockingUiCommandListener(mBlockingUiCommandListener); 428 if (mToggleDebug != null) { 429 mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener( 430 mOnGlobalLayoutListener); 431 } 432 mHandler.removeCallbacksAndMessages(null); 433 mCar.disconnect(); 434 } 435 436 // If no distraction optimization is required in the new restrictions, then dismiss the 437 // blocking activity (self). handleUxRChange(CarUxRestrictions restrictions)438 private void handleUxRChange(CarUxRestrictions restrictions) { 439 if (restrictions == null) { 440 return; 441 } 442 if (!restrictions.isRequiresDistractionOptimization()) { 443 finish(); 444 } 445 } 446 handleCloseApplication()447 private void handleCloseApplication() { 448 if (isFinishing()) { 449 return; 450 } 451 452 int userOnDisplay = getUserForCurrentDisplay(); 453 if (userOnDisplay == CarOccupantZoneManager.INVALID_USER_ID) { 454 Slog.e(TAG, "can not find user on display " + getDisplay() 455 + " to start Home"); 456 finish(); 457 } 458 459 Intent startMain = new Intent(Intent.ACTION_MAIN); 460 461 int driverDisplayId = mCarOccupantZoneManager.getDisplayIdForDriver( 462 CarOccupantZoneManager.DISPLAY_TYPE_MAIN); 463 if (Log.isLoggable(TAG, Log.DEBUG)) { 464 Slog.d(TAG, String.format("display id: %d, driver display id: %d", 465 getDisplayId(), driverDisplayId)); 466 } 467 startMain.addCategory(Intent.CATEGORY_HOME); 468 startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 469 startActivityAsUser(startMain, UserHandle.of(userOnDisplay)); 470 finish(); 471 } 472 handleRestartingTask()473 private void handleRestartingTask() { 474 // Lock on self to avoid restarting the same task twice. 475 synchronized (mLock) { 476 if (isFinishing()) { 477 return; 478 } 479 480 if (Log.isLoggable(TAG, Log.INFO)) { 481 Slog.i(TAG, "Restarting task " + mBlockedTaskId); 482 } 483 mCarPackageManager.restartTask(mBlockedTaskId); 484 finish(); 485 } 486 } 487 startBlockingActivity(String blockingActivity)488 private void startBlockingActivity(String blockingActivity) { 489 int userOnDisplay = getUserForCurrentDisplay(); 490 if (userOnDisplay == CarOccupantZoneManager.INVALID_USER_ID) { 491 Slog.w(TAG, "Can't find user on display " + getDisplayId() 492 + " defaulting to USER_CURRENT"); 493 userOnDisplay = UserHandle.USER_CURRENT; 494 } 495 496 ComponentName componentName = ComponentName.unflattenFromString(blockingActivity); 497 Intent intent = new Intent(); 498 intent.setComponent(componentName); 499 intent.putExtra(Intent.EXTRA_COMPONENT_NAME, mBlockedActivityName); 500 try { 501 startActivityAsUser(intent, UserHandle.of(userOnDisplay)); 502 } catch (ActivityNotFoundException ex) { 503 Slog.e(TAG, "Unable to resolve blocking activity " + blockingActivity, ex); 504 } catch (RuntimeException ex) { 505 Slog.w(TAG, "Failed to launch blocking activity " + blockingActivity, ex); 506 } 507 } 508 getUserForCurrentDisplay()509 private int getUserForCurrentDisplay() { 510 int displayId = getDisplayId(); 511 return mCarOccupantZoneManager.getUserForDisplayId(displayId); 512 } 513 } 514