1 /* 2 * Copyright (C) 2023 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 17 package com.example.android.vdmdemo.host; 18 19 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; 20 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; 21 import static android.companion.virtual.VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED; 22 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; 23 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; 24 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; 25 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; 26 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; 27 28 import android.annotation.SuppressLint; 29 import android.app.Notification; 30 import android.app.NotificationChannel; 31 import android.app.NotificationManager; 32 import android.app.PendingIntent; 33 import android.app.Service; 34 import android.app.role.RoleManager; 35 import android.companion.AssociationInfo; 36 import android.companion.AssociationRequest; 37 import android.companion.CompanionDeviceManager; 38 import android.companion.virtual.VirtualDeviceManager; 39 import android.companion.virtual.VirtualDeviceManager.ActivityListener; 40 import android.companion.virtual.VirtualDeviceParams; 41 import android.companion.virtual.sensor.VirtualSensorConfig; 42 import android.content.ComponentName; 43 import android.content.Intent; 44 import android.content.IntentSender; 45 import android.content.IntentSender.SendIntentException; 46 import android.content.pm.ActivityInfo; 47 import android.content.pm.PackageManager.NameNotFoundException; 48 import android.graphics.drawable.Icon; 49 import android.hardware.display.DisplayManager; 50 import android.media.AudioManager; 51 import android.os.Binder; 52 import android.os.IBinder; 53 import android.util.Log; 54 import android.view.Display; 55 56 import androidx.annotation.NonNull; 57 import androidx.core.os.BuildCompat; 58 59 import com.example.android.vdmdemo.common.ConnectionManager; 60 import com.example.android.vdmdemo.common.RemoteEventProto.DeviceCapabilities; 61 import com.example.android.vdmdemo.common.RemoteEventProto.DisplayChangeEvent; 62 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent; 63 import com.example.android.vdmdemo.common.RemoteEventProto.SensorCapabilities; 64 import com.example.android.vdmdemo.common.RemoteEventProto.StartStreaming; 65 import com.example.android.vdmdemo.common.RemoteIo; 66 import com.google.common.util.concurrent.MoreExecutors; 67 68 import dagger.hilt.android.AndroidEntryPoint; 69 70 import java.util.Arrays; 71 import java.util.HashMap; 72 import java.util.Map; 73 import java.util.Objects; 74 import java.util.concurrent.Executors; 75 import java.util.function.Consumer; 76 77 import javax.inject.Inject; 78 79 /** 80 * VDM Host service, streaming apps to a remote device and processing the input coming from there. 81 */ 82 @AndroidEntryPoint(Service.class) 83 @SuppressLint("NewApi") 84 public final class VdmService extends Hilt_VdmService { 85 86 public static final String TAG = "VdmHost"; 87 88 private static final String CHANNEL_ID = "com.example.android.vdmdemo.host.VdmService"; 89 private static final int NOTIFICATION_ID = 1; 90 91 private static final String ACTION_STOP = "com.example.android.vdmdemo.host.VdmService.STOP"; 92 93 public static final String ACTION_LOCKDOWN = 94 "com.example.android.vdmdemo.host.VdmService.LOCKDOWN"; 95 private int mRecordingAudioSessionId; 96 private int mPlaybackAudioSessionId; 97 98 /** Provides an instance of this service to bound clients. */ 99 public class LocalBinder extends Binder { getService()100 VdmService getService() { 101 return VdmService.this; 102 } 103 } 104 105 private final IBinder mBinder = new LocalBinder(); 106 107 private final Map<Integer, Consumer<Object>> mPreferenceObservers = createPreferenceObservers(); 108 109 @Inject 110 ConnectionManager mConnectionManager; 111 @Inject 112 RemoteIo mRemoteIo; 113 @Inject 114 AudioStreamer mAudioStreamer; 115 @Inject 116 AudioInjector mAudioInjector; 117 @Inject 118 PreferenceController mPreferenceController; 119 @Inject 120 DisplayRepository mDisplayRepository; 121 @Inject 122 InputController mInputController; 123 124 private RemoteSensorManager mRemoteSensorManager = null; 125 126 private RemoteCameraManager mRemoteCameraManager; 127 private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent; 128 private VirtualDeviceManager.VirtualDevice mVirtualDevice; 129 private DeviceCapabilities mDeviceCapabilities; 130 private Intent mPendingRemoteIntent = null; 131 private @RemoteDisplay.DisplayType int mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP; 132 private DisplayManager mDisplayManager; 133 private VirtualDeviceManager mVirtualDeviceManager; 134 private Consumer<Boolean> mLocalVirtualDeviceLifecycleListener; 135 136 private VirtualDeviceManager.VirtualDeviceListener mVirtualDeviceListener; 137 138 private final DisplayManager.DisplayListener mDisplayListener = 139 new DisplayManager.DisplayListener() { 140 @Override 141 public void onDisplayAdded(int displayId) { 142 } 143 144 @Override 145 public void onDisplayRemoved(int displayId) { 146 } 147 148 @Override 149 public void onDisplayChanged(int displayId) { 150 mDisplayRepository.onDisplayChanged(displayId); 151 } 152 }; 153 154 private final Consumer<ConnectionManager.ConnectionStatus> mConnectionCallback = 155 (status) -> { 156 if (status.state != ConnectionManager.ConnectionStatus.State.CONNECTED) { 157 mDeviceCapabilities = null; 158 closeVirtualDevice(); 159 } 160 }; 161 VdmService()162 public VdmService() { 163 } 164 165 @Override onBind(Intent intent)166 public IBinder onBind(Intent intent) { 167 return mBinder; 168 } 169 170 @Override onStartCommand(Intent intent, int flags, int startId)171 public int onStartCommand(Intent intent, int flags, int startId) { 172 if (intent != null && ACTION_STOP.equals(intent.getAction())) { 173 Log.i(TAG, "Stopping VDM Service."); 174 mConnectionManager.disconnect(); 175 stopForeground(STOP_FOREGROUND_REMOVE); 176 stopSelf(); 177 return START_NOT_STICKY; 178 } 179 180 if (intent != null && ACTION_LOCKDOWN.equals(intent.getAction())) { 181 lockdown(); 182 return START_STICKY; 183 } 184 185 NotificationChannel notificationChannel = 186 new NotificationChannel( 187 CHANNEL_ID, "VDM Service Channel", NotificationManager.IMPORTANCE_LOW); 188 notificationChannel.enableVibration(false); 189 NotificationManager notificationManager = getSystemService(NotificationManager.class); 190 Objects.requireNonNull(notificationManager).createNotificationChannel(notificationChannel); 191 192 Intent openIntent = new Intent(this, MainActivity.class); 193 PendingIntent pendingIntentOpen = 194 PendingIntent.getActivity(this, 0, openIntent, PendingIntent.FLAG_IMMUTABLE); 195 Intent stopIntent = new Intent(this, VdmService.class); 196 stopIntent.setAction(ACTION_STOP); 197 PendingIntent pendingIntentStop = 198 PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE); 199 200 Notification notification = 201 new Notification.Builder(this, CHANNEL_ID) 202 .setSmallIcon(R.drawable.connected) 203 .setContentTitle("VDM Demo running") 204 .setContentText("Click to open") 205 .setContentIntent(pendingIntentOpen) 206 .addAction( 207 new Notification.Action.Builder( 208 Icon.createWithResource("", R.drawable.close), "Stop", 209 pendingIntentStop) 210 .build()) 211 .setOngoing(true) 212 .build(); 213 startForeground(NOTIFICATION_ID, notification); 214 215 return START_STICKY; 216 } 217 218 @Override onCreate()219 public void onCreate() { 220 super.onCreate(); 221 222 mConnectionManager.addConnectionCallback(mConnectionCallback); 223 mConnectionManager.startHostSession(); 224 225 mDisplayManager = getSystemService(DisplayManager.class); 226 Objects.requireNonNull(mDisplayManager).registerDisplayListener(mDisplayListener, null); 227 228 mRemoteIo.addMessageConsumer(mRemoteEventConsumer); 229 230 mPreferenceController.addPreferenceObserver(this, mPreferenceObservers); 231 232 mVirtualDeviceManager = 233 Objects.requireNonNull(getSystemService(VirtualDeviceManager.class)); 234 235 if (BuildCompat.isAtLeastV()) { 236 mVirtualDeviceListener = new VirtualDeviceManager.VirtualDeviceListener() { 237 @Override 238 public void onVirtualDeviceClosed(int deviceId) { 239 if (mVirtualDevice != null && mVirtualDevice.getDeviceId() == deviceId) { 240 closeVirtualDevice(); 241 } 242 } 243 }; 244 mVirtualDeviceManager.registerVirtualDeviceListener( 245 Executors.newSingleThreadExecutor(), mVirtualDeviceListener); 246 } 247 } 248 249 @Override onDestroy()250 public void onDestroy() { 251 super.onDestroy(); 252 if (BuildCompat.isAtLeastV()) { 253 mVirtualDeviceManager.unregisterVirtualDeviceListener(mVirtualDeviceListener); 254 } 255 mPreferenceController.removePreferenceObserver(this); 256 mConnectionManager.removeConnectionCallback(mConnectionCallback); 257 closeVirtualDevice(); 258 mRemoteIo.removeMessageConsumer(mRemoteEventConsumer); 259 mDisplayManager.unregisterDisplayListener(mDisplayListener); 260 mAudioStreamer.stop(); 261 mAudioInjector.stop(); 262 } 263 setVirtualDeviceListener(Consumer<Boolean> listener)264 void setVirtualDeviceListener(Consumer<Boolean> listener) { 265 mLocalVirtualDeviceLifecycleListener = listener; 266 } 267 processRemoteEvent(RemoteEvent event)268 private void processRemoteEvent(RemoteEvent event) { 269 if (event.hasDeviceCapabilities()) { 270 mDeviceCapabilities = event.getDeviceCapabilities(); 271 associateAndCreateVirtualDevice(); 272 handleAudioCapabilities(); 273 } else if (event.hasDisplayCapabilities() && !mDisplayRepository.resetDisplay(event)) { 274 RemoteDisplay remoteDisplay = 275 new RemoteDisplay( 276 this, 277 event, 278 mVirtualDevice, 279 mRemoteIo, 280 mPendingDisplayType, 281 mPreferenceController); 282 mDisplayRepository.addDisplay(remoteDisplay); 283 mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP; 284 if (mPendingRemoteIntent != null) { 285 remoteDisplay.launchIntent(mPendingRemoteIntent); 286 mPendingRemoteIntent = null; 287 } 288 } else if (event.hasStopStreaming() && !event.getStopStreaming().getPause()) { 289 mDisplayRepository.removeDisplayByRemoteId(event.getDisplayId()); 290 } else if (event.hasDisplayChangeEvent() && event.getDisplayChangeEvent().getFocused()) { 291 mInputController.setFocusedRemoteDisplayId(event.getDisplayId()); 292 } 293 } 294 handleAudioCapabilities()295 private void handleAudioCapabilities() { 296 if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) { 297 if (mVirtualDevice != null) { 298 if (mDeviceCapabilities.getSupportsAudioOutput()) { 299 mAudioStreamer.start(mVirtualDevice.getDeviceId(), mPlaybackAudioSessionId); 300 } 301 if (mDeviceCapabilities.getSupportsAudioInput()) { 302 mAudioInjector.start(mVirtualDevice.getDeviceId(), mRecordingAudioSessionId); 303 } 304 } 305 } else { 306 mAudioStreamer.stop(); 307 mAudioInjector.stop(); 308 } 309 } 310 associateAndCreateVirtualDevice()311 private void associateAndCreateVirtualDevice() { 312 CompanionDeviceManager cdm = 313 Objects.requireNonNull(getSystemService(CompanionDeviceManager.class)); 314 RoleManager rm = Objects.requireNonNull(getSystemService(RoleManager.class)); 315 final String deviceProfile = mPreferenceController.getString(R.string.pref_device_profile); 316 for (AssociationInfo associationInfo : cdm.getMyAssociations()) { 317 if (!Objects.equals(associationInfo.getDeviceProfile(), deviceProfile) 318 || !Objects.equals(associationInfo.getPackageName(), getPackageName()) 319 || associationInfo.getDisplayName() == null 320 || !Objects.equals( 321 associationInfo.getDisplayName().toString(), 322 mDeviceCapabilities.getDeviceName())) { 323 continue; 324 } 325 // It is possible that the role was revoked but the CDM association remained. 326 if (!rm.isRoleHeld(deviceProfile)) { 327 cdm.disassociate(associationInfo.getId()); 328 break; 329 } else { 330 createVirtualDevice(associationInfo); 331 return; 332 } 333 } 334 335 @SuppressLint("MissingPermission") 336 AssociationRequest.Builder associationRequest = 337 new AssociationRequest.Builder() 338 .setDeviceProfile(deviceProfile) 339 .setDisplayName(mDeviceCapabilities.getDeviceName()) 340 .setSelfManaged(true); 341 cdm.associate( 342 associationRequest.build(), 343 new CompanionDeviceManager.Callback() { 344 @Override 345 public void onAssociationPending(@NonNull IntentSender intentSender) { 346 try { 347 startIntentSender(intentSender, null, 0, 0, 0); 348 } catch (SendIntentException e) { 349 Log.e( 350 TAG, 351 "onAssociationPending: Failed to send device selection intent", 352 e); 353 } 354 } 355 356 @Override 357 public void onAssociationCreated(@NonNull AssociationInfo associationInfo) { 358 Log.i(TAG, "onAssociationCreated: ID " + associationInfo.getId()); 359 createVirtualDevice(associationInfo); 360 } 361 362 @Override 363 public void onFailure(CharSequence error) { 364 Log.e(TAG, "onFailure: RemoteDevice Association failed " + error); 365 } 366 }, 367 null); 368 Log.i(TAG, "createCdmAssociation: Waiting for association to happen"); 369 } 370 createVirtualDevice(AssociationInfo associationInfo)371 private void createVirtualDevice(AssociationInfo associationInfo) { 372 VirtualDeviceParams.Builder virtualDeviceBuilder = 373 new VirtualDeviceParams.Builder() 374 .setName("VirtualDevice - " + mDeviceCapabilities.getDeviceName()); 375 376 AudioManager audioManager = getSystemService(AudioManager.class); 377 mPlaybackAudioSessionId = audioManager.generateAudioSessionId(); 378 mRecordingAudioSessionId = audioManager.generateAudioSessionId(); 379 380 if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) { 381 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM) 382 .setAudioPlaybackSessionId(mPlaybackAudioSessionId) 383 .setAudioRecordingSessionId(mRecordingAudioSessionId); 384 } 385 386 if (mPreferenceController.getBoolean(R.string.pref_always_unlocked_device)) { 387 virtualDeviceBuilder.setLockState(LOCK_STATE_ALWAYS_UNLOCKED); 388 } 389 390 if (mPreferenceController.getBoolean(R.string.pref_enable_custom_home)) { 391 virtualDeviceBuilder.setHomeComponent( 392 new ComponentName(this, CustomLauncherActivity.class)); 393 } 394 395 if (mPreferenceController.getBoolean(R.string.pref_hide_from_recents)) { 396 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM); 397 } 398 399 if (mPreferenceController.getBoolean(R.string.pref_enable_cross_device_clipboard)) { 400 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_CUSTOM); 401 } 402 403 if (mPreferenceController.getBoolean(R.string.pref_enable_client_native_ime)) { 404 virtualDeviceBuilder.setInputMethodComponent( 405 new ComponentName(this, VdmProxyIme.class)); 406 } 407 408 if (mPreferenceController.getBoolean(R.string.pref_enable_client_sensors)) { 409 for (SensorCapabilities sensor : mDeviceCapabilities.getSensorCapabilitiesList()) { 410 virtualDeviceBuilder.addVirtualSensorConfig( 411 new VirtualSensorConfig.Builder( 412 sensor.getType(), "Remote-" + sensor.getName()) 413 .setMinDelay(sensor.getMinDelayUs()) 414 .setMaxDelay(sensor.getMaxDelayUs()) 415 .setPower(sensor.getPower()) 416 .setResolution(sensor.getResolution()) 417 .setMaximumRange(sensor.getMaxRange()) 418 .build()); 419 } 420 421 if (mDeviceCapabilities.getSensorCapabilitiesCount() > 0) { 422 mRemoteSensorManager = new RemoteSensorManager(mRemoteIo); 423 virtualDeviceBuilder 424 .setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) 425 .setVirtualSensorCallback( 426 MoreExecutors.directExecutor(), 427 mRemoteSensorManager.getVirtualSensorCallback()); 428 } 429 } 430 431 if (mPreferenceController.getBoolean(R.string.pref_enable_client_camera)) { 432 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CAMERA, DEVICE_POLICY_CUSTOM); 433 } 434 435 mVirtualDevice = mVirtualDeviceManager 436 .createVirtualDevice(associationInfo.getId(), virtualDeviceBuilder.build()); 437 if (mRemoteSensorManager != null) { 438 mRemoteSensorManager.setVirtualSensors(mVirtualDevice.getVirtualSensorList()); 439 } 440 441 mVirtualDevice.setShowPointerIcon( 442 mPreferenceController.getBoolean(R.string.pref_show_pointer_icon)); 443 444 mVirtualDevice.addActivityListener( 445 MoreExecutors.directExecutor(), 446 new ActivityListener() { 447 448 @Override 449 public void onTopActivityChanged( 450 int displayId, @NonNull ComponentName componentName) { 451 Log.w(TAG, "onTopActivityChanged " + displayId + ": " + componentName); 452 int remoteDisplayId = mDisplayRepository.getRemoteDisplayId(displayId); 453 if (remoteDisplayId == Display.INVALID_DISPLAY) { 454 return; 455 } 456 457 String title = ""; 458 try { 459 ActivityInfo activityInfo = 460 getPackageManager().getActivityInfo(componentName, 0); 461 title = activityInfo.loadLabel(getPackageManager()).toString(); 462 } catch (NameNotFoundException e) { 463 Log.w(TAG, "Failed to get activity label for " + componentName); 464 } 465 mRemoteIo.sendMessage( 466 RemoteEvent.newBuilder() 467 .setDisplayId(remoteDisplayId) 468 .setDisplayChangeEvent( 469 DisplayChangeEvent.newBuilder().setTitle(title)) 470 .build()); 471 } 472 473 @Override 474 public void onDisplayEmpty(int displayId) { 475 Log.i(TAG, "Display " + displayId + " is empty, removing"); 476 mDisplayRepository.removeDisplay(displayId); 477 } 478 }); 479 mVirtualDevice.addActivityListener( 480 MoreExecutors.directExecutor(), 481 new RunningVdmUidsTracker(getApplicationContext(), mPreferenceController, 482 mAudioStreamer, mAudioInjector)); 483 484 if (mPreferenceController.getBoolean(R.string.pref_enable_client_camera)) { 485 if (mRemoteCameraManager != null) { 486 mRemoteCameraManager.close(); 487 } 488 mRemoteCameraManager = new RemoteCameraManager(mVirtualDevice, mRemoteIo); 489 mRemoteCameraManager.createCameras(mDeviceCapabilities.getCameraCapabilitiesList()); 490 } 491 492 Log.i(TAG, "Created virtual device"); 493 if (mLocalVirtualDeviceLifecycleListener != null) { 494 mLocalVirtualDeviceLifecycleListener.accept(true); 495 } 496 } 497 lockdown()498 private void lockdown() { 499 Log.i(TAG, "Initiating Lockdown."); 500 mDisplayRepository.clear(); 501 } 502 closeVirtualDevice()503 private synchronized void closeVirtualDevice() { 504 if (mLocalVirtualDeviceLifecycleListener != null) { 505 mLocalVirtualDeviceLifecycleListener.accept(false); 506 } 507 if (mRemoteSensorManager != null) { 508 mRemoteSensorManager.close(); 509 mRemoteSensorManager = null; 510 } 511 if (mVirtualDevice != null) { 512 Log.i(TAG, "Closing virtual device"); 513 mDisplayRepository.clear(); 514 mVirtualDevice.close(); 515 mVirtualDevice = null; 516 } 517 } 518 isVirtualDeviceActive()519 boolean isVirtualDeviceActive() { 520 return mVirtualDevice != null; 521 } 522 getRemoteDisplayIds()523 int[] getRemoteDisplayIds() { 524 return mDisplayRepository.getRemoteDisplayIds(); 525 } 526 startStreamingHome()527 void startStreamingHome() { 528 mPendingRemoteIntent = null; 529 mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_HOME; 530 mRemoteIo.sendMessage(RemoteEvent.newBuilder() 531 .setStartStreaming(StartStreaming.newBuilder().setHomeEnabled(true)).build()); 532 } 533 startMirroring()534 void startMirroring() { 535 mPendingRemoteIntent = null; 536 mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_MIRROR; 537 mRemoteIo.sendMessage( 538 RemoteEvent.newBuilder() 539 .setStartStreaming(StartStreaming.newBuilder().setHomeEnabled(true)) 540 .build()); 541 } 542 startStreaming(Intent intent)543 void startStreaming(Intent intent) { 544 mPendingRemoteIntent = intent; 545 mPendingRemoteIntent.addFlags( 546 Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 547 mRemoteIo.sendMessage( 548 RemoteEvent.newBuilder().setStartStreaming(StartStreaming.newBuilder()).build()); 549 } 550 startIntentOnDisplayIndex(Intent intent, int displayIndex)551 void startIntentOnDisplayIndex(Intent intent, int displayIndex) { 552 mDisplayRepository 553 .getDisplayByIndex(displayIndex) 554 .ifPresent(d -> d.launchIntent(intent)); 555 } 556 recreateVirtualDevice()557 private void recreateVirtualDevice() { 558 if (mVirtualDevice != null) { 559 closeVirtualDevice(); 560 if (mDeviceCapabilities != null) { 561 associateAndCreateVirtualDevice(); 562 } 563 } 564 } 565 updateDevicePolicy(int policyType, boolean custom)566 private void updateDevicePolicy(int policyType, boolean custom) { 567 if (!BuildCompat.isAtLeastV()) { 568 recreateVirtualDevice(); 569 } else if (mVirtualDevice != null) { 570 mVirtualDevice.setDevicePolicy( 571 policyType, custom ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT); 572 } 573 } 574 createPreferenceObservers()575 private Map<Integer, Consumer<Object>> createPreferenceObservers() { 576 HashMap<Integer, Consumer<Object>> observers = new HashMap<>(); 577 578 observers.put(R.string.pref_hide_from_recents, 579 b -> updateDevicePolicy(POLICY_TYPE_RECENTS, (Boolean) b)); 580 observers.put(R.string.pref_enable_cross_device_clipboard, 581 b -> updateDevicePolicy(POLICY_TYPE_CLIPBOARD, (Boolean) b)); 582 observers.put(R.string.pref_show_pointer_icon, 583 b -> { 584 if (mVirtualDevice != null) mVirtualDevice.setShowPointerIcon((Boolean) b); 585 }); 586 observers.put(R.string.pref_enable_client_audio, b -> handleAudioCapabilities()); 587 observers.put(R.string.pref_display_ime_policy, 588 s -> { 589 if (mVirtualDevice != null) { 590 int policy = Integer.valueOf((String) s); 591 Arrays.stream(mDisplayRepository.getDisplayIds()).forEach( 592 displayId -> mVirtualDevice.setDisplayImePolicy(displayId, policy)); 593 } 594 }); 595 observers.put(R.string.pref_enable_client_camera, v -> recreateVirtualDevice()); 596 observers.put(R.string.pref_enable_client_sensors, v -> recreateVirtualDevice()); 597 observers.put(R.string.pref_device_profile, v -> recreateVirtualDevice()); 598 observers.put(R.string.pref_always_unlocked_device, v -> recreateVirtualDevice()); 599 observers.put(R.string.pref_enable_client_native_ime, v -> recreateVirtualDevice()); 600 observers.put(R.string.pref_enable_custom_home, v -> recreateVirtualDevice()); 601 602 return observers; 603 } 604 } 605