blob: 1addd8e16a54118f967f761c4c4da72d260404de [file] [log] [blame]
/*
* Copyright (C) 2017 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 android.car.cluster;
import static android.content.Intent.ACTION_USER_UNLOCKED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.view.Display.INVALID_DISPLAY;
import static java.lang.Integer.parseInt;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.car.Car;
import android.car.CarAppFocusManager;
import android.car.cluster.navigation.NavigationState.NavigationStateProto;
import android.car.cluster.renderer.InstrumentClusterRenderingService;
import android.car.cluster.renderer.NavigationRenderer;
import android.car.navigation.CarNavigationInstrumentCluster;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import com.android.car.internal.common.UserHelperLite;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
* virtual display that is transmitted to an external screen.
*/
public class ClusterRenderingService extends InstrumentClusterRenderingService implements
ImageResolver.BitmapFetcher, CarAppFocusManager.OnAppFocusChangedListener {
private static final String TAG = "Cluster.Service";
static final String LOCAL_BINDING_ACTION = "local";
static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";
private List<ServiceClient> mClients = new ArrayList<>();
private ClusterDisplayProvider mDisplayProvider;
private int mClusterDisplayId = INVALID_DISPLAY;
private boolean mInstrumentClusterHelperReady;
private final IBinder mLocalBinder = new LocalBinder();
private final ImageResolver mImageResolver = new ImageResolver(this);
private final Handler mHandler = new Handler();
private final Runnable mLaunchMainActivity = this::launchMainActivity;
private ComponentName mNavigationClusterActivity = null;
private int mNavigationClusterUserId = UserHandle.USER_SYSTEM;
private CarAppFocusManager mAppFocusManager = null;
private final UserReceiver mUserReceiver = new UserReceiver();
public interface ServiceClient {
void onKeyEvent(KeyEvent keyEvent);
void onNavigationStateChange(NavigationStateProto navState);
}
public class LocalBinder extends Binder {
ClusterRenderingService getService() {
return ClusterRenderingService.this;
}
}
private final DisplayListener mDisplayListener = new DisplayListener() {
// Called in the main thread, since ClusterDisplayProvider.DisplayListener was registered
// with null handler.
@Override
public void onDisplayAdded(int displayId) {
Log.i(TAG, "Cluster display found, displayId: " + displayId);
mClusterDisplayId = displayId;
if (mInstrumentClusterHelperReady) {
mHandler.post(mLaunchMainActivity);
}
}
@Override
public void onDisplayRemoved(int displayId) {
Log.w(TAG, "Cluster display has been removed");
}
@Override
public void onDisplayChanged(int displayId) {
}
};
public void setActivityLaunchOptions(int displayId, ClusterActivityState state) {
ActivityOptions options = displayId != INVALID_DISPLAY
? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
: null;
setClusterActivityLaunchOptions(options);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("activity options set: %s (displayeId: %d)",
options, options != null ? options.getLaunchDisplayId() : -1));
}
setClusterActivityState(state);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("activity state set: %s", state));
}
}
public void registerClient(ServiceClient client) {
mClients.add(client);
}
public void unregisterClient(ServiceClient client) {
mClients.remove(client);
}
public ImageResolver getImageResolver() {
return mImageResolver;
}
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind, intent: " + intent);
if (LOCAL_BINDING_ACTION.equals(intent.getAction())) {
return mLocalBinder;
}
IBinder binder = super.onBind(intent);
mInstrumentClusterHelperReady = true;
if (mClusterDisplayId != INVALID_DISPLAY) {
mHandler.post(mLaunchMainActivity);
}
return binder;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
// The following will never be null, as this service is initiated by CarService itself.
Car car = Car.createCar(this);
mAppFocusManager = (CarAppFocusManager) car.getCarManager(Car.APP_FOCUS_SERVICE);
mAppFocusManager.addFocusListener(this, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener);
mUserReceiver.register(this);
mNavigationClusterActivity = getNavigationClusterActivity();
Log.i(TAG, "onCreate: set cluster to " + mNavigationClusterActivity);
}
@Override
public void onDestroy() {
super.onDestroy();
mAppFocusManager.removeFocusListener(this, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
mUserReceiver.unregister(this);
mDisplayProvider.release();
}
@Override
public void onAppFocusChanged(int appType, boolean active) {
boolean useNavigationOnly = getResources().getBoolean(R.bool.navigationOnly);
Log.i(TAG, "onAppFocusChanged: " + appType + ", active: " + active);
if (useNavigationOnly) {
launchMainActivity();
} else {
// TODO(b/193931272): Update MainClusterActivity
}
}
private void launchMainActivity() {
mHandler.removeCallbacks(mLaunchMainActivity);
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(mClusterDisplayId);
boolean useNavigationOnly = getResources().getBoolean(R.bool.navigationOnly);
Intent intent;
int userId = UserHandle.USER_SYSTEM;
if (useNavigationOnly) {
userId = ActivityManager.getCurrentUser();
if (UserHelperLite.isHeadlessSystemUser(userId)) {
Log.i(TAG, "Skipping the navigation activity for User 0");
return;
}
ComponentName newClusterActivity = getNavigationClusterActivity();
if (Objects.equals(newClusterActivity, mNavigationClusterActivity)
&& userId == mNavigationClusterUserId) {
Log.i(TAG, "Cluster activity hasn't changed. Skipping.");
return;
}
Log.i(TAG, "Set cluster to " + newClusterActivity);
onNavigationComponentChanged(newClusterActivity);
mNavigationClusterActivity = newClusterActivity;
mNavigationClusterUserId = userId;
intent = getNavigationActivityIntent(mNavigationClusterActivity, mClusterDisplayId);
startFixedActivityModeForDisplayAndUser(intent, options, userId);
} else {
intent = getMainClusterActivityIntent();
startActivityAsUser(intent, options.toBundle(), UserHandle.SYSTEM);
}
Log.i(TAG, "launching main activity=" + intent + ", display=" + mClusterDisplayId
+ ", userId=" + userId);
}
/**
* Invoked when the activity to show in the cluster changes
*
* @param clusterActivity current activity displayed in cluster. If no application is holding
* {@link CarAppFocusManager#APP_FOCUS_TYPE_NAVIGATION}, this will be the
* default map cluster activity. Otherwise, this will be the cluster
* activity of the focused application (if it has one) or {@code null} if
* the application doesn't have a cluster activity or the activity is
* disabled.
*/
protected void onNavigationComponentChanged(@Nullable ComponentName clusterActivity) {
// This method can be used by OEMs to send a signal to the cluster hardware indicating
// whether Android has or doesn't have a cluster activity.
//
// OEMs can use this signal to let the cluster show some other view, or to hide Android's
// video feed altogether.
}
private Intent getMainClusterActivityIntent() {
return new Intent(this, MainClusterActivity.class).setFlags(FLAG_ACTIVITY_NEW_TASK);
}
private ComponentName getNavigationClusterActivity() {
List<String> focusOwnerPackageNames = mAppFocusManager.getAppTypeOwner(
CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
if (focusOwnerPackageNames == null || focusOwnerPackageNames.isEmpty()) {
// No application has focus. We use the default navigation app.
Log.i(TAG, "getNavigationClusterActivity(): no focus owner -> "
+ "using default nav app");
ActivityInfo activityInfo = MainClusterActivity.getNavigationActivity(this);
return new ComponentName(activityInfo.packageName, activityInfo.name);
} else {
ComponentName clusterActivity = getComponentFromPackages(focusOwnerPackageNames);
if (clusterActivity == null) {
// If currently focused app has no cluster activity, we indicate so.
Log.i(TAG, "getNavigationClusterActivity(): focus owned by "
+ focusOwnerPackageNames + " but it has no cluster activity -> "
+ "using empty activity");
return null;
}
// Otherwise, we use the activity of the currently focused app
Log.i(TAG, "getNavigationClusterActivity(): focus owned and it has a cluster "
+ "activity -> using " + focusOwnerPackageNames + " app");
return clusterActivity;
}
}
private ComponentName getComponentFromPackages(List<String> packageNames) {
for (String packageName : packageNames) {
ComponentName result = getComponentFromPackage(packageName);
if (result != null) {
return result;
}
}
return null;
}
private Intent getNavigationActivityIntent(ComponentName component, int displayId) {
if (component == null) {
Log.i(TAG, "Focused application doesn't have a cluster activity. Using fallback.");
component = new ComponentName(this, EmptyNavigationActivity.class);
}
Rect displaySize = new Rect(0, 0, 240, 320); // Arbitrary size, better than nothing.
DisplayManager dm = getSystemService(DisplayManager.class);
Display display = dm.getDisplay(displayId);
if (display != null) {
display.getRectSize(displaySize);
}
setClusterActivityState(ClusterActivityState.create(/* visible= */ true,
/* unobscuredBounds= */ new Rect(0, 0, 240, 320)));
return new Intent(Intent.ACTION_MAIN)
.setComponent(component)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE,
ClusterActivityState.create(/* visible= */ true,
/* unobscuredBounds= */ displaySize).toBundle());
}
@Override
public void onKeyEvent(KeyEvent keyEvent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent);
}
broadcastClientEvent(client -> client.onKeyEvent(keyEvent));
}
/**
* Broadcasts an event to all the registered service clients
*
* @param event event to broadcast
*/
private void broadcastClientEvent(Consumer<ServiceClient> event) {
for (ServiceClient client : mClients) {
event.accept(client);
}
}
@Override
public NavigationRenderer getNavigationRenderer() {
NavigationRenderer navigationRenderer = new NavigationRenderer() {
@Override
public CarNavigationInstrumentCluster getNavigationProperties() {
CarNavigationInstrumentCluster config =
CarNavigationInstrumentCluster.createCluster(1000);
Log.d(TAG, "getNavigationProperties, returns: " + config);
return config;
}
@Override
public void onNavigationStateChanged(Bundle bundle) {
StringBuilder bundleSummary = new StringBuilder();
// Attempt to read proto byte array
byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
if (protoBytes != null) {
try {
NavigationStateProto navState = NavigationStateProto.parseFrom(
protoBytes);
bundleSummary.append(navState.toString());
// Update clients
broadcastClientEvent(
client -> client.onNavigationStateChange(navState));
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Error parsing navigation state proto", e);
}
} else {
Log.e(TAG, "Received nav state byte array is null");
}
Log.d(TAG, "onNavigationStateChanged(" + bundleSummary + ")");
}
};
Log.i(TAG, "createNavigationRenderer, returns: " + navigationRenderer);
return navigationRenderer;
}
@Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
if (args != null && args.length > 0) {
execShellCommand(args);
} else {
super.dump(fd, writer, args);
writer.println("DisplayProvider: " + mDisplayProvider);
}
}
private void emulateKeyEvent(int keyCode) {
Log.i(TAG, "emulateKeyEvent, keyCode: " + keyCode);
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis();
KeyEvent event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_DOWN);
onKeyEvent(event);
eventTime = SystemClock.uptimeMillis();
event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_UP);
onKeyEvent(event);
}
private KeyEvent obtainKeyEvent(int keyCode, long downTime, long eventTime, int action) {
int scanCode = 0;
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
scanCode = 108;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
scanCode = 106;
}
return KeyEvent.obtain(
downTime,
eventTime,
action,
keyCode,
0 /* repeat */,
0 /* meta state */,
0 /* deviceId*/,
scanCode /* scancode */,
KeyEvent.FLAG_FROM_SYSTEM /* flags */,
InputDevice.SOURCE_KEYBOARD,
null /* characters */);
}
private void execShellCommand(String[] args) {
Log.i(TAG, "execShellCommand, args: " + Arrays.toString(args));
String command = args[0];
switch (command) {
case "injectKey": {
if (args.length > 1) {
emulateKeyEvent(parseInt(args[1]));
} else {
Log.i(TAG, "Not enough arguments");
}
break;
}
case "destroyOverlayDisplay": {
Settings.Global.putString(getContentResolver(),
Global.OVERLAY_DISPLAY_DEVICES, "");
break;
}
case "createOverlayDisplay": {
if (args.length > 1) {
Settings.Global.putString(getContentResolver(),
Global.OVERLAY_DISPLAY_DEVICES, args[1]);
} else {
Log.i(TAG, "Not enough arguments, expected 2");
}
break;
}
case "setUnobscuredArea": {
if (args.length > 5) {
setClusterActivityState(ClusterActivityState.create(true,
new Rect(parseInt(args[2]), parseInt(args[3]),
parseInt(args[4]), parseInt(args[5]))));
} else {
Log.i(TAG, "wrong format, expected: category left top right bottom");
}
}
}
}
private class UserReceiver extends BroadcastReceiver {
void register(Context context) {
IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED);
context.registerReceiverAsUser(this, UserHandle.ALL, intentFilter, null, null);
}
void unregister(Context context) {
context.unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Broadcast received: " + intent);
}
int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
if (userId == ActivityManager.getCurrentUser() &&
mInstrumentClusterHelperReady && mClusterDisplayId != INVALID_DISPLAY) {
mHandler.post(mLaunchMainActivity);
}
}
}
}