| /* |
| * 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_SWITCHED; |
| 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.app.ActivityManager; |
| import android.app.ActivityOptions; |
| import android.car.CarNotConnectedException; |
| 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.Intent; |
| import android.content.IntentFilter; |
| 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 androidx.versionedparcelable.ParcelUtils; |
| |
| 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.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 { |
| private static final String TAG = "Cluster.Service"; |
| private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000; |
| |
| static final int NAV_STATE_EVENT_ID = 1; |
| 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 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) { |
| try { |
| ActivityOptions options = displayId != INVALID_DISPLAY |
| ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId) |
| : null; |
| setClusterActivityLaunchOptions(CarInstrumentClusterManager.CATEGORY_NAVIGATION, |
| options); |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, String.format("activity options set: %s (displayeId: %d)", |
| options, options.getLaunchDisplayId())); |
| } |
| setClusterActivityState(CarInstrumentClusterManager.CATEGORY_NAVIGATION, |
| state.toBundle()); |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, String.format("activity state set: %s", state)); |
| } |
| } catch (CarNotConnectedException ex) { |
| Log.e(TAG, "Unable to update service", ex); |
| } |
| } |
| |
| 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"); |
| mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener); |
| |
| mUserReceiver.register(this); |
| } |
| |
| public void onDestroy() { |
| super.onDestroy(); |
| mUserReceiver.unregister(this); |
| } |
| |
| 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) { |
| intent = getNavigationActivityIntent(mClusterDisplayId); |
| if (intent == null) { |
| mHandler.postDelayed(mLaunchMainActivity, NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS); |
| return; |
| } |
| userId = ActivityManager.getCurrentUser(); |
| startFixedActivityModeForDisplayAndUser(intent, options, userId); |
| } else { |
| intent = getMainClusterActivityIntent(); |
| startActivityAsUser(intent, options.toBundle(), UserHandle.SYSTEM); |
| } |
| Log.i(TAG, "launching main activity=" + intent + ", display=" + mClusterDisplayId |
| + ", userId=" + userId); |
| } |
| |
| private Intent getMainClusterActivityIntent() { |
| return new Intent(this, MainClusterActivity.class).setFlags(FLAG_ACTIVITY_NEW_TASK); |
| } |
| |
| private Intent getNavigationActivityIntent(int displayId) { |
| ComponentName component = MainClusterActivity.getNavigationActivity(this); |
| if (component == null) { |
| Log.e(TAG, "Failed to resolve the navigation activity"); |
| return null; |
| } |
| Rect displaySize = new Rect(0, 0, 320, 240); // Arbitrary size, better than nothing. |
| DisplayManager dm = (DisplayManager) getSystemService(DisplayManager.class); |
| Display display = dm.getDisplay(displayId); |
| if (display != null) { |
| display.getRectSize(displaySize); |
| } |
| return new Intent(Intent.ACTION_MAIN) |
| .setComponent(component) |
| .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
| .putExtra(CarInstrumentClusterManager.KEY_EXTRA_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 onEvent(int eventType, Bundle bundle) { |
| StringBuilder bundleSummary = new StringBuilder(); |
| if (eventType == NAV_STATE_EVENT_ID) { |
| // Required to prevent backwards compatibility crash with old map providers |
| // sending androidx.versionedparcelables |
| bundle.setClassLoader(ParcelUtils.class.getClassLoader()); |
| |
| // 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"); |
| } |
| } else { |
| for (String key : bundle.keySet()) { |
| bundleSummary.append(key); |
| bundleSummary.append("="); |
| bundleSummary.append(bundle.get(key)); |
| bundleSummary.append(" "); |
| } |
| } |
| Log.d(TAG, "onEvent(" + eventType + ", " + 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) { |
| 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"); |
| } |
| } |
| } |
| } |
| |
| 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); |
| } |
| } |
| } |
| } |