Merge "VDM Settings" into main

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

View File

@@ -79,6 +79,8 @@ android_library {
"BiometricsSharedLib", "BiometricsSharedLib",
"SystemUIUnfoldLib", "SystemUIUnfoldLib",
"WifiTrackerLib", "WifiTrackerLib",
"android.companion.flags-aconfig-java",
"android.companion.virtualdevice.flags-aconfig-java",
"android.hardware.biometrics.flags-aconfig-java", "android.hardware.biometrics.flags-aconfig-java",
"android.hardware.dumpstate-V1-java", "android.hardware.dumpstate-V1-java",
"android.hardware.dumpstate-V1.0-java", "android.hardware.dumpstate-V1.0-java",

View File

@@ -148,6 +148,7 @@
<uses-permission android:name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> <uses-permission android:name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" />
<uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" /> <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" />
<uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" /> <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
<uses-permission android:name="android.permission.MANAGE_COMPANION_DEVICES" />
<application <application
android:name=".SettingsApplication" android:name=".SettingsApplication"

View File

@@ -457,6 +457,18 @@
<string name="bluetooth_unpair_dialog_body" product="tablet">Your tablet will no longer be paired with <xliff:g id="device_name">%1$s</xliff:g></string> <string name="bluetooth_unpair_dialog_body" product="tablet">Your tablet will no longer be paired with <xliff:g id="device_name">%1$s</xliff:g></string>
<!-- Bluetooth device details. The body of a confirmation dialog for unpairing a paired device. --> <!-- Bluetooth device details. The body of a confirmation dialog for unpairing a paired device. -->
<string name="bluetooth_unpair_dialog_body" product="device">Your device will no longer be paired with <xliff:g id="device_name">%1$s</xliff:g></string> <string name="bluetooth_unpair_dialog_body" product="device">Your device will no longer be paired with <xliff:g id="device_name">%1$s</xliff:g></string>
<!-- Virtual device details. The body of a confirmation dialog for unpairing a paired device. [CHAR LIMIT=none] -->
<string name="virtual_device_forget_dialog_body" product="default"><xliff:g id="device_name">%1$s</xliff:g> will no longer be connected to this phone. If you continue, some apps and app streaming may stop working.</string>
<!-- Virtual device details. The body of a confirmation dialog for unpairing a paired device. [CHAR LIMIT=none] -->
<string name="virtual_device_forget_dialog_body" product="tablet"><xliff:g id="device_name">%1$s</xliff:g> will no longer be connected to this tablet. If you continue, some apps and app streaming may stop working.</string>
<!-- Virtual device details. The body of a confirmation dialog for unpairing a paired device. [CHAR LIMIT=none] -->
<string name="virtual_device_forget_dialog_body" product="device"><xliff:g id="device_name">%1$s</xliff:g> will no longer be connected to this device. If you continue, some apps and app streaming may stop working.</string>
<!-- Virtual device details. Footer [CHAR LIMIT=none] -->
<string name="virtual_device_details_footer_title" product="default">If you forget <xliff:g id="device_name">%1$s</xliff:g>, it will no longer be associated to this phone. Some permissions will be removed and some apps and app streaming may stop working.</string>
<!-- Virtual device details. Footer [CHAR LIMIT=none] -->
<string name="virtual_device_details_footer_title" product="tablet">If you forget <xliff:g id="device_name">%1$s</xliff:g>, it will no longer be associated to this tablet. Some permissions will be removed and some apps and app streaming may stop working.</string>
<!-- Virtual device details. Footer [CHAR LIMIT=none] -->
<string name="virtual_device_details_footer_title" product="device">If you forget <xliff:g id="device_name">%1$s</xliff:g>, it will no longer be associated to this device. Some permissions will be removed and some apps and app streaming may stop working.</string>
<!-- Description of Secure NFC in the 1st level settings screen. [CHAR LIMIT=NONE] --> <!-- Description of Secure NFC in the 1st level settings screen. [CHAR LIMIT=NONE] -->
<string name="nfc_secure_toggle_summary" product="default">Allow NFC use only when screen is unlocked</string> <string name="nfc_secure_toggle_summary" product="default">Allow NFC use only when screen is unlocked</string>
<!-- Summary for the panel of add Wi-Fi network from APP [CHAR LIMIT=NONE] --> <!-- Summary for the panel of add Wi-Fi network from APP [CHAR LIMIT=NONE] -->

View File

@@ -17,7 +17,8 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0"
android:tint="?android:attr/colorControlNormal">
<path <path
android:pathData="M3,6.003H21V4.003H3C1.9,4.003 1,4.903 1,6.003V18.003C1,19.103 1.9,20.003 3,20.003H7V18.003H3V6.003ZM13.002,12.001H9.002V13.781C8.392,14.331 8.002,15.111 8.002,16.001C8.002,16.891 8.392,17.671 9.002,18.221V20.001H13.002V18.221C13.612,17.671 14.002,16.881 14.002,16.001C14.002,15.121 13.612,14.331 13.002,13.781V12.001ZM9.501,16.003C9.501,16.833 10.171,17.503 11.001,17.503C11.831,17.503 12.501,16.833 12.501,16.003C12.501,15.173 11.831,14.503 11.001,14.503C10.171,14.503 9.501,15.173 9.501,16.003ZM22,8.004H16C15.5,8.004 15,8.504 15,9.004V19.004C15,19.504 15.5,20.004 16,20.004H22C22.5,20.004 23,19.504 23,19.004V9.004C23,8.504 22.5,8.004 22,8.004ZM17.002,18H21.002V10H17.002V18Z" android:pathData="M3,6.003H21V4.003H3C1.9,4.003 1,4.903 1,6.003V18.003C1,19.103 1.9,20.003 3,20.003H7V18.003H3V6.003ZM13.002,12.001H9.002V13.781C8.392,14.331 8.002,15.111 8.002,16.001C8.002,16.891 8.392,17.671 9.002,18.221V20.001H13.002V18.221C13.612,17.671 14.002,16.881 14.002,16.001C14.002,15.121 13.612,14.331 13.002,13.781V12.001ZM9.501,16.003C9.501,16.833 10.171,17.503 11.001,17.503C11.831,17.503 12.501,16.833 12.501,16.003C12.501,15.173 11.831,14.503 11.001,14.503C10.171,14.503 9.501,15.173 9.501,16.003ZM22,8.004H16C15.5,8.004 15,8.504 15,9.004V19.004C15,19.504 15.5,20.004 16,20.004H22C22.5,20.004 23,19.504 23,19.004V9.004C23,8.504 22.5,8.004 22,8.004ZM17.002,18H21.002V10H17.002V18Z"
android:fillType="evenOdd" android:fillType="evenOdd"

View File

@@ -13290,6 +13290,21 @@
<!-- Label for button to not allow grant the permission for remote devices. [CHAR_LIMIT=50] --> <!-- Label for button to not allow grant the permission for remote devices. [CHAR_LIMIT=50] -->
<string name="request_manage_bluetooth_permission_dont_allow">Don\u2019t allow</string> <string name="request_manage_bluetooth_permission_dont_allow">Don\u2019t allow</string>
<!-- Summary for a connected Virtual Device [CHAR LIMIT=50] -->
<string name="virtual_device_connected">Connected</string>
<!-- Summary for a disconnected Virtual Device [CHAR LIMIT=50] -->
<string name="virtual_device_disconnected">Disconnected</string>
<!-- The name of virtual device if there's no display name available. [CHAR LIMIT=60] -->
<string name="virtual_device_unknown">Unknown device</string>
<!-- Title for virtual device details page [CHAR LIMIT=50] -->
<string name="virtual_device_details_title">Device details</string>
<!-- The title of a confirmation dialog for forgetting a virtual device. [CHAR LIMIT=30] -->
<string name="virtual_device_forget_dialog_title">Forget <xliff:g id="device_name">%1$s</xliff:g>?</string>
<!-- In the confirmation dialog for forgetting a virtual device, this is the label on the button that will complete the forget action. -->
<string name="virtual_device_forget_dialog_confirm_button">Forget</string>
<!-- The title of the companion app section in the virtual device details page [CHAR LIMIT=50] -->
<string name="virtual_device_companion_app_category">Connection managed by</string>
<!-- Title for UWB preference [CHAR_LIMIT=60] --> <!-- Title for UWB preference [CHAR_LIMIT=60] -->
<string name="uwb_settings_title">Ultra-Wideband (UWB)</string> <string name="uwb_settings_title">Ultra-Wideband (UWB)</string>

View File

@@ -74,6 +74,13 @@
android:summary="@string/summary_placeholder" android:summary="@string/summary_placeholder"
android:title="@string/print_settings" /> android:title="@string/print_settings" />
<PreferenceCategory
android:key="virtual_device_list"
android:layout="@layout/preference_category_no_label"
android:order="99"
settings:controller="com.android.settings.connecteddevice.virtual.VirtualDeviceListController"
settings:searchable="true"/>
<com.android.settingslib.RestrictedSwitchPreference <com.android.settingslib.RestrictedSwitchPreference
android:key="uwb_settings" android:key="uwb_settings"
android:order="100" android:order="100"

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/virtual_device_details_title">
<com.android.settingslib.widget.LayoutPreference
android:key="virtual_device_details_header"
android:layout="@layout/settings_entity_header"
android:selectable="false"
settings:allowDividerBelow="true"
settings:controller="com.android.settings.connecteddevice.virtual.VirtualDeviceDetailsHeaderController"/>
<com.android.settingslib.widget.ActionButtonsPreference
android:key="virtual_device_action_buttons"
settings:allowDividerBelow="true"
settings:controller="com.android.settings.connecteddevice.virtual.VirtualDeviceDetailsButtonsController"/>
<PreferenceCategory
android:key="recent_apps_category"
android:title="@string/virtual_device_companion_app_category"
settings:searchable="false">
<com.android.settingslib.widget.AppPreference
android:key="virtual_device_companion_app"
settings:controller="com.android.settings.connecteddevice.virtual.VirtualDeviceDetailsCompanionAppController"/>
</PreferenceCategory>
<com.android.settingslib.widget.FooterPreference
android:key="virtual_device_details_footer"
settings:controller="com.android.settings.connecteddevice.virtual.VirtualDeviceDetailsFooterController"
android:selectable="false"
settings:searchable="false" />
</PreferenceScreen>

View File

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

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import android.app.Activity;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
/**
* Implements an AlertDialog for confirming that a user wishes to unpair or "forget" a paired
* device.
*/
public class ForgetDeviceDialogFragment extends InstrumentedDialogFragment {
public static final String TAG = ForgetDeviceDialogFragment.class.getSimpleName();
private static final String DEVICE_ARG = "virtual_device_arg";
@VisibleForTesting
CompanionDeviceManager mCompanionDeviceManager;
@VisibleForTesting
VirtualDeviceWrapper mDevice;
static ForgetDeviceDialogFragment newInstance(VirtualDeviceWrapper device) {
Bundle args = new Bundle(1);
args.putParcelable(DEVICE_ARG, device);
ForgetDeviceDialogFragment dialog = new ForgetDeviceDialogFragment();
dialog.setArguments(args);
return dialog;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_VIRTUAL_DEVICE_FORGET;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class);
mDevice = getArguments().getParcelable(DEVICE_ARG, VirtualDeviceWrapper.class);
}
@Override
@NonNull
public Dialog onCreateDialog(@Nullable Bundle inState) {
Context context = getContext();
CharSequence deviceName = mDevice.getDeviceName(context);
AlertDialog dialog = new AlertDialog.Builder(context)
.setPositiveButton(R.string.virtual_device_forget_dialog_confirm_button,
this::onForgetButtonClick)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setTitle(
context.getString(R.string.virtual_device_forget_dialog_title, deviceName));
dialog.setMessage(
context.getString(R.string.virtual_device_forget_dialog_body, deviceName));
return dialog;
}
private void onForgetButtonClick(DialogInterface dialog, int which) {
mCompanionDeviceManager.disassociate(mDevice.getAssociationInfo().getId());
Activity activity = getActivity();
if (activity != null) {
activity.finish();
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.ActionButtonsPreference;
import java.util.Objects;
/** This class adds one button to "forget" (ie unpair) the device. */
public class VirtualDeviceDetailsButtonsController extends BasePreferenceController {
private static final String KEY_VIRTUAL_DEVICE_ACTION_BUTTONS = "virtual_device_action_buttons";
@Nullable
private PreferenceFragmentCompat mFragment;
@Nullable
private VirtualDeviceWrapper mDevice;
public VirtualDeviceDetailsButtonsController(@NonNull Context context) {
super(context, KEY_VIRTUAL_DEVICE_ACTION_BUTTONS);
}
/** One-time initialization when the controller is first created. */
void init(@NonNull PreferenceFragmentCompat fragment, @NonNull VirtualDeviceWrapper device) {
mFragment = fragment;
mDevice = device;
}
private void onForgetButtonPressed() {
ForgetDeviceDialogFragment fragment =
ForgetDeviceDialogFragment.newInstance(Objects.requireNonNull(mDevice));
fragment.show(Objects.requireNonNull(mFragment).getParentFragmentManager(),
ForgetDeviceDialogFragment.TAG);
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
((ActionButtonsPreference) screen.findPreference(getPreferenceKey()))
.setButton1Text(R.string.forget)
.setButton1Icon(R.drawable.ic_settings_delete)
.setButton1OnClickListener((view) -> onForgetButtonPressed())
.setButton1Enabled(true);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import static com.android.settings.spa.app.appinfo.AppInfoSettingsProvider.startAppInfoSettings;
import android.app.Application;
import android.content.Context;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.Utils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.widget.AppPreference;
import java.util.Objects;
/** This class adds the details about the virtual device companion app. */
public class VirtualDeviceDetailsCompanionAppController extends BasePreferenceController {
private static final String KEY_VIRTUAL_DEVICE_COMPANION_APP = "virtual_device_companion_app";
@Nullable
private PreferenceFragmentCompat mFragment;
@Nullable
private String mPackageName;
public VirtualDeviceDetailsCompanionAppController(@NonNull Context context) {
super(context, KEY_VIRTUAL_DEVICE_COMPANION_APP);
}
/** One-time initialization when the controller is first created. */
void init(@NonNull PreferenceFragmentCompat fragment, @NonNull String packageName) {
mFragment = fragment;
mPackageName = packageName;
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
ApplicationsState applicationsState = ApplicationsState.getInstance(
(Application) mContext.getApplicationContext());
final ApplicationsState.AppEntry appEntry = applicationsState.getEntry(
mPackageName, UserHandle.myUserId());
final AppPreference preference = screen.findPreference(getPreferenceKey());
preference.setTitle(appEntry.label);
preference.setIcon(Utils.getBadgedIcon(mContext, appEntry.info));
preference.setOnPreferenceClickListener(pref -> {
startAppInfoSettings(Objects.requireNonNull(mPackageName), appEntry.info.uid,
Objects.requireNonNull(mFragment), /* request= */ 1001,
getMetricsCategory());
return true;
});
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
/** Adds footer text on the virtual device details page. */
public class VirtualDeviceDetailsFooterController extends BasePreferenceController {
private static final String KEY_VIRTUAL_DEVICE_FOOTER = "virtual_device_details_footer";
@Nullable
private CharSequence mDeviceName;
public VirtualDeviceDetailsFooterController(@NonNull Context context) {
super(context, KEY_VIRTUAL_DEVICE_FOOTER);
}
/** One-time initialization when the controller is first created. */
void init(@NonNull CharSequence deviceName) {
mDeviceName = deviceName;
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
Preference preference = screen.findPreference(getPreferenceKey());
preference.setTitle(mContext.getString(R.string.virtual_device_details_footer_title,
mDeviceName));
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import android.app.settings.SettingsEnums;
import android.content.Context;
import androidx.annotation.NonNull;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
/**
* Dedicated screen displaying the information for a single virtual device to the user and allowing
* them to manage that device.
*/
public class VirtualDeviceDetailsFragment extends DashboardFragment {
private static final String TAG = VirtualDeviceDetailsFragment.class.getSimpleName();
static final String DEVICE_ARG = "virtual_device_arg";
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
VirtualDeviceWrapper device =
getArguments().getParcelable(DEVICE_ARG, VirtualDeviceWrapper.class);
use(VirtualDeviceDetailsHeaderController.class).init(device);
use(VirtualDeviceDetailsButtonsController.class).init(this, device);
use(VirtualDeviceDetailsCompanionAppController.class)
.init(this, device.getAssociationInfo().getPackageName());
use(VirtualDeviceDetailsFooterController.class).init(device.getDeviceName(context));
}
@Override
public int getMetricsCategory() {
return SettingsEnums.VIRTUAL_DEVICE_DETAILS;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.virtual_device_details_fragment;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import android.companion.virtual.VirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
import android.content.Context;
import android.graphics.drawable.Icon;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.LayoutPreference;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/** This class adds a header for a virtual device with a heading and icon. */
public class VirtualDeviceDetailsHeaderController extends BasePreferenceController implements
LifecycleObserver, VirtualDeviceManager.VirtualDeviceListener {
private static final String KEY_VIRTUAL_DEVICE_DETAILS_HEADER = "virtual_device_details_header";
@Nullable
private final VirtualDeviceManager mVirtualDeviceManager;
@Nullable
private VirtualDeviceWrapper mDevice;
@Nullable
private TextView mSummaryView;
private final Executor mExecutor = Executors.newSingleThreadExecutor();
public VirtualDeviceDetailsHeaderController(@NonNull Context context) {
super(context, KEY_VIRTUAL_DEVICE_DETAILS_HEADER);
mVirtualDeviceManager =
Objects.requireNonNull(context.getSystemService(VirtualDeviceManager.class));
}
/** One-time initialization when the controller is first created. */
void init(@NonNull VirtualDeviceWrapper device) {
mDevice = device;
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void onStart() {
if (mVirtualDeviceManager != null) {
mVirtualDeviceManager.registerVirtualDeviceListener(mExecutor, this);
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void onStop() {
if (mVirtualDeviceManager != null) {
mVirtualDeviceManager.unregisterVirtualDeviceListener(this);
}
}
@Override
public void onVirtualDeviceCreated(int deviceId) {
VirtualDevice device =
Objects.requireNonNull(mVirtualDeviceManager).getVirtualDevice(deviceId);
if (mDevice != null && device != null
&& mDevice.getPersistentDeviceId().equals(device.getPersistentDeviceId())) {
mDevice.setDeviceId(deviceId);
mContext.getMainExecutor().execute(this::updateSummary);
}
}
@Override
public void onVirtualDeviceClosed(int deviceId) {
if (mDevice != null && deviceId == mDevice.getDeviceId()) {
mDevice.setDeviceId(Context.DEVICE_ID_INVALID);
mContext.getMainExecutor().execute(this::updateSummary);
}
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
LayoutPreference headerPreference = screen.findPreference(getPreferenceKey());
View view = headerPreference.findViewById(R.id.entity_header);
TextView titleView = view.findViewById(R.id.entity_header_title);
ImageView iconView = headerPreference.findViewById(R.id.entity_header_icon);
mSummaryView = view.findViewById(R.id.entity_header_summary);
updateSummary();
if (mDevice != null) {
titleView.setText(mDevice.getDeviceName(mContext));
Icon deviceIcon = android.companion.Flags.associationDeviceIcon()
? mDevice.getAssociationInfo().getDeviceIcon() : null;
if (deviceIcon == null) {
iconView.setImageResource(R.drawable.ic_devices_other);
} else {
iconView.setImageIcon(deviceIcon);
}
}
iconView.setContentDescription("Icon for device");
}
private void updateSummary() {
if (mSummaryView != null && mDevice != null) {
mSummaryView.setText(mDevice.getDeviceId() != Context.DEVICE_ID_INVALID
? R.string.virtual_device_connected : R.string.virtual_device_disconnected);
}
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import android.content.Context;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.util.ArrayMap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.search.SearchIndexableRaw;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/** Displays the list of all virtual devices. */
public class VirtualDeviceListController extends BasePreferenceController
implements LifecycleObserver, VirtualDeviceUpdater.DeviceListener {
private final MetricsFeatureProvider mMetricsFeatureProvider;
@VisibleForTesting
VirtualDeviceUpdater mVirtualDeviceUpdater;
@VisibleForTesting
ArrayMap<String, Preference> mPreferences = new ArrayMap<>();
@Nullable
private PreferenceGroup mPreferenceGroup;
@Nullable
private DashboardFragment mFragment;
public VirtualDeviceListController(@NonNull Context context, @NonNull String preferenceKey) {
super(context, preferenceKey);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
mVirtualDeviceUpdater = new VirtualDeviceUpdater(context, this);
}
public void setFragment(@NonNull DashboardFragment fragment) {
mFragment = fragment;
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void onStart() {
if (isAvailable()) {
mVirtualDeviceUpdater.registerListener();
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void onStop() {
if (isAvailable()) {
mVirtualDeviceUpdater.unregisterListener();
}
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceGroup = screen.findPreference(getPreferenceKey());
if (isAvailable()) {
mVirtualDeviceUpdater.loadDevices();
}
}
@Override
public void onDeviceAdded(@NonNull VirtualDeviceWrapper device) {
Preference preference = new Preference(mContext);
CharSequence deviceName = device.getDeviceName(mContext);
preference.setTitle(deviceName);
preference.setKey(device.getPersistentDeviceId() + "_" + deviceName);
final CharSequence title = preference.getTitle();
Icon deviceIcon = android.companion.Flags.associationDeviceIcon()
? device.getAssociationInfo().getDeviceIcon() : null;
if (deviceIcon == null) {
preference.setIcon(R.drawable.ic_devices_other);
} else {
preference.setIcon(deviceIcon.loadDrawable(mContext));
}
if (device.getDeviceId() != Context.DEVICE_ID_INVALID) {
preference.setSummary(R.string.virtual_device_connected);
} else {
preference.setSummary(R.string.virtual_device_disconnected);
}
preference.setOnPreferenceClickListener((Preference p) -> {
mMetricsFeatureProvider.logClickedPreference(p, getMetricsCategory());
final Bundle args = new Bundle();
args.putParcelable(VirtualDeviceDetailsFragment.DEVICE_ARG, device);
if (mFragment != null) {
new SubSettingLauncher(mFragment.getContext())
.setDestination(VirtualDeviceDetailsFragment.class.getName())
.setTitleText(title)
.setArguments(args)
.setSourceMetricsCategory(getMetricsCategory())
.launch();
}
return true;
});
mPreferences.put(device.getPersistentDeviceId(), preference);
if (mPreferenceGroup != null) {
mContext.getMainExecutor().execute(() ->
Objects.requireNonNull(mPreferenceGroup).addPreference(preference));
}
}
@Override
public void onDeviceRemoved(@NonNull VirtualDeviceWrapper device) {
Preference preference = mPreferences.remove(device.getPersistentDeviceId());
if (mPreferenceGroup != null) {
mContext.getMainExecutor().execute(() ->
Objects.requireNonNull(mPreferenceGroup).removePreference(preference));
}
}
@Override
public void onDeviceChanged(@NonNull VirtualDeviceWrapper device) {
Preference preference = mPreferences.get(device.getPersistentDeviceId());
if (preference != null) {
int summaryResId = device.getDeviceId() != Context.DEVICE_ID_INVALID
? R.string.virtual_device_connected : R.string.virtual_device_disconnected;
mContext.getMainExecutor().execute(() ->
Objects.requireNonNull(preference).setSummary(summaryResId));
}
}
@Override
public void updateDynamicRawDataToIndex(@NonNull List<SearchIndexableRaw> rawData) {
if (!isAvailable()) {
return;
}
Collection<VirtualDeviceWrapper> devices = mVirtualDeviceUpdater.loadDevices();
for (VirtualDeviceWrapper device : devices) {
SearchIndexableRaw data = new SearchIndexableRaw(mContext);
String deviceName = device.getDeviceName(mContext).toString();
data.key = device.getPersistentDeviceId() + "_" + deviceName;
data.title = deviceName;
data.summaryOn = mContext.getString(R.string.connected_device_connections_title);
rawData.add(data);
}
}
@Override
public int getAvailabilityStatus() {
if (!mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableVirtualDeviceManager)) {
return UNSUPPORTED_ON_DEVICE;
}
if (!android.companion.virtualdevice.flags.Flags.vdmSettings()) {
return CONDITIONALLY_UNAVAILABLE;
}
return AVAILABLE;
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import static com.android.settingslib.drawer.TileUtils.IA_SETTINGS_ACTION;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceManager;
import android.companion.virtual.VirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.google.common.collect.ImmutableSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/** Maintains a collection of all virtual devices and propagates any changes to its listener. */
class VirtualDeviceUpdater implements VirtualDeviceManager.VirtualDeviceListener {
private static final String CDM_PERSISTENT_DEVICE_ID_PREFIX = "companion:";
// TODO(b/384400670): Detect these packages via PackageManager instead of hardcoding them.
private static final ImmutableSet<String> IGNORED_PACKAGES =
ImmutableSet.of("com.google.ambient.streaming");
private final VirtualDeviceManager mVirtualDeviceManager;
private final CompanionDeviceManager mCompanionDeviceManager;
private final PackageManager mPackageManager;
private final DeviceListener mDeviceListener;
private final Executor mBackgroundExecutor = Executors.newSingleThreadExecutor();
// Up-to-date list of active and inactive devices, keyed by persistent device id.
@VisibleForTesting
ArrayMap<String, VirtualDeviceWrapper> mDevices = new ArrayMap<>();
interface DeviceListener {
void onDeviceAdded(@NonNull VirtualDeviceWrapper device);
void onDeviceRemoved(@NonNull VirtualDeviceWrapper device);
void onDeviceChanged(@NonNull VirtualDeviceWrapper device);
}
VirtualDeviceUpdater(Context context, DeviceListener deviceListener) {
mVirtualDeviceManager = context.getSystemService(VirtualDeviceManager.class);
mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class);
mPackageManager = context.getPackageManager();
mDeviceListener = deviceListener;
}
void registerListener() {
mVirtualDeviceManager.registerVirtualDeviceListener(mBackgroundExecutor, this);
mBackgroundExecutor.execute(this::loadDevices);
}
void unregisterListener() {
mVirtualDeviceManager.unregisterVirtualDeviceListener(this);
}
@Override
public void onVirtualDeviceCreated(int deviceId) {
loadDevices();
}
@Override
public void onVirtualDeviceClosed(int deviceId) {
loadDevices();
}
Collection<VirtualDeviceWrapper> loadDevices() {
final Set<String> persistentDeviceIds = mVirtualDeviceManager.getAllPersistentDeviceIds();
final Set<String> deviceIdsToRemove = new ArraySet<>();
for (String persistentDeviceId : mDevices.keySet()) {
if (!persistentDeviceIds.contains(persistentDeviceId)) {
deviceIdsToRemove.add(persistentDeviceId);
}
}
for (String persistentDeviceId : deviceIdsToRemove) {
mDeviceListener.onDeviceRemoved(mDevices.remove(persistentDeviceId));
}
if (!persistentDeviceIds.isEmpty()) {
for (VirtualDevice device : mVirtualDeviceManager.getVirtualDevices()) {
String persistentDeviceId = device.getPersistentDeviceId();
persistentDeviceIds.remove(persistentDeviceId);
addOrUpdateDevice(persistentDeviceId, device.getDeviceId());
}
}
for (String persistentDeviceId : persistentDeviceIds) {
addOrUpdateDevice(persistentDeviceId, Context.DEVICE_ID_INVALID);
}
return mDevices.values();
}
private void addOrUpdateDevice(String persistentDeviceId, int deviceId) {
VirtualDeviceWrapper device = mDevices.get(persistentDeviceId);
if (device == null) {
AssociationInfo associationInfo = getAssociationInfo(persistentDeviceId);
if (associationInfo == null) {
return;
}
device = new VirtualDeviceWrapper(associationInfo, persistentDeviceId, deviceId);
mDevices.put(persistentDeviceId, device);
mDeviceListener.onDeviceAdded(device);
}
if (device.getDeviceId() != deviceId) {
device.setDeviceId(deviceId);
mDeviceListener.onDeviceChanged(device);
}
}
@Nullable
private AssociationInfo getAssociationInfo(String persistentDeviceId) {
if (persistentDeviceId == null) {
return null;
}
VirtualDeviceWrapper device = mDevices.get(persistentDeviceId);
if (device != null) {
return device.getAssociationInfo();
}
if (!persistentDeviceId.startsWith(CDM_PERSISTENT_DEVICE_ID_PREFIX)) {
return null;
}
final int associationId = Integer.parseInt(
persistentDeviceId.replaceFirst(CDM_PERSISTENT_DEVICE_ID_PREFIX, ""));
final List<AssociationInfo> associations =
mCompanionDeviceManager.getAllAssociations(UserHandle.USER_ALL);
final AssociationInfo associationInfo = associations.stream()
.filter(a -> a.getId() == associationId)
.findFirst()
.orElse(null);
if (associationInfo == null) {
return null;
}
if (shouldExcludePackageFromSettings(associationInfo.getPackageName())) {
return null;
}
return associationInfo;
}
// Some packages already inject custom settings entries that allow the users to manage the
// virtual devices and the companion associations, so they should be ignored from the generic
// settings page.
private boolean shouldExcludePackageFromSettings(String packageName) {
if (packageName == null || IGNORED_PACKAGES.contains(packageName)) {
return true;
}
final Intent intent = new Intent(IA_SETTINGS_ACTION);
intent.setPackage(packageName);
return intent.resolveActivity(mPackageManager) != null;
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.connecteddevice.virtual;
import android.companion.AssociationInfo;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import java.util.Objects;
/** Parcelable representing a virtual device along with its association properties. */
class VirtualDeviceWrapper implements Parcelable {
/** The CDM Association for this device. */
@NonNull
private final AssociationInfo mAssociationInfo;
/** The unique VDM identifier for the device, persisted even when the device is inactive. */
@NonNull
private final String mPersistentDeviceId;
/** The identifier for the device if it's active, Context.DEVICE_ID_INVALID otherwise. */
private int mDeviceId;
VirtualDeviceWrapper(@NonNull AssociationInfo associationInfo,
@NonNull String persistentDeviceId, int deviceId) {
mAssociationInfo = associationInfo;
mPersistentDeviceId = persistentDeviceId;
mDeviceId = deviceId;
}
@NonNull
AssociationInfo getAssociationInfo() {
return mAssociationInfo;
}
@NonNull
String getPersistentDeviceId() {
return mPersistentDeviceId;
}
@NonNull
CharSequence getDeviceName(Context context) {
return mAssociationInfo.getDisplayName() != null
? mAssociationInfo.getDisplayName()
: context.getString(R.string.virtual_device_unknown);
}
int getDeviceId() {
return mDeviceId;
}
void setDeviceId(int deviceId) {
mDeviceId = deviceId;
}
private VirtualDeviceWrapper(Parcel in) {
mAssociationInfo = in.readTypedObject(AssociationInfo.CREATOR);
mPersistentDeviceId = in.readString8();
mDeviceId = in.readInt();
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeTypedObject(mAssociationInfo, flags);
dest.writeString8(mPersistentDeviceId);
dest.writeInt(mDeviceId);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (!(o instanceof VirtualDeviceWrapper that)) return false;
return Objects.equals(mAssociationInfo, that.mAssociationInfo)
&& Objects.equals(mPersistentDeviceId, that.mPersistentDeviceId)
&& mDeviceId == that.mDeviceId;
}
@Override
public int hashCode() {
return Objects.hash(mAssociationInfo, mPersistentDeviceId, mDeviceId);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<VirtualDeviceWrapper> CREATOR =
new Parcelable.Creator<>() {
@NonNull
public VirtualDeviceWrapper createFromParcel(@NonNull Parcel in) {
return new VirtualDeviceWrapper(in);
}
@NonNull
public VirtualDeviceWrapper[] newArray(int size) {
return new VirtualDeviceWrapper[size];
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

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