1 /*
2  * Copyright (C) 2023 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.distantdisplay.common;
17 
18 import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO;
19 
20 import static com.android.systemui.car.distantdisplay.common.DistantDisplayForegroundTaskMap.TaskData;
21 
22 import android.app.ActivityManager;
23 import android.app.ActivityOptions;
24 import android.app.ActivityTaskManager;
25 import android.content.BroadcastReceiver;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.hardware.display.DisplayManager;
31 import android.hardware.input.InputManager;
32 import android.media.session.MediaSessionManager;
33 import android.os.Build;
34 import android.os.UserHandle;
35 import android.os.UserManager;
36 import android.util.Log;
37 
38 import androidx.annotation.Nullable;
39 
40 import com.android.car.apps.common.util.IntentUtils;
41 import com.android.car.ui.utils.CarUxRestrictionsUtil;
42 import com.android.systemui.R;
43 import com.android.systemui.broadcast.BroadcastDispatcher;
44 import com.android.systemui.car.distantdisplay.activity.DistantDisplayCompanionActivity;
45 import com.android.systemui.car.distantdisplay.activity.DistantDisplayGameController;
46 import com.android.systemui.car.distantdisplay.activity.MoveTaskReceiver;
47 import com.android.systemui.car.distantdisplay.activity.RootTaskViewWallpaperActivity;
48 import com.android.systemui.car.distantdisplay.util.AppCategoryDetector;
49 import com.android.systemui.dagger.SysUISingleton;
50 import com.android.systemui.settings.UserTracker;
51 import com.android.systemui.shared.system.TaskStackChangeListener;
52 import com.android.systemui.shared.system.TaskStackChangeListeners;
53 
54 import com.google.android.car.common.DisplayCompatVirtualDisplay;
55 import com.google.android.car.distantdisplay.service.DistantDisplayService;
56 import com.google.android.car.distantdisplay.service.DistantDisplayService.ServiceConnectedListener;
57 
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.List;
61 import java.util.Objects;
62 
63 import javax.inject.Inject;
64 
65 /**
66  * TaskView Controller that manages the implementation details for task views on a distant display.
67  * <p>
68  * This is also a common class used between two different technical implementation where in one
69  * TaskViews are hosted in a window created by systemUI Vs in another where TaskView are created
70  * via an activity.
71  */
72 @SysUISingleton
73 public class TaskViewController {
74     public static final String TAG = TaskViewController.class.getSimpleName();
75     private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
76     private static final int DEFAULT_DISPLAY_ID = 0;
77     private static final int INVALID_TASK_ID = -1;
78 
79     private final Context mContext;
80     private final BroadcastDispatcher mBroadcastDispatcher;
81     private final UserTracker mUserTracker;
82     private final UserManager mUserManager;
83     private final InputManager mInputManager;
84     private final DisplayManager mDisplayManager;
85     private final MediaSessionManager mMediaSessionManager;
86     private final String mMediaBlockingComponentName;
87     private final List<ComponentName> mRestrictedActivities;
88     private List<String> mGameControllerPackages;
89     private final List<Callback> mCallbacks = new ArrayList<>();
90     private boolean mInitialized;
91     private int mDistantDisplayId;
92     private DistantDisplayService mDisplayCompatService;
93     private final DistantDisplayForegroundTaskMap mForegroundTasks =
94             new DistantDisplayForegroundTaskMap();
95     private int mDistantDisplayRootWallpaperTaskId = INVALID_TASK_ID;
96     private MoveTaskReceiver mMoveTaskReceiver;
97     private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
98 
99     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener
100             mOnUxRestrictionsChangedListener = carUxRestrictions -> {
101         int uxr = carUxRestrictions.getActiveRestrictions();
102         if (isVideoRestricted(uxr)) {
103             // pull back the video from DD
104             changeDisplayForTask(MoveTaskReceiver.MOVE_FROM_DISTANT_DISPLAY);
105         }
106     };
107 
108     private final BroadcastReceiver mUserEventReceiver = new BroadcastReceiver() {
109         @Override
110         public void onReceive(Context context, Intent intent) {
111             if (Objects.equals(intent.getAction(), Intent.ACTION_USER_UNLOCKED)) {
112                 launchActivity(mDistantDisplayId,
113                         RootTaskViewWallpaperActivity.createIntent(mContext));
114             }
115         }
116     };
117 
118     private final TaskStackChangeListener mTaskStackChangeLister = new TaskStackChangeListener() {
119         @Override
120         public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
121             logIfDebuggable("onTaskMovedToFront: displayId: " + taskInfo.displayId + ", " + taskInfo
122                     + " token: " + taskInfo.token);
123             Intent intent = taskInfo.baseIntent;
124             mForegroundTasks.put(taskInfo.taskId, taskInfo.displayId, taskInfo.baseIntent);
125             if (taskInfo.displayId == DEFAULT_DISPLAY_ID) {
126                 notifyListeners(DEFAULT_DISPLAY_ID);
127             } else if (taskInfo.displayId == mDistantDisplayId) {
128                 if (getPackageNameFromBaseIntent(taskInfo.baseIntent).equals(
129                         mContext.getPackageName())) {
130                     mDistantDisplayRootWallpaperTaskId = taskInfo.taskId;
131                 }
132                 notifyListeners(mDistantDisplayId);
133             }
134         }
135 
136         @Override
137         public void onTaskDisplayChanged(int taskId, int newDisplayId) {
138             TaskData oldData = mForegroundTasks.get(taskId);
139             if (oldData != null) {
140                 // If a task has not changed displays, do nothing. If it has truly been moved to
141                 // the top of the same display, it should be handled by onTaskMovedToFront
142                 if (oldData.mDisplayId == newDisplayId) return;
143                 mForegroundTasks.remove(taskId);
144                 mForegroundTasks.put(taskId, newDisplayId, oldData.mBaseIntent);
145             } else {
146                 mForegroundTasks.put(taskId, newDisplayId, null);
147             }
148 
149             if (newDisplayId == DEFAULT_DISPLAY_ID || (oldData != null
150                     && oldData.mDisplayId == DEFAULT_DISPLAY_ID)) {
151                 // Task on the default display has changed (by either a new task being added or an
152                 // old task being moved away) - notify listeners
153                 notifyListeners(DEFAULT_DISPLAY_ID);
154                 logIfDebuggable(
155                         "onTaskDisplayChanged: taskId: " + taskId + " newDisplayId: "
156                                 + newDisplayId);
157             }
158             if (newDisplayId == mDistantDisplayId || (oldData != null
159                     && oldData.mDisplayId == mDistantDisplayId)) {
160                 // Task on the distant display has changed  (by either a new task being added or an
161                 // old task being moved away) - notify listeners
162                 notifyListeners(mDistantDisplayId);
163                 logIfDebuggable(
164                         "onTaskDisplayChanged: taskId: " + taskId + " newDisplayId: "
165                                 + newDisplayId);
166             }
167         }
168     };
169 
170     @Inject
TaskViewController(Context context, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker)171     public TaskViewController(Context context, BroadcastDispatcher broadcastDispatcher,
172             UserTracker userTracker) {
173 
174         mContext = context;
175         mBroadcastDispatcher = broadcastDispatcher;
176         mUserTracker = userTracker;
177         mUserManager = context.getSystemService(UserManager.class);
178         mInputManager = context.getSystemService(InputManager.class);
179         mDisplayManager = context.getSystemService(DisplayManager.class);
180         mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
181         mRestrictedActivities = new ArrayList<>();
182         String[] ddRestrictedActivities = mContext.getResources().getStringArray(
183                 R.array.config_restrictedActivities);
184         mMediaBlockingComponentName = mContext.getResources().getString(
185                 R.string.config_mediaBlockingActivity);
186         for (int i = 0; i < ddRestrictedActivities.length; i++) {
187             mRestrictedActivities.add(
188                     ComponentName.unflattenFromString(ddRestrictedActivities[i]));
189         }
190         mGameControllerPackages = Arrays.asList(mContext.getResources().getStringArray(
191                 R.array.config_distantDisplayGameControllerPackages));
192 
193         DistantDisplayService.registerService(
194                 new ServiceConnectedListener() {
195                     @Override
196                     public void onServiceConnected(DistantDisplayService service) {
197                         Log.d(TAG, "TaskViewController onServiceConnected: " + service);
198                         mDisplayCompatService = service;
199                     }
200 
201                     @Override
202                     public void onDisplayCreated(DisplayCompatVirtualDisplay virtualDisplay) {
203                         Log.d(TAG,
204                                 "TaskViewController onDisplayCreated: "
205                                         + virtualDisplay.getDisplayId());
206                         mDistantDisplayId = virtualDisplay.getDisplayId();
207                         initialize(virtualDisplay.getDisplayId());
208                     }
209                 });
210         mDistantDisplayId = mContext.getResources().getInteger(R.integer.config_distantDisplayId);
211     }
212 
213     /**
214      * Initializes the listeners and initial wallpaper task for a particular distant display id.
215      * Can only be called once - if reinitialization is required, {@link #unregister} must be called
216      * before initialize is called again.
217      */
initialize(int distantDisplayId)218     public void initialize(int distantDisplayId) {
219         if (mInitialized) return;
220 
221         mInitialized = true;
222         mDistantDisplayId = distantDisplayId;
223 
224         TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackChangeLister);
225         mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(mContext);
226         mCarUxRestrictionsUtil.register(mOnUxRestrictionsChangedListener);
227 
228         if (DEBUG) {
229             Log.i(TAG, "Setup adb debugging : ");
230             setupDebuggingThroughAdb();
231 
232             // Register user unlock receiver for future user switches and unlocks
233             mBroadcastDispatcher.registerReceiver(mUserEventReceiver,
234                     new IntentFilter(Intent.ACTION_USER_UNLOCKED),
235                     /* executor= */ null, UserHandle.ALL);
236         }
237     }
238 
239     /** Unregister listeners - call before re-initialization. */
unregister()240     public void unregister() {
241         if (!mInitialized) return;
242 
243         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackChangeLister);
244         CarUxRestrictionsUtil carUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(mContext);
245         carUxRestrictionsUtil.unregister(mOnUxRestrictionsChangedListener);
246         clearListeners();
247 
248         if (DEBUG) {
249             mContext.unregisterReceiver(mMoveTaskReceiver);
250             mMoveTaskReceiver = null;
251             mBroadcastDispatcher.unregisterReceiver(mUserEventReceiver);
252         }
253     }
254 
255     /** Move task from default display to distant display. */
moveTaskToDistantDisplay()256     public void moveTaskToDistantDisplay() {
257         if (!mInitialized) return;
258         changeDisplayForTask(MoveTaskReceiver.MOVE_TO_DISTANT_DISPLAY);
259     }
260 
261     /** Move task from default display to distant display. */
moveTaskToRightDistantDisplay()262     public void moveTaskToRightDistantDisplay() {
263         if (!mInitialized) return;
264         changeDisplayForTask(MoveTaskReceiver.MOVE_TO_DISTANT_DISPLAY_PASSENGER);
265     }
266 
267     /** Move task from distant display to default display. */
moveTaskFromDistantDisplay()268     public void moveTaskFromDistantDisplay() {
269         if (!mInitialized) return;
270         changeDisplayForTask(MoveTaskReceiver.MOVE_FROM_DISTANT_DISPLAY);
271     }
272 
273     /** Add a callback to listen to task changes on the default and distant displays. */
addCallback(Callback callback)274     public void addCallback(Callback callback) {
275         synchronized (mCallbacks) {
276             mCallbacks.add(callback);
277         }
278     }
279 
280     /** Remove callback for task changes on the default and distant displays. */
removeCallback(Callback callback)281     public void removeCallback(Callback callback) {
282         synchronized (mCallbacks) {
283             mCallbacks.remove(callback);
284         }
285     }
286 
setupDebuggingThroughAdb()287     private void setupDebuggingThroughAdb() {
288         IntentFilter filter = new IntentFilter(MoveTaskReceiver.MOVE_ACTION);
289         mMoveTaskReceiver = new MoveTaskReceiver();
290         mMoveTaskReceiver.registerOnChangeDisplayForTask(this::changeDisplayForTask);
291         mContext.registerReceiverAsUser(mMoveTaskReceiver,
292                 mUserTracker.getUserHandle(),
293                 filter, null, null,
294                 Context.RECEIVER_EXPORTED);
295     }
296 
isVideoRestricted(int uxr)297     private boolean isVideoRestricted(int uxr) {
298         return ((uxr & UX_RESTRICTIONS_NO_VIDEO) == UX_RESTRICTIONS_NO_VIDEO);
299     }
300 
changeDisplayForTask(String movement)301     private void changeDisplayForTask(String movement) {
302         Log.i(TAG, "Handling movement command : " + movement);
303         if (movement.equals(MoveTaskReceiver.MOVE_FROM_DISTANT_DISPLAY)
304                 && !mForegroundTasks.isEmpty()) {
305             TaskData data = mForegroundTasks.getTopTaskOnDisplay(mDistantDisplayId);
306             if (data == null || data.mTaskId == mDistantDisplayRootWallpaperTaskId) return;
307             moveTaskToDisplay(data.mTaskId, DEFAULT_DISPLAY_ID);
308             mDisplayCompatService.updateState(DistantDisplayService.State.DEFAULT);
309         } else if ((movement.equals(MoveTaskReceiver.MOVE_TO_DISTANT_DISPLAY) || movement.equals(
310                 MoveTaskReceiver.MOVE_TO_DISTANT_DISPLAY_PASSENGER))
311                 && !mForegroundTasks.isEmpty()) {
312             int uxr = mCarUxRestrictionsUtil.getCurrentRestrictions().getActiveRestrictions();
313             if (isVideoRestricted(uxr)) {
314                 Log.i(TAG, "uxr restriction in effect can't move task: ");
315                 return;
316             }
317             TaskData data = mForegroundTasks.getTopTaskOnDisplay(
318                     DEFAULT_DISPLAY_ID);
319             if (data == null) return;
320             ComponentName componentName =
321                     data.mBaseIntent == null ? null : data.mBaseIntent.getComponent();
322             if (mRestrictedActivities.contains(componentName)) {
323                 Log.w(TAG, "restricted activity: " + componentName);
324                 return;
325             }
326             moveTaskToDisplay(data.mTaskId, mDistantDisplayId);
327             launchCompanionUI(componentName);
328             DistantDisplayService.State state = movement.equals(
329                     MoveTaskReceiver.MOVE_TO_DISTANT_DISPLAY)
330                     ? DistantDisplayService.State.DRIVER_DD
331                     : DistantDisplayService.State.PASSENGER_DD;
332             mDisplayCompatService.updateState(state);
333         }
334     }
335 
moveTaskToDisplay(int taskId, int displayId)336     private void moveTaskToDisplay(int taskId, int displayId) {
337         try {
338             ActivityTaskManager.getService().moveRootTaskToDisplay(taskId, displayId);
339         } catch (Exception e) {
340             Log.e(TAG, "Error moving task " + taskId + " to display " + displayId, e);
341         }
342     }
343 
launchCompanionUI(@ullable ComponentName componentName)344     private void launchCompanionUI(@Nullable ComponentName componentName) {
345         String packageName = componentName != null ? componentName.getPackageName() : null;
346         Intent intent;
347         UserHandle launchUserHandle = UserHandle.SYSTEM;
348         if (isGameApp(packageName)) {
349             intent = DistantDisplayGameController.createIntent(mContext, packageName);
350         } else if (componentName != null && hasActiveMediaSession(componentName)) {
351             //TODO: b/344983836 Currently there is no reliable way to figure out if the current
352             // application supports video media
353             ComponentName mediaComponent = ComponentName.unflattenFromString(
354                     mMediaBlockingComponentName);
355             intent = new Intent();
356             intent.setComponent(mediaComponent);
357             intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName.flattenToShortString());
358             intent.putExtra(IntentUtils.EXTRA_MEDIA_BLOCKING_ACTIVITY_DISMISS_ON_PARK, false);
359             launchUserHandle = mUserTracker.getUserHandle();
360         } else {
361             intent = DistantDisplayCompanionActivity.createIntent(mContext, packageName);
362         }
363         ActivityOptions options = ActivityOptions.makeBasic()
364                 .setLaunchDisplayId(DEFAULT_DISPLAY_ID);
365         mContext.startActivityAsUser(intent, options.toBundle(), launchUserHandle);
366     }
367 
isVideoApp(@ullable String packageName)368     private boolean isVideoApp(@Nullable String packageName) {
369         if (packageName == null) {
370             Log.w(TAG, "package name is null");
371             return false;
372         }
373 
374         Context userContext = mContext.createContextAsUser(
375                 mUserTracker.getUserHandle(), /* flags= */ 0);
376         return AppCategoryDetector.isVideoApp(userContext.getPackageManager(),
377                 packageName);
378     }
379 
isGameApp(@ullable String packageName)380     private boolean isGameApp(@Nullable String packageName) {
381         return mGameControllerPackages.contains(packageName);
382     }
383 
hasActiveMediaSession(ComponentName componentName)384     private boolean hasActiveMediaSession(ComponentName componentName) {
385         return mMediaSessionManager.getActiveSessionsForUser(null,
386                         mUserTracker.getUserHandle())
387                 .stream().anyMatch(mediaController -> componentName.getPackageName()
388                         .equals(mediaController.getPackageName()));
389     }
390 
391     /**
392      * Provides a DisplayManager.
393      * This method will be called from the SurfaceHolderCallback to create a virtual display.
394      */
getDisplayManager()395     public DisplayManager getDisplayManager() {
396         return mDisplayManager;
397     }
398 
launchActivity(int displayId, Intent intent)399     private void launchActivity(int displayId, Intent intent) {
400         ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
401         options.setLaunchDisplayId(displayId);
402         mContext.startActivityAsUser(intent, options.toBundle(), mUserTracker.getUserHandle());
403     }
404 
405     @Nullable
getComponentNameFromBaseIntent(Intent intent)406     private ComponentName getComponentNameFromBaseIntent(Intent intent) {
407         if (intent == null) {
408             return null;
409         }
410         return intent.getComponent();
411     }
412 
413     @Nullable
getPackageNameFromBaseIntent(Intent intent)414     private String getPackageNameFromBaseIntent(Intent intent) {
415         ComponentName componentName = getComponentNameFromBaseIntent(intent);
416         if (componentName == null) {
417             return null;
418         }
419         return componentName.getPackageName();
420     }
421 
logIfDebuggable(String message)422     private static void logIfDebuggable(String message) {
423         if (DEBUG) {
424             Log.d(TAG, message);
425         }
426     }
427 
428     /**
429      * Called when unregistered - since the top packages can no longer be guaranteed known, notify
430      * listeners for both displays that the top package is null.
431      */
clearListeners()432     private void clearListeners() {
433         notifyListeners(DEFAULT_DISPLAY_ID, null);
434         notifyListeners(mDistantDisplayId, null);
435     }
436 
notifyListeners(int displayId)437     private void notifyListeners(int displayId) {
438         if (displayId != DEFAULT_DISPLAY_ID && displayId != mDistantDisplayId) return;
439         ComponentName componentName;
440         TaskData data = mForegroundTasks.getTopTaskOnDisplay(displayId);
441         if (data != null) {
442             componentName = getComponentNameFromBaseIntent(data.mBaseIntent);
443         } else {
444             componentName = null;
445         }
446 
447         notifyListeners(displayId, componentName);
448     }
449 
notifyListeners(int displayId, ComponentName componentName)450     private void notifyListeners(int displayId, ComponentName componentName) {
451         synchronized (mCallbacks) {
452             for (Callback callback : mCallbacks) {
453                 callback.topAppOnDisplayChanged(displayId, componentName);
454             }
455         }
456     }
457 
458     /** Callback to listen to task changes on the default and distant displays. */
459     public interface Callback {
460         /**
461          * Called when the top app on a particular display changes, including the relevant
462          * display id and component name. Note that this will only be called for the default and the
463          * configured distant display ids.
464          */
topAppOnDisplayChanged(int displayId, @Nullable ComponentName componentName)465         void topAppOnDisplayChanged(int displayId, @Nullable ComponentName componentName);
466     }
467 }
468