blob: dc4fdc13841b8455456224265c1c761ed51506df [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.ClusterHomeCallback;
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.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.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.content.ComponentName;
import android.content.Intent;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import java.util.List;
public final class ClusterHomeApplication extends Application {
public static final String TAG = "ClusterHome";
private static final boolean DBG = false;
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 byte HOME_AVAILABILITY = 1;
private static final byte MAPS_AVAILABILITY = 1;
private static final byte PHONE_AVAILABILITY = 1;
private static final byte MUSIC_AVAILABILITY = 1;
private IActivityTaskManager mAtm;
private InputManager mInputManager;
private ClusterHomeManager mHomeManager;
private CarUserManager mUserManager;
private CarInputManager mCarInputManager;
private ClusterState mClusterState;
private byte mUiAvailability[];
private int mUserLifeCycleEvent = USER_LIFECYCLE_EVENT_TYPE_STARTING;
private ComponentName[] mClusterActivities;
private int mLastLaunchedUiType = UI_TYPE_CLUSTER_NONE;
private int mLastReportedUiType = UI_TYPE_CLUSTER_NONE;
@Override
public void onCreate() {
super.onCreate();
mClusterActivities = new ComponentName[] {
new ComponentName(getApplicationContext(), ClusterHomeActivity.class),
ComponentName.unflattenFromString(
getString(R.string.config_clusterMapActivity)),
ComponentName.unflattenFromString(
getString(R.string.config_clusterMusicActivity)),
ComponentName.unflattenFromString(
getString(R.string.config_clusterPhoneActivity)),
};
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);
mUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
mCarInputManager = (CarInputManager) car.getCarManager(Car.CAR_INPUT_SERVICE);
initClusterHome();
});
}
private void initClusterHome() {
mHomeManager.registerClusterHomeCallback(getMainExecutor(),mClusterHomeCalback);
mClusterState = mHomeManager.getClusterState();
if (!mClusterState.on) {
mHomeManager.requestDisplay(UI_TYPE_HOME);
}
mUiAvailability = buildUiAvailability();
mHomeManager.reportState(mClusterState.uiType, UI_TYPE_CLUSTER_NONE, mUiAvailability);
mHomeManager.registerClusterHomeCallback(getMainExecutor(), mClusterHomeCalback);
mUserManager.addListener(getMainExecutor(), mUserLifecycleListener);
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() {
mCarInputManager.releaseInputEventCapture(DISPLAY_TYPE_INSTRUMENT_CLUSTER);
mUserManager.removeListener(mUserLifecycleListener);
mHomeManager.unregisterClusterHomeCallback(mClusterHomeCalback);
try {
mAtm.unregisterTaskStackListener(mTaskStackListener);
} catch (RemoteException e) {
Log.e(TAG, "remote exception from AM", e);
}
super.onTerminate();
}
private void startClusterActivity(int uiType) {
if (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;
}
mLastLaunchedUiType = uiType;
ComponentName activity = mClusterActivities[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 byte[] buildUiAvailability() {
// TODO(b/183115088): populate uiAvailability based on the package availability
return new byte[] {
HOME_AVAILABILITY, MAPS_AVAILABILITY, PHONE_AVAILABILITY, MUSIC_AVAILABILITY
};
}
private final ClusterHomeCallback mClusterHomeCalback = new ClusterHomeCallback() {
@Override
public void onClusterStateChanged(
ClusterState state, @ClusterHomeManager.Config int changes) {
mClusterState = state;
if ((changes & ClusterHomeManager.CONFIG_UI_TYPE) != 0
&& mLastLaunchedUiType != state.uiType) {
startClusterActivity(state.uiType);
}
}
@Override
public void onNavigationState(byte[] navigationState) {
// TODO(b/173454430): handle onNavigationState
}
};
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.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.length - 1; i >=0; --i) {
if (mClusterActivities[i].equals(taskInfo.topActivity)) {
return i;
}
}
return UI_TYPE_CLUSTER_NONE;
}
private final UserLifecycleListener mUserLifecycleListener = (event) -> {
mUserLifeCycleEvent = event.getEventType();
if (mUserLifeCycleEvent == USER_LIFECYCLE_EVENT_TYPE_STARTING) {
startClusterActivity(UI_TYPE_HOME);
}
};
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 (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MENU) {
if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) return;
int nextUiType = (mLastLaunchedUiType + 1) % mUiAvailability.length;
startClusterActivity(nextUiType);
return;
}
// Use Android InputManager to forward KeyEvent.
mInputManager.injectInputEvent(keyEvent, INJECT_INPUT_EVENT_MODE_ASYNC);
}
}