Snap for 11220357 from 305b00a16d to 24Q1-release

Change-Id: If8d4e64ef66ae1e5914f5479dabcda08c2fe09c8
This commit is contained in:
Android Build Coastguard Worker
2023-12-15 00:20:53 +00:00
22 changed files with 791 additions and 382 deletions

View File

@@ -15,8 +15,11 @@ android_app {
], ],
static_libs: [ static_libs: [
"VdmCommonLib", "VdmCommonLib",
"android.companion.virtual.flags-aconfig-java",
"android.companion.virtualdevice.flags-aconfig-java",
"androidx.annotation_annotation", "androidx.annotation_annotation",
"androidx.appcompat_appcompat", "androidx.appcompat_appcompat",
"androidx.preference_preference",
"guava", "guava",
"hilt_android", "hilt_android",
], ],

View File

@@ -6,8 +6,8 @@
[Prerequisites](#prerequisites) \ [Prerequisites](#prerequisites) \
[Build & Install](#build-and-install) \ [Build & Install](#build-and-install) \
[Run](#run) \ [Run](#run) \
[Host Settings](#host-settings) \ [Host Options](#host-options) \
[Client Settings](#client-settings) \ [Client Options](#client-options) \
[Demos](#demos) [Demos](#demos)
## Overview ## Overview
@@ -71,14 +71,14 @@ available devices, build the APKs and install them.
1. Build the Host app. 1. Build the Host app.
``` ```shell
m -j VdmHost m -j VdmHost
``` ```
1. Install the application as a system app on the host device. 1. Install the application as a system app on the host device.
<!-- TODO(b/314436863): Add a bash script for easy host app install. --> <!-- TODO(b/314436863): Add a bash script for easy host app install. -->
``` ```shell
adb root && adb disable-verity && adb reboot # one time adb root && adb disable-verity && adb reboot # one time
adb root && adb remount adb root && adb remount
adb push $ANDROID_BUILD_TOP/development/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml /system/etc/permissions/com.example.android.vdmdemo.host.xml adb push $ANDROID_BUILD_TOP/development/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml /system/etc/permissions/com.example.android.vdmdemo.host.xml
@@ -88,18 +88,19 @@ available devices, build the APKs and install them.
``` ```
**Tip:** Subsequent installs without changes to permissions, etc. do not **Tip:** Subsequent installs without changes to permissions, etc. do not
need all the commands above - you can just do \ need all the commands above - you can just run \
\
`adb install -r -d -g $OUT/system/priv-app/VdmHost/VdmHost.apk` `adb install -r -d -g $OUT/system/priv-app/VdmHost/VdmHost.apk`
1. Build and install the Demo app on the host device. 1. Build and install the Demo app on the host device.
``` ```shell
m -j VdmDemos && adb install -r -d -g $OUT/system/app/VdmDemos/VdmDemos.apk m -j VdmDemos && adb install -r -d -g $OUT/system/app/VdmDemos/VdmDemos.apk
``` ```
1. Build and install the Client app on the client device. 1. Build and install the Client app on the client device.
``` ```shell
m -j VdmClient && adb install -r -d -g $OUT/system/app/VdmClient/VdmClient.apk m -j VdmClient && adb install -r -d -g $OUT/system/app/VdmClient/VdmClient.apk
``` ```
@@ -113,97 +114,148 @@ available devices, build the APKs and install them.
WARNING: If there are other devices in the vicinity with one of these apps WARNING: If there are other devices in the vicinity with one of these apps
running, they might interfere. running, they might interfere.
1. Once the connection switches to high bandwidth medium, the Host app will 1. Check out the different [Host Options](#host-options) and
show a launcher-like list of installed apps on the host device. [Client Options](#client-options) that allow for changing the behavior of
1. Clicking an app icon will create a new virtual display, launch the app there
and start streaming the display contents to the client. The client will show
the surface of that display and render its contents.
1. Long pressing on an app icon will open a dialog to select an existing
display to launch the app on instead of creating a new one.
1. Each display on the Client app has a "Back" and "Close" buttons. When a
display becomes empty, it's automatically removed.
1. Each display on the Client app has a "Rotate" button to switch between
portrait and landscape orientation. This simulates the physical rotation of
the display of the streamed activity. The "Resize" button can be used to
change the display dimensions.
1. Each display on the Client app has a "Fullscreen" button which will move
the contents of that display to an immersive fullscreen activity. The
client's back button/gestures are sent back to the streamed app. Use
Volume Down on the client device to exit fullscreen. Volume Up acts as a
home key, if the streamed display is a home display.
1. The Host app has a "CREATE HOME DISPLAY" button, clicking it will create a
new virtual display, launch the secondary home activity there and start
streaming the display contents to the client. The display on the Client app
will have a home button, clicking it will navigate the streaming experience
back to the home activity.
1. The Host app has a "CREATE MIRROR DISPLAY" button, clicking it will create a
new virtual display, mirror the default host display there and start
streaming the display contents to the client.
1. Check out the different [Host Settings](#host-settings) and
[Client Settings](#client-settings) that allow for changing the behavior of
the streamed apps and the virtual device in general. the streamed apps and the virtual device in general.
1. Check out the [Demo apps](#demos) that are specifically meant to showcase 1. Check out the [Demo apps](#demos) that are specifically meant to showcase
the VDM features. the VDM features.
<!-- LINT.IfChange(host_settings) --> <!-- LINT.IfChange(host_options) -->
## Host Settings ## Host Options
- **Client Sensors**: Enables sensor injection from the client device into the NOTE: Any flag changes require device reboot or "Force stop" of the host app
host device. Any context that is associated with the virtual device will because the flag values are cached and evaluated only when the host app is
access the virtual sensors by default. \ starting. Alternatively, run: \
\
`adb shell am force-stop com.example.android.vdmdemo.host`
### Launcher
Once the connection with the client device is established, the Host app will
show a launcher-like list of installed apps on the host device.
- Clicking an app icon will create a new virtual display, launch the app there
and start streaming the display contents to the client. The client will show
the surface of that display and render its contents.
- Long pressing on an app icon will open a dialog to select an existing
display to launch the app on instead of creating a new one.
- The Host app has a **CREATE HOME DISPLAY** button, clicking it will create a
new virtual display, launch the secondary home activity there and start
streaming the display contents to the client. The display on the Client app
will have a home button, clicking it will navigate the streaming experience
back to the home activity. Run the commands below to enable this
functionality.
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_home true
adb shell am force-stop com.example.android.vdmdemo.host
```
- The Host app has a **CREATE MIRROR DISPLAY** button, clicking it will create
a new virtual display, mirror the default host display there and start
streaming the display contents to the client. Run the commands below to
enable this functionality.
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.consistent_display_flags true
adb shell device_config put virtual_devices android.companion.virtual.flags.interactive_screen_mirror true
adb shell am force-stop com.example.android.vdmdemo.host
```
### Settings
#### General
- **Device profile**: Enables device streaming CDM role as opposed to app
streaming role, with all differences in policies that this entails. \
*Changing this will recreate the virtual device.* *Changing this will recreate the virtual device.*
- **Client Audio**: Enables audio output on the client device. Any context
that is associated with the virtual device will play audio on the client by
default. \
*This can be changed dynamically.*
- **Include streamed apps in recents**: Whether streamed apps should show up - **Include streamed apps in recents**: Whether streamed apps should show up
in the host device's recent apps. Run the command below to enable this in the host device's recent apps. Run the commands below to enable this
functionality. \ functionality. \
*This can be changed dynamically.* *This can be changed dynamically.*
```shell ```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.dynamic_policy true adb shell device_config put virtual_devices android.companion.virtual.flags.dynamic_policy true
adb shell am force-stop com.example.android.vdmdemo.host
``` ```
- **Cross-device clipboard**: Whether to share the clipboard between the host - **Enable cross-device clipboard**: Whether to share the clipboard between
and the virtual device. If disabled, both devices will have their own the host and the virtual device. If disabled, both devices will have their
isolated clipboards. Run the command below to enable this functionality. \ own isolated clipboards. Run the commands below to enable this
functionality. \
*This can be changed dynamically.* *This can be changed dynamically.*
```shell ```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.cross_device_clipboard true adb shell device_config put virtual_devices android.companion.virtual.flags.cross_device_clipboard true
adb shell am force-stop com.example.android.vdmdemo.host
``` ```
#### Client capabilities
- **Enable client Sensors**: Enables sensor injection from the client device
into the host device. Any context that is associated with the virtual device
will access the virtual sensors by default. \
*Changing this will recreate the virtual device.*
- **Enable client Audio**: Enables audio output on the client device. Any
context that is associated with the virtual device will play audio on the
client by default. \
*This can be changed dynamically.*
#### Displays
- **Display rotation**: Whether orientation change requests from streamed apps - **Display rotation**: Whether orientation change requests from streamed apps
should trigger orientation change of the relevant display. The client will should trigger orientation change of the relevant display. The client will
automatically rotate the relevant display upon such request. Disabling this automatically rotate the relevant display upon such request. Disabling this
simulates a fixed orientation display that cannot physically rotate. Then simulates a fixed orientation display that cannot physically rotate. Then
any streamed apps on that display will be letterboxed/pillarboxed if they any streamed apps on that display will be letterboxed/pillarboxed if they
request orientation change. \ request orientation change. Run the commands below to enable this
functionality. \
*This can be changed dynamically but only applies to newly created *This can be changed dynamically but only applies to newly created
displays.* displays.*
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.consistent_display_flags true
adb shell am force-stop com.example.android.vdmdemo.host
```
- **Always unlocked**: Whether the virtual displays should remain unlocked and - **Always unlocked**: Whether the virtual displays should remain unlocked and
interactive when the host device is locked. Disabling this will result in a interactive when the host device is locked. Disabling this will result in a
simple lock screen shown on these displays when the host device is locked. \ simple lock screen shown on these displays when the host device is locked. \
*Changing this will recreate the virtual device.* *Changing this will recreate the virtual device.*
- **Device streaming profile**: Enables device streaming CDM role as opposed - **Show pointer icon**: Whether pointer icon should be shown for virtual
to app streaming role, with all differences in policies that this entails. \ input pointer devices. \
*This can be changed dynamically.*
- **Custom home**: Whether to use a custom activity as home on home displays,
or use the device-default secondary home activity. Run the commands below to
enable this functionality. \
*Changing this will recreate the virtual device.* *Changing this will recreate the virtual device.*
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_home true
adb shell am force-stop com.example.android.vdmdemo.host
```
#### Input method
- **Display IME policy**: Choose the IME behavior on remote displays. Run the
commands below to enable this functionality. \
*This can be changed dynamically.*
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_ime true
adb shell am force-stop com.example.android.vdmdemo.host
```
#### Debug
- **Record encoder output**: Enables recording the output of the encoder on - **Record encoder output**: Enables recording the output of the encoder on
the host device to a local file on the device. This can be helpful with the host device to a local file on the device. This can be helpful with
debugging Encoding related issues. To download and play the file locally: debugging Encoding related issues. To download and play the file locally:
@@ -213,24 +265,27 @@ available devices, build the APKs and install them.
ffplay -f h264 vdmdemo_encoder_output_<displayId>.h264 ffplay -f h264 vdmdemo_encoder_output_<displayId>.h264
``` ```
- **Show pointer icon**: Whether pointer icon should be shown for virtual <!-- LINT.ThenChange(README.md) -->
input pointer devices. \
*This can be changed dynamically.*
- **Custom home**: Whether to use a custom activity as home on home displays,
or use the device-default secondary home activity. Run the command below to
enable this functionality. \
*Changing this will recreate the virtual device.*
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_home true
```
<!-- LINT.ThenChange(/samples/VirtualDeviceManager/host/res/menu/settings.xml) -->
<!-- LINT.IfChange(client_options) --> <!-- LINT.IfChange(client_options) -->
## Client Options ## Client Options
### Streamed displays
- Each display on the Client app has a "Back" and "Close" buttons. When a
display becomes empty, it's automatically removed.
- Each display on the Client app has a "Rotate" button to switch between
portrait and landscape orientation. This simulates the physical rotation of
the display of the streamed activity. The "Resize" button can be used to
change the display dimensions.
- Each display on the Client app has a "Fullscreen" button which will move the
contents of that display to an immersive fullscreen activity. The client's
back button/gestures are sent back to the streamed app. Use Volume Down on
the client device to exit fullscreen. Volume Up acts as a home key, if the
streamed display is a home display.
### Input ### Input
The input menu button enables **on-screen D-Pad and touchpad** for navigating The input menu button enables **on-screen D-Pad and touchpad** for navigating
@@ -244,7 +299,7 @@ keyboard** are forwarded to the activity streamed on the focused display.
**Externally connected mouse** events are also forwarded to the relevant **Externally connected mouse** events are also forwarded to the relevant
display, if the mouse pointer is currently positioned on a streamed display. display, if the mouse pointer is currently positioned on a streamed display.
<!-- LINT.ThenChange(/samples/VirtualDeviceManager/client/res/menu/options.xml) --> <!-- LINT.ThenChange(README.md) -->
<!-- LINT.IfChange(demos) --> <!-- LINT.IfChange(demos) -->
## Demos ## Demos
@@ -277,4 +332,4 @@ display, if the mouse pointer is currently positioned on a streamed display.
is no vibration support on virtual devices, so vibration requests from is no vibration support on virtual devices, so vibration requests from
streamed activities are ignored. streamed activities are ignored.
<!-- LINT.ThenChange(/samples/VirtualDeviceManager/demos/AndroidManifest.xml) --> <!-- LINT.ThenChange(README.md) -->

View File

@@ -56,10 +56,13 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".SettingsActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<activity <activity
android:name=".CustomLauncherActivity" android:name=".CustomLauncherActivity"
android:exported="true" android:exported="true"
android:label="@string/custom_home"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" /> android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" />

View File

@@ -17,13 +17,10 @@
<permissions> <permissions>
<privapp-permissions package="com.example.android.vdmdemo.host"> <privapp-permissions package="com.example.android.vdmdemo.host">
<permission name="android.permission.CREATE_VIRTUAL_DEVICE" />
<permission name="android.permission.MODIFY_AUDIO_ROUTING" /> <permission name="android.permission.MODIFY_AUDIO_ROUTING" />
<permission name="android.permission.QUERY_AUDIO_STATE" /> <permission name="android.permission.QUERY_AUDIO_STATE" />
<permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED" /> <permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
<permission name="android.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING" /> <permission name="android.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING" />
<permission name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING" /> <permission name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING" />
<permission name="android.permission.ADD_ALWAYS_UNLOCKED_DISPLAY" />
<permission name="android.permission.ADD_TRUSTED_DISPLAY" />
</privapp-permissions> </privapp-permissions>
</permissions> </permissions>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/main_tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:navigationIcon="?homeAsUpIndicator"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_fragment_container"
android:name="com.example.android.vdmdemo.host.SettingsActivity$SettingsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- LINT.IfChange -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/settings"
android:icon="@drawable/settings"
android:title="@string/settings"
app:showAsAction="always" />
</menu>
<!-- LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_options) -->

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- LINT.IfChange -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="all">
<item
android:id="@+id/enable_sensors"
android:title="@string/enable_sensors" />
<item
android:id="@+id/enable_audio"
android:title="@string/enable_audio" />
<item
android:id="@+id/enable_recents"
android:title="@string/enable_recents" />
<item
android:id="@+id/enable_clipboard"
android:title="@string/enable_clipboard" />
<item
android:id="@+id/enable_rotation"
android:title="@string/enable_rotation" />
<item
android:id="@+id/always_unlocked"
android:title="@string/always_unlocked" />
<item
android:id="@+id/use_device_streaming"
android:title="@string/use_device_streaming" />
<item
android:id="@+id/record_encoder_output"
android:title="@string/record_encoder_output" />
<item
android:id="@+id/show_pointer_icon"
android:title="@string/show_pointer_icon" />
<item
android:id="@+id/custom_home"
android:title="@string/custom_home" />
</group>
</menu>
<!-- LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_settings) -->

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array translatable="false" name="device_profile_labels">
<item>App streaming</item>
<item>Nearby device streaming</item>
</string-array>
<string-array translatable="false" name="device_profiles">
<item>@string/app_streaming</item>
<item>@string/nearby_device_streaming</item>
</string-array>
<string-array translatable="false" name="display_ime_policy_labels">
<item>Show IME on the remote display</item>
<item>Show IME on the default display</item>
<item>Do not show IME</item>
</string-array>
<!-- Values correspond to WindowManager#DisplayImePolicy enums. -->
<string-array translatable="false" name="display_ime_policies">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
</resources>

View File

@@ -4,15 +4,23 @@
<string name="create_home_display" translatable="false">Create Home Display</string> <string name="create_home_display" translatable="false">Create Home Display</string>
<string name="create_mirror_display" translatable="false">Create Mirror Display</string> <string name="create_mirror_display" translatable="false">Create Mirror Display</string>
<string name="app_icon_description" translatable="false">Application Icon</string> <string name="app_icon_description" translatable="false">Application Icon</string>
<string name="settings" translatable="false">Settings</string>
<string name="enable_audio" translatable="false">Client audio</string> <string name="pref_device_profile" translatable="false">device_profile</string>
<string name="enable_sensors" translatable="false">Client sensors</string> <string name="pref_enable_recents" translatable="false">enable_recents</string>
<string name="enable_recents" translatable="false">Include streamed apps in recents</string> <string name="pref_enable_cross_device_clipboard" translatable="false">enable_cross_device_clipboard</string>
<string name="enable_clipboard" translatable="false">Cross-device clipboard</string> <string name="pref_enable_client_sensors" translatable="false">enable_client_sensors</string>
<string name="enable_rotation" translatable="false">Display rotation</string> <string name="pref_enable_client_audio" translatable="false">enable_client_audio</string>
<string name="always_unlocked" translatable="false">Always unlocked</string> <string name="pref_enable_display_rotation" translatable="false">enable_display_rotation</string>
<string name="use_device_streaming" translatable="false">Device streaming profile</string> <string name="pref_always_unlocked_device" translatable="false">always_unlocked_device</string>
<string name="record_encoder_output" translatable="false">Record encoder output</string> <string name="pref_show_pointer_icon" translatable="false">show_pointer_icon</string>
<string name="show_pointer_icon" translatable="false">Show pointer icon</string> <string name="pref_enable_custom_home" translatable="false">enable_custom_home</string>
<string name="custom_home" translatable="false">Custom home</string> <string name="pref_display_ime_policy" translatable="false">display_ime_policy</string>
<string name="pref_record_encoder_output" translatable="false">record_encoder_output</string>
<string name="internal_pref_enable_home_displays" translatable="false">enable_home_displays</string>
<string name="internal_pref_enable_mirror_displays" translatable="false">enable_mirror_displays</string>
<string name="app_streaming" translatable="false">android.app.role.COMPANION_DEVICE_APP_STREAMING</string>
<string name="nearby_device_streaming" translatable="false">android.app.role.COMPANION_DEVICE_NEARBY_DEVICE_STREAMING</string>
</resources> </resources>

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- LINT.IfChange -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<PreferenceCategory
android:key="general"
android:title="General"
app:iconSpaceReserved="false">
<ListPreference
android:key="@string/pref_device_profile"
android:title="Device profile"
android:entries="@array/device_profile_labels"
android:entryValues="@array/device_profiles"
android:defaultValue="@string/app_streaming"
app:useSimpleSummaryProvider="true"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:key="@string/pref_enable_recents"
android:title="Include streamed app in recents"
android:defaultValue="false"
app:iconSpaceReserved="false"/>
<SwitchPreferenceCompat
android:key="@string/pref_enable_cross_device_clipboard"
android:title="Enable cross-device clipboard"
android:defaultValue="false"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
android:key="client_capabilities"
android:title="Client capabilities"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
android:key="@string/pref_enable_client_sensors"
android:title="Enable client sensors"
android:defaultValue="true"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:key="@string/pref_enable_client_audio"
android:title="Enable client audio"
android:defaultValue="true"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
android:key="display"
android:title="Displays"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
android:key="@string/pref_enable_display_rotation"
android:title="Enable display rotation"
android:summary="Rotate the remote display instead of letterboxing or pillarboxing"
android:defaultValue="true"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:key="@string/pref_always_unlocked_device"
android:title="Always unlocked"
android:summary="Remote displays remain unlocked even when the host is locked"
android:defaultValue="false"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:key="@string/pref_show_pointer_icon"
android:title="Show pointer icon"
android:summary="Mouse pointer on remote displays is visible"
android:defaultValue="false"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:key="@string/pref_enable_custom_home"
android:title="Custom home"
android:summary="Use a custom home activity instead of the default one on home displays"
android:defaultValue="false"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
android:key="ime"
android:title="Input Method"
app:iconSpaceReserved="false">
<ListPreference
android:key="@string/pref_display_ime_policy"
android:title="Display IME policy"
android:entries="@array/display_ime_policy_labels"
android:entryValues="@array/display_ime_policies"
android:defaultValue="0"
app:useSimpleSummaryProvider="true"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
android:key="debug"
android:title="Debug"
app:iconSpaceReserved="false">
<!--
When enabled, the encoder output of the host will be stored in:
/sdcard/Download/vdmdemo_encoder_output_[displayId].h264
After pulling this file to your machine this can be played back with:
ffplay -f h264 vdmdemo_encoder_output_[displayId].h264
-->
<SwitchPreferenceCompat
android:key="@string/pref_record_encoder_output"
android:title="Record encoder output"
android:summary="Store the host's media encoder output to a local file"
android:defaultValue="false"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</PreferenceScreen>
<!-- LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_options) -->

View File

@@ -69,6 +69,14 @@ final class DisplayRepository {
} }
} }
int[] getDisplayIds() {
synchronized (mDisplayRepository) {
return mDisplayRepository.stream()
.mapToInt(RemoteDisplay::getDisplayId)
.toArray();
}
}
int[] getRemoteDisplayIds() { int[] getRemoteDisplayIds() {
synchronized (mDisplayRepository) { synchronized (mDisplayRepository) {
return mDisplayRepository.stream() return mDisplayRepository.stream()

View File

@@ -84,7 +84,7 @@ public class MainActivity extends Hilt_MainActivity {
}; };
@Inject ConnectionManager mConnectionManager; @Inject ConnectionManager mConnectionManager;
@Inject Settings mSettings; @Inject PreferenceController mPreferenceController;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -92,11 +92,15 @@ public class MainActivity extends Hilt_MainActivity {
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
Toolbar toolbar = requireViewById(R.id.main_tool_bar); Toolbar toolbar = requireViewById(R.id.main_tool_bar);
toolbar.setOverflowIcon(getDrawable(R.drawable.settings));
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
mHomeDisplayButton = requireViewById(R.id.create_home_display); mHomeDisplayButton = requireViewById(R.id.create_home_display);
mHomeDisplayButton.setEnabled(
mPreferenceController.getBoolean(R.string.internal_pref_enable_home_displays));
mMirrorDisplayButton = requireViewById(R.id.create_mirror_display); mMirrorDisplayButton = requireViewById(R.id.create_mirror_display);
mMirrorDisplayButton.setEnabled(
mPreferenceController.getBoolean(R.string.internal_pref_enable_mirror_displays));
mLauncher = requireViewById(R.id.app_grid); mLauncher = requireViewById(R.id.app_grid);
mLauncher.setVisibility(View.GONE); mLauncher.setVisibility(View.GONE);
LauncherAdapter launcherAdapter = new LauncherAdapter(getPackageManager()); LauncherAdapter launcherAdapter = new LauncherAdapter(getPackageManager());
@@ -189,79 +193,15 @@ public class MainActivity extends Hilt_MainActivity {
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.settings, menu); inflater.inflate(R.menu.options, menu);
for (int i = 0; i < menu.size(); ++i) {
MenuItem item = menu.getItem(i);
switch (item.getItemId()) {
case R.id.enable_sensors:
item.setChecked(mSettings.sensorsEnabled);
break;
case R.id.enable_audio:
item.setChecked(mSettings.audioEnabled);
break;
case R.id.enable_recents:
item.setChecked(mSettings.includeInRecents);
break;
case R.id.enable_clipboard:
item.setChecked(mSettings.crossDeviceClipboardEnabled);
break;
case R.id.enable_rotation:
item.setChecked(mSettings.displayRotationEnabled);
break;
case R.id.always_unlocked:
item.setChecked(mSettings.alwaysUnlocked);
break;
case R.id.use_device_streaming:
item.setChecked(mSettings.deviceStreaming);
break;
case R.id.show_pointer_icon:
item.setChecked(mSettings.showPointerIcon);
break;
case R.id.record_encoder_output:
item.setChecked(mSettings.recordEncoderOutput);
break;
case R.id.custom_home:
item.setChecked(mSettings.customHome);
break;
}
}
return true; return true;
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
item.setChecked(!item.isChecked());
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.enable_sensors: case R.id.settings:
mVdmService.setSensorsEnabled(item.isChecked()); startActivity(new Intent(this, SettingsActivity.class));
return true;
case R.id.enable_audio:
mVdmService.setAudioEnabled(item.isChecked());
return true;
case R.id.enable_recents:
mVdmService.setIncludeInRecents(item.isChecked());
return true;
case R.id.enable_clipboard:
mVdmService.setCrossDeviceClipboardEnabled(item.isChecked());
return true;
case R.id.enable_rotation:
mVdmService.setDisplayRotationEnabled(item.isChecked());
return true;
case R.id.always_unlocked:
mVdmService.setAlwaysUnlocked(item.isChecked());
return true;
case R.id.use_device_streaming:
mVdmService.setDeviceStreaming(item.isChecked());
return true;
case R.id.record_encoder_output:
mVdmService.setRecordEncoderOutput(item.isChecked());
return true;
case R.id.show_pointer_icon:
mVdmService.setShowPointerIcon(item.isChecked());
return true;
case R.id.custom_home:
mVdmService.setCustomHome(item.isChecked());
return true; return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);

View File

@@ -0,0 +1,245 @@
/*
* 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.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM;
import android.companion.AssociationRequest;
import android.companion.virtual.flags.Flags;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.ArrayMap;
import androidx.annotation.StringRes;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import dagger.hilt.android.qualifiers.ApplicationContext;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Manages the VDM Demo Host application settings and feature switches.
*
* <p>Upon creation, it will automatically update the preference values based on the current SDK
* version and the relevant feature flags.</p>
*/
@Singleton
final class PreferenceController {
// LINT.IfChange
private static final Set<PrefRule<?>> RULES = Set.of(
// Exposed in the settings page
new BoolRule(R.string.pref_enable_cross_device_clipboard,
VANILLA_ICE_CREAM, Flags::crossDeviceClipboard),
new BoolRule(R.string.pref_enable_client_sensors, UPSIDE_DOWN_CAKE),
new BoolRule(R.string.pref_enable_display_rotation,
VANILLA_ICE_CREAM, Flags::consistentDisplayFlags)
.withDefaultValue(true),
new BoolRule(R.string.pref_enable_custom_home, VANILLA_ICE_CREAM, Flags::vdmCustomHome),
new StringRule(R.string.pref_display_ime_policy, VANILLA_ICE_CREAM, Flags::vdmCustomIme)
.withDefaultValue(String.valueOf(0)),
// TODO(b/316098039): Evaluate the minSdk of the prefs below.
new StringRule(R.string.pref_device_profile, VANILLA_ICE_CREAM)
.withDefaultValue(AssociationRequest.DEVICE_PROFILE_APP_STREAMING),
new BoolRule(R.string.pref_enable_recents, VANILLA_ICE_CREAM),
new BoolRule(R.string.pref_enable_client_audio, VANILLA_ICE_CREAM),
new BoolRule(R.string.pref_always_unlocked_device, VANILLA_ICE_CREAM),
new BoolRule(R.string.pref_show_pointer_icon, VANILLA_ICE_CREAM),
new BoolRule(R.string.pref_record_encoder_output, VANILLA_ICE_CREAM),
// Internal-only switches not exposed in the settings page.
// All of these are booleans acting as switches, while the above ones may be any type.
// TODO(b/316098039): Use the SysDecor flag on <= VIC
new InternalBoolRule(R.string.internal_pref_enable_home_displays,
VANILLA_ICE_CREAM, Flags::vdmCustomHome),
new InternalBoolRule(R.string.internal_pref_enable_mirror_displays,
VANILLA_ICE_CREAM,
Flags::consistentDisplayFlags, Flags::interactiveScreenMirror)
);
// LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_options)
private final ArrayMap<Object, Map<String, Consumer<Object>>> mObservers = new ArrayMap<>();
private final SharedPreferences.OnSharedPreferenceChangeListener mPreferenceChangeListener =
this::onPreferencesChanged;
private final Context mContext;
private final SharedPreferences mSharedPreferences;
@Inject
PreferenceController(@ApplicationContext Context context) {
mContext = context;
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = mSharedPreferences.edit();
RULES.forEach(r -> r.evaluate(mContext, editor));
editor.commit();
mSharedPreferences.registerOnSharedPreferenceChangeListener(mPreferenceChangeListener);
}
/**
* Adds an observer for preference changes.
*
* @param key an object used only for bookkeeping.
* @param preferenceObserver a map from resource ID corresponding to the preference string key
* to the function that should be executed when that preference changes.
*/
void addPreferenceObserver(Object key, Map<Integer, Consumer<Object>> preferenceObserver) {
ArrayMap<String, Consumer<Object>> stringObserver = new ArrayMap<>();
for (int resId : preferenceObserver.keySet()) {
stringObserver.put(
Objects.requireNonNull(mContext.getString(resId)),
preferenceObserver.get(resId));
}
mObservers.put(key, stringObserver);
}
/** Removes a previously added preference observer for the given key. */
void removePreferenceObserver(Object key) {
mObservers.remove(key);
}
/**
* Disables any {@link androidx.preference.Preference}, which is not satisfied by the current
* SDK version or the relevant feature flags.
*
* <p>This doesn't change any of the preference values, only disables the relevant UI elements
* in the preference screen.</p>
*/
void evaluate(PreferenceManager preferenceManager) {
RULES.forEach(r -> r.evaluate(mContext, preferenceManager));
}
boolean getBoolean(@StringRes int resId) {
return mSharedPreferences.getBoolean(mContext.getString(resId), false);
}
String getString(@StringRes int resId) {
return Objects.requireNonNull(
mSharedPreferences.getString(mContext.getString(resId), null));
}
int getInt(@StringRes int resId) {
return Integer.valueOf(getString(resId));
}
private void onPreferencesChanged(SharedPreferences sharedPreferences, String key) {
Map<String, ?> currentPreferences = sharedPreferences.getAll();
for (Map<String, Consumer<Object>> observer : mObservers.values()) {
Consumer<Object> consumer = observer.get(key);
if (consumer != null) {
consumer.accept(currentPreferences.get(key));
}
}
}
private abstract static class PrefRule<T> {
final @StringRes int mKey;
final int mMinSdk;
final BooleanSupplier[] mRequiredFlags;
protected T mDefaultValue;
PrefRule(@StringRes int key, T defaultValue, int minSdk, BooleanSupplier... requiredFlags) {
mKey = key;
mMinSdk = minSdk;
mRequiredFlags = requiredFlags;
mDefaultValue = defaultValue;
}
void evaluate(Context context, SharedPreferences.Editor editor) {
if (!isSatisfied()) {
reset(context, editor);
}
}
void evaluate(Context context, PreferenceManager preferenceManager) {
Preference preference = preferenceManager.findPreference(context.getString(mKey));
if (preference != null) {
boolean enabled = isSatisfied();
if (preference.isEnabled() != enabled) {
preference.setEnabled(enabled);
}
}
}
protected abstract void reset(Context context, SharedPreferences.Editor editor);
protected boolean isSatisfied() {
return mMinSdk >= SDK_INT
&& Arrays.stream(mRequiredFlags).allMatch(BooleanSupplier::getAsBoolean);
}
PrefRule<T> withDefaultValue(T defaultValue) {
mDefaultValue = defaultValue;
return this;
}
}
private static class BoolRule extends PrefRule<Boolean> {
BoolRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) {
super(key, false, minSdk, requiredFlags);
}
@Override
protected void reset(Context context, SharedPreferences.Editor editor) {
editor.putBoolean(context.getString(mKey), mDefaultValue);
}
}
private static class InternalBoolRule extends BoolRule {
InternalBoolRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) {
super(key, minSdk, requiredFlags);
}
@Override
void evaluate(Context context, SharedPreferences.Editor editor) {
editor.putBoolean(context.getString(mKey), isSatisfied());
}
}
private static class StringRule extends PrefRule<String> {
StringRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) {
super(key, null, minSdk, requiredFlags);
}
@Override
protected void reset(Context context, SharedPreferences.Editor editor) {
editor.putString(context.getString(mKey), mDefaultValue);
}
}
}

View File

@@ -86,7 +86,7 @@ class RemoteDisplay implements AutoCloseable {
private final Context mContext; private final Context mContext;
private final RemoteIo mRemoteIo; private final RemoteIo mRemoteIo;
private final Settings mSettings; private final PreferenceController mPreferenceController;
private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent; private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
private final VirtualDisplay mVirtualDisplay; private final VirtualDisplay mVirtualDisplay;
private final VirtualDpad mDpad; private final VirtualDpad mDpad;
@@ -113,19 +113,19 @@ class RemoteDisplay implements AutoCloseable {
VirtualDevice virtualDevice, VirtualDevice virtualDevice,
RemoteIo remoteIo, RemoteIo remoteIo,
@DisplayType int displayType, @DisplayType int displayType,
Settings settings) { PreferenceController preferenceController) {
mContext = context; mContext = context;
mRemoteIo = remoteIo; mRemoteIo = remoteIo;
mRemoteDisplayId = event.getDisplayId(); mRemoteDisplayId = event.getDisplayId();
mVirtualDevice = virtualDevice; mVirtualDevice = virtualDevice;
mPendingIntentExecutor = context.getMainExecutor(); mPendingIntentExecutor = context.getMainExecutor();
mDisplayType = displayType; mDisplayType = displayType;
mSettings = settings; mPreferenceController = preferenceController;
setCapabilities(event.getDisplayCapabilities()); setCapabilities(event.getDisplayCapabilities());
int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS; int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
if (settings.displayRotationEnabled) { if (mPreferenceController.getBoolean(R.string.pref_enable_display_rotation)) {
flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT; flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
} }
if (mDisplayType == DISPLAY_TYPE_MIRROR) { if (mDisplayType == DISPLAY_TYPE_MIRROR) {
@@ -142,6 +142,9 @@ class RemoteDisplay implements AutoCloseable {
/* executor= */ Runnable::run, /* executor= */ Runnable::run,
/* callback= */ null); /* callback= */ null);
mVirtualDevice.setDisplayImePolicy(
getDisplayId(), mPreferenceController.getInt(R.string.pref_display_ime_policy));
mDpad = mDpad =
virtualDevice.createVirtualDpad( virtualDevice.createVirtualDpad(
new VirtualDpadConfig.Builder() new VirtualDpadConfig.Builder()
@@ -164,9 +167,8 @@ class RemoteDisplay implements AutoCloseable {
if (mVideoManager != null) { if (mVideoManager != null) {
mVideoManager.stop(); mVideoManager.stop();
} }
mVideoManager = mVideoManager = VideoManager.createEncoder(mRemoteDisplayId, mRemoteIo,
VideoManager.createEncoder( mPreferenceController.getBoolean(R.string.pref_record_encoder_output));
mRemoteDisplayId, mRemoteIo, mSettings.recordEncoderOutput);
Surface surface = mVideoManager.createInputSurface(mWidth, mHeight, DISPLAY_FPS); Surface surface = mVideoManager.createInputSurface(mWidth, mHeight, DISPLAY_FPS);
mVirtualDisplay.setSurface(surface); mVirtualDisplay.setSurface(surface);

View File

@@ -1,46 +0,0 @@
/*
* 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 javax.inject.Inject;
import javax.inject.Singleton;
/** Settings known to the VDM Demo Host application */
@Singleton
final class Settings {
public boolean displayRotationEnabled = true;
public boolean sensorsEnabled = true;
public boolean audioEnabled = true;
public boolean includeInRecents = false;
public boolean crossDeviceClipboardEnabled = false;
public boolean alwaysUnlocked = true;
public boolean deviceStreaming = false;
public boolean showPointerIcon = true;
public boolean customHome = false;
/**
* When enabled, the encoder output of the host will be stored in:
* /sdcard/Download/vdmdemo_encoder_output_[displayId].h264
*
* <p>After pulling this file to your machine this can be played back with:
* {@code ffplay -f h264 vdmdemo_encoder_output_[displayId].h264}
*/
public boolean recordEncoderOutput = false;
@Inject
Settings() {}
}

View File

@@ -0,0 +1,55 @@
/*
* 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 android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.preference.PreferenceFragmentCompat;
import dagger.hilt.android.AndroidEntryPoint;
import javax.inject.Inject;
/** VDM Host Settings activity. */
@AndroidEntryPoint(AppCompatActivity.class)
public class SettingsActivity extends Hilt_SettingsActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
Toolbar toolbar = requireViewById(R.id.main_tool_bar);
setSupportActionBar(toolbar);
toolbar.setNavigationOnClickListener(v -> finish());
setTitle(getTitle() + " " + getString(R.string.settings));
}
@AndroidEntryPoint(PreferenceFragmentCompat.class)
public static final class SettingsFragment extends Hilt_SettingsActivity_SettingsFragment {
@Inject PreferenceController mPreferenceController;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.preferences, rootKey);
mPreferenceController.evaluate(getPreferenceManager());
}
}
}

View File

@@ -63,6 +63,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -96,7 +98,7 @@ public final class VdmService extends Hilt_VdmService {
@Inject ConnectionManager mConnectionManager; @Inject ConnectionManager mConnectionManager;
@Inject RemoteIo mRemoteIo; @Inject RemoteIo mRemoteIo;
@Inject AudioStreamer mAudioStreamer; @Inject AudioStreamer mAudioStreamer;
@Inject Settings mSettings; @Inject PreferenceController mPreferenceController;
@Inject DisplayRepository mDisplayRepository; @Inject DisplayRepository mDisplayRepository;
private RemoteSensorManager mRemoteSensorManager = null; private RemoteSensorManager mRemoteSensorManager = null;
@@ -196,14 +198,47 @@ public final class VdmService extends Hilt_VdmService {
mRemoteIo.addMessageConsumer(mRemoteEventConsumer); mRemoteIo.addMessageConsumer(mRemoteEventConsumer);
if (mSettings.audioEnabled) { if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) {
mAudioStreamer.start(); mAudioStreamer.start();
} }
mPreferenceController.addPreferenceObserver(this, Map.of(
R.string.pref_enable_recents,
b -> updateDevicePolicy(POLICY_TYPE_RECENTS, !(Boolean) b),
R.string.pref_enable_cross_device_clipboard,
b -> updateDevicePolicy(POLICY_TYPE_CLIPBOARD, (Boolean) b),
R.string.pref_show_pointer_icon,
b -> {
if (mVirtualDevice != null) mVirtualDevice.setShowPointerIcon((Boolean) b);
},
R.string.pref_enable_client_audio,
b -> {
if ((Boolean) b) mAudioStreamer.start(); else mAudioStreamer.stop();
},
R.string.pref_display_ime_policy,
s -> {
if (mVirtualDevice != null) {
int policy = Integer.valueOf((String) s);
Arrays.stream(mDisplayRepository.getDisplayIds()).forEach(
displayId -> mVirtualDevice.setDisplayImePolicy(displayId, policy));
}
},
R.string.pref_enable_client_sensors, v -> recreateVirtualDevice(),
R.string.pref_device_profile, v -> recreateVirtualDevice(),
R.string.pref_always_unlocked_device, v -> recreateVirtualDevice(),
R.string.pref_enable_custom_home, v -> recreateVirtualDevice()
));
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
mPreferenceController.removePreferenceObserver(this);
mConnectionManager.removeConnectionCallback(mConnectionCallback); mConnectionManager.removeConnectionCallback(mConnectionCallback);
closeVirtualDevice(); closeVirtualDevice();
mRemoteIo.removeMessageConsumer(mRemoteEventConsumer); mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
@@ -224,7 +259,7 @@ public final class VdmService extends Hilt_VdmService {
mVirtualDevice, mVirtualDevice,
mRemoteIo, mRemoteIo,
mPendingDisplayType, mPendingDisplayType,
mSettings); mPreferenceController);
mDisplayRepository.addDisplay(remoteDisplay); mDisplayRepository.addDisplay(remoteDisplay);
mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP; mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP;
if (mPendingRemoteIntent != null) { if (mPendingRemoteIntent != null) {
@@ -241,10 +276,7 @@ public final class VdmService extends Hilt_VdmService {
CompanionDeviceManager cdm = CompanionDeviceManager cdm =
Objects.requireNonNull(getSystemService(CompanionDeviceManager.class)); Objects.requireNonNull(getSystemService(CompanionDeviceManager.class));
RoleManager rm = Objects.requireNonNull(getSystemService(RoleManager.class)); RoleManager rm = Objects.requireNonNull(getSystemService(RoleManager.class));
final String deviceProfile = final String deviceProfile = mPreferenceController.getString(R.string.pref_device_profile);
mSettings.deviceStreaming
? AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING
: AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
for (AssociationInfo associationInfo : cdm.getMyAssociations()) { for (AssociationInfo associationInfo : cdm.getMyAssociations()) {
// Flashing the device clears the role and the permissions, but not the CDM // Flashing the device clears the role and the permissions, but not the CDM
// associations. // associations.
@@ -305,24 +337,24 @@ public final class VdmService extends Hilt_VdmService {
.setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM) .setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM)
.setAudioPlaybackSessionId(mAudioStreamer.getPlaybackSessionId()); .setAudioPlaybackSessionId(mAudioStreamer.getPlaybackSessionId());
if (mSettings.alwaysUnlocked) { if (mPreferenceController.getBoolean(R.string.pref_always_unlocked_device)) {
virtualDeviceBuilder.setLockState(LOCK_STATE_ALWAYS_UNLOCKED); virtualDeviceBuilder.setLockState(LOCK_STATE_ALWAYS_UNLOCKED);
} }
if (mSettings.customHome) { if (mPreferenceController.getBoolean(R.string.pref_enable_custom_home)) {
virtualDeviceBuilder.setHomeComponent( virtualDeviceBuilder.setHomeComponent(
new ComponentName(this, CustomLauncherActivity.class)); new ComponentName(this, CustomLauncherActivity.class));
} }
if (!mSettings.includeInRecents) { if (!mPreferenceController.getBoolean(R.string.pref_enable_recents)) {
virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM); virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM);
} }
if (mSettings.crossDeviceClipboardEnabled) { if (mPreferenceController.getBoolean(R.string.pref_enable_cross_device_clipboard)) {
virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_CUSTOM); virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_CUSTOM);
} }
if (mSettings.sensorsEnabled) { if (mPreferenceController.getBoolean(R.string.pref_enable_client_sensors)) {
for (SensorCapabilities sensor : mDeviceCapabilities.getSensorCapabilitiesList()) { for (SensorCapabilities sensor : mDeviceCapabilities.getSensorCapabilitiesList()) {
virtualDeviceBuilder.addVirtualSensorConfig( virtualDeviceBuilder.addVirtualSensorConfig(
new VirtualSensorConfig.Builder( new VirtualSensorConfig.Builder(
@@ -353,7 +385,8 @@ public final class VdmService extends Hilt_VdmService {
mRemoteSensorManager.setVirtualSensors(mVirtualDevice.getVirtualSensorList()); mRemoteSensorManager.setVirtualSensors(mVirtualDevice.getVirtualSensorList());
} }
mVirtualDevice.setShowPointerIcon(mSettings.showPointerIcon); mVirtualDevice.setShowPointerIcon(
mPreferenceController.getBoolean(R.string.pref_show_pointer_icon));
mVirtualDevice.addActivityListener( mVirtualDevice.addActivityListener(
MoreExecutors.directExecutor(), MoreExecutors.directExecutor(),
@@ -447,73 +480,19 @@ public final class VdmService extends Hilt_VdmService {
.ifPresent(d -> d.launchIntent(pendingIntent)); .ifPresent(d -> d.launchIntent(pendingIntent));
} }
void setDisplayRotationEnabled(boolean enabled) { private void recreateVirtualDevice() {
mSettings.displayRotationEnabled = enabled;
}
void setSensorsEnabled(boolean enabled) {
recreateVirtualDevice(() -> mSettings.sensorsEnabled = enabled);
}
void setIncludeInRecents(boolean include) {
mSettings.includeInRecents = include;
if (mVirtualDevice != null) {
mVirtualDevice.setDevicePolicy(
POLICY_TYPE_RECENTS, include ? DEVICE_POLICY_DEFAULT : DEVICE_POLICY_CUSTOM);
}
}
void setCrossDeviceClipboardEnabled(boolean enabled) {
mSettings.crossDeviceClipboardEnabled = enabled;
if (mVirtualDevice != null) {
mVirtualDevice.setDevicePolicy(
POLICY_TYPE_CLIPBOARD, enabled ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT);
}
}
void setAlwaysUnlocked(boolean enabled) {
recreateVirtualDevice(() -> mSettings.alwaysUnlocked = enabled);
}
void setDeviceStreaming(boolean enabled) {
recreateVirtualDevice(() -> mSettings.deviceStreaming = enabled);
}
void setRecordEncoderOutput(boolean enabled) {
recreateVirtualDevice(() -> mSettings.recordEncoderOutput = enabled);
}
void setShowPointerIcon(boolean enabled) {
mSettings.showPointerIcon = enabled;
if (mVirtualDevice != null) {
mVirtualDevice.setShowPointerIcon(enabled);
}
}
void setAudioEnabled(boolean enabled) {
mSettings.audioEnabled = enabled;
if (enabled) {
mAudioStreamer.start();
} else {
mAudioStreamer.stop();
}
}
void setCustomHome(boolean enabled) {
recreateVirtualDevice(() -> mSettings.customHome = enabled);
}
private interface DeviceSettingsChange {
void apply();
}
private void recreateVirtualDevice(DeviceSettingsChange settingsChange) {
if (mVirtualDevice != null) { if (mVirtualDevice != null) {
closeVirtualDevice(); closeVirtualDevice();
if (mDeviceCapabilities != null) {
associateAndCreateVirtualDevice();
}
} }
settingsChange.apply(); }
if (mDeviceCapabilities != null) {
associateAndCreateVirtualDevice(); private void updateDevicePolicy(int policyType, boolean custom) {
if (mVirtualDevice != null) {
mVirtualDevice.setDevicePolicy(
policyType, custom ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT);
} }
} }
} }

View File

@@ -27,6 +27,7 @@ rust_defaults {
"libenv_logger", "libenv_logger",
"libglob", "libglob",
"liblog_rust", "liblog_rust",
"libnix",
"libonce_cell", "libonce_cell",
"libregex", "libregex",
"libserde", "libserde",

View File

@@ -1,2 +1,3 @@
fmayle@google.com fmayle@google.com
qwandor@google.com
smoreland@google.com smoreland@google.com

View File

@@ -14,6 +14,7 @@
use super::metadata::WorkspaceMetadata; use super::metadata::WorkspaceMetadata;
use super::{Crate, CrateType, Extern, ExternType}; use super::{Crate, CrateType, Extern, ExternType};
use crate::CargoOutput;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::bail; use anyhow::bail;
use anyhow::Context; use anyhow::Context;
@@ -23,7 +24,6 @@ use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::env; use std::env;
use std::fs::{read_to_string, File};
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@@ -31,16 +31,14 @@ use std::path::PathBuf;
/// the rustc invocations. /// the rustc invocations.
/// ///
/// Ignores crates outside the current directory and build script crates. /// Ignores crates outside the current directory and build script crates.
pub fn parse_cargo_out( pub fn parse_cargo_out(cargo_output: &CargoOutput) -> Result<Vec<Crate>> {
cargo_out_path: impl AsRef<Path>, let metadata = serde_json::from_str(&cargo_output.cargo_metadata)
cargo_metadata_path: impl AsRef<Path>, .context("failed to parse cargo metadata")?;
) -> Result<Vec<Crate>> { parse_cargo_out_str(
let cargo_out = read_to_string(cargo_out_path).context("failed to read cargo.out")?; &cargo_output.cargo_out,
let metadata = serde_json::from_reader( &metadata,
File::open(cargo_metadata_path).context("failed to open cargo.metadata")?, env::current_dir().unwrap().canonicalize().unwrap(),
) )
.context("failed to parse cargo.metadata")?;
parse_cargo_out_str(&cargo_out, &metadata, env::current_dir().unwrap().canonicalize().unwrap())
} }
/// Parses the given `cargo.out` and `cargo.metadata` file contents and generates a list of crates /// Parses the given `cargo.out` and `cargo.metadata` file contents and generates a list of crates

View File

@@ -19,7 +19,6 @@ use crate::config::VariantConfig;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use serde::Deserialize; use serde::Deserialize;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fs::File;
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -100,14 +99,9 @@ pub enum TargetKind {
Test, Test,
} }
pub fn parse_cargo_metadata_file( pub fn parse_cargo_metadata_str(cargo_metadata: &str, cfg: &VariantConfig) -> Result<Vec<Crate>> {
cargo_metadata_path: impl AsRef<Path>, let metadata =
cfg: &VariantConfig, serde_json::from_str(cargo_metadata).context("failed to parse cargo metadata")?;
) -> Result<Vec<Crate>> {
let metadata: WorkspaceMetadata = serde_json::from_reader(
File::open(cargo_metadata_path).context("failed to open cargo.metadata")?,
)
.context("failed to parse cargo.metadata")?;
parse_cargo_metadata(&metadata, &cfg.features, cfg.tests) parse_cargo_metadata(&metadata, &cfg.features, cfg.tests)
} }
@@ -439,7 +433,13 @@ mod tests {
.variants .variants
.iter() .iter()
.map(|variant_cfg| { .map(|variant_cfg| {
parse_cargo_metadata_file(&cargo_metadata_path, variant_cfg).unwrap() parse_cargo_metadata_str(
&read_to_string(&cargo_metadata_path)
.with_context(|| format!("Failed to open {:?}", cargo_metadata_path))
.unwrap(),
variant_cfg,
)
.unwrap()
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(crates, expected_crates); assert_eq!(crates, expected_crates);

View File

@@ -40,20 +40,23 @@ use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use bp::*; use bp::*;
use cargo::{ use cargo::{
cargo_out::parse_cargo_out, metadata::parse_cargo_metadata_file, Crate, CrateType, ExternType, cargo_out::parse_cargo_out, metadata::parse_cargo_metadata_str, Crate, CrateType, ExternType,
}; };
use clap::Parser; use clap::Parser;
use clap::Subcommand; use clap::Subcommand;
use log::debug; use log::debug;
use nix::fcntl::OFlag;
use nix::unistd::pipe2;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::env; use std::env;
use std::fs::{write, File}; use std::fs::{read_to_string, write, File};
use std::io::Write; use std::io::{Read, Write};
use std::os::fd::{FromRawFd, OwnedFd};
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::{Command, Stdio};
// Major TODOs // Major TODOs
// * handle errors, esp. in cargo.out parsing. they should fail the program with an error code // * handle errors, esp. in cargo.out parsing. they should fail the program with an error code
@@ -286,15 +289,22 @@ fn make_crates(args: &Args, cfg: &VariantConfig) -> Result<Vec<Crate>> {
let cargo_out_path = "cargo.out"; let cargo_out_path = "cargo.out";
let cargo_metadata_path = "cargo.metadata"; let cargo_metadata_path = "cargo.metadata";
if !args.reuse_cargo_out || !Path::new(cargo_out_path).exists() { let cargo_output = if args.reuse_cargo_out && Path::new(cargo_out_path).exists() {
generate_cargo_out(cfg, cargo_out_path, cargo_metadata_path) CargoOutput {
.context("generate_cargo_out failed")?; cargo_out: read_to_string(cargo_out_path)?,
} cargo_metadata: read_to_string(cargo_metadata_path)?,
}
} else {
let cargo_output = generate_cargo_out(cfg).context("generate_cargo_out failed")?;
write(cargo_out_path, &cargo_output.cargo_out)?;
write(cargo_metadata_path, &cargo_output.cargo_metadata)?;
cargo_output
};
if cfg.run_cargo { if cfg.run_cargo {
parse_cargo_out(cargo_out_path, cargo_metadata_path).context("parse_cargo_out failed") parse_cargo_out(&cargo_output).context("parse_cargo_out failed")
} else { } else {
parse_cargo_metadata_file(cargo_metadata_path, cfg) parse_cargo_metadata_str(&cargo_output.cargo_metadata, cfg)
} }
} }
@@ -375,32 +385,54 @@ fn write_all_bp(
Ok(()) Ok(())
} }
fn run_cargo(cargo_out: &mut File, cmd: &mut Command) -> Result<()> { /// Runs the given command, and returns its standard output and standard error as a string.
use std::os::unix::io::OwnedFd; fn run_cargo(cmd: &mut Command) -> Result<String> {
use std::process::Stdio; let (pipe_read, pipe_write) = pipe()?;
let fd: OwnedFd = cargo_out.try_clone()?.into(); cmd.stdout(pipe_write.try_clone()?).stderr(pipe_write).stdin(Stdio::null());
debug!("Running: {:?}\n", cmd); debug!("Running: {:?}\n", cmd);
let output = cmd.stdout(Stdio::from(fd.try_clone()?)).stderr(Stdio::from(fd)).output()?; let mut child = cmd.spawn()?;
if !output.status.success() {
bail!("cargo command failed with exit status: {:?}", output.status); // Unset the stdout and stderr for the command so that they are dropped in this process.
// Otherwise the `read_to_string` below will block forever as there is still an open write file
// descriptor for the pipe even after the child finishes.
cmd.stderr(Stdio::null()).stdout(Stdio::null());
let mut output = String::new();
File::from(pipe_read).read_to_string(&mut output)?;
let status = child.wait()?;
if !status.success() {
bail!(
"cargo command `{:?}` failed with exit status: {:?}.\nOutput: \n------\n{}\n------",
cmd,
status,
output
);
} }
Ok(())
Ok(output)
} }
/// Run various cargo commands and save the output to `cargo_out_path`. /// Creates a new pipe and returns the file descriptors for each end of it.
fn generate_cargo_out( fn pipe() -> Result<(OwnedFd, OwnedFd), nix::Error> {
cfg: &VariantConfig, let (a, b) = pipe2(OFlag::O_CLOEXEC)?;
cargo_out_path: &str, // SAFETY: We just created the file descriptors, so we own them.
cargo_metadata_path: &str, unsafe { Ok((OwnedFd::from_raw_fd(a), OwnedFd::from_raw_fd(b))) }
) -> Result<()> { }
let mut cargo_out_file = std::fs::File::create(cargo_out_path)?;
let mut cargo_metadata_file = std::fs::File::create(cargo_metadata_path)?;
/// The raw output from running `cargo metadata`, `cargo build` and other commands.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CargoOutput {
cargo_metadata: String,
cargo_out: String,
}
/// Run various cargo commands and returns the output.
fn generate_cargo_out(cfg: &VariantConfig) -> Result<CargoOutput> {
let verbose_args = ["-v"]; let verbose_args = ["-v"];
let target_dir_args = ["--target-dir", "target.tmp"]; let target_dir_args = ["--target-dir", "target.tmp"];
// cargo clean // cargo clean
run_cargo(&mut cargo_out_file, Command::new("cargo").arg("clean").args(target_dir_args)) run_cargo(Command::new("cargo").arg("clean").args(target_dir_args))
.context("Running cargo clean")?; .context("Running cargo clean")?;
let default_target = "x86_64-unknown-linux-gnu"; let default_target = "x86_64-unknown-linux-gnu";
@@ -428,8 +460,7 @@ fn generate_cargo_out(
}; };
// cargo metadata // cargo metadata
run_cargo( let cargo_metadata = run_cargo(
&mut cargo_metadata_file,
Command::new("cargo") Command::new("cargo")
.arg("metadata") .arg("metadata")
.arg("-q") // don't output warnings to stderr .arg("-q") // don't output warnings to stderr
@@ -439,10 +470,10 @@ fn generate_cargo_out(
) )
.context("Running cargo metadata")?; .context("Running cargo metadata")?;
let mut cargo_out = String::new();
if cfg.run_cargo { if cfg.run_cargo {
// cargo build // cargo build
run_cargo( cargo_out += &run_cargo(
&mut cargo_out_file,
Command::new("cargo") Command::new("cargo")
.args(["build", "--target", default_target]) .args(["build", "--target", default_target])
.args(verbose_args) .args(verbose_args)
@@ -453,8 +484,7 @@ fn generate_cargo_out(
if cfg.tests { if cfg.tests {
// cargo build --tests // cargo build --tests
run_cargo( cargo_out += &run_cargo(
&mut cargo_out_file,
Command::new("cargo") Command::new("cargo")
.args(["build", "--target", default_target, "--tests"]) .args(["build", "--target", default_target, "--tests"])
.args(verbose_args) .args(verbose_args)
@@ -463,8 +493,7 @@ fn generate_cargo_out(
.args(&feature_args), .args(&feature_args),
)?; )?;
// cargo test -- --list // cargo test -- --list
run_cargo( cargo_out += &run_cargo(
&mut cargo_out_file,
Command::new("cargo") Command::new("cargo")
.args(["test", "--target", default_target]) .args(["test", "--target", default_target])
.args(target_dir_args) .args(target_dir_args)
@@ -475,7 +504,7 @@ fn generate_cargo_out(
} }
} }
Ok(()) Ok(CargoOutput { cargo_metadata, cargo_out })
} }
/// Create the Android.bp file for `package_dir`. /// Create the Android.bp file for `package_dir`.