• 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.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