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