Snap for 11220357 from 305b00a16d to 24Q1-release

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

View File

@@ -15,8 +15,11 @@ android_app {
],
static_libs: [
"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",
],

View File

@@ -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,97 +114,148 @@ available devices, build the APKs and install them.
WARNING: If there are other devices in the vicinity with one of these apps
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. Clicking an app icon will create a new virtual display, launch the app there
and start streaming the display contents to the client. The client will show
the surface of that display and render its contents.
1. Long pressing on an app icon will open a dialog to select an existing
display to launch the app on instead of creating a new one.
1. Each display on the Client app has a "Back" and "Close" buttons. When a
display becomes empty, it's automatically removed.
1. Each display on the Client app has a "Rotate" button to switch between
portrait and landscape orientation. This simulates the physical rotation of
the display of the streamed activity. The "Resize" button can be used to
change the display dimensions.
1. Each display on the Client app has a "Fullscreen" button which will move
the contents of that display to an immersive fullscreen activity. The
client's back button/gestures are sent back to the streamed app. Use
Volume Down on the client device to exit fullscreen. Volume Up acts as a
home key, if the streamed display is a home display.
1. The Host app has a "CREATE HOME DISPLAY" button, clicking it will create a
new virtual display, launch the secondary home activity there and start
streaming the display contents to the client. The display on the Client app
will have a home button, clicking it will navigate the streaming experience
back to the home activity.
1. The Host app has a "CREATE MIRROR DISPLAY" button, clicking it will create a
new virtual display, mirror the default host display there and start
streaming the display contents to the client.
1. Check out the different [Host Settings](#host-settings) and
[Client Settings](#client-settings) that allow for changing the behavior of
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. Check out the [Demo apps](#demos) that are specifically meant to showcase
the VDM features.
<!-- LINT.IfChange(host_settings) -->
<!-- LINT.IfChange(host_options) -->
## Host Settings
## Host Options
- **Client Sensors**: Enables sensor injection from the client device into the
host device. Any context that is associated with the virtual device will
access the virtual sensors by default. \
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.
- Clicking an app icon will create a new virtual display, launch the app there
and start streaming the display contents to the client. The client will show
the surface of that display and render its contents.
- Long pressing on an app icon will open a dialog to select an existing
display to launch the app on instead of creating a new one.
- The Host app has a **CREATE HOME DISPLAY** button, clicking it will create a
new virtual display, launch the secondary home activity there and start
streaming the display contents to the client. The display on the Client app
will have a home button, clicking it will navigate the streaming experience
back to the home activity. Run the commands below to enable this
functionality.
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_home true
adb shell am force-stop com.example.android.vdmdemo.host
```
- The Host app has a **CREATE MIRROR DISPLAY** button, clicking it will create
a new virtual display, mirror the default host display there and start
streaming the display contents to the client. Run the commands below to
enable this functionality.
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.consistent_display_flags true
adb shell device_config put virtual_devices android.companion.virtual.flags.interactive_screen_mirror true
adb shell am force-stop com.example.android.vdmdemo.host
```
### Settings
#### General
- **Device profile**: Enables device streaming CDM role as opposed to app
streaming role, with all differences in policies that this entails. \
*Changing this will recreate the virtual device.*
- **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. \
- **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 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:
@@ -213,24 +265,27 @@ available devices, build the APKs and install them.
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
enable this functionality. \
*Changing this will recreate the virtual device.*
```shell
adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_home true
```
<!-- LINT.ThenChange(/samples/VirtualDeviceManager/host/res/menu/settings.xml) -->
<!-- LINT.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) -->

View File

@@ -56,10 +56,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</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" />

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,15 +4,23 @@
<string name="create_home_display" translatable="false">Create Home Display</string>
<string name="create_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>

View File

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

View File

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

View File

@@ -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);

View File

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

View File

@@ -86,7 +86,7 @@ class RemoteDisplay implements AutoCloseable {
private final Context mContext;
private final 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);

View File

@@ -1,46 +0,0 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.vdmdemo.host;
import javax.inject.Inject;
import javax.inject.Singleton;
/** Settings known to the VDM Demo Host application */
@Singleton
final class Settings {
public boolean displayRotationEnabled = true;
public boolean sensorsEnabled = true;
public boolean audioEnabled = true;
public boolean includeInRecents = false;
public boolean crossDeviceClipboardEnabled = false;
public boolean alwaysUnlocked = true;
public boolean deviceStreaming = false;
public boolean showPointerIcon = true;
public boolean customHome = false;
/**
* When enabled, the encoder output of the host will be stored in:
* /sdcard/Download/vdmdemo_encoder_output_[displayId].h264
*
* <p>After pulling this file to your machine this can be played back with:
* {@code ffplay -f h264 vdmdemo_encoder_output_[displayId].h264}
*/
public boolean recordEncoderOutput = false;
@Inject
Settings() {}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.vdmdemo.host;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.preference.PreferenceFragmentCompat;
import dagger.hilt.android.AndroidEntryPoint;
import javax.inject.Inject;
/** VDM Host Settings activity. */
@AndroidEntryPoint(AppCompatActivity.class)
public class SettingsActivity extends Hilt_SettingsActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
Toolbar toolbar = requireViewById(R.id.main_tool_bar);
setSupportActionBar(toolbar);
toolbar.setNavigationOnClickListener(v -> finish());
setTitle(getTitle() + " " + getString(R.string.settings));
}
@AndroidEntryPoint(PreferenceFragmentCompat.class)
public static final class SettingsFragment extends Hilt_SettingsActivity_SettingsFragment {
@Inject PreferenceController mPreferenceController;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.preferences, rootKey);
mPreferenceController.evaluate(getPreferenceManager());
}
}
}

