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_UNLOCKED; 19 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 20 import static android.view.Display.INVALID_DISPLAY; 21 22 import static java.lang.Integer.parseInt; 23 24 import android.annotation.Nullable; 25 import android.app.ActivityManager; 26 import android.app.ActivityOptions; 27 import android.car.Car; 28 import android.car.CarAppFocusManager; 29 import android.car.cluster.navigation.NavigationState.NavigationStateProto; 30 import android.car.cluster.renderer.InstrumentClusterRenderingService; 31 import android.car.cluster.renderer.NavigationRenderer; 32 import android.car.navigation.CarNavigationInstrumentCluster; 33 import android.content.BroadcastReceiver; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.ContextWrapper; 37 import android.content.Intent; 38 import android.content.IntentFilter; 39 import android.content.pm.ActivityInfo; 40 import android.content.pm.PackageManager; 41 import android.graphics.Rect; 42 import android.hardware.display.DisplayManager; 43 import android.hardware.display.DisplayManager.DisplayListener; 44 import android.os.Binder; 45 import android.os.Bundle; 46 import android.os.Handler; 47 import android.os.IBinder; 48 import android.os.SystemClock; 49 import android.os.UserHandle; 50 import android.provider.Settings; 51 import android.provider.Settings.Global; 52 import android.util.Log; 53 import android.view.Display; 54 import android.view.InputDevice; 55 import android.view.KeyEvent; 56 57 import com.android.car.internal.common.UserHelperLite; 58 59 import com.google.protobuf.InvalidProtocolBufferException; 60 61 import java.io.FileDescriptor; 62 import java.io.PrintWriter; 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.function.Consumer; 68 69 /** 70 * Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a 71 * virtual display that is transmitted to an external screen. 72 */ 73 public class ClusterRenderingService extends InstrumentClusterRenderingService implements 74 ImageResolver.BitmapFetcher, CarAppFocusManager.OnAppFocusChangedListener { 75 private static final String TAG = "Cluster.Service"; 76 77 static final String LOCAL_BINDING_ACTION = "local"; 78 static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2"; 79 80 private List<ServiceClient> mClients = new ArrayList<>(); 81 private ClusterDisplayProvider mDisplayProvider; 82 83 private int mClusterDisplayId = INVALID_DISPLAY; 84 85 private boolean mInstrumentClusterHelperReady; 86 87 private final IBinder mLocalBinder = new LocalBinder(); 88 private final ImageResolver mImageResolver = new ImageResolver(this); 89 private final Handler mHandler = new Handler(); 90 private final Runnable mLaunchMainActivity = this::launchMainActivity; 91 private ComponentName mNavigationClusterActivity = null; 92 private int mNavigationClusterUserId = UserHandle.USER_SYSTEM; 93 private CarAppFocusManager mAppFocusManager = null; 94 95 private final UserReceiver mUserReceiver = new UserReceiver(); 96 97 public interface ServiceClient { onKeyEvent(KeyEvent keyEvent)98 void onKeyEvent(KeyEvent keyEvent); 99 onNavigationStateChange(NavigationStateProto navState)100 void onNavigationStateChange(NavigationStateProto navState); 101 } 102 103 public class LocalBinder extends Binder { getService()104 ClusterRenderingService getService() { 105 return ClusterRenderingService.this; 106 } 107 } 108 109 private final DisplayListener mDisplayListener = new DisplayListener() { 110 // Called in the main thread, since ClusterDisplayProvider.DisplayListener was registered 111 // with null handler. 112 @Override 113 public void onDisplayAdded(int displayId) { 114 Log.i(TAG, "Cluster display found, displayId: " + displayId); 115 mClusterDisplayId = displayId; 116 if (mInstrumentClusterHelperReady) { 117 mHandler.post(mLaunchMainActivity); 118 } 119 } 120 121 @Override 122 public void onDisplayRemoved(int displayId) { 123 Log.w(TAG, "Cluster display has been removed"); 124 } 125 126 @Override 127 public void onDisplayChanged(int displayId) { 128 129 } 130 }; 131 setActivityLaunchOptions(int displayId, ClusterActivityState state)132 public void setActivityLaunchOptions(int displayId, ClusterActivityState state) { 133 ActivityOptions options = displayId != INVALID_DISPLAY 134 ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId) 135 : null; 136 setClusterActivityLaunchOptions(options); 137 if (Log.isLoggable(TAG, Log.DEBUG)) { 138 Log.d(TAG, String.format("activity options set: %s (displayeId: %d)", 139 options, options != null ? options.getLaunchDisplayId() : -1)); 140 } 141 setClusterActivityState(state); 142 if (Log.isLoggable(TAG, Log.DEBUG)) { 143 Log.d(TAG, String.format("activity state set: %s", state)); 144 } 145 } 146 registerClient(ServiceClient client)147 public void registerClient(ServiceClient client) { 148 mClients.add(client); 149 } 150 unregisterClient(ServiceClient client)151 public void unregisterClient(ServiceClient client) { 152 mClients.remove(client); 153 } 154 getImageResolver()155 public ImageResolver getImageResolver() { 156 return mImageResolver; 157 } 158 159 @Override onBind(Intent intent)160 public IBinder onBind(Intent intent) { 161 Log.d(TAG, "onBind, intent: " + intent); 162 if (LOCAL_BINDING_ACTION.equals(intent.getAction())) { 163 return mLocalBinder; 164 } 165 IBinder binder = super.onBind(intent); 166 mInstrumentClusterHelperReady = true; 167 if (mClusterDisplayId != INVALID_DISPLAY) { 168 mHandler.post(mLaunchMainActivity); 169 } 170 return binder; 171 } 172 173 @Override onCreate()174 public void onCreate() { 175 super.onCreate(); 176 Log.d(TAG, "onCreate"); 177 // The following will never be null, as this service is initiated by CarService itself. 178 Car car = Car.createCar(this); 179 mAppFocusManager = (CarAppFocusManager) car.getCarManager(Car.APP_FOCUS_SERVICE); 180 mAppFocusManager.addFocusListener(this, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 181 mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener); 182 mUserReceiver.register(this); 183 mNavigationClusterActivity = getNavigationClusterActivity(); 184 Log.i(TAG, "onCreate: set cluster to " + mNavigationClusterActivity); 185 } 186 187 @Override onDestroy()188 public void onDestroy() { 189 super.onDestroy(); 190 mAppFocusManager.removeFocusListener(this, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 191 mUserReceiver.unregister(this); 192 mDisplayProvider.release(); 193 } 194 195 @Override onAppFocusChanged(int appType, boolean active)196 public void onAppFocusChanged(int appType, boolean active) { 197 boolean useNavigationOnly = getResources().getBoolean(R.bool.navigationOnly); 198 Log.i(TAG, "onAppFocusChanged: " + appType + ", active: " + active); 199 if (useNavigationOnly) { 200 launchMainActivity(); 201 } else { 202 // TODO(b/193931272): Update MainClusterActivity 203 } 204 } 205 launchMainActivity()206 private void launchMainActivity() { 207 mHandler.removeCallbacks(mLaunchMainActivity); 208 ActivityOptions options = ActivityOptions.makeBasic(); 209 options.setLaunchDisplayId(mClusterDisplayId); 210 boolean useNavigationOnly = getResources().getBoolean(R.bool.navigationOnly); 211 Intent intent; 212 int userId = UserHandle.USER_SYSTEM; 213 if (useNavigationOnly) { 214 userId = ActivityManager.getCurrentUser(); 215 if (UserHelperLite.isHeadlessSystemUser(userId)) { 216 Log.i(TAG, "Skipping the navigation activity for User 0"); 217 return; 218 } 219 ComponentName newClusterActivity = getNavigationClusterActivity(); 220 if (Objects.equals(newClusterActivity, mNavigationClusterActivity) 221 && userId == mNavigationClusterUserId) { 222 Log.i(TAG, "Cluster activity hasn't changed. Skipping."); 223 return; 224 } 225 Log.i(TAG, "Set cluster to " + newClusterActivity); 226 onNavigationComponentChanged(newClusterActivity); 227 mNavigationClusterActivity = newClusterActivity; 228 mNavigationClusterUserId = userId; 229 intent = getNavigationActivityIntent(mNavigationClusterActivity, mClusterDisplayId); 230 startFixedActivityModeForDisplayAndUser(intent, options, userId); 231 } else { 232 intent = getMainClusterActivityIntent(); 233 startActivityAsUser(intent, options.toBundle(), UserHandle.SYSTEM); 234 } 235 Log.i(TAG, "launching main activity=" + intent + ", display=" + mClusterDisplayId 236 + ", userId=" + userId); 237 } 238 239 /** 240 * Invoked when the activity to show in the cluster changes 241 * 242 * @param clusterActivity current activity displayed in cluster. If no application is holding 243 * {@link CarAppFocusManager#APP_FOCUS_TYPE_NAVIGATION}, this will be the 244 * default map cluster activity. Otherwise, this will be the cluster 245 * activity of the focused application (if it has one) or {@code null} if 246 * the application doesn't have a cluster activity or the activity is 247 * disabled. 248 */ onNavigationComponentChanged(@ullable ComponentName clusterActivity)249 protected void onNavigationComponentChanged(@Nullable ComponentName clusterActivity) { 250 // This method can be used by OEMs to send a signal to the cluster hardware indicating 251 // whether Android has or doesn't have a cluster activity. 252 // 253 // OEMs can use this signal to let the cluster show some other view, or to hide Android's 254 // video feed altogether. 255 } 256 getMainClusterActivityIntent()257 private Intent getMainClusterActivityIntent() { 258 return new Intent(this, MainClusterActivity.class).setFlags(FLAG_ACTIVITY_NEW_TASK); 259 } 260 getNavigationClusterActivity()261 private ComponentName getNavigationClusterActivity() { 262 List<String> focusOwnerPackageNames = mAppFocusManager.getAppTypeOwner( 263 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 264 265 if (focusOwnerPackageNames == null || focusOwnerPackageNames.isEmpty()) { 266 // No application has focus. We use the default navigation app. 267 Log.i(TAG, "getNavigationClusterActivity(): no focus owner -> " 268 + "using default nav app"); 269 ActivityInfo activityInfo = MainClusterActivity.getNavigationActivity(this); 270 return new ComponentName(activityInfo.packageName, activityInfo.name); 271 } else { 272 ComponentName clusterActivity = getComponentFromPackages(focusOwnerPackageNames); 273 if (clusterActivity == null) { 274 // If currently focused app has no cluster activity, we indicate so. 275 Log.i(TAG, "getNavigationClusterActivity(): focus owned by " 276 + focusOwnerPackageNames + " but it has no cluster activity -> " 277 + "using empty activity"); 278 return null; 279 } 280 // Otherwise, we use the activity of the currently focused app 281 Log.i(TAG, "getNavigationClusterActivity(): focus owned and it has a cluster " 282 + "activity -> using " + focusOwnerPackageNames + " app"); 283 return clusterActivity; 284 } 285 } 286 getComponentFromPackages(List<String> packageNames)287 private ComponentName getComponentFromPackages(List<String> packageNames) { 288 for (String packageName : packageNames) { 289 ComponentName result = getComponentFromPackage(packageName); 290 if (result != null) { 291 return result; 292 } 293 } 294 return null; 295 } 296 getNavigationActivityIntent(ComponentName component, int displayId)297 private Intent getNavigationActivityIntent(ComponentName component, int displayId) { 298 if (component == null) { 299 Log.i(TAG, "Focused application doesn't have a cluster activity. Using fallback."); 300 component = new ComponentName(this, EmptyNavigationActivity.class); 301 } 302 Rect displaySize = new Rect(0, 0, 240, 320); // Arbitrary size, better than nothing. 303 DisplayManager dm = getSystemService(DisplayManager.class); 304 Display display = dm.getDisplay(displayId); 305 if (display != null) { 306 display.getRectSize(displaySize); 307 } 308 setClusterActivityState(ClusterActivityState.create(/* visible= */ true, 309 /* unobscuredBounds= */ new Rect(0, 0, 240, 320))); 310 return new Intent(Intent.ACTION_MAIN) 311 .setComponent(component) 312 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 313 .putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE, 314 ClusterActivityState.create(/* visible= */ true, 315 /* unobscuredBounds= */ displaySize).toBundle()); 316 } 317 318 @Override onKeyEvent(KeyEvent keyEvent)319 public void onKeyEvent(KeyEvent keyEvent) { 320 if (Log.isLoggable(TAG, Log.DEBUG)) { 321 Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent); 322 } 323 broadcastClientEvent(client -> client.onKeyEvent(keyEvent)); 324 } 325 326 /** 327 * Broadcasts an event to all the registered service clients 328 * 329 * @param event event to broadcast 330 */ broadcastClientEvent(Consumer<ServiceClient> event)331 private void broadcastClientEvent(Consumer<ServiceClient> event) { 332 for (ServiceClient client : mClients) { 333 event.accept(client); 334 } 335 } 336 337 @Override getNavigationRenderer()338 public NavigationRenderer getNavigationRenderer() { 339 NavigationRenderer navigationRenderer = new NavigationRenderer() { 340 @Override 341 public CarNavigationInstrumentCluster getNavigationProperties() { 342 CarNavigationInstrumentCluster config = 343 CarNavigationInstrumentCluster.createCluster(1000); 344 Log.d(TAG, "getNavigationProperties, returns: " + config); 345 return config; 346 } 347 348 @Override 349 public void onNavigationStateChanged(Bundle bundle) { 350 StringBuilder bundleSummary = new StringBuilder(); 351 352 // Attempt to read proto byte array 353 byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY); 354 if (protoBytes != null) { 355 try { 356 NavigationStateProto navState = NavigationStateProto.parseFrom( 357 protoBytes); 358 bundleSummary.append(navState.toString()); 359 360 // Update clients 361 broadcastClientEvent( 362 client -> client.onNavigationStateChange(navState)); 363 } catch (InvalidProtocolBufferException e) { 364 Log.e(TAG, "Error parsing navigation state proto", e); 365 } 366 } else { 367 Log.e(TAG, "Received nav state byte array is null"); 368 } 369 Log.d(TAG, "onNavigationStateChanged(" + bundleSummary + ")"); 370 } 371 }; 372 373 Log.i(TAG, "createNavigationRenderer, returns: " + navigationRenderer); 374 return navigationRenderer; 375 } 376 377 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)378 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 379 if (args != null && args.length > 0) { 380 execShellCommand(args); 381 } else { 382 super.dump(fd, writer, args); 383 writer.println("DisplayProvider: " + mDisplayProvider); 384 } 385 } 386 emulateKeyEvent(int keyCode)387 private void emulateKeyEvent(int keyCode) { 388 Log.i(TAG, "emulateKeyEvent, keyCode: " + keyCode); 389 long downTime = SystemClock.uptimeMillis(); 390 long eventTime = SystemClock.uptimeMillis(); 391 KeyEvent event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_DOWN); 392 onKeyEvent(event); 393 394 eventTime = SystemClock.uptimeMillis(); 395 event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_UP); 396 onKeyEvent(event); 397 } 398 obtainKeyEvent(int keyCode, long downTime, long eventTime, int action)399 private KeyEvent obtainKeyEvent(int keyCode, long downTime, long eventTime, int action) { 400 int scanCode = 0; 401 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 402 scanCode = 108; 403 } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 404 scanCode = 106; 405 } 406 return KeyEvent.obtain( 407 downTime, 408 eventTime, 409 action, 410 keyCode, 411 0 /* repeat */, 412 0 /* meta state */, 413 0 /* deviceId*/, 414 scanCode /* scancode */, 415 KeyEvent.FLAG_FROM_SYSTEM /* flags */, 416 InputDevice.SOURCE_KEYBOARD, 417 null /* characters */); 418 } 419 execShellCommand(String[] args)420 private void execShellCommand(String[] args) { 421 Log.i(TAG, "execShellCommand, args: " + Arrays.toString(args)); 422 423 String command = args[0]; 424 425 switch (command) { 426 case "injectKey": { 427 if (args.length > 1) { 428 emulateKeyEvent(parseInt(args[1])); 429 } else { 430 Log.i(TAG, "Not enough arguments"); 431 } 432 break; 433 } 434 case "destroyOverlayDisplay": { 435 Settings.Global.putString(getContentResolver(), 436 Global.OVERLAY_DISPLAY_DEVICES, ""); 437 break; 438 } 439 440 case "createOverlayDisplay": { 441 if (args.length > 1) { 442 Settings.Global.putString(getContentResolver(), 443 Global.OVERLAY_DISPLAY_DEVICES, args[1]); 444 } else { 445 Log.i(TAG, "Not enough arguments, expected 2"); 446 } 447 break; 448 } 449 450 case "setUnobscuredArea": { 451 if (args.length > 5) { 452 setClusterActivityState(ClusterActivityState.create(true, 453 new Rect(parseInt(args[2]), parseInt(args[3]), 454 parseInt(args[4]), parseInt(args[5])))); 455 } else { 456 Log.i(TAG, "wrong format, expected: category left top right bottom"); 457 } 458 } 459 } 460 } 461 462 private class UserReceiver extends BroadcastReceiver { register(Context context)463 void register(Context context) { 464 IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED); 465 context.registerReceiverAsUser(this, UserHandle.ALL, intentFilter, null, null); 466 } 467 unregister(Context context)468 void unregister(Context context) { 469 context.unregisterReceiver(this); 470 } 471 472 @Override onReceive(Context context, Intent intent)473 public void onReceive(Context context, Intent intent) { 474 if (Log.isLoggable(TAG, Log.DEBUG)) { 475 Log.d(TAG, "Broadcast received: " + intent); 476 } 477 int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); 478 if (userId == ActivityManager.getCurrentUser() && 479 mInstrumentClusterHelperReady && mClusterDisplayId != INVALID_DISPLAY) { 480 mHandler.post(mLaunchMainActivity); 481 } 482 } 483 } 484 } 485