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.car.Car; 25 import android.car.content.pm.CarPackageManager; 26 import android.car.drivingstate.CarUxRestrictions; 27 import android.car.drivingstate.CarUxRestrictionsManager; 28 import android.content.ComponentName; 29 import android.content.Intent; 30 import android.os.Build; 31 import android.os.Bundle; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.view.View; 35 import android.view.ViewTreeObserver; 36 import android.widget.Button; 37 import android.widget.TextView; 38 39 import com.android.car.CarLog; 40 import com.android.car.R; 41 42 /** 43 * Default activity that will be launched when the current foreground activity is not allowed. 44 * Additional information on blocked Activity should be passed as intent extras. 45 */ 46 public class ActivityBlockingActivity extends Activity { 47 private static final int INVALID_TASK_ID = -1; 48 49 private Car mCar; 50 private CarUxRestrictionsManager mUxRManager; 51 52 private Button mExitButton; 53 private Button mToggleDebug; 54 55 private int mBlockedTaskId; 56 57 private final View.OnClickListener mOnExitButtonClickedListener = 58 v -> { 59 if (isExitOptionCloseApplication()) { 60 handleCloseApplication(); 61 } else { 62 handleRestartingTask(); 63 } 64 }; 65 66 private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = 67 new ViewTreeObserver.OnGlobalLayoutListener() { 68 @Override 69 public void onGlobalLayout() { 70 mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener(this); 71 updateButtonWidths(); 72 } 73 }; 74 75 @Override onCreate(Bundle savedInstanceState)76 protected void onCreate(Bundle savedInstanceState) { 77 super.onCreate(savedInstanceState); 78 setContentView(R.layout.activity_blocking); 79 80 mExitButton = findViewById(R.id.exit_button); 81 82 // Listen to the CarUxRestrictions so this blocking activity can be dismissed when the 83 // restrictions are lifted. 84 // This Activity should be launched only after car service is initialized. Currently this 85 // Activity is only launched from CPMS. So this is safe to do. 86 mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 87 (car, ready) -> { 88 if (!ready) { 89 return; 90 } 91 mUxRManager = (CarUxRestrictionsManager) car.getCarManager( 92 Car.CAR_UX_RESTRICTION_SERVICE); 93 // This activity would have been launched only in a restricted state. 94 // But ensuring when the service connection is established, that we are still 95 // in a restricted state. 96 handleUxRChange(mUxRManager.getCurrentCarUxRestrictions()); 97 mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange); 98 }); 99 } 100 101 @Override onResume()102 protected void onResume() { 103 super.onResume(); 104 105 // Display info about the current blocked activity, and optionally show an exit button 106 // to restart the blocked task (stack of activities) if its root activity is DO. 107 mBlockedTaskId = getIntent().getIntExtra(BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID, 108 INVALID_TASK_ID); 109 110 // blockedActivity is expected to be always passed in as the topmost activity of task. 111 String blockedActivity = getIntent().getStringExtra( 112 BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 113 if (!TextUtils.isEmpty(blockedActivity)) { 114 if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) { 115 Log.d(CarLog.TAG_AM, "Blocking activity " + blockedActivity); 116 } 117 } 118 119 displayExitButton(); 120 121 // Show more debug info for non-user build. 122 if (Build.IS_ENG || Build.IS_USERDEBUG) { 123 displayDebugInfo(); 124 } 125 } 126 displayExitButton()127 private void displayExitButton() { 128 String exitButtonText = getExitButtonText(); 129 130 mExitButton.setText(exitButtonText); 131 mExitButton.setOnClickListener(mOnExitButtonClickedListener); 132 } 133 134 // If the root activity is DO, the user will have the option to go back to that activity, 135 // otherwise, the user will have the option to close the blocked application isExitOptionCloseApplication()136 private boolean isExitOptionCloseApplication() { 137 boolean isRootDO = getIntent().getBooleanExtra( 138 BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO, false); 139 return mBlockedTaskId == INVALID_TASK_ID || !isRootDO; 140 } 141 getExitButtonText()142 private String getExitButtonText() { 143 return isExitOptionCloseApplication() ? getString(R.string.exit_button_close_application) 144 : getString(R.string.exit_button_go_back); 145 } 146 displayDebugInfo()147 private void displayDebugInfo() { 148 String blockedActivity = getIntent().getStringExtra( 149 BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); 150 String rootActivity = getIntent().getStringExtra(BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME); 151 152 TextView debugInfo = findViewById(R.id.debug_info); 153 debugInfo.setText(getDebugInfo(blockedActivity, rootActivity)); 154 155 // We still want to ensure driving safety for non-user build; 156 // toggle visibility of debug info with this button. 157 mToggleDebug = findViewById(R.id.toggle_debug_info); 158 mToggleDebug.setVisibility(View.VISIBLE); 159 mToggleDebug.setOnClickListener(v -> { 160 boolean isDebugVisible = debugInfo.getVisibility() == View.VISIBLE; 161 debugInfo.setVisibility(isDebugVisible ? View.GONE : View.VISIBLE); 162 }); 163 164 mToggleDebug.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); 165 } 166 167 // When the Debug button is visible, we set both of the visible buttons to have the width 168 // of whichever button is wider updateButtonWidths()169 private void updateButtonWidths() { 170 Button debugButton = findViewById(R.id.toggle_debug_info); 171 172 int exitButtonWidth = mExitButton.getWidth(); 173 int debugButtonWidth = debugButton.getWidth(); 174 175 if (exitButtonWidth > debugButtonWidth) { 176 debugButton.setWidth(exitButtonWidth); 177 } else { 178 mExitButton.setWidth(debugButtonWidth); 179 } 180 } 181 getDebugInfo(String blockedActivity, String rootActivity)182 private String getDebugInfo(String blockedActivity, String rootActivity) { 183 StringBuilder debug = new StringBuilder(); 184 185 ComponentName blocked = ComponentName.unflattenFromString(blockedActivity); 186 debug.append("Blocked activity is ") 187 .append(blocked.getShortClassName()) 188 .append("\nBlocked activity package is ") 189 .append(blocked.getPackageName()); 190 191 if (rootActivity != null) { 192 ComponentName root = ComponentName.unflattenFromString(rootActivity); 193 // Optionally show root activity info if it differs from the blocked activity. 194 if (!root.equals(blocked)) { 195 debug.append("\n\nRoot activity is ").append(root.getShortClassName()); 196 } 197 if (!root.getPackageName().equals(blocked.getPackageName())) { 198 debug.append("\nRoot activity package is ").append(root.getPackageName()); 199 } 200 } 201 return debug.toString(); 202 } 203 204 @Override onNewIntent(Intent intent)205 protected void onNewIntent(Intent intent) { 206 super.onNewIntent(intent); 207 setIntent(intent); 208 } 209 210 @Override onStop()211 protected void onStop() { 212 super.onStop(); 213 // Finish when blocking activity goes invisible to avoid it accidentally re-surfaces with 214 // stale string regarding blocked activity. 215 finish(); 216 } 217 218 @Override onDestroy()219 protected void onDestroy() { 220 super.onDestroy(); 221 mUxRManager.unregisterListener(); 222 mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener(mOnGlobalLayoutListener); 223 mCar.disconnect(); 224 } 225 226 // If no distraction optimization is required in the new restrictions, then dismiss the 227 // blocking activity (self). handleUxRChange(CarUxRestrictions restrictions)228 private void handleUxRChange(CarUxRestrictions restrictions) { 229 if (restrictions == null) { 230 return; 231 } 232 if (!restrictions.isRequiresDistractionOptimization()) { 233 finish(); 234 } 235 } 236 handleCloseApplication()237 private void handleCloseApplication() { 238 if (isFinishing()) { 239 return; 240 } 241 242 Intent startMain = new Intent(Intent.ACTION_MAIN); 243 startMain.addCategory(Intent.CATEGORY_HOME); 244 startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 245 startActivity(startMain); 246 finish(); 247 } 248 handleRestartingTask()249 private void handleRestartingTask() { 250 if (isFinishing()) { 251 return; 252 } 253 254 // Lock on self to avoid restarting the same task twice. 255 synchronized (this) { 256 if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) { 257 Log.i(CarLog.TAG_AM, "Restarting task " + mBlockedTaskId); 258 } 259 CarPackageManager carPm = (CarPackageManager) 260 mCar.getCarManager(Car.PACKAGE_SERVICE); 261 carPm.restartTask(mBlockedTaskId); 262 finish(); 263 } 264 } 265 } 266