VDM Settings
Demo: go/vdm-settings-demo Bug: 371713473 Bug: 338974320 Test: atest Flag: android.companion.virtualdevice.flags.vdm_settings Change-Id: I4a818b1b31ad59ee3de22105b969aec4c7f4d529
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user