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