summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-15 00:20:53 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-15 00:20:53 +0000
commitf10f21bf6c8e63fc258523e849e9ea75898fe3cc (patch)
treedcd9fee2e12b5b5abcd7065a7e3fac27f2b200e7
parent530ece053a543ba4213e3ef46d81bbbc07a06ce9 (diff)
parent305b00a16de2b77d51d6daa68a7e2a8cb86c94c3 (diff)
downloaddevelopment-android14-qpr2-release.tar.gz
Change-Id: If8d4e64ef66ae1e5914f5479dabcda08c2fe09c8
-rw-r--r--samples/VirtualDeviceManager/Android.bp3
-rw-r--r--samples/VirtualDeviceManager/README.md189
-rw-r--r--samples/VirtualDeviceManager/host/AndroidManifest.xml5
-rw-r--r--samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml3
-rw-r--r--samples/VirtualDeviceManager/host/res/layout/activity_settings.xml24
-rw-r--r--samples/VirtualDeviceManager/host/res/menu/options.xml11
-rw-r--r--samples/VirtualDeviceManager/host/res/menu/settings.xml37
-rw-r--r--samples/VirtualDeviceManager/host/res/values/arrays.xml23
-rw-r--r--samples/VirtualDeviceManager/host/res/values/strings.xml28
-rw-r--r--samples/VirtualDeviceManager/host/res/xml/preferences.xml110
-rw-r--r--samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayRepository.java8
-rw-r--r--samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java78
-rw-r--r--samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java245
-rw-r--r--samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java16
-rw-r--r--samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/Settings.java46
-rw-r--r--samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java55
-rw-r--r--samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java127
-rw-r--r--tools/cargo_embargo/Android.bp1
-rw-r--r--tools/cargo_embargo/OWNERS1
-rw-r--r--tools/cargo_embargo/src/cargo/cargo_out.rs18
-rw-r--r--tools/cargo_embargo/src/cargo/metadata.rs20
-rw-r--r--tools/cargo_embargo/src/main.rs101
22 files changed, 779 insertions, 370 deletions
diff --git a/samples/VirtualDeviceManager/Android.bp b/samples/VirtualDeviceManager/Android.bp
index 763820d6a..e7689b848 100644
--- a/samples/VirtualDeviceManager/Android.bp
+++ b/samples/VirtualDeviceManager/Android.bp
@@ -15,8 +15,11 @@ android_app {
],
static_libs: [
"VdmCommonLib",
+ "android.companion.virtual.flags-aconfig-java",
+ "android.companion.virtualdevice.flags-aconfig-java",
"androidx.annotation_annotation",
"androidx.appcompat_appcompat",
+ "androidx.preference_preference",
"guava",
"hilt_android",
],
diff --git a/samples/VirtualDeviceManager/README.md b/samples/VirtualDeviceManager/README.md
index 1b43de14c..7eb8fac87 100644
--- a/samples/VirtualDeviceManager/README.md
+++ b/samples/VirtualDeviceManager/README.md
@@ -6,8 +6,8 @@
[Prerequisites](#prerequisites) \
[Build & Install](#build-and-install) \
[Run](#run) \
-[Host Settings](#host-settings) \
-[Client Settings](#client-settings) \
+[Host Options](#host-options) \
+[Client Options](#client-options) \
[Demos](#demos)
## Overview
@@ -71,14 +71,14 @@ available devices, build the APKs and install them.
1. Build the Host app.
- ```
+ ```shell
m -j VdmHost
```
1. Install the application as a system app on the host device.
<!-- 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 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
@@ -88,18 +88,19 @@ available devices, build the APKs and install them.
```
**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`
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
```
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
```
@@ -113,124 +114,178 @@ available devices, build the APKs and install them.
WARNING: If there are other devices in the vicinity with one of these apps
running, they might interfere.
-1. Once the connection switches to high bandwidth medium, the Host app will
- show a launcher-like list of installed apps on the host device.
+1. Check out the different [Host Options](#host-options) and
+ [Client Options](#client-options) that allow for changing the behavior of
+ the streamed apps and the virtual device in general.
-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. Check out the [Demo apps](#demos) that are specifically meant to showcase
+ the VDM features.
-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.
+<!-- LINT.IfChange(host_options) -->
-1. Each display on the Client app has a "Back" and "Close" buttons. When a
- display becomes empty, it's automatically removed.
+## Host Options
-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.
+NOTE: Any flag changes require device reboot or "Force stop" of the host app
+because the flag values are cached and evaluated only when the host app is
+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.
-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.
+- 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. The Host app has a "CREATE HOME DISPLAY" button, clicking it will create a
+- 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.
+ back to the home activity. Run the commands below to enable this
+ functionality.
-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.
+ ```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
+ ```
-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 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.
-1. Check out the [Demo apps](#demos) that are specifically meant to showcase
- the VDM features.
+ ```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
+ ```
-<!-- LINT.IfChange(host_settings) -->
+### Settings
-## Host Settings
+#### General
-- **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. \
+- **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.*
-- **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
- 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. \
*This can be changed dynamically.*
```shell
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
- and the virtual device. If disabled, both devices will have their own
- isolated clipboards. Run the command below to enable this functionality. \
+- **Enable cross-device clipboard**: Whether to share the clipboard between
+ the host and the virtual device. If disabled, both devices will have their
+ own isolated clipboards. 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.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
should trigger orientation change of the relevant display. The client will
automatically rotate the relevant display upon such request. Disabling this
simulates a fixed orientation display that cannot physically rotate. Then
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
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
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. \
*Changing this will recreate the virtual device.*
-- **Device streaming 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.*
-
-- **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
- debugging Encoding related issues. To download and play the file locally:
-
- ```shell
- adb pull /sdcard/Download/vdmdemo_encoder_output_<displayId>.h264
- ffplay -f h264 vdmdemo_encoder_output_<displayId>.h264
- ```
-
- **Show pointer icon**: Whether pointer icon should be shown for virtual
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
+ or use the device-default secondary home activity. Run the commands 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
+ 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
+ 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:
+
+ ```shell
+ adb pull /sdcard/Download/vdmdemo_encoder_output_<displayId>.h264
+ ffplay -f h264 vdmdemo_encoder_output_<displayId>.h264
```
-<!-- LINT.ThenChange(/samples/VirtualDeviceManager/host/res/menu/settings.xml) -->
+<!-- LINT.ThenChange(README.md) -->
<!-- LINT.IfChange(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
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
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) -->
## 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
streamed activities are ignored.
-<!-- LINT.ThenChange(/samples/VirtualDeviceManager/demos/AndroidManifest.xml) -->
+<!-- LINT.ThenChange(README.md) -->
diff --git a/samples/VirtualDeviceManager/host/AndroidManifest.xml b/samples/VirtualDeviceManager/host/AndroidManifest.xml
index b9361a462..9edf07d9e 100644
--- a/samples/VirtualDeviceManager/host/AndroidManifest.xml
+++ b/samples/VirtualDeviceManager/host/AndroidManifest.xml
@@ -57,9 +57,12 @@
</intent-filter>
</activity>
<activity
+ android:name=".SettingsActivity"
+ android:exported="true"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
+ <activity
android:name=".CustomLauncherActivity"
android:exported="true"
- android:label="@string/custom_home"
android:launchMode="singleTop"
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" />
diff --git a/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml b/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml
index 43062ae5e..a2ac85a30 100644
--- a/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml
+++ b/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml
@@ -17,13 +17,10 @@
<permissions>
<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.QUERY_AUDIO_STATE" />
<permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
<permission name="android.permission.REQUEST_COMPANION_PROFILE_APP_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>
</permissions> \ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/layout/activity_settings.xml b/samples/VirtualDeviceManager/host/res/layout/activity_settings.xml
new file mode 100644
index 000000000..ab08c819b
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/layout/activity_settings.xml
@@ -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> \ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/menu/options.xml b/samples/VirtualDeviceManager/host/res/menu/options.xml
new file mode 100644
index 000000000..499ab4ddf
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/menu/options.xml
@@ -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) --> \ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/menu/settings.xml b/samples/VirtualDeviceManager/host/res/menu/settings.xml
deleted file mode 100644
index 698376f9a..000000000
--- a/samples/VirtualDeviceManager/host/res/menu/settings.xml
+++ /dev/null
@@ -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) -->
diff --git a/samples/VirtualDeviceManager/host/res/values/arrays.xml b/samples/VirtualDeviceManager/host/res/values/arrays.xml
new file mode 100644
index 000000000..3a9fcd3e5
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/values/arrays.xml
@@ -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> \ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/values/strings.xml b/samples/VirtualDeviceManager/host/res/values/strings.xml
index d8966ff36..e2001eec3 100644
--- a/samples/VirtualDeviceManager/host/res/values/strings.xml
+++ b/samples/VirtualDeviceManager/host/res/values/strings.xml
@@ -4,15 +4,23 @@
<string name="create_home_display" translatable="false">Create Home 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="settings" translatable="false">Settings</string>
- <string name="enable_audio" translatable="false">Client audio</string>
- <string name="enable_sensors" translatable="false">Client sensors</string>
- <string name="enable_recents" translatable="false">Include streamed apps in recents</string>
- <string name="enable_clipboard" translatable="false">Cross-device clipboard</string>
- <string name="enable_rotation" translatable="false">Display rotation</string>
- <string name="always_unlocked" translatable="false">Always unlocked</string>
- <string name="use_device_streaming" translatable="false">Device streaming profile</string>
- <string name="record_encoder_output" translatable="false">Record encoder output</string>
- <string name="show_pointer_icon" translatable="false">Show pointer icon</string>
- <string name="custom_home" translatable="false">Custom home</string>
+ <string name="pref_device_profile" translatable="false">device_profile</string>
+ <string name="pref_enable_recents" translatable="false">enable_recents</string>
+ <string name="pref_enable_cross_device_clipboard" translatable="false">enable_cross_device_clipboard</string>
+ <string name="pref_enable_client_sensors" translatable="false">enable_client_sensors</string>
+ <string name="pref_enable_client_audio" translatable="false">enable_client_audio</string>
+ <string name="pref_enable_display_rotation" translatable="false">enable_display_rotation</string>
+ <string name="pref_always_unlocked_device" translatable="false">always_unlocked_device</string>
+ <string name="pref_show_pointer_icon" translatable="false">show_pointer_icon</string>
+ <string name="pref_enable_custom_home" translatable="false">enable_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> \ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/xml/preferences.xml b/samples/VirtualDeviceManager/host/res/xml/preferences.xml
new file mode 100644
index 000000000..9f7f7a82d
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/xml/preferences.xml
@@ -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) -->
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayRepository.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayRepository.java
index 5bd57459a..bbe0c5fe2 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayRepository.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayRepository.java
@@ -69,6 +69,14 @@ final class DisplayRepository {
}
}
+ int[] getDisplayIds() {
+ synchronized (mDisplayRepository) {
+ return mDisplayRepository.stream()
+ .mapToInt(RemoteDisplay::getDisplayId)
+ .toArray();
+ }
+ }
+
int[] getRemoteDisplayIds() {
synchronized (mDisplayRepository) {
return mDisplayRepository.stream()
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java
index f594e743e..b397100d7 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java
@@ -84,7 +84,7 @@ public class MainActivity extends Hilt_MainActivity {
};
@Inject ConnectionManager mConnectionManager;
- @Inject Settings mSettings;
+ @Inject PreferenceController mPreferenceController;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -92,11 +92,15 @@ public class MainActivity extends Hilt_MainActivity {
setContentView(R.layout.activity_main);
Toolbar toolbar = requireViewById(R.id.main_tool_bar);
- toolbar.setOverflowIcon(getDrawable(R.drawable.settings));
setSupportActionBar(toolbar);
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.setEnabled(
+ mPreferenceController.getBoolean(R.string.internal_pref_enable_mirror_displays));
+
mLauncher = requireViewById(R.id.app_grid);
mLauncher.setVisibility(View.GONE);
LauncherAdapter launcherAdapter = new LauncherAdapter(getPackageManager());
@@ -189,79 +193,15 @@ public class MainActivity extends Hilt_MainActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.settings, 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;
- }
- }
+ inflater.inflate(R.menu.options, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- item.setChecked(!item.isChecked());
-
switch (item.getItemId()) {
- case R.id.enable_sensors:
- mVdmService.setSensorsEnabled(item.isChecked());
- 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());
+ case R.id.settings:
+ startActivity(new Intent(this, SettingsActivity.class));
return true;
default:
return super.onOptionsItemSelected(item);
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java
new file mode 100644
index 000000000..13194d3fb
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java
@@ -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);
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
index 1e805f1cf..12fcdcec1 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
@@ -86,7 +86,7 @@ class RemoteDisplay implements AutoCloseable {
private final Context mContext;
private final RemoteIo mRemoteIo;
- private final Settings mSettings;
+ private final PreferenceController mPreferenceController;
private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
private final VirtualDisplay mVirtualDisplay;
private final VirtualDpad mDpad;
@@ -113,19 +113,19 @@ class RemoteDisplay implements AutoCloseable {
VirtualDevice virtualDevice,
RemoteIo remoteIo,
@DisplayType int displayType,
- Settings settings) {
+ PreferenceController preferenceController) {
mContext = context;
mRemoteIo = remoteIo;
mRemoteDisplayId = event.getDisplayId();
mVirtualDevice = virtualDevice;
mPendingIntentExecutor = context.getMainExecutor();
mDisplayType = displayType;
- mSettings = settings;
+ mPreferenceController = preferenceController;
setCapabilities(event.getDisplayCapabilities());
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;
}
if (mDisplayType == DISPLAY_TYPE_MIRROR) {
@@ -142,6 +142,9 @@ class RemoteDisplay implements AutoCloseable {
/* executor= */ Runnable::run,
/* callback= */ null);
+ mVirtualDevice.setDisplayImePolicy(
+ getDisplayId(), mPreferenceController.getInt(R.string.pref_display_ime_policy));
+
mDpad =
virtualDevice.createVirtualDpad(
new VirtualDpadConfig.Builder()
@@ -164,9 +167,8 @@ class RemoteDisplay implements AutoCloseable {
if (mVideoManager != null) {
mVideoManager.stop();
}
- mVideoManager =
- VideoManager.createEncoder(
- mRemoteDisplayId, mRemoteIo, mSettings.recordEncoderOutput);
+ mVideoManager = VideoManager.createEncoder(mRemoteDisplayId, mRemoteIo,
+ mPreferenceController.getBoolean(R.string.pref_record_encoder_output));
Surface surface = mVideoManager.createInputSurface(mWidth, mHeight, DISPLAY_FPS);
mVirtualDisplay.setSurface(surface);
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/Settings.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/Settings.java
deleted file mode 100644
index 0eaebc59a..000000000
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/Settings.java
+++ /dev/null
@@ -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() {}
-}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java
new file mode 100644
index 000000000..3cc1aaf62
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java
@@ -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());
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java
index 53c6d1d70..622a2a2f1 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java
@@ -63,6 +63,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import dagger.hilt.android.AndroidEntryPoint;
+import java.util.Arrays;
+import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
@@ -96,7 +98,7 @@ public final class VdmService extends Hilt_VdmService {
@Inject ConnectionManager mConnectionManager;
@Inject RemoteIo mRemoteIo;
@Inject AudioStreamer mAudioStreamer;
- @Inject Settings mSettings;
+ @Inject PreferenceController mPreferenceController;
@Inject DisplayRepository mDisplayRepository;
private RemoteSensorManager mRemoteSensorManager = null;
@@ -196,14 +198,47 @@ public final class VdmService extends Hilt_VdmService {
mRemoteIo.addMessageConsumer(mRemoteEventConsumer);
- if (mSettings.audioEnabled) {
+ if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) {
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
public void onDestroy() {
super.onDestroy();
+ mPreferenceController.removePreferenceObserver(this);
mConnectionManager.removeConnectionCallback(mConnectionCallback);
closeVirtualDevice();
mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
@@ -224,7 +259,7 @@ public final class VdmService extends Hilt_VdmService {
mVirtualDevice,
mRemoteIo,
mPendingDisplayType,
- mSettings);
+ mPreferenceController);
mDisplayRepository.addDisplay(remoteDisplay);
mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP;
if (mPendingRemoteIntent != null) {
@@ -241,10 +276,7 @@ public final class VdmService extends Hilt_VdmService {
CompanionDeviceManager cdm =
Objects.requireNonNull(getSystemService(CompanionDeviceManager.class));
RoleManager rm = Objects.requireNonNull(getSystemService(RoleManager.class));
- final String deviceProfile =
- mSettings.deviceStreaming
- ? AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING
- : AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
+ final String deviceProfile = mPreferenceController.getString(R.string.pref_device_profile);
for (AssociationInfo associationInfo : cdm.getMyAssociations()) {
// Flashing the device clears the role and the permissions, but not the CDM
// associations.
@@ -305,24 +337,24 @@ public final class VdmService extends Hilt_VdmService {
.setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM)
.setAudioPlaybackSessionId(mAudioStreamer.getPlaybackSessionId());
- if (mSettings.alwaysUnlocked) {
+ if (mPreferenceController.getBoolean(R.string.pref_always_unlocked_device)) {
virtualDeviceBuilder.setLockState(LOCK_STATE_ALWAYS_UNLOCKED);
}
- if (mSettings.customHome) {
+ if (mPreferenceController.getBoolean(R.string.pref_enable_custom_home)) {
virtualDeviceBuilder.setHomeComponent(
new ComponentName(this, CustomLauncherActivity.class));
}
- if (!mSettings.includeInRecents) {
+ if (!mPreferenceController.getBoolean(R.string.pref_enable_recents)) {
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);
}
- if (mSettings.sensorsEnabled) {
+ if (mPreferenceController.getBoolean(R.string.pref_enable_client_sensors)) {
for (SensorCapabilities sensor : mDeviceCapabilities.getSensorCapabilitiesList()) {
virtualDeviceBuilder.addVirtualSensorConfig(
new VirtualSensorConfig.Builder(
@@ -353,7 +385,8 @@ public final class VdmService extends Hilt_VdmService {
mRemoteSensorManager.setVirtualSensors(mVirtualDevice.getVirtualSensorList());
}
- mVirtualDevice.setShowPointerIcon(mSettings.showPointerIcon);
+ mVirtualDevice.setShowPointerIcon(
+ mPreferenceController.getBoolean(R.string.pref_show_pointer_icon));
mVirtualDevice.addActivityListener(
MoreExecutors.directExecutor(),
@@ -447,73 +480,19 @@ public final class VdmService extends Hilt_VdmService {
.ifPresent(d -> d.launchIntent(pendingIntent));
}
- void setDisplayRotationEnabled(boolean enabled) {
- mSettings.displayRotationEnabled = enabled;
- }
-
- void setSensorsEnabled(boolean enabled) {
- recreateVirtualDevice(() -> mSettings.sensorsEnabled = enabled);
- }
-
- void setIncludeInRecents(boolean include) {
- mSettings.includeInRecents = include;
+ private void recreateVirtualDevice() {
if (mVirtualDevice != null) {
- mVirtualDevice.setDevicePolicy(
- POLICY_TYPE_RECENTS, include ? DEVICE_POLICY_DEFAULT : DEVICE_POLICY_CUSTOM);
+ closeVirtualDevice();
+ if (mDeviceCapabilities != null) {
+ associateAndCreateVirtualDevice();
+ }
}
}
- void setCrossDeviceClipboardEnabled(boolean enabled) {
- mSettings.crossDeviceClipboardEnabled = enabled;
+ private void updateDevicePolicy(int policyType, boolean custom) {
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) {
- closeVirtualDevice();
- }
- settingsChange.apply();
- if (mDeviceCapabilities != null) {
- associateAndCreateVirtualDevice();
+ policyType, custom ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT);
}
}
}
diff --git a/tools/cargo_embargo/Android.bp b/tools/cargo_embargo/Android.bp
index e88297a6b..dd53b8b81 100644
--- a/tools/cargo_embargo/Android.bp
+++ b/tools/cargo_embargo/Android.bp
@@ -27,6 +27,7 @@ rust_defaults {
"libenv_logger",
"libglob",
"liblog_rust",
+ "libnix",
"libonce_cell",
"libregex",
"libserde",
diff --git a/tools/cargo_embargo/OWNERS b/tools/cargo_embargo/OWNERS
index 204e0264f..be44c2397 100644
--- a/tools/cargo_embargo/OWNERS
+++ b/tools/cargo_embargo/OWNERS
@@ -1,2 +1,3 @@
fmayle@google.com
+qwandor@google.com
smoreland@google.com
diff --git a/tools/cargo_embargo/src/cargo/cargo_out.rs b/tools/cargo_embargo/src/cargo/cargo_out.rs
index 1064d0b2b..a30623899 100644
--- a/tools/cargo_embargo/src/cargo/cargo_out.rs
+++ b/tools/cargo_embargo/src/cargo/cargo_out.rs
@@ -14,6 +14,7 @@
use super::metadata::WorkspaceMetadata;
use super::{Crate, CrateType, Extern, ExternType};
+use crate::CargoOutput;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
@@ -23,7 +24,6 @@ use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::BTreeMap;
use std::env;
-use std::fs::{read_to_string, File};
use std::path::Path;
use std::path::PathBuf;
@@ -31,16 +31,14 @@ use std::path::PathBuf;
/// the rustc invocations.
///
/// Ignores crates outside the current directory and build script crates.
-pub fn parse_cargo_out(
- cargo_out_path: impl AsRef<Path>,
- cargo_metadata_path: impl AsRef<Path>,
-) -> Result<Vec<Crate>> {
- let cargo_out = read_to_string(cargo_out_path).context("failed to read cargo.out")?;
- let metadata = serde_json::from_reader(
- File::open(cargo_metadata_path).context("failed to open cargo.metadata")?,
+pub fn parse_cargo_out(cargo_output: &CargoOutput) -> Result<Vec<Crate>> {
+ let metadata = serde_json::from_str(&cargo_output.cargo_metadata)
+ .context("failed to parse cargo metadata")?;
+ parse_cargo_out_str(
+ &cargo_output.cargo_out,
+ &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
diff --git a/tools/cargo_embargo/src/cargo/metadata.rs b/tools/cargo_embargo/src/cargo/metadata.rs
index d9a0c33e5..5c7315d19 100644
--- a/tools/cargo_embargo/src/cargo/metadata.rs
+++ b/tools/cargo_embargo/src/cargo/metadata.rs
@@ -19,7 +19,6 @@ use crate::config::VariantConfig;
use anyhow::{anyhow, bail, Context, Result};
use serde::Deserialize;
use std::collections::BTreeMap;
-use std::fs::File;
use std::ops::Deref;
use std::path::{Path, PathBuf};
@@ -100,14 +99,9 @@ pub enum TargetKind {
Test,
}
-pub fn parse_cargo_metadata_file(
- cargo_metadata_path: impl AsRef<Path>,
- cfg: &VariantConfig,
-) -> 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")?;
+pub fn parse_cargo_metadata_str(cargo_metadata: &str, cfg: &VariantConfig) -> Result<Vec<Crate>> {
+ let metadata =
+ serde_json::from_str(cargo_metadata).context("failed to parse cargo metadata")?;
parse_cargo_metadata(&metadata, &cfg.features, cfg.tests)
}
@@ -439,7 +433,13 @@ mod tests {
.variants
.iter()
.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<_>>();
assert_eq!(crates, expected_crates);
diff --git a/tools/cargo_embargo/src/main.rs b/tools/cargo_embargo/src/main.rs
index 14425150b..ec40d24a6 100644
--- a/tools/cargo_embargo/src/main.rs
+++ b/tools/cargo_embargo/src/main.rs
@@ -40,20 +40,23 @@ use anyhow::Context;
use anyhow::Result;
use bp::*;
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::Subcommand;
use log::debug;
+use nix::fcntl::OFlag;
+use nix::unistd::pipe2;
use once_cell::sync::Lazy;
use std::collections::BTreeMap;
use std::collections::VecDeque;
use std::env;
-use std::fs::{write, File};
-use std::io::Write;
+use std::fs::{read_to_string, write, File};
+use std::io::{Read, Write};
+use std::os::fd::{FromRawFd, OwnedFd};
use std::path::Path;
use std::path::PathBuf;
-use std::process::Command;
+use std::process::{Command, Stdio};
// Major TODOs
// * 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_metadata_path = "cargo.metadata";
- if !args.reuse_cargo_out || !Path::new(cargo_out_path).exists() {
- generate_cargo_out(cfg, cargo_out_path, cargo_metadata_path)
- .context("generate_cargo_out failed")?;
- }
+ let cargo_output = if args.reuse_cargo_out && Path::new(cargo_out_path).exists() {
+ CargoOutput {
+ 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 {
- 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 {
- 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(())
}
-fn run_cargo(cargo_out: &mut File, cmd: &mut Command) -> Result<()> {
- use std::os::unix::io::OwnedFd;
- use std::process::Stdio;
- let fd: OwnedFd = cargo_out.try_clone()?.into();
+/// Runs the given command, and returns its standard output and standard error as a string.
+fn run_cargo(cmd: &mut Command) -> Result<String> {
+ let (pipe_read, pipe_write) = pipe()?;
+ cmd.stdout(pipe_write.try_clone()?).stderr(pipe_write).stdin(Stdio::null());
debug!("Running: {:?}\n", cmd);
- let output = cmd.stdout(Stdio::from(fd.try_clone()?)).stderr(Stdio::from(fd)).output()?;
- if !output.status.success() {
- bail!("cargo command failed with exit status: {:?}", output.status);
+ let mut child = cmd.spawn()?;
+
+ // 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`.
-fn generate_cargo_out(
- cfg: &VariantConfig,
- cargo_out_path: &str,
- cargo_metadata_path: &str,
-) -> 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)?;
+/// Creates a new pipe and returns the file descriptors for each end of it.
+fn pipe() -> Result<(OwnedFd, OwnedFd), nix::Error> {
+ let (a, b) = pipe2(OFlag::O_CLOEXEC)?;
+ // SAFETY: We just created the file descriptors, so we own them.
+ unsafe { Ok((OwnedFd::from_raw_fd(a), OwnedFd::from_raw_fd(b))) }
+}
+/// 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 target_dir_args = ["--target-dir", "target.tmp"];
// 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")?;
let default_target = "x86_64-unknown-linux-gnu";
@@ -428,8 +460,7 @@ fn generate_cargo_out(
};
// cargo metadata
- run_cargo(
- &mut cargo_metadata_file,
+ let cargo_metadata = run_cargo(
Command::new("cargo")
.arg("metadata")
.arg("-q") // don't output warnings to stderr
@@ -439,10 +470,10 @@ fn generate_cargo_out(
)
.context("Running cargo metadata")?;
+ let mut cargo_out = String::new();
if cfg.run_cargo {
// cargo build
- run_cargo(
- &mut cargo_out_file,
+ cargo_out += &run_cargo(
Command::new("cargo")
.args(["build", "--target", default_target])
.args(verbose_args)
@@ -453,8 +484,7 @@ fn generate_cargo_out(
if cfg.tests {
// cargo build --tests
- run_cargo(
- &mut cargo_out_file,
+ cargo_out += &run_cargo(
Command::new("cargo")
.args(["build", "--target", default_target, "--tests"])
.args(verbose_args)
@@ -463,8 +493,7 @@ fn generate_cargo_out(
.args(&feature_args),
)?;
// cargo test -- --list
- run_cargo(
- &mut cargo_out_file,
+ cargo_out += &run_cargo(
Command::new("cargo")
.args(["test", "--target", default_target])
.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`.