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_BLOCKED_ACTIVITY; 24 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; 25 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; 26 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; 27 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; 28 29 import android.annotation.SuppressLint; 30 import android.app.ActivityOptions; 31 import android.app.KeyguardManager; 32 import android.app.Notification; 33 import android.app.NotificationChannel; 34 import android.app.NotificationManager; 35 import android.app.PendingIntent; 36 import android.app.Service; 37 import android.app.role.RoleManager; 38 import android.companion.AssociationInfo; 39 import android.companion.AssociationRequest; 40 import android.companion.BluetoothDeviceFilter; 41 import android.companion.CompanionDeviceManager; 42 import android.companion.virtual.VirtualDeviceManager; 43 import android.companion.virtual.VirtualDeviceManager.ActivityListener; 44 import android.companion.virtual.VirtualDeviceParams; 45 import android.companion.virtual.sensor.VirtualSensorConfig; 46 import android.companion.virtualdevice.flags.Flags; 47 import android.content.ComponentName; 48 import android.content.Context; 49 import android.content.Intent; 50 import android.content.IntentSender; 51 import android.content.IntentSender.SendIntentException; 52 import android.content.pm.ActivityInfo; 53 import android.content.pm.PackageManager.NameNotFoundException; 54 import android.graphics.drawable.Icon; 55 import android.hardware.display.DisplayManager; 56 import android.media.AudioManager; 57 import android.os.Binder; 58 import android.os.Build; 59 import android.os.Handler; 60 import android.os.IBinder; 61 import android.os.Looper; 62 import android.os.UserHandle; 63 import android.util.Log; 64 import android.view.Display; 65 import android.widget.Toast; 66 67 import androidx.annotation.NonNull; 68 import androidx.annotation.Nullable; 69 import androidx.core.os.BuildCompat; 70 71 import com.example.android.vdmdemo.common.ConnectionManager; 72 import com.example.android.vdmdemo.common.RemoteEventProto; 73 import com.example.android.vdmdemo.common.RemoteEventProto.DeviceCapabilities; 74 import com.example.android.vdmdemo.common.RemoteEventProto.DisplayChangeEvent; 75 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent; 76 import com.example.android.vdmdemo.common.RemoteEventProto.RequestBluetoothDiscoverable; 77 import com.example.android.vdmdemo.common.RemoteEventProto.SensorCapabilities; 78 import com.example.android.vdmdemo.common.RemoteEventProto.StartStreaming; 79 import com.example.android.vdmdemo.common.RemoteIo; 80 import com.google.common.util.concurrent.MoreExecutors; 81 82 import dagger.hilt.android.AndroidEntryPoint; 83 84 import java.time.Duration; 85 import java.util.ArrayList; 86 import java.util.Arrays; 87 import java.util.HashMap; 88 import java.util.Map; 89 import java.util.Objects; 90 import java.util.Optional; 91 import java.util.concurrent.Executors; 92 import java.util.function.Consumer; 93 import java.util.regex.Pattern; 94 95 import javax.inject.Inject; 96 97 /** 98 * VDM Host service, streaming apps to a remote device and processing the input coming from there. 99 */ 100 @AndroidEntryPoint(Service.class) 101 @SuppressLint("NewApi") 102 public final class VdmService extends Hilt_VdmService { 103 104 public static final String TAG = "VdmHost"; 105 106 private static final String CHANNEL_ID = "com.example.android.vdmdemo.host.VdmService"; 107 private static final int NOTIFICATION_ID = 1; 108 109 private static final String ACTION_STOP = "com.example.android.vdmdemo.host.VdmService.STOP"; 110 111 private int mRecordingAudioSessionId; 112 private int mPlaybackAudioSessionId; 113 114 /** Provides an instance of this service to bound clients. */ 115 public class LocalBinder extends Binder { getService()116 VdmService getService() { 117 return VdmService.this; 118 } 119 } 120 121 private final IBinder mBinder = new LocalBinder(); 122 123 private final Map<Integer, Consumer<Object>> mPreferenceObservers = createPreferenceObservers(); 124 125 @Inject 126 ConnectionManager mConnectionManager; 127 @Inject 128 RemoteIo mRemoteIo; 129 @Inject 130 AudioStreamer mAudioStreamer; 131 @Inject 132 AudioInjector mAudioInjector; 133 @Inject 134 PreferenceController mPreferenceController; 135 @Inject 136 DisplayRepository mDisplayRepository; 137 @Inject 138 InputController mInputController; 139 140 private RemoteSensorManager mRemoteSensorManager = null; 141 142 private RemoteCameraManager mRemoteCameraManager; 143 private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent; 144 private VirtualDeviceManager.VirtualDevice mVirtualDevice; 145 private DeviceCapabilities mDeviceCapabilities; 146 private Intent mPendingRemoteIntent = null; 147 private int mNextLocalDisplayId = 0; 148 private @RemoteDisplay.DisplayType int mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP; 149 private DisplayManager mDisplayManager; 150 private KeyguardManager mKeyguardManager; 151 private VirtualDeviceManager mVirtualDeviceManager; 152 private ArrayList<Consumer<Boolean>> mLocalVirtualDeviceLifecycleListeners = new ArrayList<>(); 153 154 private VirtualDeviceManager.VirtualDeviceListener mVirtualDeviceListener; 155 156 private final DisplayManager.DisplayListener mDisplayListener = 157 new DisplayManager.DisplayListener() { 158 @Override 159 public void onDisplayAdded(int displayId) { 160 } 161 162 @Override 163 public void onDisplayRemoved(int displayId) { 164 } 165 166 @Override 167 public void onDisplayChanged(int displayId) { 168 mDisplayRepository.onDisplayChanged(displayId); 169 } 170 }; 171 172 private final Consumer<ConnectionManager.ConnectionStatus> mConnectionCallback = 173 (status) -> { 174 if (status.state != ConnectionManager.ConnectionStatus.State.CONNECTED) { 175 mDeviceCapabilities = null; 176 closeVirtualDevice(); 177 } 178 }; 179 180 private final ActivityListener mActivityListener = new ActivityListener() { 181 @Override 182 public void onActivityLaunchBlocked( 183 int displayId, @NonNull ComponentName componentName, @NonNull UserHandle user, 184 @Nullable IntentSender intentSender) { 185 Log.w(TAG, "onActivityLaunchBlocked " + displayId + ": " + componentName); 186 187 if (!mPreferenceController.getBoolean(R.string.pref_enable_custom_activity_policy)) { 188 // The system dialog is shown on the virtual display. 189 return; 190 } 191 192 if (intentSender == null) { 193 showToast(displayId, componentName, 194 R.string.custom_activity_launch_blocked_message); 195 return; 196 } 197 198 // When the keyguard is locked, show a dialog prompting the user to unlock it. 199 if (mKeyguardManager.isKeyguardLocked()) { 200 startActivity( 201 UnlockKeyguardDialog.createIntent(VdmService.this, intentSender), 202 ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle()); 203 return; 204 } 205 206 // Try to launch the activity on the default display with NEW_TASK flag. 207 Intent fillInIntent = new Intent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 208 ActivityOptions activityOptions = ActivityOptions.makeBasic() 209 .setPendingIntentBackgroundActivityStartMode( 210 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) 211 .setLaunchDisplayId(Display.DEFAULT_DISPLAY); 212 try { 213 startIntentSender(intentSender, fillInIntent, /* flagsMask= */ 0, 214 /* flagsValues= */ 0, /* extraFlags= */ 0, activityOptions.toBundle()); 215 showToast(displayId, componentName, 216 R.string.custom_activity_launch_fallback_message); 217 } catch (IntentSender.SendIntentException e) { 218 Log.e(TAG, "Error while starting intent sender", e); 219 showToast(displayId, componentName, 220 R.string.custom_activity_launch_blocked_message); 221 } 222 } 223 224 @Override 225 public void onTopActivityChanged( 226 int displayId, @NonNull ComponentName componentName) { 227 Log.w(TAG, "onTopActivityChanged " + displayId + ": " + componentName); 228 int remoteDisplayId = mDisplayRepository.getRemoteDisplayId(displayId); 229 if (remoteDisplayId == Display.INVALID_DISPLAY) { 230 return; 231 } 232 final CharSequence title = getTitle(componentName); 233 mRemoteIo.sendMessage( 234 RemoteEvent.newBuilder() 235 .setDisplayId(remoteDisplayId) 236 .setDisplayChangeEvent( 237 DisplayChangeEvent.newBuilder().setTitle(title.toString())) 238 .build()); 239 } 240 241 @Override 242 public void onDisplayEmpty(int displayId) { 243 Log.i(TAG, "Display " + displayId + " is empty, removing"); 244 mDisplayRepository.removeDisplay(displayId); 245 } 246 247 @Override 248 public void onSecureWindowShown( 249 int displayId, @NonNull ComponentName componentName, @NonNull UserHandle user) { 250 Log.i(TAG, "Secure window shown on display " + displayId + " by " + componentName); 251 } 252 253 private CharSequence getTitle(ComponentName componentName) { 254 CharSequence title; 255 try { 256 ActivityInfo activityInfo = 257 getPackageManager().getActivityInfo(componentName, 0); 258 title = activityInfo.loadLabel(getPackageManager()).toString(); 259 } catch (NameNotFoundException e) { 260 Log.w(TAG, "Failed to get activity label for " + componentName); 261 title = ""; 262 } 263 return title; 264 } 265 266 private void showToast(int displayId, ComponentName componentName, int resId) { 267 final CharSequence title = getTitle(componentName); 268 final CharSequence text = getString(resId, title == null ? "this" : title, Build.MODEL); 269 new Handler(Looper.getMainLooper()).post(() -> Toast.makeText( 270 createDisplayContext(mDisplayManager.getDisplay(displayId)), 271 text, Toast.LENGTH_LONG) 272 .show()); 273 } 274 }; 275 VdmService()276 public VdmService() { 277 } 278 279 @Override onBind(Intent intent)280 public IBinder onBind(Intent intent) { 281 return mBinder; 282 } 283 284 @Override onStartCommand(Intent intent, int flags, int startId)285 public int onStartCommand(Intent intent, int flags, int startId) { 286 if (intent != null && ACTION_STOP.equals(intent.getAction())) { 287 Log.i(TAG, "Stopping VDM Service."); 288 mConnectionManager.disconnect(); 289 stopForeground(STOP_FOREGROUND_REMOVE); 290 stopSelf(); 291 return START_NOT_STICKY; 292 } 293 294 NotificationChannel notificationChannel = 295 new NotificationChannel( 296 CHANNEL_ID, "VDM Service Channel", NotificationManager.IMPORTANCE_LOW); 297 notificationChannel.enableVibration(false); 298 NotificationManager notificationManager = getSystemService(NotificationManager.class); 299 Objects.requireNonNull(notificationManager).createNotificationChannel(notificationChannel); 300 301 Intent openIntent = new Intent(this, MainActivity.class); 302 PendingIntent pendingIntentOpen = 303 PendingIntent.getActivity(this, 0, openIntent, PendingIntent.FLAG_IMMUTABLE); 304 Intent stopIntent = new Intent(this, VdmService.class); 305 stopIntent.setAction(ACTION_STOP); 306 PendingIntent pendingIntentStop = 307 PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE); 308 309 Notification notification = 310 new Notification.Builder(this, CHANNEL_ID) 311 .setSmallIcon(R.drawable.connected) 312 .setContentTitle("VDM Demo running") 313 .setContentText("Click to open") 314 .setContentIntent(pendingIntentOpen) 315 .addAction( 316 new Notification.Action.Builder( 317 Icon.createWithResource("", R.drawable.close), "Stop", 318 pendingIntentStop) 319 .build()) 320 .setOngoing(true) 321 .build(); 322 startForeground(NOTIFICATION_ID, notification); 323 324 return START_STICKY; 325 } 326 327 @Override onCreate()328 public void onCreate() { 329 super.onCreate(); 330 331 mKeyguardManager = getSystemService(KeyguardManager.class); 332 mDisplayManager = getSystemService(DisplayManager.class); 333 Objects.requireNonNull(mDisplayManager).registerDisplayListener(mDisplayListener, null); 334 335 mPreferenceController.addPreferenceObserver(this, mPreferenceObservers); 336 337 mVirtualDeviceManager = 338 Objects.requireNonNull(getSystemService(VirtualDeviceManager.class)); 339 340 if (BuildCompat.isAtLeastV()) { 341 mVirtualDeviceListener = new VirtualDeviceManager.VirtualDeviceListener() { 342 @Override 343 public void onVirtualDeviceClosed(int deviceId) { 344 if (mVirtualDevice != null && mVirtualDevice.getDeviceId() == deviceId) { 345 closeVirtualDevice(); 346 } 347 } 348 }; 349 mVirtualDeviceManager.registerVirtualDeviceListener( 350 Executors.newSingleThreadExecutor(), mVirtualDeviceListener); 351 } 352 353 if (!mPreferenceController.getBoolean(R.string.pref_standalone_host_demo)) { 354 mConnectionManager.addConnectionCallback(mConnectionCallback); 355 mConnectionManager.startHostSession( 356 mPreferenceController.getString(R.string.pref_network_channel)); 357 mRemoteIo.addMessageConsumer(mRemoteEventConsumer); 358 } else { 359 mDeviceCapabilities = DeviceCapabilities.newBuilder() 360 .setDeviceName("Synthetic VDM Client") 361 .build(); 362 associateAndCreateVirtualDevice(); 363 } 364 } 365 366 @Override onDestroy()367 public void onDestroy() { 368 super.onDestroy(); 369 if (BuildCompat.isAtLeastV()) { 370 mVirtualDeviceManager.unregisterVirtualDeviceListener(mVirtualDeviceListener); 371 } 372 mPreferenceController.removePreferenceObserver(this); 373 mConnectionManager.removeConnectionCallback(mConnectionCallback); 374 closeVirtualDevice(); 375 mRemoteIo.removeMessageConsumer(mRemoteEventConsumer); 376 mDisplayManager.unregisterDisplayListener(mDisplayListener); 377 } 378 addVirtualDeviceListener(Consumer<Boolean> listener)379 void addVirtualDeviceListener(Consumer<Boolean> listener) { 380 mLocalVirtualDeviceLifecycleListeners.add(listener); 381 } 382 removeVirtualDeviceListener(Consumer<Boolean> listener)383 void removeVirtualDeviceListener(Consumer<Boolean> listener) { 384 mLocalVirtualDeviceLifecycleListeners.remove(listener); 385 } 386 processRemoteEvent(RemoteEvent event)387 private void processRemoteEvent(RemoteEvent event) { 388 if (event.hasDeviceCapabilities()) { 389 mDeviceCapabilities = event.getDeviceCapabilities(); 390 associateAndCreateVirtualDevice(); 391 } else if (event.hasDisplayCapabilities() && !mDisplayRepository.resetDisplay(event)) { 392 createRemoteDisplay(this, event.getDisplayId(), 393 event.getDisplayCapabilities().getViewportWidth(), 394 event.getDisplayCapabilities().getViewportHeight(), 395 event.getDisplayCapabilities().getDensityDpi(), mRemoteIo); 396 } else if (event.hasStopStreaming() && !event.getStopStreaming().getPause()) { 397 closeRemoteDisplay(event.getDisplayId()); 398 } else if (event.hasDisplayChangeEvent() && event.getDisplayChangeEvent().getFocused()) { 399 mInputController.setFocusedRemoteDisplayId(event.getDisplayId()); 400 } else if (event.hasDeviceState()) { 401 setPowerState(event.getDeviceState().getPowerOn()); 402 } 403 } 404 handleAudioCapabilities()405 private void handleAudioCapabilities() { 406 if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) { 407 openAudio(); 408 } else { 409 closeAudio(); 410 } 411 } 412 openAudio()413 private void openAudio() { 414 AudioManager audioManager = getSystemService(AudioManager.class); 415 if (audioManager != null) { 416 // Assuming one playback session id and one recording session id per host (for now) 417 // Reuse them if already initialized 418 if (mPlaybackAudioSessionId == 0) { 419 mPlaybackAudioSessionId = audioManager.generateAudioSessionId(); 420 } 421 if (mRecordingAudioSessionId == 0) { 422 mRecordingAudioSessionId = audioManager.generateAudioSessionId(); 423 } 424 425 if (mVirtualDevice != null) { 426 Log.d(TAG, "openAudio on virtual device id: " + mVirtualDevice.getDeviceId() 427 + " with playbackAudioSessionId: " + mPlaybackAudioSessionId 428 + " and recordingAudioSessionId: " + mRecordingAudioSessionId 429 + " Audio virtual device capabilities: " 430 + " output: " + mDeviceCapabilities.getSupportsAudioOutput() 431 + " input: " + mDeviceCapabilities.getSupportsAudioInput()); 432 433 if (mDeviceCapabilities.getSupportsAudioOutput()) { 434 mAudioStreamer.start(mVirtualDevice.getDeviceId(), mPlaybackAudioSessionId); 435 } else { 436 mAudioStreamer.stop(); 437 } 438 439 if (mDeviceCapabilities.getSupportsAudioInput()) { 440 mAudioInjector.start(mVirtualDevice.getDeviceId(), mRecordingAudioSessionId); 441 } else { 442 mAudioInjector.stop(); 443 } 444 } 445 } 446 } 447 closeAudio()448 private void closeAudio() { 449 mAudioStreamer.stop(); 450 mAudioInjector.stop(); 451 } 452 453 @SuppressLint("MissingPermission") associateAndCreateVirtualDevice()454 private void associateAndCreateVirtualDevice() { 455 CompanionDeviceManager cdm = 456 Objects.requireNonNull(getSystemService(CompanionDeviceManager.class)); 457 RoleManager rm = Objects.requireNonNull(getSystemService(RoleManager.class)); 458 final String deviceProfile = mPreferenceController.getString(R.string.pref_device_profile); 459 AssociationInfo existingAssociation = null; 460 for (AssociationInfo associationInfo : cdm.getMyAssociations()) { 461 if (rm.isRoleHeld(deviceProfile) 462 && Objects.equals(associationInfo.getPackageName(), getPackageName()) 463 && Objects.equals(associationInfo.getDeviceProfile(), deviceProfile) 464 && associationInfo.getDisplayName() != null 465 && Objects.equals(associationInfo.getDisplayName().toString(), 466 mDeviceCapabilities.getDeviceName())) { 467 Log.d(TAG, "Reusing association " + associationInfo.getDisplayName() + " for " 468 + deviceProfile); 469 existingAssociation = associationInfo; 470 } else { 471 Log.d(TAG, "Removing association " + associationInfo.getDisplayName() + " / " 472 + associationInfo.getDeviceProfile()); 473 cdm.disassociate(associationInfo.getId()); 474 } 475 } 476 if (existingAssociation != null) { 477 createVirtualDevice(existingAssociation); 478 return; 479 } 480 481 AssociationRequest.Builder associationRequest = 482 new AssociationRequest.Builder() 483 .setDeviceProfile(deviceProfile) 484 .setDisplayName(mDeviceCapabilities.getDeviceName()); 485 486 if (deviceProfile.equals(AssociationRequest.DEVICE_PROFILE_VIRTUAL_DEVICE)) { 487 Log.i(TAG, "Looking for bluetooth device " 488 + mDeviceCapabilities.getBluetoothDeviceName()); 489 associationRequest 490 .setSingleDevice(true) 491 .addDeviceFilter(new BluetoothDeviceFilter.Builder() 492 .setNamePattern( 493 Pattern.compile(mDeviceCapabilities.getBluetoothDeviceName())) 494 .build()); 495 mRemoteIo.sendMessage(RemoteEvent.newBuilder() 496 .setRequestBluetoothDiscoverable(RequestBluetoothDiscoverable.newBuilder()) 497 .build()); 498 } else { 499 associationRequest.setSelfManaged(true); 500 if (VdmCompat.isAtLeastB() && android.companion.Flags.associationDeviceIcon()) { 501 associationRequest.setDeviceIcon( 502 Icon.createWithResource(this, R.drawable.device_icon)); 503 } 504 } 505 506 cdm.associate( 507 associationRequest.build(), 508 new CompanionDeviceManager.Callback() { 509 510 @Override 511 public void onAssociationPending(@NonNull IntentSender intentSender) { 512 try { 513 startIntentSender(intentSender, null, 0, 0, 0); 514 } catch (SendIntentException e) { 515 Log.e( 516 TAG, 517 "onAssociationPending: Failed to send device selection intent", 518 e); 519 } 520 } 521 522 @Override 523 public void onAssociationCreated(@NonNull AssociationInfo associationInfo) { 524 Log.i(TAG, "onAssociationCreated: ID " + associationInfo.getId()); 525 createVirtualDevice(associationInfo); 526 } 527 528 @Override 529 public void onFailure(CharSequence error) { 530 Log.e(TAG, "onFailure: RemoteDevice Association failed " + error); 531 } 532 }, 533 null); 534 Log.i(TAG, "createCdmAssociation: Waiting for association to happen"); 535 } 536 createVirtualDevice(AssociationInfo associationInfo)537 private void createVirtualDevice(AssociationInfo associationInfo) { 538 Log.d(TAG, "VdmService createVirtualDevice name: " 539 + mDeviceCapabilities.getDeviceName() + " with association: " + associationInfo); 540 541 VirtualDeviceParams.Builder virtualDeviceBuilder = 542 new VirtualDeviceParams.Builder() 543 .setName("VirtualDevice - " + mDeviceCapabilities.getDeviceName()); 544 545 mPreferenceController.evaluate(); 546 547 if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) { 548 openAudio(); 549 550 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM) 551 .setAudioPlaybackSessionId(mPlaybackAudioSessionId) 552 .setAudioRecordingSessionId(mRecordingAudioSessionId); 553 } 554 555 if (mPreferenceController.getBoolean(R.string.pref_always_unlocked_device)) { 556 virtualDeviceBuilder.setLockState(LOCK_STATE_ALWAYS_UNLOCKED); 557 } 558 559 if (mPreferenceController.getBoolean(R.string.pref_enable_custom_home)) { 560 if (mPreferenceController.getBoolean(R.string.pref_enable_display_category)) { 561 virtualDeviceBuilder.setHomeComponent( 562 new ComponentName( 563 this, CustomLauncherActivityWithRequiredDisplayCategory.class)); 564 } else { 565 virtualDeviceBuilder.setHomeComponent( 566 new ComponentName(this, CustomLauncherActivity.class)); 567 } 568 } 569 570 if (VdmCompat.isAtLeastB() && Flags.deviceAwareDisplayPower()) { 571 int displayTimeout = Integer.parseInt( 572 mPreferenceController.getString(R.string.pref_display_timeout)); 573 virtualDeviceBuilder 574 .setDimDuration(Duration.ofMillis(displayTimeout / 2)) 575 .setScreenOffTimeout(Duration.ofMillis(displayTimeout)); 576 } 577 578 if (mPreferenceController.getBoolean(R.string.pref_hide_from_recents)) { 579 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM); 580 } 581 582 if (mPreferenceController.getBoolean(R.string.pref_enable_cross_device_clipboard)) { 583 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_CUSTOM); 584 } 585 586 if (mPreferenceController.getBoolean(R.string.pref_enable_custom_activity_policy)) { 587 virtualDeviceBuilder.setDevicePolicy( 588 POLICY_TYPE_BLOCKED_ACTIVITY, DEVICE_POLICY_CUSTOM); 589 } 590 591 if (mPreferenceController.getBoolean(R.string.pref_enable_client_native_ime)) { 592 virtualDeviceBuilder.setInputMethodComponent( 593 new ComponentName(this, VdmProxyIme.class)); 594 } 595 596 if (mPreferenceController.getBoolean(R.string.pref_enable_client_sensors)) { 597 for (SensorCapabilities sensor : mDeviceCapabilities.getSensorCapabilitiesList()) { 598 var builder = new VirtualSensorConfig.Builder( 599 sensor.getType(), "Remote-" + sensor.getName()) 600 .setMinDelay(sensor.getMinDelayUs()) 601 .setMaxDelay(sensor.getMaxDelayUs()) 602 .setPower(sensor.getPower()) 603 .setResolution(sensor.getResolution()) 604 .setMaximumRange(sensor.getMaxRange()); 605 if (VdmCompat.isAtLeastB() && Flags.deviceAwareDisplayPower()) { 606 builder.setWakeUpSensor(sensor.getIsWakeUpSensor()) 607 .setReportingMode(sensor.getReportingMode()); 608 } 609 if (Flags.virtualSensorAdditionalInfo()) { 610 builder.setAdditionalInfoSupported(sensor.getIsAdditionalInfoSupported()); 611 } 612 virtualDeviceBuilder.addVirtualSensorConfig(builder.build()); 613 } 614 615 if (mDeviceCapabilities.getSensorCapabilitiesCount() > 0) { 616 mRemoteSensorManager = new RemoteSensorManager(mRemoteIo); 617 virtualDeviceBuilder.setVirtualSensorCallback( 618 MoreExecutors.directExecutor(), 619 mRemoteSensorManager.getVirtualSensorCallback()); 620 } 621 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM); 622 } 623 624 if (mPreferenceController.getBoolean(R.string.pref_enable_client_camera)) { 625 virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CAMERA, DEVICE_POLICY_CUSTOM); 626 } 627 628 mVirtualDevice = mVirtualDeviceManager 629 .createVirtualDevice(associationInfo.getId(), virtualDeviceBuilder.build()); 630 if (mRemoteSensorManager != null) { 631 mRemoteSensorManager.setVirtualSensors(mVirtualDevice.getVirtualSensorList()); 632 } 633 634 mVirtualDevice.setShowPointerIcon( 635 mPreferenceController.getBoolean(R.string.pref_show_pointer_icon)); 636 637 if (BuildCompat.isAtLeastV()) { 638 mVirtualDevice.addActivityPolicyExemption(new ComponentName( 639 "com.example.android.vdmdemo.demos", 640 "com.example.android.vdmdemo.demos.BlockedActivity")); 641 } 642 643 mVirtualDevice.addActivityListener(MoreExecutors.directExecutor(), mActivityListener); 644 mVirtualDevice.addActivityListener( 645 MoreExecutors.directExecutor(), 646 new RunningVdmUidsTracker(getApplicationContext(), mPreferenceController, 647 mAudioStreamer, mAudioInjector)); 648 649 if (mPreferenceController.getBoolean(R.string.pref_enable_client_camera)) { 650 if (mRemoteCameraManager != null) { 651 mRemoteCameraManager.close(); 652 } 653 mRemoteCameraManager = new RemoteCameraManager(mVirtualDevice, mRemoteIo); 654 mRemoteCameraManager.createCameras(mDeviceCapabilities.getCameraCapabilitiesList()); 655 } 656 657 handleAudioCapabilities(); 658 659 Log.i(TAG, "Created virtual device"); 660 for (Consumer<Boolean> listener : mLocalVirtualDeviceLifecycleListeners) { 661 listener.accept(true); 662 } 663 mRemoteIo.sendMessage(RemoteEvent.newBuilder() 664 .setDeviceState(RemoteEventProto.DeviceState.newBuilder().setPowerOn(true)) 665 .build()); 666 } 667 closeVirtualDevice()668 private synchronized void closeVirtualDevice() { 669 for (Consumer<Boolean> listener : mLocalVirtualDeviceLifecycleListeners) { 670 listener.accept(false); 671 } 672 673 if (mRemoteSensorManager != null) { 674 mRemoteSensorManager.close(); 675 mRemoteSensorManager = null; 676 } 677 678 closeAudio(); 679 680 if (mVirtualDevice != null) { 681 Log.i(TAG, "Closing virtual device"); 682 mDisplayRepository.clear(); 683 mVirtualDevice.close(); 684 mVirtualDevice = null; 685 } 686 } 687 isVirtualDeviceActive()688 boolean isVirtualDeviceActive() { 689 return mVirtualDevice != null; 690 } 691 getRemoteDisplayIds()692 int[] getRemoteDisplayIds() { 693 return mDisplayRepository.getRemoteDisplayIds(); 694 } 695 startStreamingHome()696 void startStreamingHome() { 697 startStreaming(null, RemoteDisplay.DISPLAY_TYPE_HOME); 698 } 699 startMirroring()700 void startMirroring() { 701 startStreaming(null, RemoteDisplay.DISPLAY_TYPE_MIRROR); 702 } 703 startStreaming(Intent intent)704 void startStreaming(Intent intent) { 705 startStreaming(intent, RemoteDisplay.DISPLAY_TYPE_APP); 706 } 707 startStreaming(Intent intent, int type)708 private void startStreaming(Intent intent, int type) { 709 mPendingRemoteIntent = intent; 710 if (mPendingRemoteIntent != null) { 711 mPendingRemoteIntent.addFlags( 712 Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 713 } 714 mPendingDisplayType = type; 715 716 if (mPreferenceController.getBoolean(R.string.pref_standalone_host_demo)) { 717 Intent displayIntent = new Intent(this, DisplayActivity.class); 718 displayIntent 719 .addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 720 displayIntent.putExtra(DisplayActivity.EXTRA_DISPLAY_ID, ++mNextLocalDisplayId); 721 startActivity(displayIntent); 722 return; 723 } 724 725 boolean homeEnabled = mPendingDisplayType == RemoteDisplay.DISPLAY_TYPE_HOME 726 || mPendingDisplayType == RemoteDisplay.DISPLAY_TYPE_MIRROR; 727 mRemoteIo.sendMessage(RemoteEvent.newBuilder() 728 .setStartStreaming(StartStreaming.newBuilder() 729 .setHomeEnabled(homeEnabled) 730 .setRotationSupported(mPreferenceController.getBoolean( 731 R.string.internal_pref_display_rotation_supported))) 732 .build()); 733 } 734 createRemoteDisplay( Context context, int remoteDisplayId, int width, int height, int dpi, RemoteIo remoteIo)735 RemoteDisplay createRemoteDisplay( 736 Context context, int remoteDisplayId, int width, int height, int dpi, 737 RemoteIo remoteIo) { 738 RemoteDisplay remoteDisplay = new RemoteDisplay(context, remoteDisplayId, width, height, 739 dpi, mVirtualDevice, remoteIo, mPendingDisplayType, mPreferenceController); 740 mDisplayRepository.addDisplay(remoteDisplay); 741 if (mPendingRemoteIntent != null) { 742 remoteDisplay.launchIntent(mPendingRemoteIntent); 743 mPendingRemoteIntent = null; 744 } 745 return remoteDisplay; 746 } 747 getRemoteDisplay(int remoteDisplayId)748 Optional<RemoteDisplay> getRemoteDisplay(int remoteDisplayId) { 749 return mDisplayRepository.getDisplayByRemoteId(remoteDisplayId); 750 } 751 closeRemoteDisplay(int remoteDisplayId)752 void closeRemoteDisplay(int remoteDisplayId) { 753 mDisplayRepository.removeDisplayByRemoteId(remoteDisplayId); 754 } 755 setPowerState(boolean poweredOn)756 void setPowerState(boolean poweredOn) { 757 if (VdmCompat.isAtLeastB() && Flags.deviceAwareDisplayPower() && mVirtualDevice != null) { 758 if (poweredOn) { 759 mVirtualDevice.wakeUp(); 760 } else { 761 mVirtualDevice.goToSleep(); 762 } 763 } 764 } 765 startIntentOnDisplayIndex(Intent intent, int displayIndex)766 void startIntentOnDisplayIndex(Intent intent, int displayIndex) { 767 mDisplayRepository 768 .getDisplayByIndex(displayIndex) 769 .ifPresent(d -> d.launchIntent(intent)); 770 } 771 recreateVirtualDevice()772 private void recreateVirtualDevice() { 773 if (mVirtualDevice != null) { 774 closeVirtualDevice(); 775 if (mDeviceCapabilities != null) { 776 associateAndCreateVirtualDevice(); 777 } 778 } 779 } 780 updateDevicePolicy(int policyType, boolean custom)781 private void updateDevicePolicy(int policyType, boolean custom) { 782 if (!BuildCompat.isAtLeastV()) { 783 recreateVirtualDevice(); 784 } else if (mVirtualDevice != null) { 785 mVirtualDevice.setDevicePolicy( 786 policyType, custom ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT); 787 } 788 } 789 createPreferenceObservers()790 private Map<Integer, Consumer<Object>> createPreferenceObservers() { 791 HashMap<Integer, Consumer<Object>> observers = new HashMap<>(); 792 793 observers.put(R.string.pref_hide_from_recents, 794 b -> updateDevicePolicy(POLICY_TYPE_RECENTS, (Boolean) b)); 795 observers.put(R.string.pref_enable_cross_device_clipboard, 796 b -> updateDevicePolicy(POLICY_TYPE_CLIPBOARD, (Boolean) b)); 797 observers.put(R.string.pref_enable_custom_activity_policy, 798 b -> updateDevicePolicy(POLICY_TYPE_BLOCKED_ACTIVITY, (Boolean) b)); 799 observers.put(R.string.pref_show_pointer_icon, 800 b -> { 801 if (mVirtualDevice != null) mVirtualDevice.setShowPointerIcon((Boolean) b); 802 }); 803 observers.put(R.string.pref_enable_client_audio, b -> handleAudioCapabilities()); 804 observers.put(R.string.pref_display_ime_policy, 805 s -> { 806 if (mVirtualDevice != null) { 807 int policy = Integer.parseInt((String) s); 808 Arrays.stream(mDisplayRepository.getDisplayIds()).forEach( 809 displayId -> mVirtualDevice.setDisplayImePolicy(displayId, policy)); 810 } 811 }); 812 observers.put(R.string.pref_enable_client_camera, v -> recreateVirtualDevice()); 813 observers.put(R.string.pref_enable_client_sensors, v -> recreateVirtualDevice()); 814 observers.put(R.string.pref_device_profile, v -> recreateVirtualDevice()); 815 observers.put(R.string.pref_always_unlocked_device, v -> recreateVirtualDevice()); 816 observers.put(R.string.pref_enable_client_native_ime, v -> recreateVirtualDevice()); 817 observers.put(R.string.pref_enable_custom_home, v -> recreateVirtualDevice()); 818 observers.put(R.string.pref_display_timeout, v -> recreateVirtualDevice()); 819 observers.put(R.string.pref_enable_display_category, v -> recreateVirtualDevice()); 820 observers.put(R.string.pref_network_channel, s -> { 821 if (!mPreferenceController.getBoolean(R.string.pref_standalone_host_demo)) { 822 mConnectionManager.disconnect(); 823 mConnectionManager.startHostSession((String) s); 824 } 825 }); 826 observers.put(R.string.pref_standalone_host_demo, b -> { 827 if ((Boolean) b) { 828 mRemoteIo.removeMessageConsumer(mRemoteEventConsumer); 829 mConnectionManager.removeConnectionCallback(mConnectionCallback); 830 mConnectionManager.disconnect(); 831 mDeviceCapabilities = DeviceCapabilities.newBuilder() 832 .setDeviceName("Synthetic VDM Client") 833 .build(); 834 associateAndCreateVirtualDevice(); 835 } else { 836 mDeviceCapabilities = null; 837 mConnectionManager.addConnectionCallback(mConnectionCallback); 838 mConnectionManager.startHostSession( 839 mPreferenceController.getString(R.string.pref_network_channel)); 840 mRemoteIo.addMessageConsumer(mRemoteEventConsumer); 841 } 842 }); 843 844 return observers; 845 } 846 } 847