1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.car.cluster; 17 18 import static android.content.Intent.ACTION_USER_SWITCHED; 19 import static android.content.Intent.ACTION_USER_UNLOCKED; 20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 21 import static android.view.Display.INVALID_DISPLAY; 22 23 import static java.lang.Integer.parseInt; 24 25 import android.app.ActivityManager; 26 import android.app.ActivityOptions; 27 import android.car.CarNotConnectedException; 28 import android.car.cluster.navigation.NavigationState.NavigationStateProto; 29 import android.car.cluster.renderer.InstrumentClusterRenderingService; 30 import android.car.cluster.renderer.NavigationRenderer; 31 import android.car.navigation.CarNavigationInstrumentCluster; 32 import android.content.BroadcastReceiver; 33 import android.content.ComponentName; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.content.IntentFilter; 37 import android.graphics.Rect; 38 import android.hardware.display.DisplayManager; 39 import android.hardware.display.DisplayManager.DisplayListener; 40 import android.os.Binder; 41 import android.os.Bundle; 42 import android.os.Handler; 43 import android.os.IBinder; 44 import android.os.SystemClock; 45 import android.os.UserHandle; 46 import android.provider.Settings; 47 import android.provider.Settings.Global; 48 import android.util.Log; 49 import android.view.Display; 50 import android.view.InputDevice; 51 import android.view.KeyEvent; 52 53 import com.google.protobuf.InvalidProtocolBufferException; 54 55 import java.io.FileDescriptor; 56 import java.io.PrintWriter; 57 import java.util.ArrayList; 58 import java.util.Arrays; 59 import java.util.List; 60 import java.util.function.Consumer; 61 62 /** 63 * Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a 64 * virtual display that is transmitted to an external screen. 65 */ 66 public class ClusterRenderingService extends InstrumentClusterRenderingService implements 67 ImageResolver.BitmapFetcher { 68 private static final String TAG = "Cluster.Service"; 69 private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000; 70 71 static final int NAV_STATE_EVENT_ID = 1; 72 static final String LOCAL_BINDING_ACTION = "local"; 73 static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2"; 74 75 private List<ServiceClient> mClients = new ArrayList<>(); 76 private ClusterDisplayProvider mDisplayProvider; 77 78 private int mClusterDisplayId = INVALID_DISPLAY; 79 80 private boolean mInstrumentClusterHelperReady; 81 82 private final IBinder mLocalBinder = new LocalBinder(); 83 private final ImageResolver mImageResolver = new ImageResolver(this); 84 private final Handler mHandler = new Handler(); 85 private final Runnable mLaunchMainActivity = this::launchMainActivity; 86 87 private final UserReceiver mUserReceiver = new UserReceiver(); 88 89 public interface ServiceClient { onKeyEvent(KeyEvent keyEvent)90 void onKeyEvent(KeyEvent keyEvent); 91 onNavigationStateChange(NavigationStateProto navState)92 void onNavigationStateChange(NavigationStateProto navState); 93 } 94 95 public class LocalBinder extends Binder { getService()96 ClusterRenderingService getService() { 97 return ClusterRenderingService.this; 98 } 99 } 100 101 private final DisplayListener mDisplayListener = new DisplayListener() { 102 // Called in the main thread, since ClusterDisplayProvider.DisplayListener was registered 103 // with null handler. 104 @Override 105 public void onDisplayAdded(int displayId) { 106 Log.i(TAG, "Cluster display found, displayId: " + displayId); 107 mClusterDisplayId = displayId; 108 if (mInstrumentClusterHelperReady) { 109 mHandler.post(mLaunchMainActivity); 110 } 111 } 112 113 @Override 114 public void onDisplayRemoved(int displayId) { 115 Log.w(TAG, "Cluster display has been removed"); 116 } 117 118 @Override 119 public void onDisplayChanged(int displayId) { 120 121 } 122 }; 123 setActivityLaunchOptions(int displayId, ClusterActivityState state)124 public void setActivityLaunchOptions(int displayId, ClusterActivityState state) { 125 try { 126 ActivityOptions options = displayId != INVALID_DISPLAY 127 ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId) 128 : null; 129 setClusterActivityLaunchOptions(CarInstrumentClusterManager.CATEGORY_NAVIGATION, 130 options); 131 if (Log.isLoggable(TAG, Log.DEBUG)) { 132 Log.d(TAG, String.format("activity options set: %s (displayeId: %d)", 133 options, options.getLaunchDisplayId())); 134 } 135 setClusterActivityState(CarInstrumentClusterManager.CATEGORY_NAVIGATION, 136 state.toBundle()); 137 if (Log.isLoggable(TAG, Log.DEBUG)) { 138 Log.d(TAG, String.format("activity state set: %s", state)); 139 } 140 } catch (CarNotConnectedException ex) { 141 Log.e(TAG, "Unable to update service", ex); 142 } 143 } 144 registerClient(ServiceClient client)145 public void registerClient(ServiceClient client) { 146 mClients.add(client); 147 } 148 unregisterClient(ServiceClient client)149 public void unregisterClient(ServiceClient client) { 150 mClients.remove(client); 151 } 152 getImageResolver()153 public ImageResolver getImageResolver() { 154 return mImageResolver; 155 } 156 157 @Override onBind(Intent intent)158 public IBinder onBind(Intent intent) { 159 Log.d(TAG, "onBind, intent: " + intent); 160 if (LOCAL_BINDING_ACTION.equals(intent.getAction())) { 161 return mLocalBinder; 162 } 163 IBinder binder = super.onBind(intent); 164 mInstrumentClusterHelperReady = true; 165 if (mClusterDisplayId != INVALID_DISPLAY) { 166 mHandler.post(mLaunchMainActivity); 167 } 168 return binder; 169 } 170 171 @Override onCreate()172 public void onCreate() { 173 super.onCreate(); 174 Log.d(TAG, "onCreate"); 175 mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener); 176 177 mUserReceiver.register(this); 178 } 179 onDestroy()180 public void onDestroy() { 181 super.onDestroy(); 182 mUserReceiver.unregister(this); 183 } 184 launchMainActivity()185 private void launchMainActivity() { 186 mHandler.removeCallbacks(mLaunchMainActivity); 187 ActivityOptions options = ActivityOptions.makeBasic(); 188 options.setLaunchDisplayId(mClusterDisplayId); 189 boolean useNavigationOnly = getResources().getBoolean(R.bool.navigationOnly); 190 Intent intent; 191 int userId = UserHandle.USER_SYSTEM; 192 if (useNavigationOnly) { 193 intent = getNavigationActivityIntent(mClusterDisplayId); 194 if (intent == null) { 195 mHandler.postDelayed(mLaunchMainActivity, NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS); 196 return; 197 } 198 userId = ActivityManager.getCurrentUser(); 199 startFixedActivityModeForDisplayAndUser(intent, options, userId); 200 } else { 201 intent = getMainClusterActivityIntent(); 202 startActivityAsUser(intent, options.toBundle(), UserHandle.SYSTEM); 203 } 204 Log.i(TAG, "launching main activity=" + intent + ", display=" + mClusterDisplayId 205 + ", userId=" + userId); 206 } 207 getMainClusterActivityIntent()208 private Intent getMainClusterActivityIntent() { 209 return new Intent(this, MainClusterActivity.class).setFlags(FLAG_ACTIVITY_NEW_TASK); 210 } 211 getNavigationActivityIntent(int displayId)212 private Intent getNavigationActivityIntent(int displayId) { 213 ComponentName component = MainClusterActivity.getNavigationActivity(this); 214 if (component == null) { 215 Log.e(TAG, "Failed to resolve the navigation activity"); 216 return null; 217 } 218 Rect displaySize = new Rect(0, 0, 320, 240); // Arbitrary size, better than nothing. 219 DisplayManager dm = (DisplayManager) getSystemService(DisplayManager.class); 220 Display display = dm.getDisplay(displayId); 221 if (display != null) { 222 display.getRectSize(displaySize); 223 } 224 return new Intent(Intent.ACTION_MAIN) 225 .setComponent(component) 226 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 227 .putExtra(CarInstrumentClusterManager.KEY_EXTRA_ACTIVITY_STATE, 228 ClusterActivityState.create(/* visible= */ true, 229 /* unobscuredBounds= */ displaySize).toBundle()); 230 } 231 232 @Override onKeyEvent(KeyEvent keyEvent)233 public void onKeyEvent(KeyEvent keyEvent) { 234 if (Log.isLoggable(TAG, Log.DEBUG)) { 235 Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent); 236 } 237 broadcastClientEvent(client -> client.onKeyEvent(keyEvent)); 238 } 239 240 /** 241 * Broadcasts an event to all the registered service clients 242 * 243 * @param event event to broadcast 244 */ broadcastClientEvent(Consumer<ServiceClient> event)245 private void broadcastClientEvent(Consumer<ServiceClient> event) { 246 for (ServiceClient client : mClients) { 247 event.accept(client); 248 } 249 } 250 251 @Override getNavigationRenderer()252 public NavigationRenderer getNavigationRenderer() { 253 NavigationRenderer navigationRenderer = new NavigationRenderer() { 254 @Override 255 public CarNavigationInstrumentCluster getNavigationProperties() { 256 CarNavigationInstrumentCluster config = 257 CarNavigationInstrumentCluster.createCluster(1000); 258 Log.d(TAG, "getNavigationProperties, returns: " + config); 259 return config; 260 } 261 262 @Override 263 public void onNavigationStateChanged(Bundle bundle) { 264 StringBuilder bundleSummary = new StringBuilder(); 265 266 // Attempt to read proto byte array 267 byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY); 268 if (protoBytes != null) { 269 try { 270 NavigationStateProto navState = NavigationStateProto.parseFrom( 271 protoBytes); 272 bundleSummary.append(navState.toString()); 273 274 // Update clients 275 broadcastClientEvent( 276 client -> client.onNavigationStateChange(navState)); 277 } catch (InvalidProtocolBufferException e) { 278 Log.e(TAG, "Error parsing navigation state proto", e); 279 } 280 } else { 281 Log.e(TAG, "Received nav state byte array is null"); 282 } 283 Log.d(TAG, "onNavigationStateChanged(" + bundleSummary + ")"); 284 } 285 }; 286 287 Log.i(TAG, "createNavigationRenderer, returns: " + navigationRenderer); 288 return navigationRenderer; 289 } 290 291 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)292 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 293 if (args != null && args.length > 0) { 294 execShellCommand(args); 295 } else { 296 super.dump(fd, writer, args); 297 writer.println("DisplayProvider: " + mDisplayProvider); 298 } 299 } 300 emulateKeyEvent(int keyCode)301 private void emulateKeyEvent(int keyCode) { 302 Log.i(TAG, "emulateKeyEvent, keyCode: " + keyCode); 303 long downTime = SystemClock.uptimeMillis(); 304 long eventTime = SystemClock.uptimeMillis(); 305 KeyEvent event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_DOWN); 306 onKeyEvent(event); 307 308 eventTime = SystemClock.uptimeMillis(); 309 event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_UP); 310 onKeyEvent(event); 311 } 312 obtainKeyEvent(int keyCode, long downTime, long eventTime, int action)313 private KeyEvent obtainKeyEvent(int keyCode, long downTime, long eventTime, int action) { 314 int scanCode = 0; 315 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 316 scanCode = 108; 317 } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 318 scanCode = 106; 319 } 320 return KeyEvent.obtain( 321 downTime, 322 eventTime, 323 action, 324 keyCode, 325 0 /* repeat */, 326 0 /* meta state */, 327 0 /* deviceId*/, 328 scanCode /* scancode */, 329 KeyEvent.FLAG_FROM_SYSTEM /* flags */, 330 InputDevice.SOURCE_KEYBOARD, 331 null /* characters */); 332 } 333 execShellCommand(String[] args)334 private void execShellCommand(String[] args) { 335 Log.i(TAG, "execShellCommand, args: " + Arrays.toString(args)); 336 337 String command = args[0]; 338 339 switch (command) { 340 case "injectKey": { 341 if (args.length > 1) { 342 emulateKeyEvent(parseInt(args[1])); 343 } else { 344 Log.i(TAG, "Not enough arguments"); 345 } 346 break; 347 } 348 case "destroyOverlayDisplay": { 349 Settings.Global.putString(getContentResolver(), 350 Global.OVERLAY_DISPLAY_DEVICES, ""); 351 break; 352 } 353 354 case "createOverlayDisplay": { 355 if (args.length > 1) { 356 Settings.Global.putString(getContentResolver(), 357 Global.OVERLAY_DISPLAY_DEVICES, args[1]); 358 } else { 359 Log.i(TAG, "Not enough arguments, expected 2"); 360 } 361 break; 362 } 363 364 case "setUnobscuredArea": { 365 if (args.length > 5) { 366 Rect unobscuredArea = new Rect(parseInt(args[2]), parseInt(args[3]), 367 parseInt(args[4]), parseInt(args[5])); 368 try { 369 setClusterActivityState(args[1], 370 ClusterActivityState.create(true, unobscuredArea).toBundle()); 371 } catch (CarNotConnectedException e) { 372 Log.i(TAG, "Failed to set activity state.", e); 373 } 374 } else { 375 Log.i(TAG, "wrong format, expected: category left top right bottom"); 376 } 377 } 378 } 379 } 380 381 private class UserReceiver extends BroadcastReceiver { register(Context context)382 void register(Context context) { 383 IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED); 384 context.registerReceiverAsUser(this, UserHandle.ALL, intentFilter, null, null); 385 } 386 unregister(Context context)387 void unregister(Context context) { 388 context.unregisterReceiver(this); 389 } 390 391 @Override onReceive(Context context, Intent intent)392 public void onReceive(Context context, Intent intent) { 393 if (Log.isLoggable(TAG, Log.DEBUG)) { 394 Log.d(TAG, "Broadcast received: " + intent); 395 } 396 int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); 397 if (userId == ActivityManager.getCurrentUser() && 398 mInstrumentClusterHelperReady && mClusterDisplayId != INVALID_DISPLAY) { 399 mHandler.post(mLaunchMainActivity); 400 } 401 } 402 } 403 } 404