View File

@@ -63,6 +63,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import dagger.hilt.android.AndroidEntryPoint;
import 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;
if (mVirtualDevice != null) {
mVirtualDevice.setDevicePolicy(
POLICY_TYPE_RECENTS, include ? DEVICE_POLICY_DEFAULT : DEVICE_POLICY_CUSTOM);
}
}
void setCrossDeviceClipboardEnabled(boolean enabled) {
mSettings.crossDeviceClipboardEnabled = enabled;
if (mVirtualDevice != null) {
mVirtualDevice.setDevicePolicy(
POLICY_TYPE_CLIPBOARD, enabled ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT);
}
}
void setAlwaysUnlocked(boolean enabled) {
recreateVirtualDevice(() -> mSettings.alwaysUnlocked = enabled);
}
void setDeviceStreaming(boolean enabled) {
recreateVirtualDevice(() -> mSettings.deviceStreaming = enabled);
}
void setRecordEncoderOutput(boolean enabled) {
recreateVirtualDevice(() -> mSettings.recordEncoderOutput = enabled);
}
void setShowPointerIcon(boolean enabled) {
mSettings.showPointerIcon = enabled;
if (mVirtualDevice != null) {
mVirtualDevice.setShowPointerIcon(enabled);
}
}
void setAudioEnabled(boolean enabled) {
mSettings.audioEnabled = enabled;
if (enabled) {
mAudioStreamer.start();
} else {
mAudioStreamer.stop();
}
}
void setCustomHome(boolean enabled) {
recreateVirtualDevice(() -> mSettings.customHome = enabled);
}
private interface DeviceSettingsChange {
void apply();
}
private void recreateVirtualDevice(DeviceSettingsChange settingsChange) {
private void recreateVirtualDevice() {
if (mVirtualDevice != null) {
closeVirtualDevice();
}
settingsChange.apply();
if (mDeviceCapabilities != null) {
associateAndCreateVirtualDevice();
}
}
}
private void updateDevicePolicy(int policyType, boolean custom) {
if (mVirtualDevice != null) {
mVirtualDevice.setDevicePolicy(
policyType, custom ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT);
}
}
}

View File

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

View File

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

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}
Ok(())
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
);
}
/// 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)?;
Ok(output)
}
/// 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`.