/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.vdmdemo.host; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; import android.annotation.SuppressLint; import android.app.ActivityOptions; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.app.role.RoleManager; import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.BluetoothDeviceFilter; import android.companion.CompanionDeviceManager; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceManager.ActivityListener; import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.sensor.VirtualSensorConfig; import android.companion.virtualdevice.flags.Flags; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; import android.media.AudioManager; import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; import android.util.Log; import android.view.Display; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BuildCompat; import com.example.android.vdmdemo.common.ConnectionManager; import com.example.android.vdmdemo.common.RemoteEventProto; import com.example.android.vdmdemo.common.RemoteEventProto.DeviceCapabilities; import com.example.android.vdmdemo.common.RemoteEventProto.DisplayChangeEvent; import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent; import com.example.android.vdmdemo.common.RemoteEventProto.RequestBluetoothDiscoverable; import com.example.android.vdmdemo.common.RemoteEventProto.SensorCapabilities; import com.example.android.vdmdemo.common.RemoteEventProto.StartStreaming; import com.example.android.vdmdemo.common.RemoteIo; import com.google.common.util.concurrent.MoreExecutors; import dagger.hilt.android.AndroidEntryPoint; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Executors; import java.util.function.Consumer; import java.util.regex.Pattern; import javax.inject.Inject; /** * VDM Host service, streaming apps to a remote device and processing the input coming from there. */ @AndroidEntryPoint(Service.class) @SuppressLint("NewApi") public final class VdmService extends Hilt_VdmService { public static final String TAG = "VdmHost"; private static final String CHANNEL_ID = "com.example.android.vdmdemo.host.VdmService"; private static final int NOTIFICATION_ID = 1; private static final String ACTION_STOP = "com.example.android.vdmdemo.host.VdmService.STOP"; private int mRecordingAudioSessionId; private int mPlaybackAudioSessionId; /** Provides an instance of this service to bound clients. */ public class LocalBinder extends Binder { VdmService getService() { return VdmService.this; } } private final IBinder mBinder = new LocalBinder(); private final Map> mPreferenceObservers = createPreferenceObservers(); @Inject ConnectionManager mConnectionManager; @Inject RemoteIo mRemoteIo; @Inject AudioStreamer mAudioStreamer; @Inject AudioInjector mAudioInjector; @Inject PreferenceController mPreferenceController; @Inject DisplayRepository mDisplayRepository; @Inject InputController mInputController; private RemoteSensorManager mRemoteSensorManager = null; private RemoteCameraManager mRemoteCameraManager; private final Consumer mRemoteEventConsumer = this::processRemoteEvent; private VirtualDeviceManager.VirtualDevice mVirtualDevice; private DeviceCapabilities mDeviceCapabilities; private Intent mPendingRemoteIntent = null; private int mNextLocalDisplayId = 0; private @RemoteDisplay.DisplayType int mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP; private DisplayManager mDisplayManager; private KeyguardManager mKeyguardManager; private VirtualDeviceManager mVirtualDeviceManager; private ArrayList> mLocalVirtualDeviceLifecycleListeners = new ArrayList<>(); private VirtualDeviceManager.VirtualDeviceListener mVirtualDeviceListener; private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) { } @Override public void onDisplayRemoved(int displayId) { } @Override public void onDisplayChanged(int displayId) { mDisplayRepository.onDisplayChanged(displayId); } }; private final Consumer mConnectionCallback = (status) -> { if (status.state != ConnectionManager.ConnectionStatus.State.CONNECTED) { mDeviceCapabilities = null; closeVirtualDevice(); } }; private final ActivityListener mActivityListener = new ActivityListener() { @Override public void onActivityLaunchBlocked( int displayId, @NonNull ComponentName componentName, @NonNull UserHandle user, @Nullable IntentSender intentSender) { Log.w(TAG, "onActivityLaunchBlocked " + displayId + ": " + componentName); if (!mPreferenceController.getBoolean(R.string.pref_enable_custom_activity_policy)) { // The system dialog is shown on the virtual display. return; } if (intentSender == null) { showToast(displayId, componentName, R.string.custom_activity_launch_blocked_message); return; } // When the keyguard is locked, show a dialog prompting the user to unlock it. if (mKeyguardManager.isKeyguardLocked()) { startActivity( UnlockKeyguardDialog.createIntent(VdmService.this, intentSender), ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle()); return; } // Try to launch the activity on the default display with NEW_TASK flag. Intent fillInIntent = new Intent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ActivityOptions activityOptions = ActivityOptions.makeBasic() .setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) .setLaunchDisplayId(Display.DEFAULT_DISPLAY); try { startIntentSender(intentSender, fillInIntent, /* flagsMask= */ 0, /* flagsValues= */ 0, /* extraFlags= */ 0, activityOptions.toBundle()); showToast(displayId, componentName, R.string.custom_activity_launch_fallback_message); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "Error while starting intent sender", e); showToast(displayId, componentName, R.string.custom_activity_launch_blocked_message); } } @Override public void onTopActivityChanged( int displayId, @NonNull ComponentName componentName) { Log.w(TAG, "onTopActivityChanged " + displayId + ": " + componentName); int remoteDisplayId = mDisplayRepository.getRemoteDisplayId(displayId); if (remoteDisplayId == Display.INVALID_DISPLAY) { return; } final CharSequence title = getTitle(componentName); mRemoteIo.sendMessage( RemoteEvent.newBuilder() .setDisplayId(remoteDisplayId) .setDisplayChangeEvent( DisplayChangeEvent.newBuilder().setTitle(title.toString())) .build()); } @Override public void onDisplayEmpty(int displayId) { Log.i(TAG, "Display " + displayId + " is empty, removing"); mDisplayRepository.removeDisplay(displayId); } @Override public void onSecureWindowShown( int displayId, @NonNull ComponentName componentName, @NonNull UserHandle user) { Log.i(TAG, "Secure window shown on display " + displayId + " by " + componentName); } private CharSequence getTitle(ComponentName componentName) { CharSequence title; try { ActivityInfo activityInfo = getPackageManager().getActivityInfo(componentName, 0); title = activityInfo.loadLabel(getPackageManager()).toString(); } catch (NameNotFoundException e) { Log.w(TAG, "Failed to get activity label for " + componentName); title = ""; } return title; } private void showToast(int displayId, ComponentName componentName, int resId) { final CharSequence title = getTitle(componentName); final CharSequence text = getString(resId, title == null ? "this" : title, Build.MODEL); new Handler(Looper.getMainLooper()).post(() -> Toast.makeText( createDisplayContext(mDisplayManager.getDisplay(displayId)), text, Toast.LENGTH_LONG) .show()); } }; public VdmService() { } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && ACTION_STOP.equals(intent.getAction())) { Log.i(TAG, "Stopping VDM Service."); mConnectionManager.disconnect(); stopForeground(STOP_FOREGROUND_REMOVE); stopSelf(); return START_NOT_STICKY; } NotificationChannel notificationChannel = new NotificationChannel( CHANNEL_ID, "VDM Service Channel", NotificationManager.IMPORTANCE_LOW); notificationChannel.enableVibration(false); NotificationManager notificationManager = getSystemService(NotificationManager.class); Objects.requireNonNull(notificationManager).createNotificationChannel(notificationChannel); Intent openIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntentOpen = PendingIntent.getActivity(this, 0, openIntent, PendingIntent.FLAG_IMMUTABLE); Intent stopIntent = new Intent(this, VdmService.class); stopIntent.setAction(ACTION_STOP); PendingIntent pendingIntentStop = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE); Notification notification = new Notification.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.connected) .setContentTitle("VDM Demo running") .setContentText("Click to open") .setContentIntent(pendingIntentOpen) .addAction( new Notification.Action.Builder( Icon.createWithResource("", R.drawable.close), "Stop", pendingIntentStop) .build()) .setOngoing(true) .build(); startForeground(NOTIFICATION_ID, notification); return START_STICKY; } @Override public void onCreate() { super.onCreate(); mKeyguardManager = getSystemService(KeyguardManager.class); mDisplayManager = getSystemService(DisplayManager.class); Objects.requireNonNull(mDisplayManager).registerDisplayListener(mDisplayListener, null); mPreferenceController.addPreferenceObserver(this, mPreferenceObservers); mVirtualDeviceManager = Objects.requireNonNull(getSystemService(VirtualDeviceManager.class)); if (BuildCompat.isAtLeastV()) { mVirtualDeviceListener = new VirtualDeviceManager.VirtualDeviceListener() { @Override public void onVirtualDeviceClosed(int deviceId) { if (mVirtualDevice != null && mVirtualDevice.getDeviceId() == deviceId) { closeVirtualDevice(); } } }; mVirtualDeviceManager.registerVirtualDeviceListener( Executors.newSingleThreadExecutor(), mVirtualDeviceListener); } if (!mPreferenceController.getBoolean(R.string.pref_standalone_host_demo)) { mConnectionManager.addConnectionCallback(mConnectionCallback); mConnectionManager.startHostSession( mPreferenceController.getString(R.string.pref_network_channel)); mRemoteIo.addMessageConsumer(mRemoteEventConsumer); } else { mDeviceCapabilities = DeviceCapabilities.newBuilder() .setDeviceName("Synthetic VDM Client") .build(); associateAndCreateVirtualDevice(); } } @Override public void onDestroy() { super.onDestroy(); if (BuildCompat.isAtLeastV()) { mVirtualDeviceManager.unregisterVirtualDeviceListener(mVirtualDeviceListener); } mPreferenceController.removePreferenceObserver(this); mConnectionManager.removeConnectionCallback(mConnectionCallback); closeVirtualDevice(); mRemoteIo.removeMessageConsumer(mRemoteEventConsumer); mDisplayManager.unregisterDisplayListener(mDisplayListener); } void addVirtualDeviceListener(Consumer listener) { mLocalVirtualDeviceLifecycleListeners.add(listener); } void removeVirtualDeviceListener(Consumer listener) { mLocalVirtualDeviceLifecycleListeners.remove(listener); } private void processRemoteEvent(RemoteEvent event) { if (event.hasDeviceCapabilities()) { mDeviceCapabilities = event.getDeviceCapabilities(); associateAndCreateVirtualDevice(); } else if (event.hasDisplayCapabilities() && !mDisplayRepository.resetDisplay(event)) { createRemoteDisplay(this, event.getDisplayId(), event.getDisplayCapabilities().getViewportWidth(), event.getDisplayCapabilities().getViewportHeight(), event.getDisplayCapabilities().getDensityDpi(), mRemoteIo); } else if (event.hasStopStreaming() && !event.getStopStreaming().getPause()) { closeRemoteDisplay(event.getDisplayId()); } else if (event.hasDisplayChangeEvent() && event.getDisplayChangeEvent().getFocused()) { mInputController.setFocusedRemoteDisplayId(event.getDisplayId()); } else if (event.hasDeviceState()) { setPowerState(event.getDeviceState().getPowerOn()); } } private void handleAudioCapabilities() { if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) { openAudio(); } else { closeAudio(); } } private void openAudio() { AudioManager audioManager = getSystemService(AudioManager.class); if (audioManager != null) { // Assuming one playback session id and one recording session id per host (for now) // Reuse them if already initialized if (mPlaybackAudioSessionId == 0) { mPlaybackAudioSessionId = audioManager.generateAudioSessionId(); } if (mRecordingAudioSessionId == 0) { mRecordingAudioSessionId = audioManager.generateAudioSessionId(); } if (mVirtualDevice != null) { Log.d(TAG, "openAudio on virtual device id: " + mVirtualDevice.getDeviceId() + " with playbackAudioSessionId: " + mPlaybackAudioSessionId + " and recordingAudioSessionId: " + mRecordingAudioSessionId + " Audio virtual device capabilities: " + " output: " + mDeviceCapabilities.getSupportsAudioOutput() + " input: " + mDeviceCapabilities.getSupportsAudioInput()); if (mDeviceCapabilities.getSupportsAudioOutput()) { mAudioStreamer.start(mVirtualDevice.getDeviceId(), mPlaybackAudioSessionId); } else { mAudioStreamer.stop(); } if (mDeviceCapabilities.getSupportsAudioInput()) { mAudioInjector.start(mVirtualDevice.getDeviceId(), mRecordingAudioSessionId); } else { mAudioInjector.stop(); } } } } private void closeAudio() { mAudioStreamer.stop(); mAudioInjector.stop(); } @SuppressLint("MissingPermission") private void associateAndCreateVirtualDevice() { CompanionDeviceManager cdm = Objects.requireNonNull(getSystemService(CompanionDeviceManager.class)); RoleManager rm = Objects.requireNonNull(getSystemService(RoleManager.class)); final String deviceProfile = mPreferenceController.getString(R.string.pref_device_profile); AssociationInfo existingAssociation = null; for (AssociationInfo associationInfo : cdm.getMyAssociations()) { if (rm.isRoleHeld(deviceProfile) && Objects.equals(associationInfo.getPackageName(), getPackageName()) && Objects.equals(associationInfo.getDeviceProfile(), deviceProfile) && associationInfo.getDisplayName() != null && Objects.equals(associationInfo.getDisplayName().toString(), mDeviceCapabilities.getDeviceName())) { Log.d(TAG, "Reusing association " + associationInfo.getDisplayName() + " for " + deviceProfile); existingAssociation = associationInfo; } else { Log.d(TAG, "Removing association " + associationInfo.getDisplayName() + " / " + associationInfo.getDeviceProfile()); cdm.disassociate(associationInfo.getId()); } } if (existingAssociation != null) { createVirtualDevice(existingAssociation); return; } AssociationRequest.Builder associationRequest = new AssociationRequest.Builder() .setDeviceProfile(deviceProfile) .setDisplayName(mDeviceCapabilities.getDeviceName()); if (deviceProfile.equals(AssociationRequest.DEVICE_PROFILE_VIRTUAL_DEVICE)) { Log.i(TAG, "Looking for bluetooth device " + mDeviceCapabilities.getBluetoothDeviceName()); associationRequest .setSingleDevice(true) .addDeviceFilter(new BluetoothDeviceFilter.Builder() .setNamePattern( Pattern.compile(mDeviceCapabilities.getBluetoothDeviceName())) .build()); mRemoteIo.sendMessage(RemoteEvent.newBuilder() .setRequestBluetoothDiscoverable(RequestBluetoothDiscoverable.newBuilder()) .build()); } else { associationRequest.setSelfManaged(true); if (VdmCompat.isAtLeastB() && android.companion.Flags.associationDeviceIcon()) { associationRequest.setDeviceIcon( Icon.createWithResource(this, R.drawable.device_icon)); } } cdm.associate( associationRequest.build(), new CompanionDeviceManager.Callback() { @Override public void onAssociationPending(@NonNull IntentSender intentSender) { try { startIntentSender(intentSender, null, 0, 0, 0); } catch (SendIntentException e) { Log.e( TAG, "onAssociationPending: Failed to send device selection intent", e); } } @Override public void onAssociationCreated(@NonNull AssociationInfo associationInfo) { Log.i(TAG, "onAssociationCreated: ID " + associationInfo.getId()); createVirtualDevice(associationInfo); } @Override public void onFailure(CharSequence error) { Log.e(TAG, "onFailure: RemoteDevice Association failed " + error); } }, null); Log.i(TAG, "createCdmAssociation: Waiting for association to happen"); } private void createVirtualDevice(AssociationInfo associationInfo) { Log.d(TAG, "VdmService createVirtualDevice name: " + mDeviceCapabilities.getDeviceName() + " with association: " + associationInfo); VirtualDeviceParams.Builder virtualDeviceBuilder = new VirtualDeviceParams.Builder() .setName("VirtualDevice - " + mDeviceCapabilities.getDeviceName()); mPreferenceController.evaluate(); if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) { openAudio(); virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM) .setAudioPlaybackSessionId(mPlaybackAudioSessionId) .setAudioRecordingSessionId(mRecordingAudioSessionId); } if (mPreferenceController.getBoolean(R.string.pref_always_unlocked_device)) { virtualDeviceBuilder.setLockState(LOCK_STATE_ALWAYS_UNLOCKED); } if (mPreferenceController.getBoolean(R.string.pref_enable_custom_home)) { if (mPreferenceController.getBoolean(R.string.pref_enable_display_category)) { virtualDeviceBuilder.setHomeComponent( new ComponentName( this, CustomLauncherActivityWithRequiredDisplayCategory.class)); } else { virtualDeviceBuilder.setHomeComponent( new ComponentName(this, CustomLauncherActivity.class)); } } if (VdmCompat.isAtLeastB() && Flags.deviceAwareDisplayPower()) { int displayTimeout = Integer.parseInt( mPreferenceController.getString(R.string.pref_display_timeout)); virtualDeviceBuilder .setDimDuration(Duration.ofMillis(displayTimeout / 2)) .setScreenOffTimeout(Duration.ofMillis(displayTimeout)); } if (mPreferenceController.getBoolean(R.string.pref_hide_from_recents)) { virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM); } if (mPreferenceController.getBoolean(R.string.pref_enable_cross_device_clipboard)) { virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_CUSTOM); } if (mPreferenceController.getBoolean(R.string.pref_enable_custom_activity_policy)) { virtualDeviceBuilder.setDevicePolicy( POLICY_TYPE_BLOCKED_ACTIVITY, DEVICE_POLICY_CUSTOM); } if (mPreferenceController.getBoolean(R.string.pref_enable_client_native_ime)) { virtualDeviceBuilder.setInputMethodComponent( new ComponentName(this, VdmProxyIme.class)); } if (mPreferenceController.getBoolean(R.string.pref_enable_client_sensors)) { for (SensorCapabilities sensor : mDeviceCapabilities.getSensorCapabilitiesList()) { var builder = new VirtualSensorConfig.Builder( sensor.getType(), "Remote-" + sensor.getName()) .setMinDelay(sensor.getMinDelayUs()) .setMaxDelay(sensor.getMaxDelayUs()) .setPower(sensor.getPower()) .setResolution(sensor.getResolution()) .setMaximumRange(sensor.getMaxRange()); if (VdmCompat.isAtLeastB() && Flags.deviceAwareDisplayPower()) { builder.setWakeUpSensor(sensor.getIsWakeUpSensor()) .setReportingMode(sensor.getReportingMode()); } if (Flags.virtualSensorAdditionalInfo()) { builder.setAdditionalInfoSupported(sensor.getIsAdditionalInfoSupported()); } virtualDeviceBuilder.addVirtualSensorConfig(builder.build()); } if (mDeviceCapabilities.getSensorCapabilitiesCount() > 0) { mRemoteSensorManager = new RemoteSensorManager(mRemoteIo); virtualDeviceBuilder.setVirtualSensorCallback( MoreExecutors.directExecutor(), mRemoteSensorManager.getVirtualSensorCallback()); } virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM); } if (mPreferenceController.getBoolean(R.string.pref_enable_client_camera)) { virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CAMERA, DEVICE_POLICY_CUSTOM); } mVirtualDevice = mVirtualDeviceManager .createVirtualDevice(associationInfo.getId(), virtualDeviceBuilder.build()); if (mRemoteSensorManager != null) { mRemoteSensorManager.setVirtualSensors(mVirtualDevice.getVirtualSensorList()); } mVirtualDevice.setShowPointerIcon( mPreferenceController.getBoolean(R.string.pref_show_pointer_icon)); if (BuildCompat.isAtLeastV()) { mVirtualDevice.addActivityPolicyExemption(new ComponentName( "com.example.android.vdmdemo.demos", "com.example.android.vdmdemo.demos.BlockedActivity")); } mVirtualDevice.addActivityListener(MoreExecutors.directExecutor(), mActivityListener); mVirtualDevice.addActivityListener( MoreExecutors.directExecutor(), new RunningVdmUidsTracker(getApplicationContext(), mPreferenceController, mAudioStreamer, mAudioInjector)); if (mPreferenceController.getBoolean(R.string.pref_enable_client_camera)) { if (mRemoteCameraManager != null) { mRemoteCameraManager.close(); } mRemoteCameraManager = new RemoteCameraManager(mVirtualDevice, mRemoteIo); mRemoteCameraManager.createCameras(mDeviceCapabilities.getCameraCapabilitiesList()); } handleAudioCapabilities(); Log.i(TAG, "Created virtual device"); for (Consumer listener : mLocalVirtualDeviceLifecycleListeners) { listener.accept(true); } mRemoteIo.sendMessage(RemoteEvent.newBuilder() .setDeviceState(RemoteEventProto.DeviceState.newBuilder().setPowerOn(true)) .build()); } private synchronized void closeVirtualDevice() { for (Consumer listener : mLocalVirtualDeviceLifecycleListeners) { listener.accept(false); } if (mRemoteSensorManager != null) { mRemoteSensorManager.close(); mRemoteSensorManager = null; } closeAudio(); if (mVirtualDevice != null) { Log.i(TAG, "Closing virtual device"); mDisplayRepository.clear(); mVirtualDevice.close(); mVirtualDevice = null; } } boolean isVirtualDeviceActive() { return mVirtualDevice != null; } int[] getRemoteDisplayIds() { return mDisplayRepository.getRemoteDisplayIds(); } void startStreamingHome() { startStreaming(null, RemoteDisplay.DISPLAY_TYPE_HOME); } void startMirroring() { startStreaming(null, RemoteDisplay.DISPLAY_TYPE_MIRROR); } void startStreaming(Intent intent) { startStreaming(intent, RemoteDisplay.DISPLAY_TYPE_APP); } private void startStreaming(Intent intent, int type) { mPendingRemoteIntent = intent; if (mPendingRemoteIntent != null) { mPendingRemoteIntent.addFlags( Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); } mPendingDisplayType = type; if (mPreferenceController.getBoolean(R.string.pref_standalone_host_demo)) { Intent displayIntent = new Intent(this, DisplayActivity.class); displayIntent .addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); displayIntent.putExtra(DisplayActivity.EXTRA_DISPLAY_ID, ++mNextLocalDisplayId); startActivity(displayIntent); return; } boolean homeEnabled = mPendingDisplayType == RemoteDisplay.DISPLAY_TYPE_HOME || mPendingDisplayType == RemoteDisplay.DISPLAY_TYPE_MIRROR; mRemoteIo.sendMessage(RemoteEvent.newBuilder() .setStartStreaming(StartStreaming.newBuilder() .setHomeEnabled(homeEnabled) .setRotationSupported(mPreferenceController.getBoolean( R.string.internal_pref_display_rotation_supported))) .build()); } RemoteDisplay createRemoteDisplay( Context context, int remoteDisplayId, int width, int height, int dpi, RemoteIo remoteIo) { RemoteDisplay remoteDisplay = new RemoteDisplay(context, remoteDisplayId, width, height, dpi, mVirtualDevice, remoteIo, mPendingDisplayType, mPreferenceController); mDisplayRepository.addDisplay(remoteDisplay); if (mPendingRemoteIntent != null) { remoteDisplay.launchIntent(mPendingRemoteIntent); mPendingRemoteIntent = null; } return remoteDisplay; } Optional getRemoteDisplay(int remoteDisplayId) { return mDisplayRepository.getDisplayByRemoteId(remoteDisplayId); } void closeRemoteDisplay(int remoteDisplayId) { mDisplayRepository.removeDisplayByRemoteId(remoteDisplayId); } void setPowerState(boolean poweredOn) { if (VdmCompat.isAtLeastB() && Flags.deviceAwareDisplayPower() && mVirtualDevice != null) { if (poweredOn) { mVirtualDevice.wakeUp(); } else { mVirtualDevice.goToSleep(); } } } void startIntentOnDisplayIndex(Intent intent, int displayIndex) { mDisplayRepository .getDisplayByIndex(displayIndex) .ifPresent(d -> d.launchIntent(intent)); } private void recreateVirtualDevice() { if (mVirtualDevice != null) { closeVirtualDevice(); if (mDeviceCapabilities != null) { associateAndCreateVirtualDevice(); } } } private void updateDevicePolicy(int policyType, boolean custom) { if (!BuildCompat.isAtLeastV()) { recreateVirtualDevice(); } else if (mVirtualDevice != null) { mVirtualDevice.setDevicePolicy( policyType, custom ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT); } } private Map> createPreferenceObservers() { HashMap> observers = new HashMap<>(); observers.put(R.string.pref_hide_from_recents, b -> updateDevicePolicy(POLICY_TYPE_RECENTS, (Boolean) b)); observers.put(R.string.pref_enable_cross_device_clipboard, b -> updateDevicePolicy(POLICY_TYPE_CLIPBOARD, (Boolean) b)); observers.put(R.string.pref_enable_custom_activity_policy, b -> updateDevicePolicy(POLICY_TYPE_BLOCKED_ACTIVITY, (Boolean) b)); observers.put(R.string.pref_show_pointer_icon, b -> { if (mVirtualDevice != null) mVirtualDevice.setShowPointerIcon((Boolean) b); }); observers.put(R.string.pref_enable_client_audio, b -> handleAudioCapabilities()); observers.put(R.string.pref_display_ime_policy, s -> { if (mVirtualDevice != null) { int policy = Integer.parseInt((String) s); Arrays.stream(mDisplayRepository.getDisplayIds()).forEach( displayId -> mVirtualDevice.setDisplayImePolicy(displayId, policy)); } }); observers.put(R.string.pref_enable_client_camera, v -> recreateVirtualDevice()); observers.put(R.string.pref_enable_client_sensors, v -> recreateVirtualDevice()); observers.put(R.string.pref_device_profile, v -> recreateVirtualDevice()); observers.put(R.string.pref_always_unlocked_device, v -> recreateVirtualDevice()); observers.put(R.string.pref_enable_client_native_ime, v -> recreateVirtualDevice()); observers.put(R.string.pref_enable_custom_home, v -> recreateVirtualDevice()); observers.put(R.string.pref_display_timeout, v -> recreateVirtualDevice()); observers.put(R.string.pref_enable_display_category, v -> recreateVirtualDevice()); observers.put(R.string.pref_network_channel, s -> { if (!mPreferenceController.getBoolean(R.string.pref_standalone_host_demo)) { mConnectionManager.disconnect(); mConnectionManager.startHostSession((String) s); } }); observers.put(R.string.pref_standalone_host_demo, b -> { if ((Boolean) b) { mRemoteIo.removeMessageConsumer(mRemoteEventConsumer); mConnectionManager.removeConnectionCallback(mConnectionCallback); mConnectionManager.disconnect(); mDeviceCapabilities = DeviceCapabilities.newBuilder() .setDeviceName("Synthetic VDM Client") .build(); associateAndCreateVirtualDevice(); } else { mDeviceCapabilities = null; mConnectionManager.addConnectionCallback(mConnectionCallback); mConnectionManager.startHostSession( mPreferenceController.getString(R.string.pref_network_channel)); mRemoteIo.addMessageConsumer(mRemoteEventConsumer); } }); return observers; } }