• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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