blob: fdf877106448ee8b61ef31bb7c4c62a1018b19cd [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.carlauncher;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.app.TaskStackListener;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.collection.ArraySet;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import com.android.car.carlauncher.homescreen.HomeCardModule;
import com.android.car.internal.common.UserHelperLite;
import com.android.wm.shell.TaskView;
import com.android.wm.shell.common.HandlerExecutor;
import java.net.URISyntaxException;
import java.util.Set;
/**
* Basic Launcher for Android Automotive which demonstrates the use of {@link TaskView} to host
* maps content and uses a Model-View-Presenter structure to display content in cards.
*
* <p>Implementations of the Launcher that use the given layout of the main activity
* (car_launcher.xml) can customize the home screen cards by providing their own
* {@link HomeCardModule} for R.id.top_card or R.id.bottom_card. Otherwise, implementations that
* use their own layout should define their own activity rather than using this one.
*
* <p>Note: On some devices, the TaskView may render with a width, height, and/or aspect
* ratio that does not meet Android compatibility definitions. Developers should work with content
* owners to ensure content renders correctly when extending or emulating this class.
*/
public class CarLauncher extends FragmentActivity {
public static final String TAG = "CarLauncher";
private static final boolean DEBUG = false;
private TaskViewManager mTaskViewManager;
private TaskView mTaskView;
private boolean mTaskViewReady;
// Tracking this to check if the task in TaskView has crashed in the background.
private int mTaskViewTaskId = INVALID_TASK_ID;
private boolean mIsResumed;
private boolean mFocused;
private int mCarLauncherTaskId = INVALID_TASK_ID;
private Set<HomeCardModule> mHomeCardModules;
/** Set to {@code true} once we've logged that the Activity is fully drawn. */
private boolean mIsReadyLogged;
// The callback methods in {@code mTaskViewListener} are running under MainThread.
private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
@Override
public void onInitialized() {
if (DEBUG) Log.d(TAG, "onInitialized(" + getUserId() + ")");
mTaskViewReady = true;
startMapsInTaskView();
maybeLogReady();
}
@Override
public void onReleased() {
if (DEBUG) Log.d(TAG, "onReleased(" + getUserId() + ")");
mTaskViewReady = false;
}
@Override
public void onTaskCreated(int taskId, ComponentName name) {
if (DEBUG) Log.d(TAG, "onTaskCreated: taskId=" + taskId);
mTaskViewTaskId = taskId;
}
@Override
public void onTaskRemovalStarted(int taskId) {
if (DEBUG) Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId);
mTaskViewTaskId = INVALID_TASK_ID;
}
};
private final TaskStackListener mTaskStackListener = new TaskStackListener() {
@Override
public void onTaskFocusChanged(int taskId, boolean focused) {
mFocused = taskId == mCarLauncherTaskId && focused;
if (DEBUG) {
Log.d(TAG, "onTaskFocusChanged: mFocused=" + mFocused
+ ", mTaskViewTaskId=" + mTaskViewTaskId);
}
if (mFocused && mTaskViewTaskId == INVALID_TASK_ID) {
startMapsInTaskView();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCarLauncherTaskId = getTaskId();
ActivityTaskManager.getInstance().registerTaskStackListener(mTaskStackListener);
// Setting as trusted overlay to let touches pass through.
getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY);
// To pass touches to the underneath task.
getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
// Don't show the maps panel in multi window mode.
// NOTE: CTS tests for split screen are not compatible with activity views on the default
// activity of the launcher
if (isInMultiWindowMode() || isInPictureInPictureMode()) {
setContentView(R.layout.car_launcher_multiwindow);
} else {
setContentView(R.layout.car_launcher);
// We don't want to show Map card unnecessarily for the headless user 0.
if (!UserHelperLite.isHeadlessSystemUser(getUserId())) {
ViewGroup mapsCard = findViewById(R.id.maps_card);
if (mapsCard != null) {
setUpTaskView(mapsCard);
}
}
}
initializeCards();
}
private void setUpTaskView(ViewGroup parent) {
mTaskViewManager = new TaskViewManager(this,
new HandlerExecutor(getMainThreadHandler()));
mTaskViewManager.createTaskView(taskView -> {
taskView.setListener(getMainExecutor(), mTaskViewListener);
parent.addView(taskView);
mTaskView = taskView;
});
}
@Override
protected void onResume() {
super.onResume();
mIsResumed = true;
maybeLogReady();
if (DEBUG) {
Log.d(TAG, "onResume: mFocused=" + mFocused + ", mTaskViewTaskId=" + mTaskViewTaskId);
}
if (mFocused && mTaskViewTaskId == INVALID_TASK_ID) {
// If the task in TaskView is crashed during CarLauncher is background,
// We'd like to restart it when CarLauncher becomes foreground.
startMapsInTaskView();
}
}
@Override
protected void onPause() {
super.onPause();
mIsResumed = false;
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityTaskManager.getInstance().unregisterTaskStackListener(mTaskStackListener);
if (mTaskView != null && mTaskViewReady) {
mTaskView.release();
mTaskView = null;
}
}
private void startMapsInTaskView() {
if (mTaskView == null || !mTaskViewReady) {
return;
}
// If we happen to be be resurfaced into a multi display mode we skip launching content
// in the activity view as we will get recreated anyway.
if (isInMultiWindowMode() || isInPictureInPictureMode()) {
return;
}
// Don't start Maps when the display is off for ActivityVisibilityTests.
if (getDisplay().getState() != Display.STATE_ON) {
return;
}
try {
ActivityOptions options = ActivityOptions.makeCustomAnimation(this,
/* enterResId= */ 0, /* exitResId= */ 0);
// To show the Activity in TaskView, the Activity should be above the host task in
// ActivityStack. This option only effects the host Activity is in resumed.
options.setTaskAlwaysOnTop(true);
mTaskView.startActivity(
PendingIntent.getActivity(this, /* requestCode= */ 0, getMapsIntent(),
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT),
/* fillInIntent= */ null, options, null /* launchBounds */);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Maps activity not found", e);
}
}
private Intent getMapsIntent() {
Intent defaultIntent =
Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_MAPS);
PackageManager pm = getPackageManager();
ComponentName defaultActivity = defaultIntent.resolveActivity(pm);
for (String intentUri : getResources().getStringArray(
R.array.config_homeCardPreferredMapActivities)) {
Intent preferredIntent;
try {
preferredIntent = Intent.parseUri(intentUri, Intent.URI_ANDROID_APP_SCHEME);
} catch (URISyntaxException se) {
Log.w(TAG, "Invalid intent URI in config_homeCardPreferredMapActivities", se);
continue;
}
if (defaultActivity != null && !defaultActivity.getPackageName().equals(
preferredIntent.getPackage())) {
continue;
}
if (preferredIntent.resolveActivityInfo(pm, /* flags= */ 0) != null) {
return preferredIntent;
}
}
return defaultIntent;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
initializeCards();
}
private void initializeCards() {
if (mHomeCardModules == null) {
mHomeCardModules = new ArraySet<>();
for (String providerClassName : getResources().getStringArray(
R.array.config_homeCardModuleClasses)) {
try {
long reflectionStartTime = System.currentTimeMillis();
HomeCardModule cardModule = (HomeCardModule) Class.forName(
providerClassName).newInstance();
cardModule.setViewModelProvider(new ViewModelProvider( /* owner= */this));
mHomeCardModules.add(cardModule);
if (DEBUG) {
long reflectionTime = System.currentTimeMillis() - reflectionStartTime;
Log.d(TAG, "Initialization of HomeCardModule class " + providerClassName
+ " took " + reflectionTime + " ms");
}
} catch (IllegalAccessException | InstantiationException |
ClassNotFoundException e) {
Log.w(TAG, "Unable to create HomeCardProvider class " + providerClassName, e);
}
}
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
for (HomeCardModule cardModule : mHomeCardModules) {
transaction.replace(cardModule.getCardResId(), cardModule.getCardView());
}
transaction.commitNow();
}
/** Logs that the Activity is ready. Used for startup time diagnostics. */
private void maybeLogReady() {
if (DEBUG) {
Log.d(TAG, "maybeLogReady(" + getUserId() + "): activityReady=" + mTaskViewReady
+ ", started=" + mIsResumed + ", alreadyLogged: " + mIsReadyLogged);
}
if (mTaskViewReady && mIsResumed) {
// We should report every time - the Android framework will take care of logging just
// when it's effectively drawn for the first time, but....
reportFullyDrawn();
if (!mIsReadyLogged) {
// ... we want to manually check that the Log.i below (which is useful to show
// the user id) is only logged once (otherwise it would be logged every time the
// user taps Home)
Log.i(TAG, "Launcher for user " + getUserId() + " is ready");
mIsReadyLogged = true;
}
}
}
}