Merge "VDM Settings" into main

This commit is contained in:
Vladimir Komsiyski
2024-12-20 23:46:44 -08:00
committed by Android (Google) Code Review
22 changed files with 1878 additions and 1 deletions

View File

@@ -20,6 +20,7 @@ import android.content.Context;
import android.provider.SearchIndexableResource;
import com.android.settings.R;
import com.android.settings.connecteddevice.virtual.VirtualDeviceListController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.print.PrintSettingPreferenceController;
import com.android.settings.search.BaseSearchIndexProvider;
@@ -74,6 +75,7 @@ public class AdvancedConnectedDeviceDashboardFragment extends DashboardFragment
getSettingsLifecycle().addObserver(uwbPreferenceController);
}
}
use(VirtualDeviceListController.class).setFragment(this);
}
@Override

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import android.app.Activity;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
/**
* Implements an AlertDialog for confirming that a user wishes to unpair or "forget" a paired
* device.
*/
public class ForgetDeviceDialogFragment extends InstrumentedDialogFragment {
public static final String TAG = ForgetDeviceDialogFragment.class.getSimpleName();
private static final String DEVICE_ARG = "virtual_device_arg";
@VisibleForTesting
CompanionDeviceManager mCompanionDeviceManager;
@VisibleForTesting
VirtualDeviceWrapper mDevice;
static ForgetDeviceDialogFragment newInstance(VirtualDeviceWrapper device) {
Bundle args = new Bundle(1);
args.putParcelable(DEVICE_ARG, device);
ForgetDeviceDialogFragment dialog = new ForgetDeviceDialogFragment();
dialog.setArguments(args);
return dialog;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_VIRTUAL_DEVICE_FORGET;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class);
mDevice = getArguments().getParcelable(DEVICE_ARG, VirtualDeviceWrapper.class);
}
@Override
@NonNull
public Dialog onCreateDialog(@Nullable Bundle inState) {
Context context = getContext();
CharSequence deviceName = mDevice.getDeviceName(context);
AlertDialog dialog = new AlertDialog.Builder(context)
.setPositiveButton(R.string.virtual_device_forget_dialog_confirm_button,
this::onForgetButtonClick)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setTitle(
context.getString(R.string.virtual_device_forget_dialog_title, deviceName));
dialog.setMessage(
context.getString(R.string.virtual_device_forget_dialog_body, deviceName));
return dialog;
}
private void onForgetButtonClick(DialogInterface dialog, int which) {
mCompanionDeviceManager.disassociate(mDevice.getAssociationInfo().getId());
Activity activity = getActivity();
if (activity != null) {
activity.finish();
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.ActionButtonsPreference;
import java.util.Objects;
/** This class adds one button to "forget" (ie unpair) the device. */
public class VirtualDeviceDetailsButtonsController extends BasePreferenceController {
private static final String KEY_VIRTUAL_DEVICE_ACTION_BUTTONS = "virtual_device_action_buttons";
@Nullable
private PreferenceFragmentCompat mFragment;
@Nullable
private VirtualDeviceWrapper mDevice;
public VirtualDeviceDetailsButtonsController(@NonNull Context context) {
super(context, KEY_VIRTUAL_DEVICE_ACTION_BUTTONS);
}
/** One-time initialization when the controller is first created. */
void init(@NonNull PreferenceFragmentCompat fragment, @NonNull VirtualDeviceWrapper device) {
mFragment = fragment;
mDevice = device;
}
private void onForgetButtonPressed() {
ForgetDeviceDialogFragment fragment =
ForgetDeviceDialogFragment.newInstance(Objects.requireNonNull(mDevice));
fragment.show(Objects.requireNonNull(mFragment).getParentFragmentManager(),
ForgetDeviceDialogFragment.TAG);
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
((ActionButtonsPreference) screen.findPreference(getPreferenceKey()))
.setButton1Text(R.string.forget)
.setButton1Icon(R.drawable.ic_settings_delete)
.setButton1OnClickListener((view) -> onForgetButtonPressed())
.setButton1Enabled(true);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import static com.android.settings.spa.app.appinfo.AppInfoSettingsProvider.startAppInfoSettings;
import android.app.Application;
import android.content.Context;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.Utils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.widget.AppPreference;
import java.util.Objects;
/** This class adds the details about the virtual device companion app. */
public class VirtualDeviceDetailsCompanionAppController extends BasePreferenceController {
private static final String KEY_VIRTUAL_DEVICE_COMPANION_APP = "virtual_device_companion_app";
@Nullable
private PreferenceFragmentCompat mFragment;
@Nullable
private String mPackageName;
public VirtualDeviceDetailsCompanionAppController(@NonNull Context context) {
super(context, KEY_VIRTUAL_DEVICE_COMPANION_APP);
}
/** One-time initialization when the controller is first created. */
void init(@NonNull PreferenceFragmentCompat fragment, @NonNull String packageName) {
mFragment = fragment;
mPackageName = packageName;
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
ApplicationsState applicationsState = ApplicationsState.getInstance(
(Application) mContext.getApplicationContext());
final ApplicationsState.AppEntry appEntry = applicationsState.getEntry(
mPackageName, UserHandle.myUserId());
final AppPreference preference = screen.findPreference(getPreferenceKey());
preference.setTitle(appEntry.label);
preference.setIcon(Utils.getBadgedIcon(mContext, appEntry.info));
preference.setOnPreferenceClickListener(pref -> {
startAppInfoSettings(Objects.requireNonNull(mPackageName), appEntry.info.uid,
Objects.requireNonNull(mFragment), /* request= */ 1001,
getMetricsCategory());
return true;
});
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
/** Adds footer text on the virtual device details page. */
public class VirtualDeviceDetailsFooterController extends BasePreferenceController {
private static final String KEY_VIRTUAL_DEVICE_FOOTER = "virtual_device_details_footer";
@Nullable
private CharSequence mDeviceName;
public VirtualDeviceDetailsFooterController(@NonNull Context context) {
super(context, KEY_VIRTUAL_DEVICE_FOOTER);
}
/** One-time initialization when the controller is first created. */
void init(@NonNull CharSequence deviceName) {
mDeviceName = deviceName;
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
Preference preference = screen.findPreference(getPreferenceKey());
preference.setTitle(mContext.getString(R.string.virtual_device_details_footer_title,
mDeviceName));
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import android.app.settings.SettingsEnums;
import android.content.Context;
import androidx.annotation.NonNull;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
/**
* Dedicated screen displaying the information for a single virtual device to the user and allowing
* them to manage that device.
*/
public class VirtualDeviceDetailsFragment extends DashboardFragment {
private static final String TAG = VirtualDeviceDetailsFragment.class.getSimpleName();
static final String DEVICE_ARG = "virtual_device_arg";
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
VirtualDeviceWrapper device =
getArguments().getParcelable(DEVICE_ARG, VirtualDeviceWrapper.class);
use(VirtualDeviceDetailsHeaderController.class).init(device);
use(VirtualDeviceDetailsButtonsController.class).init(this, device);
use(VirtualDeviceDetailsCompanionAppController.class)
.init(this, device.getAssociationInfo().getPackageName());
use(VirtualDeviceDetailsFooterController.class).init(device.getDeviceName(context));
}
@Override
public int getMetricsCategory() {
return SettingsEnums.VIRTUAL_DEVICE_DETAILS;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.virtual_device_details_fragment;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import android.companion.virtual.VirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
import android.content.Context;
import android.graphics.drawable.Icon;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.LayoutPreference;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/** This class adds a header for a virtual device with a heading and icon. */
public class VirtualDeviceDetailsHeaderController extends BasePreferenceController implements
LifecycleObserver, VirtualDeviceManager.VirtualDeviceListener {
private static final String KEY_VIRTUAL_DEVICE_DETAILS_HEADER = "virtual_device_details_header";
@Nullable
private final VirtualDeviceManager mVirtualDeviceManager;
@Nullable
private VirtualDeviceWrapper mDevice;
@Nullable
private TextView mSummaryView;
private final Executor mExecutor = Executors.newSingleThreadExecutor();
public VirtualDeviceDetailsHeaderController(@NonNull Context context) {
super(context, KEY_VIRTUAL_DEVICE_DETAILS_HEADER);
mVirtualDeviceManager =
Objects.requireNonNull(context.getSystemService(VirtualDeviceManager.class));
}
/** One-time initialization when the controller is first created. */
void init(@NonNull VirtualDeviceWrapper device) {
mDevice = device;
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void onStart() {
if (mVirtualDeviceManager != null) {
mVirtualDeviceManager.registerVirtualDeviceListener(mExecutor, this);
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void onStop() {
if (mVirtualDeviceManager != null) {
mVirtualDeviceManager.unregisterVirtualDeviceListener(this);
}
}
@Override
public void onVirtualDeviceCreated(int deviceId) {
VirtualDevice device =
Objects.requireNonNull(mVirtualDeviceManager).getVirtualDevice(deviceId);
if (mDevice != null && device != null
&& mDevice.getPersistentDeviceId().equals(device.getPersistentDeviceId())) {
mDevice.setDeviceId(deviceId);
mContext.getMainExecutor().execute(this::updateSummary);
}
}
@Override
public void onVirtualDeviceClosed(int deviceId) {
if (mDevice != null && deviceId == mDevice.getDeviceId()) {
mDevice.setDeviceId(Context.DEVICE_ID_INVALID);
mContext.getMainExecutor().execute(this::updateSummary);
}
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
LayoutPreference headerPreference = screen.findPreference(getPreferenceKey());
View view = headerPreference.findViewById(R.id.entity_header);
TextView titleView = view.findViewById(R.id.entity_header_title);
ImageView iconView = headerPreference.findViewById(R.id.entity_header_icon);
mSummaryView = view.findViewById(R.id.entity_header_summary);
updateSummary();
if (mDevice != null) {
titleView.setText(mDevice.getDeviceName(mContext));
Icon deviceIcon = android.companion.Flags.associationDeviceIcon()
? mDevice.getAssociationInfo().getDeviceIcon() : null;
if (deviceIcon == null) {
iconView.setImageResource(R.drawable.ic_devices_other);
} else {
iconView.setImageIcon(deviceIcon);
}
}
iconView.setContentDescription("Icon for device");
}
private void updateSummary() {
if (mSummaryView != null && mDevice != null) {
mSummaryView.setText(mDevice.getDeviceId() != Context.DEVICE_ID_INVALID
? R.string.virtual_device_connected : R.string.virtual_device_disconnected);
}
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import android.content.Context;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.util.ArrayMap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.search.SearchIndexableRaw;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/** Displays the list of all virtual devices. */
public class VirtualDeviceListController extends BasePreferenceController
implements LifecycleObserver, VirtualDeviceUpdater.DeviceListener {
private final MetricsFeatureProvider mMetricsFeatureProvider;
@VisibleForTesting
VirtualDeviceUpdater mVirtualDeviceUpdater;
@VisibleForTesting
ArrayMap<String, Preference> mPreferences = new ArrayMap<>();
@Nullable
private PreferenceGroup mPreferenceGroup;
@Nullable
private DashboardFragment mFragment;
public VirtualDeviceListController(@NonNull Context context, @NonNull String preferenceKey) {
super(context, preferenceKey);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
mVirtualDeviceUpdater = new VirtualDeviceUpdater(context, this);
}
public void setFragment(@NonNull DashboardFragment fragment) {
mFragment = fragment;
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void onStart() {
if (isAvailable()) {
mVirtualDeviceUpdater.registerListener();
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void onStop() {
if (isAvailable()) {
mVirtualDeviceUpdater.unregisterListener();
}
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceGroup = screen.findPreference(getPreferenceKey());
if (isAvailable()) {
mVirtualDeviceUpdater.loadDevices();
}
}
@Override
public void onDeviceAdded(@NonNull VirtualDeviceWrapper device) {
Preference preference = new Preference(mContext);
CharSequence deviceName = device.getDeviceName(mContext);
preference.setTitle(deviceName);
preference.setKey(device.getPersistentDeviceId() + "_" + deviceName);
final CharSequence title = preference.getTitle();
Icon deviceIcon = android.companion.Flags.associationDeviceIcon()
? device.getAssociationInfo().getDeviceIcon() : null;
if (deviceIcon == null) {
preference.setIcon(R.drawable.ic_devices_other);
} else {
preference.setIcon(deviceIcon.loadDrawable(mContext));
}
if (device.getDeviceId() != Context.DEVICE_ID_INVALID) {
preference.setSummary(R.string.virtual_device_connected);
} else {
preference.setSummary(R.string.virtual_device_disconnected);
}
preference.setOnPreferenceClickListener((Preference p) -> {
mMetricsFeatureProvider.logClickedPreference(p, getMetricsCategory());
final Bundle args = new Bundle();
args.putParcelable(VirtualDeviceDetailsFragment.DEVICE_ARG, device);
if (mFragment != null) {
new SubSettingLauncher(mFragment.getContext())
.setDestination(VirtualDeviceDetailsFragment.class.getName())
.setTitleText(title)
.setArguments(args)
.setSourceMetricsCategory(getMetricsCategory())
.launch();
}
return true;
});
mPreferences.put(device.getPersistentDeviceId(), preference);
if (mPreferenceGroup != null) {
mContext.getMainExecutor().execute(() ->
Objects.requireNonNull(mPreferenceGroup).addPreference(preference));
}
}
@Override
public void onDeviceRemoved(@NonNull VirtualDeviceWrapper device) {
Preference preference = mPreferences.remove(device.getPersistentDeviceId());
if (mPreferenceGroup != null) {
mContext.getMainExecutor().execute(() ->
Objects.requireNonNull(mPreferenceGroup).removePreference(preference));
}
}
@Override
public void onDeviceChanged(@NonNull VirtualDeviceWrapper device) {
Preference preference = mPreferences.get(device.getPersistentDeviceId());
if (preference != null) {
int summaryResId = device.getDeviceId() != Context.DEVICE_ID_INVALID
? R.string.virtual_device_connected : R.string.virtual_device_disconnected;
mContext.getMainExecutor().execute(() ->
Objects.requireNonNull(preference).setSummary(summaryResId));
}
}
@Override
public void updateDynamicRawDataToIndex(@NonNull List<SearchIndexableRaw> rawData) {
if (!isAvailable()) {
return;
}
Collection<VirtualDeviceWrapper> devices = mVirtualDeviceUpdater.loadDevices();
for (VirtualDeviceWrapper device : devices) {
SearchIndexableRaw data = new SearchIndexableRaw(mContext);
String deviceName = device.getDeviceName(mContext).toString();
data.key = device.getPersistentDeviceId() + "_" + deviceName;
data.title = deviceName;
data.summaryOn = mContext.getString(R.string.connected_device_connections_title);
rawData.add(data);
}
}
@Override
public int getAvailabilityStatus() {
if (!mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableVirtualDeviceManager)) {
return UNSUPPORTED_ON_DEVICE;
}
if (!android.companion.virtualdevice.flags.Flags.vdmSettings()) {
return CONDITIONALLY_UNAVAILABLE;
}
return AVAILABLE;
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import static com.android.settingslib.drawer.TileUtils.IA_SETTINGS_ACTION;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceManager;
import android.companion.virtual.VirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.google.common.collect.ImmutableSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/** Maintains a collection of all virtual devices and propagates any changes to its listener. */
class VirtualDeviceUpdater implements VirtualDeviceManager.VirtualDeviceListener {
private static final String CDM_PERSISTENT_DEVICE_ID_PREFIX = "companion:";
// TODO(b/384400670): Detect these packages via PackageManager instead of hardcoding them.
private static final ImmutableSet<String> IGNORED_PACKAGES =
ImmutableSet.of("com.google.ambient.streaming");
private final VirtualDeviceManager mVirtualDeviceManager;
private final CompanionDeviceManager mCompanionDeviceManager;
private final PackageManager mPackageManager;
private final DeviceListener mDeviceListener;
private final Executor mBackgroundExecutor = Executors.newSingleThreadExecutor();
// Up-to-date list of active and inactive devices, keyed by persistent device id.
@VisibleForTesting
ArrayMap<String, VirtualDeviceWrapper> mDevices = new ArrayMap<>();
interface DeviceListener {
void onDeviceAdded(@NonNull VirtualDeviceWrapper device);
void onDeviceRemoved(@NonNull VirtualDeviceWrapper device);
void onDeviceChanged(@NonNull VirtualDeviceWrapper device);
}
VirtualDeviceUpdater(Context context, DeviceListener deviceListener) {
mVirtualDeviceManager = context.getSystemService(VirtualDeviceManager.class);
mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class);
mPackageManager = context.getPackageManager();
mDeviceListener = deviceListener;
}
void registerListener() {
mVirtualDeviceManager.registerVirtualDeviceListener(mBackgroundExecutor, this);
mBackgroundExecutor.execute(this::loadDevices);
}
void unregisterListener() {
mVirtualDeviceManager.unregisterVirtualDeviceListener(this);
}
@Override
public void onVirtualDeviceCreated(int deviceId) {
loadDevices();
}
@Override
public void onVirtualDeviceClosed(int deviceId) {
loadDevices();
}
Collection<VirtualDeviceWrapper> loadDevices() {
final Set<String> persistentDeviceIds = mVirtualDeviceManager.getAllPersistentDeviceIds();
final Set<String> deviceIdsToRemove = new ArraySet<>();
for (String persistentDeviceId : mDevices.keySet()) {
if (!persistentDeviceIds.contains(persistentDeviceId)) {
deviceIdsToRemove.add(persistentDeviceId);
}
}
for (String persistentDeviceId : deviceIdsToRemove) {
mDeviceListener.onDeviceRemoved(mDevices.remove(persistentDeviceId));
}
if (!persistentDeviceIds.isEmpty()) {
for (VirtualDevice device : mVirtualDeviceManager.getVirtualDevices()) {
String persistentDeviceId = device.getPersistentDeviceId();
persistentDeviceIds.remove(persistentDeviceId);
addOrUpdateDevice(persistentDeviceId, device.getDeviceId());
}
}
for (String persistentDeviceId : persistentDeviceIds) {
addOrUpdateDevice(persistentDeviceId, Context.DEVICE_ID_INVALID);
}
return mDevices.values();
}
private void addOrUpdateDevice(String persistentDeviceId, int deviceId) {
VirtualDeviceWrapper device = mDevices.get(persistentDeviceId);
if (device == null) {
AssociationInfo associationInfo = getAssociationInfo(persistentDeviceId);
if (associationInfo == null) {
return;
}
device = new VirtualDeviceWrapper(associationInfo, persistentDeviceId, deviceId);
mDevices.put(persistentDeviceId, device);
mDeviceListener.onDeviceAdded(device);
}
if (device.getDeviceId() != deviceId) {
device.setDeviceId(deviceId);
mDeviceListener.onDeviceChanged(device);
}
}
@Nullable
private AssociationInfo getAssociationInfo(String persistentDeviceId) {
if (persistentDeviceId == null) {
return null;
}
VirtualDeviceWrapper device = mDevices.get(persistentDeviceId);
if (device != null) {
return device.getAssociationInfo();
}
if (!persistentDeviceId.startsWith(CDM_PERSISTENT_DEVICE_ID_PREFIX)) {
return null;
}
final int associationId = Integer.parseInt(
persistentDeviceId.replaceFirst(CDM_PERSISTENT_DEVICE_ID_PREFIX, ""));
final List<AssociationInfo> associations =
mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL);
final AssociationInfo associationInfo = associations.stream()
.filter(a -> a.getId() == associationId)
.findFirst()
.orElse(null);
if (associationInfo == null) {
return null;
}
if (shouldExcludePackageFromSettings(associationInfo.getPackageName())) {
return null;
}
return associationInfo;
}
// Some packages already inject custom settings entries that allow the users to manage the
// virtual devices and the companion associations, so they should be ignored from the generic
// settings page.
private boolean shouldExcludePackageFromSettings(String packageName) {
if (packageName == null || IGNORED_PACKAGES.contains(packageName)) {
return true;
}
final Intent intent = new Intent(IA_SETTINGS_ACTION);
intent.setPackage(packageName);
return intent.resolveActivity(mPackageManager) != null;
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) 2024 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.android.settings.connecteddevice.virtual;
import android.companion.AssociationInfo;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import java.util.Objects;
/** Parcelable representing a virtual device along with its association properties. */
class VirtualDeviceWrapper implements Parcelable {
/** The CDM Association for this device. */
@NonNull
private final AssociationInfo mAssociationInfo;
/** The unique VDM identifier for the device, persisted even when the device is inactive. */
@NonNull
private final String mPersistentDeviceId;
/** The identifier for the device if it's active, Context.DEVICE_ID_INVALID otherwise. */
private int mDeviceId;
VirtualDeviceWrapper(@NonNull AssociationInfo associationInfo,
@NonNull String persistentDeviceId, int deviceId) {
mAssociationInfo = associationInfo;
mPersistentDeviceId = persistentDeviceId;
mDeviceId = deviceId;
}
@NonNull
AssociationInfo getAssociationInfo() {
return mAssociationInfo;
}
@NonNull
String getPersistentDeviceId() {
return mPersistentDeviceId;
}
@NonNull
CharSequence getDeviceName(Context context) {
return mAssociationInfo.getDisplayName() != null
? mAssociationInfo.getDisplayName()
: context.getString(R.string.virtual_device_unknown);
}
int getDeviceId() {
return mDeviceId;
}
void setDeviceId(int deviceId) {
mDeviceId = deviceId;
}
private VirtualDeviceWrapper(Parcel in) {
mAssociationInfo = in.readTypedObject(AssociationInfo.CREATOR);
mPersistentDeviceId = in.readString8();
mDeviceId = in.readInt();
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeTypedObject(mAssociationInfo, flags);
dest.writeString8(mPersistentDeviceId);
dest.writeInt(mDeviceId);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (!(o instanceof VirtualDeviceWrapper that)) return false;
return Objects.equals(mAssociationInfo, that.mAssociationInfo)
&& Objects.equals(mPersistentDeviceId, that.mPersistentDeviceId)
&& mDeviceId == that.mDeviceId;
}
@Override
public int hashCode() {
return Objects.hash(mAssociationInfo, mPersistentDeviceId, mDeviceId);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<VirtualDeviceWrapper> CREATOR =
new Parcelable.Creator<>() {
@NonNull
public VirtualDeviceWrapper createFromParcel(@NonNull Parcel in) {
return new VirtualDeviceWrapper(in);
}
@NonNull
public VirtualDeviceWrapper[] newArray(int size) {
return new VirtualDeviceWrapper[size];
}
};
}