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