• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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