1 /* 2 * Copyright (C) 2018 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 17 package com.android.car.carlauncher; 18 19 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; 21 22 import android.app.ActivityManager; 23 import android.app.ActivityOptions; 24 import android.app.TaskStackListener; 25 import android.car.Car; 26 import android.car.app.CarActivityManager; 27 import android.car.app.CarTaskViewController; 28 import android.car.app.CarTaskViewControllerCallback; 29 import android.car.app.ControlledRemoteCarTaskView; 30 import android.car.app.ControlledRemoteCarTaskViewCallback; 31 import android.car.app.ControlledRemoteCarTaskViewConfig; 32 import android.car.user.CarUserManager; 33 import android.content.ComponentName; 34 import android.content.Intent; 35 import android.content.res.Configuration; 36 import android.os.Bundle; 37 import android.os.UserManager; 38 import android.util.Log; 39 import android.view.Display; 40 import android.view.ViewGroup; 41 import android.view.WindowManager; 42 43 import androidx.collection.ArraySet; 44 import androidx.fragment.app.FragmentActivity; 45 import androidx.fragment.app.FragmentTransaction; 46 import androidx.lifecycle.ViewModelProvider; 47 48 import com.android.car.carlauncher.homescreen.HomeCardModule; 49 import com.android.car.carlauncher.taskstack.TaskStackChangeListeners; 50 import com.android.car.internal.common.UserHelperLite; 51 import com.android.wm.shell.taskview.TaskView; 52 53 import com.google.common.annotations.VisibleForTesting; 54 55 import java.util.Set; 56 57 /** 58 * Basic Launcher for Android Automotive which demonstrates the use of {@link TaskView} to host 59 * maps content and uses a Model-View-Presenter structure to display content in cards. 60 * 61 * <p>Implementations of the Launcher that use the given layout of the main activity 62 * (car_launcher.xml) can customize the home screen cards by providing their own 63 * {@link HomeCardModule} for R.id.top_card or R.id.bottom_card. Otherwise, implementations that 64 * use their own layout should define their own activity rather than using this one. 65 * 66 * <p>Note: On some devices, the TaskView may render with a width, height, and/or aspect 67 * ratio that does not meet Android compatibility definitions. Developers should work with content 68 * owners to ensure content renders correctly when extending or emulating this class. 69 */ 70 public class CarLauncher extends FragmentActivity { 71 public static final String TAG = "CarLauncher"; 72 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 73 74 private ActivityManager mActivityManager; 75 private TaskViewManager mTaskViewManager; 76 77 private CarTaskView mTaskView; 78 private ControlledRemoteCarTaskView mRemoteCarTaskView; 79 private int mCarLauncherTaskId = INVALID_TASK_ID; 80 private Set<HomeCardModule> mHomeCardModules; 81 82 /** Set to {@code true} once we've logged that the Activity is fully drawn. */ 83 private boolean mIsReadyLogged; 84 private boolean mUseSmallCanvasOptimizedMap; 85 private boolean mUseRemoteCarTaskView; 86 private ViewGroup mMapsCard; 87 88 private final TaskStackListener mTaskStackListener = new TaskStackListener() { 89 @Override 90 public void onTaskFocusChanged(int taskId, boolean focused) {} 91 92 @Override 93 public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, 94 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { 95 if (DEBUG) { 96 Log.d(TAG, "onActivityRestartAttempt: taskId=" + task.taskId 97 + ", homeTaskVisible=" + homeTaskVisible + ", wasVisible=" + wasVisible); 98 } 99 if (!mUseSmallCanvasOptimizedMap 100 && !homeTaskVisible 101 && getTaskViewTaskId() == task.taskId) { 102 // The embedded map component received an intent, therefore forcibly bringing the 103 // launcher to the foreground. 104 bringToForeground(); 105 return; 106 } 107 } 108 }; 109 110 @VisibleForTesting setCarUserManager(CarUserManager carUserManager)111 void setCarUserManager(CarUserManager carUserManager) { 112 if (mTaskViewManager == null) { 113 Log.w(TAG, "Task view manager is null, cannot set CarUserManager on taskview " 114 + "manager"); 115 return; 116 } 117 mTaskViewManager.setCarUserManager(carUserManager); 118 } 119 120 @Override onCreate(Bundle savedInstanceState)121 protected void onCreate(Bundle savedInstanceState) { 122 super.onCreate(savedInstanceState); 123 124 if (DEBUG) { 125 Log.d(TAG, "onCreate(" + getUserId() + ") displayId=" + getDisplayId()); 126 } 127 // Since MUMD is introduced, CarLauncher can be called in the main display of visible users. 128 // In ideal shape, CarLauncher should handle both driver and passengers together. 129 // But, in the mean time, we have separate launchers for driver and passengers, so 130 // CarLauncher needs to reroute the request to Passenger launcher if it is invoked from 131 // the main display of passengers (not driver). 132 // For MUPAND, PassengerLauncher should be the default launcher. 133 // For non-main displays, ATM will invoke SECONDARY_HOME Intent, so the secondary launcher 134 // should handle them. 135 UserManager um = getSystemService(UserManager.class); 136 boolean isPassengerDisplay = getDisplayId() != Display.DEFAULT_DISPLAY 137 || um.isVisibleBackgroundUsersOnDefaultDisplaySupported(); 138 if (isPassengerDisplay) { 139 String passengerLauncherName = getString(R.string.config_passengerLauncherComponent); 140 Intent passengerHomeIntent; 141 if (!passengerLauncherName.isEmpty()) { 142 ComponentName component = ComponentName.unflattenFromString(passengerLauncherName); 143 if (component == null) { 144 throw new IllegalStateException( 145 "Invalid passengerLauncher name=" + passengerLauncherName); 146 } 147 passengerHomeIntent = new Intent(Intent.ACTION_MAIN) 148 // passenger launcher should be launched in home task in order to 149 // fix TaskView layering issue 150 .addCategory(Intent.CATEGORY_HOME) 151 .setComponent(component); 152 } else { 153 // No passenger launcher is specified, then use AppsGrid as a fallback. 154 passengerHomeIntent = CarLauncherUtils.getAppsGridIntent(); 155 } 156 ActivityOptions options = ActivityOptions 157 // No animation for the trampoline. 158 .makeCustomAnimation(this, /* enterResId=*/ 0, /* exitResId= */ 0) 159 .setLaunchDisplayId(getDisplayId()); 160 startActivity(passengerHomeIntent, options.toBundle()); 161 finish(); 162 return; 163 } 164 165 mUseSmallCanvasOptimizedMap = 166 CarLauncherUtils.isSmallCanvasOptimizedMapIntentConfigured(this); 167 mUseRemoteCarTaskView = getResources().getBoolean(R.bool.config_useRemoteCarTaskView); 168 169 mActivityManager = getSystemService(ActivityManager.class); 170 mCarLauncherTaskId = getTaskId(); 171 TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); 172 173 // Setting as trusted overlay to let touches pass through. 174 getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY); 175 // To pass touches to the underneath task. 176 getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); 177 178 // Don't show the maps panel in multi window mode. 179 // NOTE: CTS tests for split screen are not compatible with activity views on the default 180 // activity of the launcher 181 if (isInMultiWindowMode() || isInPictureInPictureMode()) { 182 setContentView(R.layout.car_launcher_multiwindow); 183 } else { 184 setContentView(R.layout.car_launcher); 185 // We don't want to show Map card unnecessarily for the headless user 0. 186 if (!UserHelperLite.isHeadlessSystemUser(getUserId())) { 187 mMapsCard = findViewById(R.id.maps_card); 188 if (mMapsCard != null) { 189 if (mUseRemoteCarTaskView) { 190 setupRemoteCarTaskView(mMapsCard); 191 } else { 192 setUpTaskView(mMapsCard); 193 } 194 } 195 } 196 } 197 initializeCards(); 198 } 199 setupRemoteCarTaskView(ViewGroup parent)200 private void setupRemoteCarTaskView(ViewGroup parent) { 201 Car.createCar(/* context= */ this, /* handler= */ null, 202 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 203 (car, ready) -> { 204 if (!ready) { 205 Log.w(TAG, "CarService is not ready."); 206 return; 207 } 208 CarActivityManager carAM = (CarActivityManager) car.getCarManager( 209 Car.CAR_ACTIVITY_SERVICE); 210 211 carAM.getCarTaskViewController( 212 this, 213 getMainExecutor(), 214 new CarTaskViewControllerCallback() { 215 @Override 216 public void onConnected( 217 CarTaskViewController carTaskViewController) { 218 carTaskViewController.createControlledRemoteCarTaskView( 219 new ControlledRemoteCarTaskViewConfig.Builder() 220 .setActivityIntent(getMapsIntent()) 221 .setShouldAutoRestartOnTaskRemoval(true) 222 .build(), 223 getMainExecutor(), 224 new ControlledRemoteCarTaskViewCallback() { 225 @Override 226 public void onTaskViewCreated( 227 ControlledRemoteCarTaskView taskView) { 228 mRemoteCarTaskView = taskView; 229 parent.addView(taskView); 230 } 231 232 @Override 233 public void onTaskViewInitialized() { 234 maybeLogReady(); 235 } 236 237 @Override 238 public void onTaskViewReleased() { 239 mRemoteCarTaskView = null; 240 parent.removeAllViews(); 241 } 242 }); 243 } 244 245 @Override 246 public void onDisconnected( 247 CarTaskViewController carTaskViewController) { 248 Log.d(TAG, "onDisconnected"); 249 mRemoteCarTaskView = null; 250 parent.removeAllViews(); 251 } 252 }); 253 }); 254 } 255 setUpTaskView(ViewGroup parent)256 private void setUpTaskView(ViewGroup parent) { 257 Set<String> taskViewPackages = new ArraySet<>(getResources().getStringArray( 258 R.array.config_taskViewPackages)); 259 mTaskViewManager = new TaskViewManager(this, getMainThreadHandler()); 260 261 mTaskViewManager.createControlledCarTaskView( 262 getMainExecutor(), 263 ControlledCarTaskViewConfig.builder() 264 .setActivityIntent(getMapsIntent()) 265 // TODO(b/263876526): Enable auto restart after ensuring no CTS failure. 266 .setAutoRestartOnCrash(false) 267 .build(), 268 new ControlledCarTaskViewCallbacks() { 269 @Override 270 public void onTaskViewCreated(CarTaskView taskView) { 271 parent.addView(taskView); 272 mTaskView = taskView; 273 } 274 275 @Override 276 public void onTaskViewReady() { 277 maybeLogReady(); 278 } 279 280 @Override 281 public Set<String> getDependingPackageNames() { 282 return taskViewPackages; 283 } 284 }); 285 } 286 287 @Override onStart()288 protected void onStart() { 289 super.onStart(); 290 // The TaskViewManager might have been released if the user was switched to some other user 291 // and then switched back to the previous user before the previous user is stopped. 292 // In such a case, the TaskViewManager should be recreated. 293 if (!mUseRemoteCarTaskView && mMapsCard != null && mTaskViewManager.isReleased()) { 294 setUpTaskView(mMapsCard); 295 } 296 } 297 298 @Override onResume()299 protected void onResume() { 300 super.onResume(); 301 maybeLogReady(); 302 } 303 304 @Override onDestroy()305 protected void onDestroy() { 306 super.onDestroy(); 307 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); 308 release(); 309 } 310 isTaskViewInitialized()311 private boolean isTaskViewInitialized() { 312 return (mTaskView != null && mTaskView.isInitialized()) 313 || (mRemoteCarTaskView != null && mRemoteCarTaskView.isInitialized()); 314 } 315 getTaskViewTaskId()316 private int getTaskViewTaskId() { 317 if (mTaskView != null) { 318 return mTaskView.getTaskId(); 319 } 320 if (mRemoteCarTaskView != null) { 321 return mRemoteCarTaskView.getTaskInfo() == null 322 ? INVALID_TASK_ID : mRemoteCarTaskView.getTaskInfo().taskId; 323 } 324 return INVALID_TASK_ID; 325 } 326 release()327 private void release() { 328 mTaskView = null; 329 mRemoteCarTaskView = null; 330 } 331 332 @Override onConfigurationChanged(Configuration newConfig)333 public void onConfigurationChanged(Configuration newConfig) { 334 super.onConfigurationChanged(newConfig); 335 initializeCards(); 336 } 337 initializeCards()338 private void initializeCards() { 339 if (mHomeCardModules == null) { 340 mHomeCardModules = new ArraySet<>(); 341 for (String providerClassName : getResources().getStringArray( 342 R.array.config_homeCardModuleClasses)) { 343 try { 344 long reflectionStartTime = System.currentTimeMillis(); 345 HomeCardModule cardModule = (HomeCardModule) Class.forName( 346 providerClassName).newInstance(); 347 cardModule.setViewModelProvider(new ViewModelProvider( /* owner= */this)); 348 mHomeCardModules.add(cardModule); 349 if (DEBUG) { 350 long reflectionTime = System.currentTimeMillis() - reflectionStartTime; 351 Log.d(TAG, "Initialization of HomeCardModule class " + providerClassName 352 + " took " + reflectionTime + " ms"); 353 } 354 } catch (IllegalAccessException | InstantiationException | 355 ClassNotFoundException e) { 356 Log.w(TAG, "Unable to create HomeCardProvider class " + providerClassName, e); 357 } 358 } 359 } 360 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 361 for (HomeCardModule cardModule : mHomeCardModules) { 362 transaction.replace(cardModule.getCardResId(), cardModule.getCardView().getFragment()); 363 } 364 transaction.commitNow(); 365 } 366 367 /** Logs that the Activity is ready. Used for startup time diagnostics. */ maybeLogReady()368 private void maybeLogReady() { 369 boolean isResumed = isResumed(); 370 boolean taskViewInitialized = isTaskViewInitialized(); 371 if (DEBUG) { 372 Log.d(TAG, "maybeLogReady(" + getUserId() + "): mapsReady=" 373 + taskViewInitialized + ", started=" + isResumed + ", alreadyLogged: " 374 + mIsReadyLogged); 375 } 376 if (taskViewInitialized && isResumed) { 377 // We should report every time - the Android framework will take care of logging just 378 // when it's effectively drawn for the first time, but.... 379 reportFullyDrawn(); 380 if (!mIsReadyLogged) { 381 // ... we want to manually check that the Log.i below (which is useful to show 382 // the user id) is only logged once (otherwise it would be logged every time the 383 // user taps Home) 384 Log.i(TAG, "Launcher for user " + getUserId() + " is ready"); 385 mIsReadyLogged = true; 386 } 387 } 388 } 389 390 /** Brings the Car Launcher to the foreground. */ bringToForeground()391 private void bringToForeground() { 392 if (mCarLauncherTaskId != INVALID_TASK_ID) { 393 mActivityManager.moveTaskToFront(mCarLauncherTaskId, /* flags= */ 0); 394 } 395 } 396 getMapsIntent()397 private Intent getMapsIntent() { 398 Intent mapIntent = mUseSmallCanvasOptimizedMap 399 ? CarLauncherUtils.getSmallCanvasOptimizedMapIntent(this) 400 : CarLauncherUtils.getMapsIntent(this); 401 // Don't want to show this Activity in Recents. 402 mapIntent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 403 return mapIntent; 404 } 405 } 406