From ccc76819a262523a85a769173a845ebdd2f746f3 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Wed, 22 Nov 2023 16:26:53 +0000 Subject: Pass cargo output around as string rather than writing to file. This lets us show the output on failure, and is a step towards reducing the number of temporary files. Bug: 311373500 Test: Ran on csv crate with errors added Change-Id: Ib25786440bb63415bfd8d5658e6b23389e9064ea --- tools/cargo_embargo/Android.bp | 1 + tools/cargo_embargo/src/cargo/cargo_out.rs | 18 +++-- tools/cargo_embargo/src/cargo/metadata.rs | 20 +++--- tools/cargo_embargo/src/main.rs | 101 +++++++++++++++++++---------- 4 files changed, 84 insertions(+), 56 deletions(-) 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/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, - cargo_metadata_path: impl AsRef, -) -> Result> { - 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> { + 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, - cfg: &VariantConfig, -) -> Result> { - 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> { + 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::>(); 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> { 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 { + 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 { 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`. -- cgit v1.2.3 From a04c6465b3fc606b6f14d748bd887362a724f6a1 Mon Sep 17 00:00:00 2001 From: Frederick Mayle Date: Wed, 13 Dec 2023 12:17:04 -0800 Subject: cargo_embargo: add qwandor@ to OWNERS He is the main author at this point. Test: n/a Change-Id: I73f0519493bb1bb93ca8d2cff1ac25ff5d4fa8ed --- tools/cargo_embargo/OWNERS | 1 + 1 file changed, 1 insertion(+) 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 -- cgit v1.2.3 From 7fd26bc759ce9d9886cf9d59d3a59a5bac86ac4f Mon Sep 17 00:00:00 2001 From: Vladimir Komsiyski Date: Wed, 13 Dec 2023 10:37:36 +0100 Subject: VDM Host dedicated settings activity. Improved settings handling that can allow for easy disabling of features based on feature flags or SDK version Fix: 316093453 Bug: 314429442 Test: manual Change-Id: Ib530e81b106e2c25aa5e2f29e92b4bdcf4a3c29d --- samples/VirtualDeviceManager/Android.bp | 3 + samples/VirtualDeviceManager/README.md | 178 +++++++++------ .../VirtualDeviceManager/host/AndroidManifest.xml | 5 +- .../host/com.example.android.vdmdemo.host.xml | 3 - .../host/res/layout/activity_settings.xml | 24 +++ .../VirtualDeviceManager/host/res/menu/options.xml | 11 + .../host/res/menu/settings.xml | 37 ---- .../host/res/values/arrays.xml | 11 + .../host/res/values/strings.xml | 27 ++- .../host/res/xml/preferences.xml | 96 +++++++++ .../example/android/vdmdemo/host/MainActivity.java | 78 +------ .../android/vdmdemo/host/PreferenceController.java | 238 +++++++++++++++++++++ .../android/vdmdemo/host/RemoteDisplay.java | 13 +- .../com/example/android/vdmdemo/host/Settings.java | 46 ---- .../android/vdmdemo/host/SettingsActivity.java | 55 +++++ .../example/android/vdmdemo/host/VdmService.java | 117 ++++------ 16 files changed, 628 insertions(+), 314 deletions(-) create mode 100644 samples/VirtualDeviceManager/host/res/layout/activity_settings.xml create mode 100644 samples/VirtualDeviceManager/host/res/menu/options.xml delete mode 100644 samples/VirtualDeviceManager/host/res/menu/settings.xml create mode 100644 samples/VirtualDeviceManager/host/res/values/arrays.xml create mode 100644 samples/VirtualDeviceManager/host/res/xml/preferences.xml create mode 100644 samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java delete mode 100644 samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/Settings.java create mode 100644 samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java 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..b2bd0056d 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. - ``` + ```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,167 @@ 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. + -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 -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. +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. The Host app has a "CREATE HOME DISPLAY" button, clicking it will create a +- 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. + 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 + ``` - +### 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_.h264 - ffplay -f h264 vdmdemo_encoder_output_.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 ``` - +#### 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_.h264 + ffplay -f h264 vdmdemo_encoder_output_.h264 + ``` + + ## 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 +288,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. - + ## Demos @@ -277,4 +321,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. - + 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 @@ -56,10 +56,13 @@ + 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 @@ - - - \ 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 @@ + + + + + + + + \ 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 @@ + + + + + + \ 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 @@ - - - - - - - - - - - - - - - - - diff --git a/samples/VirtualDeviceManager/host/res/values/arrays.xml b/samples/VirtualDeviceManager/host/res/values/arrays.xml new file mode 100644 index 000000000..eed6fa18d --- /dev/null +++ b/samples/VirtualDeviceManager/host/res/values/arrays.xml @@ -0,0 +1,11 @@ + + + + App streaming + Nearby device streaming + + + @string/app_streaming + @string/nearby_device_streaming + + \ 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..3d9533dfe 100644 --- a/samples/VirtualDeviceManager/host/res/values/strings.xml +++ b/samples/VirtualDeviceManager/host/res/values/strings.xml @@ -4,15 +4,22 @@ Create Home Display Create Mirror Display Application Icon + Settings - Client audio - Client sensors - Include streamed apps in recents - Cross-device clipboard - Display rotation - Always unlocked - Device streaming profile - Record encoder output - Show pointer icon - Custom home + device_profile + enable_recents + enable_cross_device_clipboard + enable_client_sensors + enable_client_audio + enable_display_rotation + always_unlocked_device + show_pointer_icon + enable_custom_home + record_encoder_output + + enable_home_displays + enable_mirror_displays + + android.app.role.COMPANION_DEVICE_APP_STREAMING + android.app.role.COMPANION_DEVICE_NEARBY_DEVICE_STREAMING \ 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..9151bc86a --- /dev/null +++ b/samples/VirtualDeviceManager/host/res/xml/preferences.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..a6f4f931e --- /dev/null +++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java @@ -0,0 +1,238 @@ +/* + * 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. + * + *

Upon creation, it will automatically update the preference values based on the current SDK + * version and the relevant feature flags.

+ */ +@Singleton +final class PreferenceController { + + // LINT.IfChange + private static final Set> 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), + + // 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>> 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> preferenceObserver) { + ArrayMap> 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. + * + *

This doesn't change any of the preference values, only disables the relevant UI elements + * in the preference screen.

+ */ + 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)); + } + + private void onPreferencesChanged(SharedPreferences sharedPreferences, String key) { + Map currentPreferences = sharedPreferences.getAll(); + for (Map> observer : mObservers.values()) { + Consumer consumer = observer.get(key); + if (consumer != null) { + consumer.accept(currentPreferences.get(key)); + } + } + } + + private abstract static class PrefRule { + 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 withDefaultValue(T defaultValue) { + mDefaultValue = defaultValue; + return this; + } + } + + private static class BoolRule extends PrefRule { + 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 { + 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..a235f64af 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 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) { @@ -164,9 +164,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 - * - *

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..cae06f7e5 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,7 @@ import com.google.common.util.concurrent.MoreExecutors; import dagger.hilt.android.AndroidEntryPoint; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -96,7 +97,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 +197,38 @@ 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_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 +249,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 +266,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 +327,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 +375,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 +470,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); } } } -- cgit v1.2.3 From 44e5e00b413cf42fc2609b4f97f8a4de0456ce01 Mon Sep 17 00:00:00 2001 From: Vladimir Komsiyski Date: Thu, 14 Dec 2023 11:15:19 +0100 Subject: Add IME display policy support to VDM Demo Showcasing how the new preference infra can be used. Bug: 316292189 Test: manual Change-Id: Ief76d4968c3827758fc7e094cefcb80a44aa872d --- samples/VirtualDeviceManager/README.md | 11 +++++++++++ samples/VirtualDeviceManager/host/res/values/arrays.xml | 12 ++++++++++++ samples/VirtualDeviceManager/host/res/values/strings.xml | 1 + samples/VirtualDeviceManager/host/res/xml/preferences.xml | 14 ++++++++++++++ .../example/android/vdmdemo/host/DisplayRepository.java | 8 ++++++++ .../example/android/vdmdemo/host/PreferenceController.java | 7 +++++++ .../com/example/android/vdmdemo/host/RemoteDisplay.java | 3 +++ .../src/com/example/android/vdmdemo/host/VdmService.java | 10 ++++++++++ 8 files changed, 66 insertions(+) diff --git a/samples/VirtualDeviceManager/README.md b/samples/VirtualDeviceManager/README.md index b2bd0056d..7eb8fac87 100644 --- a/samples/VirtualDeviceManager/README.md +++ b/samples/VirtualDeviceManager/README.md @@ -243,6 +243,17 @@ show a launcher-like list of installed apps on the host device. 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 diff --git a/samples/VirtualDeviceManager/host/res/values/arrays.xml b/samples/VirtualDeviceManager/host/res/values/arrays.xml index eed6fa18d..3a9fcd3e5 100644 --- a/samples/VirtualDeviceManager/host/res/values/arrays.xml +++ b/samples/VirtualDeviceManager/host/res/values/arrays.xml @@ -8,4 +8,16 @@ @string/app_streaming @string/nearby_device_streaming + + + Show IME on the remote display + Show IME on the default display + Do not show IME + + + + 0 + 1 + 2 + \ 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 3d9533dfe..e2001eec3 100644 --- a/samples/VirtualDeviceManager/host/res/values/strings.xml +++ b/samples/VirtualDeviceManager/host/res/values/strings.xml @@ -15,6 +15,7 @@ always_unlocked_device show_pointer_icon enable_custom_home + display_ime_policy record_encoder_output enable_home_displays diff --git a/samples/VirtualDeviceManager/host/res/xml/preferences.xml b/samples/VirtualDeviceManager/host/res/xml/preferences.xml index 9151bc86a..9f7f7a82d 100644 --- a/samples/VirtualDeviceManager/host/res/xml/preferences.xml +++ b/samples/VirtualDeviceManager/host/res/xml/preferences.xml @@ -73,6 +73,20 @@ app:iconSpaceReserved="false" /> + + + + currentPreferences = sharedPreferences.getAll(); for (Map> observer : mObservers.values()) { 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 a235f64af..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 @@ -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() 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 cae06f7e5..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,7 @@ 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; @@ -218,6 +219,15 @@ public final class VdmService extends Hilt_VdmService { 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(), -- cgit v1.2.3