blob: e8a2a128f0de0a13cfa45d1be93c375850f145b0 [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.sample;
import static android.content.Intent.ACTION_USER_SWITCHED;
import static android.content.Intent.ACTION_USER_UNLOCKED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static java.lang.Integer.parseInt;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.car.CarNotConnectedException;
import android.car.cluster.ClusterActivityState;
import android.car.cluster.renderer.InstrumentClusterRenderingService;
import android.car.cluster.renderer.NavigationRenderer;
import android.car.navigation.CarNavigationInstrumentCluster;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Rect;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import androidx.car.cluster.navigation.NavigationState;
import androidx.versionedparcelable.ParcelUtils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
* virtual display that is transmitted to an external screen.
*/
public class SampleClusterServiceImpl extends InstrumentClusterRenderingService {
private static final String TAG = "Cluster.SampleService";
private static final int NO_DISPLAY = -1;
static final String LOCAL_BINDING_ACTION = "local";
static final String NAV_STATE_BUNDLE_KEY = "navstate";
static final int NAV_STATE_EVENT_ID = 1;
static final int MSG_SET_ACTIVITY_LAUNCH_OPTIONS = 1;
static final int MSG_SET_ACTIVITY_STATE = 2;
static final int MSG_ON_NAVIGATION_STATE_CHANGED = 3;
static final int MSG_ON_KEY_EVENT = 4;
static final int MSG_REGISTER_CLIENT = 5;
static final int MSG_UNREGISTER_CLIENT = 6;
static final String MSG_KEY_CATEGORY = "category";
static final String MSG_KEY_ACTIVITY_OPTIONS = "activity_options";
static final String MSG_KEY_ACTIVITY_STATE = "activity_state";
static final String MSG_KEY_KEY_EVENT = "key_event";
private List<Messenger> mClients = new ArrayList<>();
private ClusterDisplayProvider mDisplayProvider;
private int mDisplayId = NO_DISPLAY;
private UserReceiver mUserReceiver;
private final DisplayListener mDisplayListener = new DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
Log.i(TAG, "Cluster display found, displayId: " + displayId);
mDisplayId = displayId;
tryLaunchActivity();
}
@Override
public void onDisplayRemoved(int displayId) {
Log.w(TAG, "Cluster display has been removed");
}
@Override
public void onDisplayChanged(int displayId) {
}
};
private static class UserReceiver extends BroadcastReceiver {
private WeakReference<SampleClusterServiceImpl> mService;
UserReceiver(SampleClusterServiceImpl service) {
mService = new WeakReference<>(service);
}
public void register(Context context) {
IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED);
intentFilter.addAction(ACTION_USER_SWITCHED);
context.registerReceiver(this, intentFilter);
}
public void unregister(Context context) {
context.unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
SampleClusterServiceImpl service = mService.get();
Log.d(TAG, "Broadcast received: " + intent);
service.tryLaunchActivity();
}
}
private static class MessageHandler extends Handler {
private final WeakReference<SampleClusterServiceImpl> mService;
MessageHandler(SampleClusterServiceImpl service) {
mService = new WeakReference<>(service);
}
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "handleMessage: " + msg.what);
try {
switch (msg.what) {
case MSG_SET_ACTIVITY_LAUNCH_OPTIONS: {
ActivityOptions options = ActivityOptions.fromBundle(
msg.getData().getBundle(MSG_KEY_ACTIVITY_OPTIONS));
String category = msg.getData().getString(MSG_KEY_CATEGORY);
mService.get().setClusterActivityLaunchOptions(category, options);
Log.d(TAG, String.format("activity options set: %s = %s (displayeId: %d)",
category, options, options.getLaunchDisplayId()));
break;
}
case MSG_SET_ACTIVITY_STATE: {
Bundle state = msg.getData().getBundle(MSG_KEY_ACTIVITY_STATE);
String category = msg.getData().getString(MSG_KEY_CATEGORY);
mService.get().setClusterActivityState(category, state);
Log.d(TAG, String.format("activity state set: %s = %s", category, state));
break;
}
case MSG_REGISTER_CLIENT:
mService.get().mClients.add(msg.replyTo);
break;
case MSG_UNREGISTER_CLIENT:
mService.get().mClients.remove(msg.replyTo);
break;
default:
super.handleMessage(msg);
}
} catch (CarNotConnectedException ex) {
Log.e(TAG, "Unable to execute message " + msg.what, ex);
}
}
}
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind, intent: " + intent);
return (LOCAL_BINDING_ACTION.equals(intent.getAction()))
? new Messenger(new MessageHandler(this)).getBinder()
: super.onBind(intent);
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener);
mUserReceiver = new UserReceiver(this);
mUserReceiver.register(this);
}
private void tryLaunchActivity() {
int userHandle = ActivityManager.getCurrentUser();
if (userHandle == UserHandle.USER_SYSTEM || mDisplayId == NO_DISPLAY) {
Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)", userHandle,
mDisplayId));
// Not ready to launch yet.
return;
}
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(mDisplayId);
Intent intent = new Intent(this, MainClusterActivity.class);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
startActivityAsUser(intent, options.toBundle(), UserHandle.of(userHandle));
Log.i(TAG, String.format("launching main activity: %s (user: %d, display: %d)", intent,
userHandle, mDisplayId));
}
@Override
protected void onKeyEvent(KeyEvent keyEvent) {
Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent);
Bundle data = new Bundle();
data.putParcelable(MSG_KEY_KEY_EVENT, keyEvent);
broadcastClientMessage(MSG_ON_KEY_EVENT, data);
}
/**
* Broadcasts a message to all the registered service clients
*
* @param what event identifier
* @param data event data
*/
private void broadcastClientMessage(int what, Bundle data) {
Log.d(TAG, "broadcast message " + what + " to " + mClients.size() + " clients");
for (int i = mClients.size() - 1; i >= 0; i--) {
Messenger client = mClients.get(i);
try {
Message msg = Message.obtain(null, what);
msg.setData(data);
client.send(msg);
} catch (RemoteException ex) {
Log.e(TAG, "Client " + i + " is dead", ex);
mClients.remove(i);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
Log.w(TAG, "onDestroy");
mUserReceiver.unregister(this);
}
@Override
protected 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 onEvent(int eventType, Bundle bundle) {
try {
StringBuilder bundleSummary = new StringBuilder();
if (eventType == NAV_STATE_EVENT_ID) {
bundle.setClassLoader(ParcelUtils.class.getClassLoader());
NavigationState navState = NavigationState
.fromParcelable(bundle.getParcelable(NAV_STATE_BUNDLE_KEY));
bundleSummary.append(navState.toString());
// Update clients
broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
} else {
for (String key : bundle.keySet()) {
bundleSummary.append(key);
bundleSummary.append("=");
bundleSummary.append(bundle.get(key));
bundleSummary.append(" ");
}
}
Log.d(TAG, "onEvent(" + eventType + ", " + bundleSummary + ")");
} catch (Exception e) {
Log.e(TAG, "Error parsing event data (" + eventType + ", " + bundle + ")", e);
bundle.putParcelable(NAV_STATE_BUNDLE_KEY, new NavigationState.Builder().build()
.toParcelable());
broadcastClientMessage(MSG_ON_NAVIGATION_STATE_CHANGED, bundle);
}
}
};
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 {
if (args == null || args.length == 0) {
writer.println("* dump " + getClass().getCanonicalName() + " *");
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) {
Rect unobscuredArea = new Rect(parseInt(args[2]), parseInt(args[3]),
parseInt(args[4]), parseInt(args[5]));
try {
setClusterActivityState(args[1],
ClusterActivityState.create(true, unobscuredArea).toBundle());
} catch (CarNotConnectedException e) {
Log.i(TAG, "Failed to set activity state.", e);
}
} else {
Log.i(TAG, "wrong format, expected: category left top right bottom");
}
}
}
}
}