blob: 280e589aadcd0be1b65fcaa0d528199e6d7152cb [file] [log] [blame]
/*
* Copyright (C) 2021 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.cluster.home;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.car.CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER;
import static android.car.cluster.ClusterHomeManager.ClusterStateListener;
import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_HOME;
import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_NONE;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STARTING;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
import static android.content.Intent.ACTION_MAIN;
import static android.hardware.input.InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.Application;
import android.app.IActivityTaskManager;
import android.app.TaskInfo;
import android.app.TaskStackListener;
import android.car.Car;
import android.car.CarAppFocusManager;
import android.car.CarAppFocusManager.OnAppFocusChangedListener;
import android.car.CarOccupantZoneManager;
import android.car.cluster.ClusterActivityState;
import android.car.cluster.ClusterHomeManager;
import android.car.cluster.ClusterState;
import android.car.input.CarInputManager;
import android.car.input.CarInputManager.CarInputCaptureCallback;
import android.car.user.CarUserManager;
import android.car.user.CarUserManager.UserLifecycleListener;
import android.car.user.UserLifecycleEventFilter;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ResolveInfoFlags;
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArraySet;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
public final class ClusterHomeApplication extends Application {
public static final String TAG = "ClusterHome";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final int UI_TYPE_HOME = UI_TYPE_CLUSTER_HOME;
private static final int UI_TYPE_MAPS = UI_TYPE_HOME + 1;
private static final int UI_TYPE_MUSIC = UI_TYPE_HOME + 2;
private static final int UI_TYPE_PHONE = UI_TYPE_HOME + 3;
private static final int UI_TYPE_START = UI_TYPE_MAPS;
private static final byte UI_UNAVAILABLE = 0;
private static final byte UI_AVAILABLE = 1;
private PackageManager mPackageManager;
private UserManager mUserManager;
private IActivityTaskManager mAtm;
private InputManager mInputManager;
private ClusterHomeManager mHomeManager;
private CarUserManager mCarUserManager;
private CarInputManager mCarInputManager;
private CarAppFocusManager mAppFocusManager;
private ClusterState mClusterState;
private byte mUiAvailability[];
private int mUserLifeCycleEvent = USER_LIFECYCLE_EVENT_TYPE_STARTING;
private ArrayList<ComponentName> mClusterActivities = new ArrayList<>();
private int mDefaultClusterActivitySize = 0;
private int mLastLaunchedUiType = UI_TYPE_CLUSTER_NONE;
private int mLastReportedUiType = UI_TYPE_CLUSTER_NONE;
private boolean mIsLightMode = false;
private boolean mIsInitialized = false;
// Note that we use this callback to detect which cluster service mode (either FULL or LIGHT),
// by looking at what cluster activity is being created. This is a hack to support both service
// modes with a single sample application. In actual production scenarios, only one service
// will be supported on a given device, thus there is no need for this callback mechanism.
private final ActivityLifecycleCallbacks mActivityLifecycleCallbacks =
new ActivityLifecycleCallbacks() {
@Override
public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
// Set the mode based on the home activity class that is being created.
if (activity instanceof ClusterHomeActivityInterface) {
mIsLightMode =
((ClusterHomeActivityInterface) activity).isClusterInLightMode();
}
// Initialize before the first activity is created.
if (!mIsInitialized) {
mIsInitialized = true;
initClusterHome();
}
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
};
@Override
public void onCreate() {
super.onCreate();
mClusterActivities.add(UI_TYPE_HOME,
new ComponentName(getApplicationContext(), ClusterHomeActivity.class));
mClusterActivities.add(UI_TYPE_MAPS,
ComponentName.unflattenFromString(getString(R.string.config_clusterMapActivity)));
mClusterActivities.add(UI_TYPE_MUSIC,
ComponentName.unflattenFromString(getString(R.string.config_clusterMusicActivity)));
mClusterActivities.add(UI_TYPE_PHONE,
ComponentName.unflattenFromString(getString(R.string.config_clusterPhoneActivity)));
mDefaultClusterActivitySize = mClusterActivities.size();
mPackageManager = getApplicationContext().getPackageManager();
mUserManager = getApplicationContext().getSystemService(UserManager.class);
mAtm = ActivityTaskManager.getService();
try {
mAtm.registerTaskStackListener(mTaskStackListener);
} catch (RemoteException e) {
Log.e(TAG, "remote exception from AM", e);
}
mInputManager = getApplicationContext().getSystemService(InputManager.class);
Car.createCar(getApplicationContext(), /* handler= */ null,
Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
(car, ready) -> {
if (!ready) return;
mHomeManager = (ClusterHomeManager) car.getCarManager(Car.CLUSTER_HOME_SERVICE);
mCarUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
mCarInputManager = (CarInputManager) car.getCarManager(Car.CAR_INPUT_SERVICE);
mAppFocusManager = (CarAppFocusManager) car.getCarManager(
Car.APP_FOCUS_SERVICE);
});
registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
}
private void initClusterHome() {
Log.i(TAG, "initClusterHome() in " + (mIsLightMode ? "LIGHT" : "FULL") + " mode");
if (mHomeManager == null) {
Log.e(TAG, "ClusterHome is null (ClusterHomeService may not be enabled), "
+ "Stopping ClusterHomeSample.");
return;
}
// In the LIGHT mode, the HOME activity (DriverUI) takes care of everything, so we just
// stay as the UI_TYPE_HOME, and do not need any logic to switch activities to different
// types.
if (mIsLightMode) {
return;
}
mHomeManager.registerClusterStateListener(getMainExecutor(), mClusterHomeCallback);
mClusterState = mHomeManager.getClusterState();
if (!mClusterState.on) {
mHomeManager.requestDisplay(UI_TYPE_HOME);
}
mUiAvailability = buildUiAvailability(ActivityManager.getCurrentUser());
mHomeManager.reportState(mClusterState.uiType, UI_TYPE_CLUSTER_NONE, mUiAvailability);
// Using the filter, only listens to the current user starting or unlocked events.
UserLifecycleEventFilter filter = new UserLifecycleEventFilter.Builder()
.addUser(UserHandle.CURRENT)
.addEventType(USER_LIFECYCLE_EVENT_TYPE_STARTING)
.addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED).build();
mCarUserManager.addListener(getMainExecutor(), filter, mUserLifecycleListener);
if (mUserManager.isUserUnlocked(UserHandle.of(ActivityManager.getCurrentUser()))) {
mUserLifeCycleEvent = USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
}
mAppFocusManager.addFocusListener(mAppFocusChangedListener,
CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
int r = mCarInputManager.requestInputEventCapture(
DISPLAY_TYPE_INSTRUMENT_CLUSTER,
new int[]{CarInputManager.INPUT_TYPE_ALL_INPUTS},
CarInputManager.CAPTURE_REQ_FLAGS_TAKE_ALL_EVENTS_FOR_DISPLAY,
mInputCaptureCallback);
if (r != CarInputManager.INPUT_CAPTURE_RESPONSE_SUCCEEDED) {
Log.e(TAG, "Failed to capture InputEvent on Cluster: r=" + r);
}
if (mClusterState.uiType != UI_TYPE_HOME) {
startClusterActivity(mClusterState.uiType);
}
}
@Override
public void onTerminate() {
if (!mIsLightMode) {
mCarInputManager.releaseInputEventCapture(DISPLAY_TYPE_INSTRUMENT_CLUSTER);
mCarUserManager.removeListener(mUserLifecycleListener);
mHomeManager.unregisterClusterStateListener(mClusterHomeCallback);
try {
mAtm.unregisterTaskStackListener(mTaskStackListener);
} catch (RemoteException e) {
Log.e(TAG, "remote exception from AM", e);
}
}
unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
super.onTerminate();
}
private void startClusterActivity(int uiType) {
// Because ClusterHomeActivity runs as a user 0, so it can run in the locked state.
if (uiType != UI_TYPE_HOME && mUserLifeCycleEvent != USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) {
Log.i(TAG, "Ignore to start Activity(" + uiType + ") during user-switching");
return;
}
if (mClusterState == null || mClusterState.displayId == Display.INVALID_DISPLAY) {
Log.w(TAG, "Cluster display is not ready");
return;
}
// If this is the first activity to start, and the user is already unlocked,
// use UI_TYPE_START activity instead of UI_TYPE_HOME activity.
if (mLastLaunchedUiType == UI_TYPE_CLUSTER_NONE && uiType == UI_TYPE_HOME
&& mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) {
Log.i(TAG, "Starting START UI instead of HOME UI, since user is already unlocked.");
uiType = UI_TYPE_START;
}
mLastLaunchedUiType = uiType;
ComponentName activity = mClusterActivities.get(uiType);
Intent intent = new Intent(ACTION_MAIN).setComponent(activity);
if (mClusterState.bounds != null && mClusterState.insets != null) {
Rect unobscured = new Rect(mClusterState.bounds);
unobscured.inset(mClusterState.insets);
ClusterActivityState state = ClusterActivityState.create(mClusterState.on, unobscured);
intent.putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE, state.toBundle());
}
ActivityOptions options = ActivityOptions.makeBasic();
// This sample assumes the Activities in this package are running as the system user,
// and the other Activities are running as a current user.
int userId = ActivityManager.getCurrentUser();
if (getApplicationContext().getPackageName().equals(activity.getPackageName())) {
userId = UserHandle.USER_SYSTEM;
}
mHomeManager.startFixedActivityModeAsUser(intent, options.toBundle(), userId);
}
private void add3PNavigationActivities(int currentUser) {
// Clean up the 3P Navigations from the previous user.
mClusterActivities.subList(mDefaultClusterActivitySize, mClusterActivities.size()).clear();
ArraySet<String> clusterPackages = new ArraySet<>();
for (int i = mDefaultClusterActivitySize - 1; i >= 0; --i) {
clusterPackages.add(mClusterActivities.get(i).getPackageName());
}
Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Car.CAR_CATEGORY_NAVIGATION);
List<ResolveInfo> resolveList = mPackageManager.queryIntentActivitiesAsUser(
intent, ResolveInfoFlags.of(PackageManager.GET_RESOLVED_FILTER),
UserHandle.of(currentUser));
for (int i = resolveList.size() - 1; i >= 0; --i) {
ActivityInfo activityInfo = resolveList.get(i).activityInfo;
if (DBG) Log.d(TAG, "Found: " + activityInfo.packageName + "/" + activityInfo.name);
// Some package can have multiple navigation Activities, we choose the default one only.
if (clusterPackages.contains(activityInfo.packageName)) {
if (DBG) {
Log.d(TAG, "Skip this, because another Activity in the package is registered.");
};
continue;
}
mClusterActivities.add(new ComponentName(activityInfo.packageName, activityInfo.name));
}
mUiAvailability = buildUiAvailability(currentUser);
}
private byte[] buildUiAvailability(int currentUser) {
byte[] availability = new byte[mClusterActivities.size()];
Intent intent = new Intent(ACTION_MAIN);
for (int i = mClusterActivities.size() - 1; i >= 0; --i) {
ComponentName clusterActivity = mClusterActivities.get(i);
if (clusterActivity.getPackageName().equals(getPackageName())) {
// Assume that all Activities in ClusterHome are available.
availability[i] = UI_AVAILABLE;
continue;
}
intent.setComponent(clusterActivity);
ResolveInfo resolveInfo = mPackageManager.resolveActivityAsUser(
intent, PackageManager.MATCH_DEFAULT_ONLY, currentUser);
availability[i] = resolveInfo == null ? UI_UNAVAILABLE : UI_AVAILABLE;
if (DBG) {
Log.d(TAG, "availability=" + availability[i] + ", activity=" + clusterActivity
+ ", userId=" + currentUser);
}
}
return availability;
}
private final ClusterStateListener mClusterHomeCallback = new ClusterStateListener() {
@Override
public void onClusterStateChanged(
ClusterState state, @ClusterHomeManager.Config int changes) {
if (DBG) {
Log.d(TAG, "onClusterStateChanged: changes=" + Integer.toHexString(changes) +
", state=" + clusterStateToString(state));
}
mClusterState = state;
// We'll restart Activity when the display bounds or insets are changed, to let Activity
// redraw itself to fit the changed attributes.
if ((changes & ClusterHomeManager.CONFIG_DISPLAY_BOUNDS) != 0
|| (changes & ClusterHomeManager.CONFIG_DISPLAY_INSETS) != 0
|| ((changes & ClusterHomeManager.CONFIG_UI_TYPE) != 0
&& mLastLaunchedUiType != state.uiType)) {
startClusterActivity(state.uiType);
}
}
};
private final TaskStackListener mTaskStackListener = new TaskStackListener() {
// onTaskMovedToFront isn't called when Activity-change happens within the same task.
@Override
public void onTaskStackChanged() {
getMainExecutor().execute(ClusterHomeApplication.this::handleTaskStackChanged);
}
};
private void handleTaskStackChanged() {
if (mClusterState == null || mClusterState.displayId == Display.INVALID_DISPLAY) {
return;
}
TaskInfo taskInfo;
try {
taskInfo = mAtm.getRootTaskInfoOnDisplay(
WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_UNDEFINED, mClusterState.displayId);
} catch (RemoteException e) {
Log.e(TAG, "remote exception from AM", e);
return;
}
if (taskInfo == null) {
return;
}
int uiType = identifyTopTask(taskInfo);
if (uiType == UI_TYPE_CLUSTER_NONE) {
Log.w(TAG, "Unexpected top Activity on Cluster: " + taskInfo.topActivity);
return;
}
if (mLastReportedUiType == uiType) {
// Don't report the same UI type repeatedly.
return;
}
mLastReportedUiType = uiType;
mHomeManager.reportState(uiType, UI_TYPE_CLUSTER_NONE, mUiAvailability);
}
private int identifyTopTask(TaskInfo taskInfo) {
for (int i = mClusterActivities.size() - 1; i >=0; --i) {
if (mClusterActivities.get(i).equals(taskInfo.topActivity)) {
return i;
}
}
return UI_TYPE_CLUSTER_NONE;
}
private final UserLifecycleListener mUserLifecycleListener = (event) -> {
if (DBG) Log.d(TAG, "UserLifecycleListener.onEvent: event=" + event);
mUserLifeCycleEvent = event.getEventType();
if (mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_STARTING) {
startClusterActivity(UI_TYPE_HOME);
} else if (mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) {
add3PNavigationActivities(event.getUserId());
if (UI_TYPE_START != UI_TYPE_HOME) {
startClusterActivity(UI_TYPE_START);
}
}
};
private final CarInputCaptureCallback mInputCaptureCallback = new CarInputCaptureCallback() {
@Override
public void onKeyEvents(@CarOccupantZoneManager.DisplayTypeEnum int targetDisplayType,
List<KeyEvent> keyEvents) {
keyEvents.forEach((keyEvent) -> onKeyEvent(keyEvent));
}
};
private void onKeyEvent(KeyEvent keyEvent) {
if (DBG) Log.d(TAG, "onKeyEvent: " + keyEvent);
if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MENU) {
if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) return;
int nextUiType;
do {
// Select the Cluster Activity within the preinstalled ones.
nextUiType = mLastLaunchedUiType + 1;
if (nextUiType >= mDefaultClusterActivitySize) nextUiType = 0;
} while (mUiAvailability[nextUiType] == UI_UNAVAILABLE);
startClusterActivity(nextUiType);
return;
}
// Use Android InputManager to forward KeyEvent.
mInputManager.injectInputEvent(keyEvent, INJECT_INPUT_EVENT_MODE_ASYNC);
}
private OnAppFocusChangedListener mAppFocusChangedListener = new OnAppFocusChangedListener() {
@Override
public void onAppFocusChanged(int appType, boolean active) {
if (!active || appType != CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION) {
return;
}
int navigationUi = getFocusedNavigationUi();
if (navigationUi != UI_TYPE_CLUSTER_NONE) {
startClusterActivity(navigationUi);
}
}
};
private int getFocusedNavigationUi() {
List<String> focusOwnerPackageNames = mAppFocusManager.getAppTypeOwner(
CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
if (focusOwnerPackageNames == null || focusOwnerPackageNames.isEmpty()) {
Log.e(TAG, "Can't find the navigation owner");
return UI_TYPE_CLUSTER_NONE;
}
for (int i = 0; i < focusOwnerPackageNames.size(); ++i) {
String focusOwnerPackage = focusOwnerPackageNames.get(i);
for (int j = mClusterActivities.size() - 1; j >= 0; --j) {
if (mUiAvailability[j] == UI_UNAVAILABLE) {
continue;
}
if (mClusterActivities.get(j).getPackageName().equals(focusOwnerPackage)) {
if (DBG) {
Log.d(TAG, "Found focused NavigationUI: " + j
+ ", package=" + focusOwnerPackage);
}
return j;
}
}
}
Log.e(TAG, "Can't find the navigation UI for "
+ String.join(", ", focusOwnerPackageNames) + ".");
return UI_TYPE_CLUSTER_NONE;
}
private static String clusterStateToString(ClusterState state) {
StringBuilder sb = new StringBuilder("ClusterState[");
sb.append("on="); sb.append(state.on);
if (state.bounds != null) {
sb.append(", bounds="); sb.append(state.bounds);
}
if (state.insets != null) {
sb.append(", insets="); sb.append(state.insets);
}
if (state.insets != null) {
sb.append(", insets="); sb.append(state.insets);
}
sb.append(", uiType="); sb.append(state.uiType);
sb.append(", displayId="); sb.append(state.displayId);
sb.append(']');
return sb.toString();
}
}