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