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.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING; 21 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; 22 23 import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; 24 25 import android.app.ActivityManager; 26 import android.app.ActivityOptions; 27 import android.app.ActivityTaskManager; 28 import android.app.PendingIntent; 29 import android.app.TaskInfo; 30 import android.app.TaskStackListener; 31 import android.car.Car; 32 import android.car.user.CarUserManager; 33 import android.car.user.CarUserManager.UserLifecycleListener; 34 import android.content.ActivityNotFoundException; 35 import android.content.ComponentName; 36 import android.content.res.Configuration; 37 import android.os.Bundle; 38 import android.util.Log; 39 import android.view.Display; 40 import android.view.ViewGroup; 41 import android.view.WindowManager; 42 import android.window.TaskAppearedInfo; 43 44 import androidx.annotation.NonNull; 45 import androidx.collection.ArraySet; 46 import androidx.fragment.app.FragmentActivity; 47 import androidx.fragment.app.FragmentTransaction; 48 import androidx.lifecycle.ViewModelProvider; 49 50 import com.android.car.carlauncher.displayarea.CarDisplayAreaController; 51 import com.android.car.carlauncher.displayarea.CarDisplayAreaOrganizer; 52 import com.android.car.carlauncher.displayarea.CarFullscreenTaskListener; 53 import com.android.car.carlauncher.homescreen.HomeCardModule; 54 import com.android.car.carlauncher.taskstack.TaskStackChangeListeners; 55 import com.android.car.internal.common.UserHelperLite; 56 import com.android.launcher3.icons.IconProvider; 57 import com.android.wm.shell.ShellTaskOrganizer; 58 import com.android.wm.shell.TaskView; 59 import com.android.wm.shell.common.HandlerExecutor; 60 import com.android.wm.shell.startingsurface.StartingWindowController; 61 import com.android.wm.shell.startingsurface.phone.PhoneStartingWindowTypeAlgorithm; 62 63 import java.util.List; 64 import java.util.Set; 65 66 /** 67 * Basic Launcher for Android Automotive which demonstrates the use of {@link TaskView} to host 68 * maps content and uses a Model-View-Presenter structure to display content in cards. 69 * 70 * <p>Implementations of the Launcher that use the given layout of the main activity 71 * (car_launcher.xml) can customize the home screen cards by providing their own 72 * {@link HomeCardModule} for R.id.top_card or R.id.bottom_card. Otherwise, implementations that 73 * use their own layout should define their own activity rather than using this one. 74 * 75 * <p>Note: On some devices, the TaskView may render with a width, height, and/or aspect 76 * ratio that does not meet Android compatibility definitions. Developers should work with content 77 * owners to ensure content renders correctly when extending or emulating this class. 78 */ 79 public class CarLauncher extends FragmentActivity { 80 public static final String TAG = "CarLauncher"; 81 private static final boolean DEBUG = false; 82 83 private ActivityManager mActivityManager; 84 private CarUserManager mCarUserManager; 85 private ShellTaskOrganizer mShellTaskOrganizer; 86 private TaskViewManager mTaskViewManager; 87 88 private TaskView mTaskView; 89 private boolean mTaskViewReady; 90 // Tracking this to check if the task in TaskView has crashed in the background. 91 private int mTaskViewTaskId = INVALID_TASK_ID; 92 private boolean mIsResumed; 93 private boolean mFocused; 94 private int mCarLauncherTaskId = INVALID_TASK_ID; 95 private Set<HomeCardModule> mHomeCardModules; 96 97 /** Set to {@code true} once we've logged that the Activity is fully drawn. */ 98 private boolean mIsReadyLogged; 99 100 // The callback methods in {@code mTaskViewListener} are running under MainThread. 101 private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { 102 @Override 103 public void onInitialized() { 104 if (DEBUG) Log.d(TAG, "onInitialized(" + getUserId() + ")"); 105 mTaskViewReady = true; 106 startMapsInTaskView(); 107 maybeLogReady(); 108 } 109 110 @Override 111 public void onReleased() { 112 if (DEBUG) Log.d(TAG, "onReleased(" + getUserId() + ")"); 113 mTaskViewReady = false; 114 } 115 116 @Override 117 public void onTaskCreated(int taskId, ComponentName name) { 118 if (DEBUG) Log.d(TAG, "onTaskCreated: taskId=" + taskId); 119 mTaskViewTaskId = taskId; 120 } 121 122 @Override 123 public void onTaskRemovalStarted(int taskId) { 124 if (DEBUG) Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId); 125 mTaskViewTaskId = INVALID_TASK_ID; 126 } 127 }; 128 129 private final TaskStackListener mTaskStackListener = new TaskStackListener() { 130 @Override 131 public void onTaskFocusChanged(int taskId, boolean focused) { 132 mFocused = taskId == mCarLauncherTaskId && focused; 133 if (DEBUG) { 134 Log.d(TAG, "onTaskFocusChanged: mFocused=" + mFocused 135 + ", mTaskViewTaskId=" + mTaskViewTaskId); 136 } 137 if (mFocused && mTaskViewTaskId == INVALID_TASK_ID) { 138 // If the task in TaskView is crashed during CarLauncher is background, 139 // We'd like to restart it when CarLauncher becomes foreground and focused. 140 startMapsInTaskView(); 141 } 142 } 143 144 @Override 145 public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, 146 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { 147 if (!homeTaskVisible && mTaskViewTaskId == task.taskId) { 148 // The embedded map component received an intent, therefore forcibly bringing the 149 // launcher to the foreground. 150 bringToForeground(); 151 } 152 } 153 }; 154 155 private final UserLifecycleListener mUserLifecyleListener = new UserLifecycleListener() { 156 @Override 157 public void onEvent(@NonNull CarUserManager.UserLifecycleEvent event) { 158 if (event.getEventType() == USER_LIFECYCLE_EVENT_TYPE_SWITCHING) { 159 // When user-switching, onDestroy in the previous user's CarLauncher isn't called. 160 // So tries to release the resource explicitly. 161 release(); 162 } 163 } 164 }; 165 166 @Override onCreate(Bundle savedInstanceState)167 protected void onCreate(Bundle savedInstanceState) { 168 super.onCreate(savedInstanceState); 169 170 // If policy provider is defined then AppGridActivity should be launched. 171 // TODO: update this code flow. Maybe have some kind of configurable activity. 172 if (CarLauncherUtils.isCustomDisplayPolicyDefined(this)) { 173 CarLauncherApplication application = (CarLauncherApplication) getApplication(); 174 175 mShellTaskOrganizer = new ShellTaskOrganizer( 176 application.getShellExecutor(), this); 177 CarFullscreenTaskListener fullscreenTaskListener = new CarFullscreenTaskListener( 178 this, application.getSyncTransactionQueue(), 179 CarDisplayAreaController.getInstance()); 180 mShellTaskOrganizer.addListenerForType( 181 fullscreenTaskListener, TASK_LISTENER_TYPE_FULLSCREEN); 182 StartingWindowController startingController = 183 new StartingWindowController(this, application.getShellExecutor(), 184 new PhoneStartingWindowTypeAlgorithm(), new IconProvider(this), 185 application.getTransactionPool()); 186 mShellTaskOrganizer.initStartingWindow(startingController); 187 List<TaskAppearedInfo> taskAppearedInfos = mShellTaskOrganizer.registerOrganizer(); 188 try { 189 cleanUpExistingTaskViewTasks(taskAppearedInfos); 190 } catch (Exception ex) { 191 Log.w(TAG, "some of the tasks couldn't be cleaned up: ", ex); 192 } 193 CarDisplayAreaController carDisplayAreaController = 194 CarDisplayAreaController.getInstance(); 195 CarDisplayAreaOrganizer org = carDisplayAreaController.getOrganizer(); 196 org.startControlBarInDisplayArea(); 197 org.startMapsInBackGroundDisplayArea(); 198 return; 199 } 200 201 Car.createCar(getApplicationContext(), /* handler= */ null, 202 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 203 (car, ready) -> { 204 if (!ready) return; 205 mCarUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE); 206 mCarUserManager.addListener(getMainExecutor(), mUserLifecyleListener); 207 }); 208 209 mActivityManager = getSystemService(ActivityManager.class); 210 mCarLauncherTaskId = getTaskId(); 211 TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); 212 213 // Setting as trusted overlay to let touches pass through. 214 getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY); 215 // To pass touches to the underneath task. 216 getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); 217 218 // Don't show the maps panel in multi window mode. 219 // NOTE: CTS tests for split screen are not compatible with activity views on the default 220 // activity of the launcher 221 if (isInMultiWindowMode() || isInPictureInPictureMode()) { 222 setContentView(R.layout.car_launcher_multiwindow); 223 } else { 224 setContentView(R.layout.car_launcher); 225 // We don't want to show Map card unnecessarily for the headless user 0. 226 if (!UserHelperLite.isHeadlessSystemUser(getUserId())) { 227 ViewGroup mapsCard = findViewById(R.id.maps_card); 228 if (mapsCard != null) { 229 setUpTaskView(mapsCard); 230 } 231 } 232 } 233 initializeCards(); 234 } 235 cleanUpExistingTaskViewTasks(List<TaskAppearedInfo> taskAppearedInfos)236 private static void cleanUpExistingTaskViewTasks(List<TaskAppearedInfo> taskAppearedInfos) { 237 ActivityTaskManager atm = ActivityTaskManager.getInstance(); 238 for (TaskAppearedInfo taskAppearedInfo : taskAppearedInfos) { 239 TaskInfo taskInfo = taskAppearedInfo.getTaskInfo(); 240 try { 241 atm.removeTask(taskInfo.taskId); 242 } catch (Exception e) { 243 if (DEBUG) { 244 Log.d(TAG, "failed to remove task likely b/c it no longer exists " + taskInfo); 245 } 246 } 247 } 248 } 249 setUpTaskView(ViewGroup parent)250 private void setUpTaskView(ViewGroup parent) { 251 mTaskViewManager = new TaskViewManager(this, 252 new HandlerExecutor(getMainThreadHandler())); 253 mTaskViewManager.createTaskView(taskView -> { 254 taskView.setListener(getMainExecutor(), mTaskViewListener); 255 parent.addView(taskView); 256 mTaskView = taskView; 257 }); 258 } 259 260 @Override onResume()261 protected void onResume() { 262 super.onResume(); 263 mIsResumed = true; 264 maybeLogReady(); 265 if (DEBUG) { 266 Log.d(TAG, "onResume: mFocused=" + mFocused + ", mTaskViewTaskId=" + mTaskViewTaskId); 267 } 268 if (!mTaskViewReady) return; 269 if (mTaskViewTaskId != INVALID_TASK_ID) { 270 // The task in TaskView should be in top to make it visible. 271 // NOTE: Tried setTaskAlwaysOnTop before, the flag has some side effect to hinder 272 // AccessibilityService from finding the correct window geometry: b/197247311 273 mActivityManager.moveTaskToFront(mTaskViewTaskId, /* flags= */ 0); 274 } 275 } 276 277 @Override onPause()278 protected void onPause() { 279 super.onPause(); 280 mIsResumed = false; 281 } 282 283 @Override onDestroy()284 protected void onDestroy() { 285 super.onDestroy(); 286 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); 287 release(); 288 } 289 release()290 private void release() { 291 if (mShellTaskOrganizer != null) { 292 mShellTaskOrganizer.unregisterOrganizer(); 293 } 294 if (mTaskView != null && mTaskViewReady) { 295 mTaskView.release(); 296 mTaskView = null; 297 } 298 if (mTaskViewManager != null) { 299 mTaskViewManager.release(); 300 } 301 } 302 startMapsInTaskView()303 private void startMapsInTaskView() { 304 if (mTaskView == null || !mTaskViewReady) { 305 return; 306 } 307 // If we happen to be be resurfaced into a multi display mode we skip launching content 308 // in the activity view as we will get recreated anyway. 309 if (isInMultiWindowMode() || isInPictureInPictureMode()) { 310 return; 311 } 312 // Don't start Maps when the display is off for ActivityVisibilityTests. 313 if (getDisplay().getState() != Display.STATE_ON) { 314 return; 315 } 316 try { 317 ActivityOptions options = ActivityOptions.makeCustomAnimation(this, 318 /* enterResId= */ 0, /* exitResId= */ 0); 319 mTaskView.startActivity( 320 PendingIntent.getActivity(this, /* requestCode= */ 0, 321 CarLauncherUtils.getMapsIntent(this), 322 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT), 323 /* fillInIntent= */ null, options, null /* launchBounds */); 324 } catch (ActivityNotFoundException e) { 325 Log.w(TAG, "Maps activity not found", e); 326 } 327 } 328 329 @Override onConfigurationChanged(Configuration newConfig)330 public void onConfigurationChanged(Configuration newConfig) { 331 super.onConfigurationChanged(newConfig); 332 if (CarLauncherUtils.isCustomDisplayPolicyDefined(this)) { 333 return; 334 } 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()); 363 } 364 transaction.commitNow(); 365 } 366 367 /** Logs that the Activity is ready. Used for startup time diagnostics. */ maybeLogReady()368 private void maybeLogReady() { 369 if (DEBUG) { 370 Log.d(TAG, "maybeLogReady(" + getUserId() + "): activityReady=" + mTaskViewReady 371 + ", started=" + mIsResumed + ", alreadyLogged: " + mIsReadyLogged); 372 } 373 if (mTaskViewReady && mIsResumed) { 374 // We should report every time - the Android framework will take care of logging just 375 // when it's effectively drawn for the first time, but.... 376 reportFullyDrawn(); 377 if (!mIsReadyLogged) { 378 // ... we want to manually check that the Log.i below (which is useful to show 379 // the user id) is only logged once (otherwise it would be logged every time the 380 // user taps Home) 381 Log.i(TAG, "Launcher for user " + getUserId() + " is ready"); 382 mIsReadyLogged = true; 383 } 384 } 385 } 386 387 /** Brings the Car Launcher to the foreground. */ bringToForeground()388 private void bringToForeground() { 389 if (mCarLauncherTaskId != INVALID_TASK_ID) { 390 mActivityManager.moveTaskToFront(mCarLauncherTaskId, /* flags= */ 0); 391 } 392 } 393 } 394