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