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