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.ActivityOptions; 23 import android.app.ActivityTaskManager; 24 import android.app.PendingIntent; 25 import android.app.TaskStackListener; 26 import android.content.ActivityNotFoundException; 27 import android.content.ComponentName; 28 import android.content.Intent; 29 import android.content.pm.PackageManager; 30 import android.content.res.Configuration; 31 import android.os.Bundle; 32 import android.util.Log; 33 import android.view.Display; 34 import android.view.ViewGroup; 35 import android.view.WindowManager; 36 37 import androidx.collection.ArraySet; 38 import androidx.fragment.app.FragmentActivity; 39 import androidx.fragment.app.FragmentTransaction; 40 import androidx.lifecycle.ViewModelProvider; 41 42 import com.android.car.carlauncher.homescreen.HomeCardModule; 43 import com.android.car.internal.common.UserHelperLite; 44 import com.android.wm.shell.TaskView; 45 import com.android.wm.shell.common.HandlerExecutor; 46 47 import java.net.URISyntaxException; 48 import java.util.Set; 49 50 /** 51 * Basic Launcher for Android Automotive which demonstrates the use of {@link TaskView} to host 52 * maps content and uses a Model-View-Presenter structure to display content in cards. 53 * 54 * <p>Implementations of the Launcher that use the given layout of the main activity 55 * (car_launcher.xml) can customize the home screen cards by providing their own 56 * {@link HomeCardModule} for R.id.top_card or R.id.bottom_card. Otherwise, implementations that 57 * use their own layout should define their own activity rather than using this one. 58 * 59 * <p>Note: On some devices, the TaskView may render with a width, height, and/or aspect 60 * ratio that does not meet Android compatibility definitions. Developers should work with content 61 * owners to ensure content renders correctly when extending or emulating this class. 62 */ 63 public class CarLauncher extends FragmentActivity { 64 public static final String TAG = "CarLauncher"; 65 private static final boolean DEBUG = false; 66 67 private TaskViewManager mTaskViewManager; 68 private TaskView mTaskView; 69 private boolean mTaskViewReady; 70 // Tracking this to check if the task in TaskView has crashed in the background. 71 private int mTaskViewTaskId = INVALID_TASK_ID; 72 private boolean mIsResumed; 73 private boolean mFocused; 74 private int mCarLauncherTaskId = INVALID_TASK_ID; 75 private Set<HomeCardModule> mHomeCardModules; 76 77 /** Set to {@code true} once we've logged that the Activity is fully drawn. */ 78 private boolean mIsReadyLogged; 79 80 // The callback methods in {@code mTaskViewListener} are running under MainThread. 81 private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { 82 @Override 83 public void onInitialized() { 84 if (DEBUG) Log.d(TAG, "onInitialized(" + getUserId() + ")"); 85 mTaskViewReady = true; 86 startMapsInTaskView(); 87 maybeLogReady(); 88 } 89 90 @Override 91 public void onReleased() { 92 if (DEBUG) Log.d(TAG, "onReleased(" + getUserId() + ")"); 93 mTaskViewReady = false; 94 } 95 96 @Override 97 public void onTaskCreated(int taskId, ComponentName name) { 98 if (DEBUG) Log.d(TAG, "onTaskCreated: taskId=" + taskId); 99 mTaskViewTaskId = taskId; 100 } 101 102 @Override 103 public void onTaskRemovalStarted(int taskId) { 104 if (DEBUG) Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId); 105 mTaskViewTaskId = INVALID_TASK_ID; 106 } 107 }; 108 109 private final TaskStackListener mTaskStackListener = new TaskStackListener() { 110 @Override 111 public void onTaskFocusChanged(int taskId, boolean focused) { 112 mFocused = taskId == mCarLauncherTaskId && focused; 113 if (DEBUG) { 114 Log.d(TAG, "onTaskFocusChanged: mFocused=" + mFocused 115 + ", mTaskViewTaskId=" + mTaskViewTaskId); 116 } 117 if (mFocused && mTaskViewTaskId == INVALID_TASK_ID) { 118 startMapsInTaskView(); 119 } 120 } 121 }; 122 123 @Override onCreate(Bundle savedInstanceState)124 protected void onCreate(Bundle savedInstanceState) { 125 super.onCreate(savedInstanceState); 126 127 mCarLauncherTaskId = getTaskId(); 128 ActivityTaskManager.getInstance().registerTaskStackListener(mTaskStackListener); 129 130 // Setting as trusted overlay to let touches pass through. 131 getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY); 132 // To pass touches to the underneath task. 133 getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); 134 135 // Don't show the maps panel in multi window mode. 136 // NOTE: CTS tests for split screen are not compatible with activity views on the default 137 // activity of the launcher 138 if (isInMultiWindowMode() || isInPictureInPictureMode()) { 139 setContentView(R.layout.car_launcher_multiwindow); 140 } else { 141 setContentView(R.layout.car_launcher); 142 // We don't want to show Map card unnecessarily for the headless user 0. 143 if (!UserHelperLite.isHeadlessSystemUser(getUserId())) { 144 ViewGroup mapsCard = findViewById(R.id.maps_card); 145 if (mapsCard != null) { 146 setUpTaskView(mapsCard); 147 } 148 } 149 } 150 initializeCards(); 151 } 152 setUpTaskView(ViewGroup parent)153 private void setUpTaskView(ViewGroup parent) { 154 mTaskViewManager = new TaskViewManager(this, 155 new HandlerExecutor(getMainThreadHandler())); 156 mTaskViewManager.createTaskView(taskView -> { 157 taskView.setListener(getMainExecutor(), mTaskViewListener); 158 parent.addView(taskView); 159 mTaskView = taskView; 160 }); 161 } 162 163 @Override onResume()164 protected void onResume() { 165 super.onResume(); 166 mIsResumed = true; 167 maybeLogReady(); 168 if (DEBUG) { 169 Log.d(TAG, "onResume: mFocused=" + mFocused + ", mTaskViewTaskId=" + mTaskViewTaskId); 170 } 171 if (mFocused && mTaskViewTaskId == INVALID_TASK_ID) { 172 // If the task in TaskView is crashed during CarLauncher is background, 173 // We'd like to restart it when CarLauncher becomes foreground. 174 startMapsInTaskView(); 175 } 176 } 177 178 @Override onPause()179 protected void onPause() { 180 super.onPause(); 181 mIsResumed = false; 182 } 183 184 @Override onDestroy()185 protected void onDestroy() { 186 super.onDestroy(); 187 ActivityTaskManager.getInstance().unregisterTaskStackListener(mTaskStackListener); 188 if (mTaskView != null && mTaskViewReady) { 189 mTaskView.release(); 190 mTaskView = null; 191 } 192 } 193 startMapsInTaskView()194 private void startMapsInTaskView() { 195 if (mTaskView == null || !mTaskViewReady) { 196 return; 197 } 198 // If we happen to be be resurfaced into a multi display mode we skip launching content 199 // in the activity view as we will get recreated anyway. 200 if (isInMultiWindowMode() || isInPictureInPictureMode()) { 201 return; 202 } 203 // Don't start Maps when the display is off for ActivityVisibilityTests. 204 if (getDisplay().getState() != Display.STATE_ON) { 205 return; 206 } 207 try { 208 ActivityOptions options = ActivityOptions.makeCustomAnimation(this, 209 /* enterResId= */ 0, /* exitResId= */ 0); 210 // To show the Activity in TaskView, the Activity should be above the host task in 211 // ActivityStack. This option only effects the host Activity is in resumed. 212 options.setTaskAlwaysOnTop(true); 213 mTaskView.startActivity( 214 PendingIntent.getActivity(this, /* requestCode= */ 0, getMapsIntent(), 215 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT), 216 /* fillInIntent= */ null, options, null /* launchBounds */); 217 } catch (ActivityNotFoundException e) { 218 Log.w(TAG, "Maps activity not found", e); 219 } 220 } 221 getMapsIntent()222 private Intent getMapsIntent() { 223 Intent defaultIntent = 224 Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_MAPS); 225 PackageManager pm = getPackageManager(); 226 ComponentName defaultActivity = defaultIntent.resolveActivity(pm); 227 228 for (String intentUri : getResources().getStringArray( 229 R.array.config_homeCardPreferredMapActivities)) { 230 Intent preferredIntent; 231 try { 232 preferredIntent = Intent.parseUri(intentUri, Intent.URI_ANDROID_APP_SCHEME); 233 } catch (URISyntaxException se) { 234 Log.w(TAG, "Invalid intent URI in config_homeCardPreferredMapActivities", se); 235 continue; 236 } 237 238 if (defaultActivity != null && !defaultActivity.getPackageName().equals( 239 preferredIntent.getPackage())) { 240 continue; 241 } 242 243 if (preferredIntent.resolveActivityInfo(pm, /* flags= */ 0) != null) { 244 return preferredIntent; 245 } 246 } 247 return defaultIntent; 248 } 249 250 @Override onConfigurationChanged(Configuration newConfig)251 public void onConfigurationChanged(Configuration newConfig) { 252 super.onConfigurationChanged(newConfig); 253 initializeCards(); 254 } 255 initializeCards()256 private void initializeCards() { 257 if (mHomeCardModules == null) { 258 mHomeCardModules = new ArraySet<>(); 259 for (String providerClassName : getResources().getStringArray( 260 R.array.config_homeCardModuleClasses)) { 261 try { 262 long reflectionStartTime = System.currentTimeMillis(); 263 HomeCardModule cardModule = (HomeCardModule) Class.forName( 264 providerClassName).newInstance(); 265 cardModule.setViewModelProvider(new ViewModelProvider( /* owner= */this)); 266 mHomeCardModules.add(cardModule); 267 if (DEBUG) { 268 long reflectionTime = System.currentTimeMillis() - reflectionStartTime; 269 Log.d(TAG, "Initialization of HomeCardModule class " + providerClassName 270 + " took " + reflectionTime + " ms"); 271 } 272 } catch (IllegalAccessException | InstantiationException | 273 ClassNotFoundException e) { 274 Log.w(TAG, "Unable to create HomeCardProvider class " + providerClassName, e); 275 } 276 } 277 } 278 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 279 for (HomeCardModule cardModule : mHomeCardModules) { 280 transaction.replace(cardModule.getCardResId(), cardModule.getCardView()); 281 } 282 transaction.commitNow(); 283 } 284 285 /** Logs that the Activity is ready. Used for startup time diagnostics. */ maybeLogReady()286 private void maybeLogReady() { 287 if (DEBUG) { 288 Log.d(TAG, "maybeLogReady(" + getUserId() + "): activityReady=" + mTaskViewReady 289 + ", started=" + mIsResumed + ", alreadyLogged: " + mIsReadyLogged); 290 } 291 if (mTaskViewReady && mIsResumed) { 292 // We should report every time - the Android framework will take care of logging just 293 // when it's effectively drawn for the first time, but.... 294 reportFullyDrawn(); 295 if (!mIsReadyLogged) { 296 // ... we want to manually check that the Log.i below (which is useful to show 297 // the user id) is only logged once (otherwise it would be logged every time the 298 // user taps Home) 299 Log.i(TAG, "Launcher for user " + getUserId() + " is ready"); 300 mIsReadyLogged = true; 301 } 302 } 303 } 304 } 305