From af3e3026ff4bbab9bd80da05ae46a9297be1f8be Mon Sep 17 00:00:00 2001 From: Vladimir Komsiyski Date: Thu, 19 Dec 2024 04:56:35 -0800 Subject: [PATCH] VDM Settings Demo: go/vdm-settings-demo Bug: 371713473 Bug: 338974320 Test: atest Flag: android.companion.virtualdevice.flags.vdm_settings Change-Id: I4a818b1b31ad59ee3de22105b969aec4c7f4d529 --- Android.bp | 2 + AndroidManifest.xml | 1 + res-product/values/strings.xml | 12 + res/drawable/ic_devices_other.xml | 3 +- res/values/strings.xml | 15 + res/xml/connected_devices_advanced.xml | 7 + res/xml/virtual_device_details_fragment.xml | 51 ++++ ...ancedConnectedDeviceDashboardFragment.java | 2 + .../virtual/ForgetDeviceDialogFragment.java | 95 ++++++ ...VirtualDeviceDetailsButtonsController.java | 72 +++++ ...alDeviceDetailsCompanionAppController.java | 80 +++++ .../VirtualDeviceDetailsFooterController.java | 58 ++++ .../virtual/VirtualDeviceDetailsFragment.java | 65 ++++ .../VirtualDeviceDetailsHeaderController.java | 134 ++++++++ .../virtual/VirtualDeviceListController.java | 182 +++++++++++ .../virtual/VirtualDeviceUpdater.java | 178 +++++++++++ .../virtual/VirtualDeviceWrapper.java | 118 +++++++ .../ForgetDeviceDialogFragmentTest.java | 82 +++++ ...tualDeviceDetailsHeaderControllerTest.java | 166 ++++++++++ .../VirtualDeviceListControllerTest.java | 194 ++++++++++++ .../virtual/VirtualDeviceUpdaterTest.java | 289 ++++++++++++++++++ .../virtual/VirtualDeviceWrapperTest.java | 73 +++++ 22 files changed, 1878 insertions(+), 1 deletion(-) create mode 100644 res/xml/virtual_device_details_fragment.xml create mode 100644 src/com/android/settings/connecteddevice/virtual/ForgetDeviceDialogFragment.java create mode 100644 src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsButtonsController.java create mode 100644 src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsCompanionAppController.java create mode 100644 src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsFooterController.java create mode 100644 src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsFragment.java create mode 100644 src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsHeaderController.java create mode 100644 src/com/android/settings/connecteddevice/virtual/VirtualDeviceListController.java create mode 100644 src/com/android/settings/connecteddevice/virtual/VirtualDeviceUpdater.java create mode 100644 src/com/android/settings/connecteddevice/virtual/VirtualDeviceWrapper.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/virtual/ForgetDeviceDialogFragmentTest.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsHeaderControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceListControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceUpdaterTest.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceWrapperTest.java diff --git a/Android.bp b/Android.bp index 907ff12bc86..64be0ebcae0 100644 --- a/Android.bp +++ b/Android.bp @@ -79,6 +79,8 @@ android_library { "BiometricsSharedLib", "SystemUIUnfoldLib", "WifiTrackerLib", + "android.companion.flags-aconfig-java", + "android.companion.virtualdevice.flags-aconfig-java", "android.hardware.biometrics.flags-aconfig-java", "android.hardware.dumpstate-V1-java", "android.hardware.dumpstate-V1.0-java", diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 0eef2108f6f..26e7e12e3d6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -148,6 +148,7 @@ + Your tablet will no longer be paired with %1$s Your device will no longer be paired with %1$s + + %1$s will no longer be connected to this phone. If you continue, some apps and app streaming may stop working. + + %1$s will no longer be connected to this tablet. If you continue, some apps and app streaming may stop working. + + %1$s will no longer be connected to this device. If you continue, some apps and app streaming may stop working. + + If you forget %1$s, it will no longer be associated to this phone. Some permissions will be removed and some apps and app streaming may stop working. + + If you forget %1$s, it will no longer be associated to this tablet. Some permissions will be removed and some apps and app streaming may stop working. + + If you forget %1$s, it will no longer be associated to this device. Some permissions will be removed and some apps and app streaming may stop working. Allow NFC use only when screen is unlocked diff --git a/res/drawable/ic_devices_other.xml b/res/drawable/ic_devices_other.xml index 3d292649c79..0fe691c9d8d 100644 --- a/res/drawable/ic_devices_other.xml +++ b/res/drawable/ic_devices_other.xml @@ -17,7 +17,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal"> Don\u2019t allow + + Connected + + Disconnected + + Unknown device + + Device details + + Forget %1$s? + + Forget + + Connection managed by + Ultra-Wideband (UWB) diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml index f491055904b..469dfae097b 100644 --- a/res/xml/connected_devices_advanced.xml +++ b/res/xml/connected_devices_advanced.xml @@ -74,6 +74,13 @@ android:summary="@string/summary_placeholder" android:title="@string/print_settings" /> + + + + + + + + + + + + + + + + + + diff --git a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java index 8e230cbb339..8edcd2ebe1d 100644 --- a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java @@ -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 diff --git a/src/com/android/settings/connecteddevice/virtual/ForgetDeviceDialogFragment.java b/src/com/android/settings/connecteddevice/virtual/ForgetDeviceDialogFragment.java new file mode 100644 index 00000000000..d6da223d58b --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/ForgetDeviceDialogFragment.java @@ -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(); + } + } +} diff --git a/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsButtonsController.java b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsButtonsController.java new file mode 100644 index 00000000000..002767b3a20 --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsButtonsController.java @@ -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; + } +} diff --git a/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsCompanionAppController.java b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsCompanionAppController.java new file mode 100644 index 00000000000..ba86ae3e056 --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsCompanionAppController.java @@ -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; + } +} diff --git a/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsFooterController.java b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsFooterController.java new file mode 100644 index 00000000000..ee3ff2baafe --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsFooterController.java @@ -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; + } +} diff --git a/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsFragment.java b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsFragment.java new file mode 100644 index 00000000000..4fa14e71623 --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsFragment.java @@ -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; + } +} diff --git a/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsHeaderController.java b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsHeaderController.java new file mode 100644 index 00000000000..261e20741ac --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsHeaderController.java @@ -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; + } +} diff --git a/src/com/android/settings/connecteddevice/virtual/VirtualDeviceListController.java b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceListController.java new file mode 100644 index 00000000000..81bf2fda6ab --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceListController.java @@ -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 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 rawData) { + if (!isAvailable()) { + return; + } + Collection 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; + } +} diff --git a/src/com/android/settings/connecteddevice/virtual/VirtualDeviceUpdater.java b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceUpdater.java new file mode 100644 index 00000000000..bcdca2d0d9c --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceUpdater.java @@ -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 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 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 loadDevices() { + final Set persistentDeviceIds = mVirtualDeviceManager.getAllPersistentDeviceIds(); + final Set 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 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; + } +} diff --git a/src/com/android/settings/connecteddevice/virtual/VirtualDeviceWrapper.java b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceWrapper.java new file mode 100644 index 00000000000..4b047b6408a --- /dev/null +++ b/src/com/android/settings/connecteddevice/virtual/VirtualDeviceWrapper.java @@ -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 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]; + } + }; +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/virtual/ForgetDeviceDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/virtual/ForgetDeviceDialogFragmentTest.java new file mode 100644 index 00000000000..4dedb55c375 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/virtual/ForgetDeviceDialogFragmentTest.java @@ -0,0 +1,82 @@ +/* + * 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 org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.companion.AssociationInfo; +import android.companion.CompanionDeviceManager; +import android.content.Context; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.androidx.fragment.FragmentController; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowAlertDialogCompat.class}) +public class ForgetDeviceDialogFragmentTest { + + private static final int ASSOCIATION_ID = 42; + + @Mock + private AssociationInfo mAssociationInfo; + @Mock + private CompanionDeviceManager mCompanionDeviceManager; + private AlertDialog mDialog; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID); + VirtualDeviceWrapper device = new VirtualDeviceWrapper( + mAssociationInfo, "PersistentDeviceId", Context.DEVICE_ID_INVALID); + ForgetDeviceDialogFragment fragment = ForgetDeviceDialogFragment.newInstance(device); + FragmentController.setupFragment(fragment, FragmentActivity.class, + 0 /* containerViewId */, null /* bundle */); + fragment.mDevice = device; + fragment.mCompanionDeviceManager = mCompanionDeviceManager; + mDialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + } + + @Test + public void cancelDialog() { + mDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick(); + ShadowLooper.idleMainLooper(); + verify(mCompanionDeviceManager, never()).disassociate(anyInt()); + } + + @Test + public void confirmDialog() { + mDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); + ShadowLooper.idleMainLooper(); + verify(mCompanionDeviceManager).disassociate(ASSOCIATION_ID); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsHeaderControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsHeaderControllerTest.java new file mode 100644 index 00000000000..083dd3f987b --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceDetailsHeaderControllerTest.java @@ -0,0 +1,166 @@ +/* + * 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.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.companion.AssociationInfo; +import android.companion.Flags; +import android.companion.virtual.VirtualDevice; +import android.companion.virtual.VirtualDeviceManager; +import android.content.Context; +import android.graphics.drawable.Icon; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settingslib.widget.LayoutPreference; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowLooper; + +@RunWith(RobolectricTestRunner.class) +public class VirtualDeviceDetailsHeaderControllerTest { + + private static final CharSequence DEVICE_NAME = "Device Name"; + private static final int DEVICE_ID = 42; + private static final String PERSISTENT_DEVICE_ID = "PersistentDeviceId"; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock + VirtualDeviceManager mVirtualDeviceManager; + @Mock + AssociationInfo mAssociationInfo; + @Mock + PreferenceScreen mScreen; + + private VirtualDeviceWrapper mDevice; + private VirtualDeviceDetailsHeaderController mController; + private TextView mTitle; + private ImageView mIcon; + private TextView mSummary; + private Context mContext; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(VirtualDeviceManager.class)) + .thenReturn(mVirtualDeviceManager); + mDevice = new VirtualDeviceWrapper(mAssociationInfo, PERSISTENT_DEVICE_ID, DEVICE_ID); + LayoutPreference headerPreference = new LayoutPreference(mContext, + LayoutInflater.from(mContext).inflate(R.layout.settings_entity_header, null)); + View view = headerPreference.findViewById(R.id.entity_header); + mTitle = view.findViewById(R.id.entity_header_title); + mIcon = headerPreference.findViewById(R.id.entity_header_icon); + mSummary = view.findViewById(R.id.entity_header_summary); + when(mScreen.findPreference(any())).thenReturn(headerPreference); + + mController = new VirtualDeviceDetailsHeaderController(mContext); + mController.init(mDevice); + } + + @Test + public void title_matchesDeviceName() { + when(mAssociationInfo.getDisplayName()).thenReturn(DEVICE_NAME); + + mController.displayPreference(mScreen); + assertThat(mTitle.getText().toString()).isEqualTo(DEVICE_NAME.toString()); + } + + @Test + @DisableFlags(Flags.FLAG_ASSOCIATION_DEVICE_ICON) + public void icon_genericIcon() { + mController.displayPreference(mScreen); + assertThat(Shadows.shadowOf(mIcon.getDrawable()).getCreatedFromResId()) + .isEqualTo(R.drawable.ic_devices_other); + } + + @Test + @EnableFlags(Flags.FLAG_ASSOCIATION_DEVICE_ICON) + public void icon_noAssociationIcon_genericIcon() { + mController.displayPreference(mScreen); + assertThat(Shadows.shadowOf(mIcon.getDrawable()).getCreatedFromResId()) + .isEqualTo(R.drawable.ic_devices_other); + } + + @Test + @EnableFlags(Flags.FLAG_ASSOCIATION_DEVICE_ICON) + public void icon_fromAssociation() { + Icon icon = Icon.createWithResource(mContext, R.drawable.ic_android); + when(mAssociationInfo.getDeviceIcon()).thenReturn(icon); + + mController.displayPreference(mScreen); + assertThat(Shadows.shadowOf(mIcon.getDrawable()).getCreatedFromResId()) + .isEqualTo(R.drawable.ic_android); + } + + @Test + public void summary_activeDevice_changeToInactive() { + mDevice.setDeviceId(DEVICE_ID); + mController.displayPreference(mScreen); + assertThat(mSummary.getText().toString()) + .isEqualTo(mContext.getString(R.string.virtual_device_connected)); + + mController.onVirtualDeviceClosed(DEVICE_ID); + ShadowLooper.idleMainLooper(); + + assertThat(mSummary.getText().toString()) + .isEqualTo(mContext.getString(R.string.virtual_device_disconnected)); + } + + @Test + public void summary_inactiveDevice_changeToActive() { + mDevice.setDeviceId(Context.DEVICE_ID_INVALID); + mController.displayPreference(mScreen); + assertThat(mSummary.getText().toString()) + .isEqualTo(mContext.getString(R.string.virtual_device_disconnected)); + + VirtualDevice virtualDevice = mock(VirtualDevice.class); + when(mDevice.getPersistentDeviceId()).thenReturn(PERSISTENT_DEVICE_ID); + when(mVirtualDeviceManager.getVirtualDevice(DEVICE_ID)).thenReturn(virtualDevice); + when(virtualDevice.getPersistentDeviceId()).thenReturn(PERSISTENT_DEVICE_ID); + + mController.onVirtualDeviceCreated(DEVICE_ID); + ShadowLooper.idleMainLooper(); + + assertThat(mSummary.getText().toString()) + .isEqualTo(mContext.getString(R.string.virtual_device_connected)); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceListControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceListControllerTest.java new file mode 100644 index 00000000000..40668553644 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceListControllerTest.java @@ -0,0 +1,194 @@ +/* + * 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.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.companion.AssociationInfo; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settingslib.search.SearchIndexableRaw; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowLooper; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class VirtualDeviceListControllerTest { + + private static final String PREFERENCE_KEY = "virtual_device_list"; + + private static final CharSequence DEVICE_NAME = "Device Name"; + private static final int DEVICE_ID = 42; + private static final String PERSISTENT_DEVICE_ID = "PersistentDeviceId"; + private static final String DEVICE_PREFERENCE_KEY = PERSISTENT_DEVICE_ID + "_" + DEVICE_NAME; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock + PreferenceManager mPreferenceManager; + @Mock + AssociationInfo mAssociationInfo; + @Mock + PreferenceScreen mScreen; + + private VirtualDeviceWrapper mDevice; + private VirtualDeviceListController mController; + private PreferenceGroup mPreferenceGroup; + private Context mContext; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(ApplicationProvider.getApplicationContext()); + mPreferenceGroup = spy(new PreferenceScreen(mContext, null)); + when(mPreferenceManager.getSharedPreferences()).thenReturn(mock(SharedPreferences.class)); + when(mPreferenceGroup.getPreferenceManager()).thenReturn(mPreferenceManager); + when(mScreen.findPreference(PREFERENCE_KEY)).thenReturn(mPreferenceGroup); + when(mScreen.getContext()).thenReturn(mContext); + mController = new VirtualDeviceListController(mContext, PREFERENCE_KEY); + DashboardFragment fragment = mock(DashboardFragment.class); + when(fragment.getContext()).thenReturn(mContext); + mController.setFragment(fragment); + + when(mAssociationInfo.getDisplayName()).thenReturn(DEVICE_NAME); + mDevice = new VirtualDeviceWrapper(mAssociationInfo, PERSISTENT_DEVICE_ID, DEVICE_ID); + } + + @Test + public void getAvailabilityStatus_vdmDisabled() { + Resources resources = spy(mContext.getResources()); + when(mContext.getResources()).thenReturn(resources); + when(resources.getBoolean(com.android.internal.R.bool.config_enableVirtualDeviceManager)) + .thenReturn(false); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + @DisableFlags(android.companion.virtualdevice.flags.Flags.FLAG_VDM_SETTINGS) + public void getAvailabilityStatus_flagDisabled() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + @EnableFlags(android.companion.virtualdevice.flags.Flags.FLAG_VDM_SETTINGS) + public void getAvailabilityStatus_available() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void onDeviceAdded_createPreference() { + mController.displayPreference(mScreen); + mController.onDeviceAdded(mDevice); + ShadowLooper.idleMainLooper(); + + assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(1); + Preference preference = mPreferenceGroup.findPreference(DEVICE_PREFERENCE_KEY); + assertThat(preference).isNotNull(); + assertThat(preference.getTitle().toString()).isEqualTo(DEVICE_NAME.toString()); + assertThat(Shadows.shadowOf(preference.getIcon()).getCreatedFromResId()) + .isEqualTo(R.drawable.ic_devices_other); + assertThat(preference.getSummary().toString()) + .isEqualTo(mContext.getString(R.string.virtual_device_connected)); + + assertThat(preference).isEqualTo(mController.mPreferences.get(PERSISTENT_DEVICE_ID)); + } + + @Test + public void onDeviceChanged_updateSummary() { + mController.displayPreference(mScreen); + mController.onDeviceAdded(mDevice); + ShadowLooper.idleMainLooper(); + + mDevice.setDeviceId(Context.DEVICE_ID_INVALID); + mController.onDeviceChanged(mDevice); + ShadowLooper.idleMainLooper(); + + assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(1); + Preference preference = mPreferenceGroup.findPreference(DEVICE_PREFERENCE_KEY); + assertThat(preference).isNotNull(); + assertThat(preference.getSummary().toString()) + .isEqualTo(mContext.getString(R.string.virtual_device_disconnected)); + + assertThat(preference).isEqualTo(mController.mPreferences.get(PERSISTENT_DEVICE_ID)); + } + + @Test + public void onDeviceRemoved_removePreference() { + mController.displayPreference(mScreen); + mController.onDeviceAdded(mDevice); + ShadowLooper.idleMainLooper(); + + mDevice.setDeviceId(Context.DEVICE_ID_INVALID); + mController.onDeviceRemoved(mDevice); + ShadowLooper.idleMainLooper(); + + assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(0); + assertThat(mController.mPreferences).isEmpty(); + } + + @Test + public void updateDynamicRawDataToIndex_available() { + assumeTrue(mController.isAvailable()); + + mController.mVirtualDeviceUpdater = mock(VirtualDeviceUpdater.class); + when(mController.mVirtualDeviceUpdater.loadDevices()).thenReturn(List.of(mDevice)); + + ArrayList searchData = new ArrayList<>(); + mController.updateDynamicRawDataToIndex(searchData); + + assertThat(searchData).hasSize(1); + SearchIndexableRaw data = searchData.getFirst(); + assertThat(data.key).isEqualTo(DEVICE_PREFERENCE_KEY); + assertThat(data.title).isEqualTo(DEVICE_NAME.toString()); + assertThat(data.summaryOn) + .isEqualTo(mContext.getString(R.string.connected_device_connections_title)); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceUpdaterTest.java new file mode 100644 index 00000000000..29123c66987 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceUpdaterTest.java @@ -0,0 +1,289 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +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.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.os.UserHandle; +import android.util.ArraySet; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.core.content.pm.ApplicationInfoBuilder; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowPackageManager; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class VirtualDeviceUpdaterTest { + + private static final String PERSISTENT_DEVICE_ID = "companion:42"; + private static final int ASSOCIATION_ID = 42; + private static final int DEVICE_ID = 7; + private static final String PACKAGE_NAME = "test.package.name"; + + @Mock + private VirtualDeviceManager mVirtualDeviceManager; + @Mock + private CompanionDeviceManager mCompanionDeviceManager; + @Mock + private VirtualDeviceUpdater.DeviceListener mDeviceListener; + @Mock + private AssociationInfo mAssociationInfo; + @Mock + private VirtualDevice mVirtualDevice; + private ShadowPackageManager mPackageManager; + + private VirtualDeviceUpdater mVirtualDeviceUpdater; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = spy(ApplicationProvider.getApplicationContext()); + when(context.getSystemService(VirtualDeviceManager.class)) + .thenReturn(mVirtualDeviceManager); + when(context.getSystemService(CompanionDeviceManager.class)) + .thenReturn(mCompanionDeviceManager); + mPackageManager = Shadows.shadowOf(context.getPackageManager()); + mVirtualDeviceUpdater = new VirtualDeviceUpdater(context, mDeviceListener); + } + + @Test + public void loadDevices_noDevices() { + mVirtualDeviceUpdater.loadDevices(); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).isEmpty(); + } + + @Test + public void loadDevices_noAssociationInfo() { + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + + mVirtualDeviceUpdater.loadDevices(); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).isEmpty(); + } + + @Test + public void loadDevices_invalidAssociationId() { + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{"NotACompanionPersistentId"})); + + mVirtualDeviceUpdater.loadDevices(); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).isEmpty(); + } + + @Test + public void loadDevices_noMatchingAssociationId() { + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + when(mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL)) + .thenReturn(List.of(mAssociationInfo)); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID + 1); + + mVirtualDeviceUpdater.loadDevices(); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).isEmpty(); + } + + @Test + public void loadDevices_excludePackageFromSettings() { + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + when(mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL)) + .thenReturn(List.of(mAssociationInfo)); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID); + when(mAssociationInfo.getPackageName()).thenReturn(PACKAGE_NAME); + final ApplicationInfo appInfo = + ApplicationInfoBuilder.newBuilder().setPackageName(PACKAGE_NAME).build(); + final ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = PACKAGE_NAME; + activityInfo.name = PACKAGE_NAME; + activityInfo.applicationInfo = appInfo; + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + final Intent intent = new Intent(IA_SETTINGS_ACTION); + intent.setPackage(PACKAGE_NAME); + mPackageManager.addResolveInfoForIntent(intent, resolveInfo); + + mVirtualDeviceUpdater.loadDevices(); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).isEmpty(); + } + + @Test + public void loadDevices_newDevice_inactive() { + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + when(mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL)) + .thenReturn(List.of(mAssociationInfo)); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID); + when(mAssociationInfo.getPackageName()).thenReturn(PACKAGE_NAME); + + mVirtualDeviceUpdater.loadDevices(); + VirtualDeviceWrapper device = new VirtualDeviceWrapper( + mAssociationInfo, PERSISTENT_DEVICE_ID, Context.DEVICE_ID_INVALID); + verify(mDeviceListener).onDeviceAdded(device); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).containsExactly(PERSISTENT_DEVICE_ID, device); + } + + @Test + public void loadDevices_newDevice_active() { + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + when(mVirtualDeviceManager.getVirtualDevices()) + .thenReturn(List.of(mVirtualDevice)); + when(mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL)) + .thenReturn(List.of(mAssociationInfo)); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID); + when(mAssociationInfo.getPackageName()).thenReturn(PACKAGE_NAME); + when(mVirtualDevice.getDeviceId()).thenReturn(DEVICE_ID); + when(mVirtualDevice.getPersistentDeviceId()).thenReturn(PERSISTENT_DEVICE_ID); + + mVirtualDeviceUpdater.loadDevices(); + VirtualDeviceWrapper device = new VirtualDeviceWrapper( + mAssociationInfo, PERSISTENT_DEVICE_ID, DEVICE_ID); + verify(mDeviceListener).onDeviceAdded(device); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).containsExactly(PERSISTENT_DEVICE_ID, device); + } + + @Test + public void loadDevices_removeDevice() { + VirtualDeviceWrapper device = new VirtualDeviceWrapper( + mAssociationInfo, PERSISTENT_DEVICE_ID, DEVICE_ID); + mVirtualDeviceUpdater.mDevices.put(PERSISTENT_DEVICE_ID, device); + + mVirtualDeviceUpdater.loadDevices(); + verify(mDeviceListener).onDeviceRemoved(device); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).isEmpty(); + } + + @Test + public void loadDevices_noChanges_activeDevice() { + VirtualDeviceWrapper device = new VirtualDeviceWrapper( + mAssociationInfo, PERSISTENT_DEVICE_ID, DEVICE_ID); + mVirtualDeviceUpdater.mDevices.put(PERSISTENT_DEVICE_ID, device); + + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + when(mVirtualDeviceManager.getVirtualDevices()) + .thenReturn(List.of(mVirtualDevice)); + when(mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL)) + .thenReturn(List.of(mAssociationInfo)); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID); + when(mAssociationInfo.getPackageName()).thenReturn(PACKAGE_NAME); + when(mVirtualDevice.getDeviceId()).thenReturn(DEVICE_ID); + when(mVirtualDevice.getPersistentDeviceId()).thenReturn(PERSISTENT_DEVICE_ID); + + mVirtualDeviceUpdater.loadDevices(); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).containsExactly(PERSISTENT_DEVICE_ID, device); + } + + @Test + public void loadDevices_noChanges_inactiveDevice() { + VirtualDeviceWrapper device = new VirtualDeviceWrapper( + mAssociationInfo, PERSISTENT_DEVICE_ID, Context.DEVICE_ID_INVALID); + mVirtualDeviceUpdater.mDevices.put(PERSISTENT_DEVICE_ID, device); + + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + when(mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL)) + .thenReturn(List.of(mAssociationInfo)); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID); + when(mAssociationInfo.getPackageName()).thenReturn(PACKAGE_NAME); + + mVirtualDeviceUpdater.loadDevices(); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).containsExactly(PERSISTENT_DEVICE_ID, device); + } + + @Test + public void loadDevices_deviceChange_activeToInactive() { + VirtualDeviceWrapper device = new VirtualDeviceWrapper( + mAssociationInfo, PERSISTENT_DEVICE_ID, DEVICE_ID); + mVirtualDeviceUpdater.mDevices.put(PERSISTENT_DEVICE_ID, device); + + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + when(mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL)) + .thenReturn(List.of(mAssociationInfo)); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID); + when(mAssociationInfo.getPackageName()).thenReturn(PACKAGE_NAME); + + mVirtualDeviceUpdater.loadDevices(); + + device.setDeviceId(Context.DEVICE_ID_INVALID); + verify(mDeviceListener).onDeviceChanged(device); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).containsExactly(PERSISTENT_DEVICE_ID, device); + } + + @Test + public void loadDevices_deviceChange_inactiveToActive() { + VirtualDeviceWrapper device = new VirtualDeviceWrapper( + mAssociationInfo, PERSISTENT_DEVICE_ID, Context.DEVICE_ID_INVALID); + mVirtualDeviceUpdater.mDevices.put(PERSISTENT_DEVICE_ID, device); + + when(mVirtualDeviceManager.getAllPersistentDeviceIds()) + .thenReturn(new ArraySet<>(new String[]{PERSISTENT_DEVICE_ID})); + when(mVirtualDeviceManager.getVirtualDevices()) + .thenReturn(List.of(mVirtualDevice)); + when(mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL)) + .thenReturn(List.of(mAssociationInfo)); + when(mAssociationInfo.getId()).thenReturn(ASSOCIATION_ID); + when(mAssociationInfo.getPackageName()).thenReturn(PACKAGE_NAME); + when(mVirtualDevice.getDeviceId()).thenReturn(DEVICE_ID); + when(mVirtualDevice.getPersistentDeviceId()).thenReturn(PERSISTENT_DEVICE_ID); + + + mVirtualDeviceUpdater.loadDevices(); + + device.setDeviceId(DEVICE_ID); + verify(mDeviceListener).onDeviceChanged(device); + verifyNoMoreInteractions(mDeviceListener); + assertThat(mVirtualDeviceUpdater.mDevices).containsExactly(PERSISTENT_DEVICE_ID, device); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceWrapperTest.java b/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceWrapperTest.java new file mode 100644 index 00000000000..6ddb7a5044e --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/virtual/VirtualDeviceWrapperTest.java @@ -0,0 +1,73 @@ +/* + * 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.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.companion.AssociationInfo; +import android.content.Context; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class VirtualDeviceWrapperTest { + + private static final String PERSISTENT_DEVICE_ID = "PersistentDeviceIdForTest"; + private static final String DEVICE_NAME = "DEVICE NAME"; + + @Mock + private AssociationInfo mAssociationInfo; + @Mock + private Context mContext; + private VirtualDeviceWrapper mVirtualDeviceWrapper; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mVirtualDeviceWrapper = new VirtualDeviceWrapper(mAssociationInfo, PERSISTENT_DEVICE_ID, + Context.DEVICE_ID_INVALID); + } + + @Test + public void setDeviceId() { + assertThat(mVirtualDeviceWrapper.getDeviceId()).isEqualTo(Context.DEVICE_ID_INVALID); + mVirtualDeviceWrapper.setDeviceId(42); + assertThat(mVirtualDeviceWrapper.getDeviceId()).isEqualTo(42); + } + + @Test + public void getDisplayName_fromAssociationInfo() { + when(mAssociationInfo.getDisplayName()).thenReturn(DEVICE_NAME); + assertThat(mVirtualDeviceWrapper.getDeviceName(mContext).toString()).isEqualTo(DEVICE_NAME); + } + + @Test + public void getDisplayName_fromResources() { + when(mAssociationInfo.getDisplayName()).thenReturn(null); + when(mContext.getString(R.string.virtual_device_unknown)).thenReturn(DEVICE_NAME); + assertThat(mVirtualDeviceWrapper.getDeviceName(mContext).toString()).isEqualTo(DEVICE_NAME); + } +}