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.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED; 21 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; 22 23 import static com.android.car.carlauncher.CarLauncherViewModel.CarLauncherViewModelFactory; 24 25 import android.app.ActivityManager; 26 import android.app.ActivityOptions; 27 import android.app.TaskStackListener; 28 import android.car.Car; 29 import android.content.ComponentName; 30 import android.content.Intent; 31 import android.content.res.Configuration; 32 import android.database.ContentObserver; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.UserManager; 36 import android.provider.Settings; 37 import android.util.Log; 38 import android.view.Display; 39 import android.view.View; 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.homescreen.audio.IntentHandler; 50 import com.android.car.carlauncher.homescreen.audio.media.MediaIntentRouter; 51 import com.android.car.carlauncher.taskstack.TaskStackChangeListeners; 52 import com.android.car.internal.common.UserHelperLite; 53 import com.android.wm.shell.taskview.TaskView; 54 55 import com.google.common.annotations.VisibleForTesting; 56 57 import java.util.Set; 58 59 /** 60 * Basic Launcher for Android Automotive which demonstrates the use of {@link TaskView} to host 61 * maps content and uses a Model-View-Presenter structure to display content in cards. 62 * 63 * <p>Implementations of the Launcher that use the given layout of the main activity 64 * (car_launcher.xml) can customize the home screen cards by providing their own 65 * {@link HomeCardModule} for R.id.top_card or R.id.bottom_card. Otherwise, implementations that 66 * use their own layout should define their own activity rather than using this one. 67 * 68 * <p>Note: On some devices, the TaskView may render with a width, height, and/or aspect 69 * ratio that does not meet Android compatibility definitions. Developers should work with content 70 * owners to ensure content renders correctly when extending or emulating this class. 71 */ 72 public class CarLauncher extends FragmentActivity { 73 public static final String TAG = "CarLauncher"; 74 public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 75 76 private ActivityManager mActivityManager; 77 private Car mCar; 78 private int mCarLauncherTaskId = INVALID_TASK_ID; 79 private Set<HomeCardModule> mHomeCardModules; 80 81 /** Set to {@code true} once we've logged that the Activity is fully drawn. */ 82 private boolean mIsReadyLogged; 83 private boolean mUseSmallCanvasOptimizedMap; 84 private ViewGroup mMapsCard; 85 private CarLauncherViewModel mCarLauncherViewModel; 86 87 @VisibleForTesting 88 ContentObserver mTosContentObserver; 89 90 private final TaskStackListener mTaskStackListener = new TaskStackListener() { 91 @Override 92 public void onTaskFocusChanged(int taskId, boolean focused) { 93 } 94 95 @Override 96 public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, 97 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { 98 if (DEBUG) { 99 Log.d(TAG, "onActivityRestartAttempt: taskId=" + task.taskId 100 + ", homeTaskVisible=" + homeTaskVisible + ", wasVisible=" + wasVisible); 101 } 102 if (!mUseSmallCanvasOptimizedMap 103 && !homeTaskVisible 104 && getTaskViewTaskId() == task.taskId) { 105 // The embedded map component received an intent, therefore forcibly bringing the 106 // launcher to the foreground. 107 bringToForeground(); 108 } 109 } 110 }; 111 112 private final IntentHandler mMediaIntentHandler = new IntentHandler() { 113 @Override 114 public void handleIntent(Intent intent) { 115 if (intent != null) { 116 ActivityOptions options = ActivityOptions.makeBasic(); 117 options.setLaunchDisplayId(getDisplay().getDisplayId()); 118 startActivity(intent, options.toBundle()); 119 } 120 } 121 }; 122 123 @Override onCreate(Bundle savedInstanceState)124 protected void onCreate(Bundle savedInstanceState) { 125 super.onCreate(savedInstanceState); 126 127 if (DEBUG) { 128 Log.d(TAG, "onCreate(" + getUserId() + ") displayId=" + getDisplayId()); 129 } 130 // Since MUMD is introduced, CarLauncher can be called in the main display of visible users. 131 // In ideal shape, CarLauncher should handle both driver and passengers together. 132 // But, in the mean time, we have separate launchers for driver and passengers, so 133 // CarLauncher needs to reroute the request to Passenger launcher if it is invoked from 134 // the main display of passengers (not driver). 135 // For MUPAND, PassengerLauncher should be the default launcher. 136 // For non-main displays, ATM will invoke SECONDARY_HOME Intent, so the secondary launcher 137 // should handle them. 138 UserManager um = getSystemService(UserManager.class); 139 boolean isPassengerDisplay = getDisplayId() != Display.DEFAULT_DISPLAY 140 || um.isVisibleBackgroundUsersOnDefaultDisplaySupported(); 141 if (isPassengerDisplay) { 142 String passengerLauncherName = getString(R.string.config_passengerLauncherComponent); 143 Intent passengerHomeIntent; 144 if (!passengerLauncherName.isEmpty()) { 145 ComponentName component = ComponentName.unflattenFromString(passengerLauncherName); 146 if (component == null) { 147 throw new IllegalStateException( 148 "Invalid passengerLauncher name=" + passengerLauncherName); 149 } 150 passengerHomeIntent = new Intent(Intent.ACTION_MAIN) 151 // passenger launcher should be launched in home task in order to 152 // fix TaskView layering issue 153 .addCategory(Intent.CATEGORY_HOME) 154 .setComponent(component); 155 } else { 156 // No passenger launcher is specified, then use AppsGrid as a fallback. 157 passengerHomeIntent = CarLauncherUtils.getAppsGridIntent(); 158 } 159 ActivityOptions options = ActivityOptions 160 // No animation for the trampoline. 161 .makeCustomAnimation(this, /* enterResId=*/ 0, /* exitResId= */ 0) 162 .setLaunchDisplayId(getDisplayId()); 163 startActivity(passengerHomeIntent, options.toBundle()); 164 finish(); 165 return; 166 } 167 168 mUseSmallCanvasOptimizedMap = 169 CarLauncherUtils.isSmallCanvasOptimizedMapIntentConfigured(this); 170 171 mActivityManager = getSystemService(ActivityManager.class); 172 mCarLauncherTaskId = getTaskId(); 173 TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); 174 175 // Setting as trusted overlay to let touches pass through. 176 getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY); 177 // To pass touches to the underneath task. 178 getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); 179 180 // Don't show the maps panel in multi window mode. 181 // NOTE: CTS tests for split screen are not compatible with activity views on the default 182 // activity of the launcher 183 if (isInMultiWindowMode() || isInPictureInPictureMode()) { 184 setContentView(R.layout.car_launcher_multiwindow); 185 } else { 186 setContentView(R.layout.car_launcher); 187 // We don't want to show Map card unnecessarily for the headless user 0. 188 if (!UserHelperLite.isHeadlessSystemUser(getUserId())) { 189 mMapsCard = findViewById(R.id.maps_card); 190 if (mMapsCard != null) { 191 setupRemoteCarTaskView(mMapsCard); 192 } 193 } 194 } 195 MediaIntentRouter.getInstance().registerMediaIntentHandler(mMediaIntentHandler); 196 initializeCards(); 197 setupContentObserversForTos(); 198 } 199 setupRemoteCarTaskView(ViewGroup parent)200 private void setupRemoteCarTaskView(ViewGroup parent) { 201 mCarLauncherViewModel = new ViewModelProvider(this, 202 new CarLauncherViewModelFactory(this, getMapsIntent())) 203 .get(CarLauncherViewModel.class); 204 205 getLifecycle().addObserver(mCarLauncherViewModel); 206 addOnNewIntentListener(mCarLauncherViewModel.getNewIntentListener()); 207 208 mCarLauncherViewModel.getRemoteCarTaskView().observe(this, taskView -> { 209 if (taskView == null || taskView.getParent() == parent) { 210 // Discard if the parent is still the same because it doesn't signify a config 211 // change. 212 return; 213 } 214 if (taskView.getParent() != null) { 215 // Discard the previous parent as its invalid now. 216 ((ViewGroup) taskView.getParent()).removeView(taskView); 217 } 218 parent.removeAllViews(); // Just a defense against a dirty parent. 219 parent.addView(taskView); 220 }); 221 } 222 223 @Override onResume()224 protected void onResume() { 225 super.onResume(); 226 maybeLogReady(); 227 } 228 229 @Override onDestroy()230 protected void onDestroy() { 231 super.onDestroy(); 232 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); 233 if (mTosContentObserver != null) { 234 Log.i(TAG, "Unregister content observer for tos state"); 235 getContentResolver().unregisterContentObserver(mTosContentObserver); 236 mTosContentObserver = null; 237 } 238 release(); 239 } 240 getTaskViewTaskId()241 private int getTaskViewTaskId() { 242 if (mCarLauncherViewModel != null) { 243 return mCarLauncherViewModel.getRemoteCarTaskViewTaskId(); 244 } 245 return INVALID_TASK_ID; 246 } 247 release()248 private void release() { 249 if (mMapsCard != null) { 250 // This is important as the TaskView is preserved during config change in ViewModel and 251 // to avoid the memory leak, it should be plugged out of the View hierarchy. 252 mMapsCard.removeAllViews(); 253 mMapsCard = null; 254 } 255 256 if (mCar != null) { 257 mCar.disconnect(); 258 mCar = null; 259 } 260 } 261 262 @Override onConfigurationChanged(Configuration newConfig)263 public void onConfigurationChanged(Configuration newConfig) { 264 super.onConfigurationChanged(newConfig); 265 initializeCards(); 266 } 267 initializeCards()268 private void initializeCards() { 269 if (mHomeCardModules == null) { 270 mHomeCardModules = new ArraySet<>(); 271 for (String providerClassName : getResources().getStringArray( 272 R.array.config_homeCardModuleClasses)) { 273 try { 274 long reflectionStartTime = System.currentTimeMillis(); 275 HomeCardModule cardModule = (HomeCardModule) 276 Class.forName(providerClassName).newInstance(); 277 if (Flags.mediaCardFullscreen()) { 278 if (cardModule.getCardResId() == R.id.top_card) { 279 findViewById(R.id.top_card).setVisibility(View.GONE); 280 } 281 } 282 cardModule.setViewModelProvider(new ViewModelProvider(/* owner= */this)); 283 mHomeCardModules.add(cardModule); 284 if (DEBUG) { 285 long reflectionTime = System.currentTimeMillis() - reflectionStartTime; 286 Log.d(TAG, "Initialization of HomeCardModule class " + providerClassName 287 + " took " + reflectionTime + " ms"); 288 } 289 } catch (IllegalAccessException | InstantiationException 290 | ClassNotFoundException e) { 291 Log.w(TAG, "Unable to create HomeCardProvider class " + providerClassName, e); 292 } 293 } 294 } 295 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 296 for (HomeCardModule cardModule : mHomeCardModules) { 297 transaction.replace(cardModule.getCardResId(), cardModule.getCardView().getFragment()); 298 } 299 transaction.commitNow(); 300 } 301 302 /** Logs that the Activity is ready. Used for startup time diagnostics. */ maybeLogReady()303 private void maybeLogReady() { 304 boolean isResumed = isResumed(); 305 if (isResumed) { 306 // We should report every time - the Android framework will take care of logging just 307 // when it's effectively drawn for the first time, but.... 308 reportFullyDrawn(); 309 if (!mIsReadyLogged) { 310 // ... we want to manually check that the Log.i below (which is useful to show 311 // the user id) is only logged once (otherwise it would be logged every time the 312 // user taps Home) 313 Log.i(TAG, "Launcher for user " + getUserId() + " is ready"); 314 mIsReadyLogged = true; 315 } 316 } 317 } 318 319 /** Brings the Car Launcher to the foreground. */ bringToForeground()320 private void bringToForeground() { 321 if (mCarLauncherTaskId != INVALID_TASK_ID) { 322 mActivityManager.moveTaskToFront(mCarLauncherTaskId, /* flags= */ 0); 323 } 324 } 325 326 @VisibleForTesting getMapsIntent()327 protected Intent getMapsIntent() { 328 Intent mapIntent = mUseSmallCanvasOptimizedMap 329 ? CarLauncherUtils.getSmallCanvasOptimizedMapIntent(this) 330 : CarLauncherUtils.getMapsIntent(this); 331 332 String packageName = mapIntent.getComponent() != null 333 ? mapIntent.getComponent().getPackageName() 334 : null; 335 Set<String> tosDisabledPackages = AppLauncherUtils.getTosDisabledPackages(this); 336 337 // Launch tos map intent when the user has not accepted tos and when the 338 // default maps package is not available to package manager, or it's disabled by tos 339 if (!AppLauncherUtils.tosAccepted(this) 340 && (packageName == null || tosDisabledPackages.contains(packageName))) { 341 mapIntent = CarLauncherUtils.getTosMapIntent(this); 342 Log.i(TAG, "Launching tos activity in task view"); 343 } 344 // Don't want to show this Activity in Recents. 345 mapIntent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 346 return mapIntent; 347 } 348 setupContentObserversForTos()349 private void setupContentObserversForTos() { 350 if (AppLauncherUtils.tosStatusUninitialized(/* context = */ this) 351 || !AppLauncherUtils.tosAccepted(/* context = */ this)) { 352 Log.i(TAG, "TOS not accepted, setting up content observers for TOS state"); 353 } else { 354 Log.i(TAG, "TOS accepted, state will remain accepted, " 355 + "don't need to observe this value"); 356 return; 357 } 358 mTosContentObserver = new ContentObserver(new Handler()) { 359 @Override 360 public void onChange(boolean selfChange) { 361 super.onChange(selfChange); 362 // TODO (b/280077391): Release the remote task view and recreate the map activity 363 Log.i(TAG, "TOS state updated:" + AppLauncherUtils.tosAccepted(getBaseContext())); 364 recreate(); 365 } 366 }; 367 getContentResolver().registerContentObserver( 368 Settings.Secure.getUriFor(KEY_USER_TOS_ACCEPTED), 369 /* notifyForDescendants*/ false, 370 mTosContentObserver); 371 } 372 } 373