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