From 7880aee8550019fa3388211956298ceed513afe2 Mon Sep 17 00:00:00 2001 From: jasonwshsu Date: Wed, 3 Aug 2022 13:20:20 +0800 Subject: [PATCH 1/6] Add pair button in bluetooth details page for hearing aid device Root Cause: Users can not connect another ear again after they cancel the pairing dialog in Accessibility -> hearing aids entry Solution: Add pair button in bluetooth details page for hearing aid device Bug: 233038449 Test: make RunSettingsRoboTests ROBOTEST_FILTER=BluetoothDetailsPairOtherControllerTest Change-Id: I6a7af1c2c2263476b040233edb072cc64a2927b0 --- res/values/strings.xml | 5 + res/xml/bluetooth_device_details_fragment.xml | 6 + .../BluetoothDetailsPairOtherController.java | 102 +++++++++++++++ .../BluetoothDeviceDetailsFragment.java | 2 + ...uetoothDetailsPairOtherControllerTest.java | 121 ++++++++++++++++++ 5 files changed, 236 insertions(+) create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsPairOtherController.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 9f8df7b6467..7cfb966837c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -184,6 +184,11 @@ bluetooth + + + Pair right ear + + Pair left ear Pair your other ear diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index f330b19e2a1..efb2bf7036b 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -42,6 +42,12 @@ settings:searchable="false" settings:controller="com.android.settings.bluetooth.LeAudioBluetoothDetailsHeaderController"/> + + + diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherController.java b/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherController.java new file mode 100644 index 00000000000..d14a9b1144a --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherController.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 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.bluetooth; + +import android.content.Context; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.ButtonPreference; + +/** + * This class handles button preference logic to display for hearing aid device. + */ +public class BluetoothDetailsPairOtherController extends BluetoothDetailsController { + private static final String KEY_PAIR_OTHER = "hearing_aid_pair_other_button"; + + private ButtonPreference mPreference; + + public BluetoothDetailsPairOtherController(Context context, + PreferenceFragmentCompat fragment, + CachedBluetoothDevice device, + Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + lifecycle.addObserver(this); + } + + @Override + public boolean isAvailable() { + return getButtonPreferenceVisibility(mCachedDevice); + } + + @Override + public String getPreferenceKey() { + return KEY_PAIR_OTHER; + } + + @Override + protected void init(PreferenceScreen screen) { + final int side = mCachedDevice.getDeviceSide(); + final int stringRes = (side == HearingAidProfile.DeviceSide.SIDE_LEFT) + ? R.string.bluetooth_pair_right_ear_button + : R.string.bluetooth_pair_left_ear_button; + + mPreference = screen.findPreference(getPreferenceKey()); + mPreference.setTitle(stringRes); + mPreference.setOnClickListener(v -> launchPairingDetail()); + } + + @Override + protected void refresh() { + mPreference.setVisible(getButtonPreferenceVisibility(mCachedDevice)); + } + + private boolean getButtonPreferenceVisibility(CachedBluetoothDevice cachedDevice) { + return isBinauralMode(cachedDevice) && isOnlyOneSideConnected(cachedDevice); + } + + private void launchPairingDetail() { + new SubSettingLauncher(mContext) + .setDestination(BluetoothPairingDetail.class.getName()) + .setSourceMetricsCategory( + ((BluetoothDeviceDetailsFragment) mFragment).getMetricsCategory()) + .launch(); + } + + private boolean isBinauralMode(CachedBluetoothDevice cachedDevice) { + return cachedDevice.getDeviceMode() == HearingAidProfile.DeviceMode.MODE_BINAURAL; + } + + private boolean isOnlyOneSideConnected(CachedBluetoothDevice cachedDevice) { + if (!cachedDevice.isConnectedHearingAidDevice()) { + return false; + } + + final CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); + if (subDevice != null && subDevice.isConnectedHearingAidDevice()) { + return false; + } + + return true; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index c118a43131d..999e34da6bf 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -251,6 +251,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment lifecycle)); controllers.add(new BluetoothDetailsRelatedToolsController(context, this, mCachedDevice, lifecycle)); + controllers.add(new BluetoothDetailsPairOtherController(context, this, mCachedDevice, + lifecycle)); } return controllers; } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherControllerTest.java new file mode 100644 index 00000000000..cfa6d41e0d7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherControllerTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 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.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import com.android.settings.R; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.widget.ButtonPreference; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link BluetoothDetailsPairOtherController}. */ +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsPairOtherControllerTest extends BluetoothDetailsControllerTestBase { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private CachedBluetoothDevice mSubCachedDevice; + private BluetoothDetailsPairOtherController mController; + private ButtonPreference mPreference; + + @Override + public void setUp() { + super.setUp(); + + mController = new BluetoothDetailsPairOtherController(mContext, mFragment, mCachedDevice, + mLifecycle); + mPreference = new ButtonPreference(mContext); + mPreference.setKey(mController.getPreferenceKey()); + mScreen.addPreference(mPreference); + } + + @Test + public void init_leftSideDevice_expectedTitle() { + when(mCachedDevice.getDeviceSide()).thenReturn(HearingAidProfile.DeviceSide.SIDE_LEFT); + + mController.init(mScreen); + + assertThat(mPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.bluetooth_pair_right_ear_button)); + } + + @Test + public void init_rightSideDevice_expectedTitle() { + when(mCachedDevice.getDeviceSide()).thenReturn(HearingAidProfile.DeviceSide.SIDE_RIGHT); + + mController.init(mScreen); + + assertThat(mPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.bluetooth_pair_left_ear_button)); + } + + @Test + public void isAvailable_isConnectedHearingAidDevice_available() { + when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(false); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_notConnectedHearingAidDevice_notAvailable() { + when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice.getDeviceMode()).thenReturn(HearingAidProfile.DeviceMode.MODE_MONAURAL); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_subDeviceIsConnectedHearingAidDevice_notAvailable() { + when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice.getDeviceMode()).thenReturn(HearingAidProfile.DeviceMode.MODE_BINAURAL); + when(mSubCachedDevice.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice.getSubDevice()).thenReturn(mSubCachedDevice); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_subDeviceNotConnectedHearingAidDevice_available() { + when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice.getDeviceMode()).thenReturn(HearingAidProfile.DeviceMode.MODE_BINAURAL); + when(mSubCachedDevice.isConnectedHearingAidDevice()).thenReturn(false); + when(mCachedDevice.getSubDevice()).thenReturn(mSubCachedDevice); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_subDeviceNotExist_available() { + when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice.getDeviceMode()).thenReturn(HearingAidProfile.DeviceMode.MODE_BINAURAL); + when(mCachedDevice.getSubDevice()).thenReturn(null); + + assertThat(mController.isAvailable()).isTrue(); + } +} From 36d320a8de709ee61790e8c1dcafd5b368614a21 Mon Sep 17 00:00:00 2001 From: Robert Luo Date: Fri, 29 Jul 2022 16:59:44 +0800 Subject: [PATCH 2/6] Allow advanced VPN to manage connection status via its UI Bug: 238641532 Test: atest -c VpnSettingsTest Change-Id: Ia6f1d84bba38bab7f13f46dc8a4fdb4eb0505f8f Merged-In: Ia6f1d84bba38bab7f13f46dc8a4fdb4eb0505f8f --- .../vpn2/AdvancedVpnFeatureProvider.java | 5 + .../vpn2/AdvancedVpnFeatureProviderImpl.java | 5 + .../android/settings/vpn2/VpnSettings.java | 12 +- .../settings/vpn2/VpnSettingsTest.java | 117 ++++++++++++++++-- 4 files changed, 124 insertions(+), 15 deletions(-) diff --git a/src/com/android/settings/vpn2/AdvancedVpnFeatureProvider.java b/src/com/android/settings/vpn2/AdvancedVpnFeatureProvider.java index cb56c351448..962b6c2e53b 100644 --- a/src/com/android/settings/vpn2/AdvancedVpnFeatureProvider.java +++ b/src/com/android/settings/vpn2/AdvancedVpnFeatureProvider.java @@ -47,4 +47,9 @@ public interface AdvancedVpnFeatureProvider { * Returns {@code true} advanced vpn is removable. */ boolean isAdvancedVpnRemovable(); + + /** + * Returns {@code true} if the disconnect dialog is enabled when advanced vpn is connected. + */ + boolean isDisconnectDialogEnabled(); } diff --git a/src/com/android/settings/vpn2/AdvancedVpnFeatureProviderImpl.java b/src/com/android/settings/vpn2/AdvancedVpnFeatureProviderImpl.java index c5bc69c042d..b8f58a9fde7 100644 --- a/src/com/android/settings/vpn2/AdvancedVpnFeatureProviderImpl.java +++ b/src/com/android/settings/vpn2/AdvancedVpnFeatureProviderImpl.java @@ -46,4 +46,9 @@ public class AdvancedVpnFeatureProviderImpl implements AdvancedVpnFeatureProvide public boolean isAdvancedVpnRemovable() { return true; } + + @Override + public boolean isDisconnectDialogEnabled() { + return true; + } } diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java index 3b875eb1293..a91bb6c7e66 100644 --- a/src/com/android/settings/vpn2/VpnSettings.java +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -366,7 +366,7 @@ public class VpnSettings extends RestrictedSettingsFragment implements public void setShownPreferences(final Collection updates) { retainAllPreference(updates); - final PreferenceGroup vpnGroup = getPreferenceScreen(); + final PreferenceGroup vpnGroup = mPreferenceScreen; updatePreferenceGroup(vpnGroup, updates); // Show all new preferences on the screen @@ -448,14 +448,16 @@ public class VpnSettings extends RestrictedSettingsFragment implements } else if (preference instanceof AppPreference) { AppPreference pref = (AppPreference) preference; boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED); + String vpnPackageName = pref.getPackageName(); - if (!connected) { + if ((!connected) || (isAdvancedVpn(mFeatureProvider, vpnPackageName, getContext()) + && !mFeatureProvider.isDisconnectDialogEnabled())) { try { UserHandle user = UserHandle.of(pref.getUserId()); - Context userContext = getActivity().createPackageContextAsUser( - getActivity().getPackageName(), 0 /* flags */, user); + Context userContext = getContext().createPackageContextAsUser( + getContext().getPackageName(), 0 /* flags */, user); PackageManager pm = userContext.getPackageManager(); - Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName()); + Intent appIntent = pm.getLaunchIntentForPackage(vpnPackageName); if (appIntent != null) { userContext.startActivityAsUser(appIntent, user); return true; diff --git a/tests/unit/src/com/android/settings/vpn2/VpnSettingsTest.java b/tests/unit/src/com/android/settings/vpn2/VpnSettingsTest.java index d6ba33a3bc2..953a524750d 100644 --- a/tests/unit/src/com/android/settings/vpn2/VpnSettingsTest.java +++ b/tests/unit/src/com/android/settings/vpn2/VpnSettingsTest.java @@ -21,16 +21,21 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.AppOpsManager; import android.content.Context; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Looper; import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArraySet; import androidx.preference.Preference; @@ -48,6 +53,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -58,13 +64,18 @@ import java.util.Set; @RunWith(AndroidJUnit4.class) public class VpnSettingsTest { - private static final String ADVANCED_VPN_GROUP_KEY = "advanced_vpn_group"; - private static final String VPN_GROUP_KEY = "vpn_group"; - private static final String ADVANCED_VPN_GROUP_TITLE = "advanced_vpn_group_title"; - private static final String VPN_GROUP_TITLE = "vpn_group_title"; - private static final String FAKE_PACKAGE_NAME = "com.fake.package.name"; - private static final String ADVANCED_VPN_GROUP_PACKAGE_NAME = "com.advanced.package.name"; private static final int USER_ID_1 = UserHandle.USER_NULL; + private static final String VPN_GROUP_KEY = "vpn_group"; + private static final String VPN_GROUP_TITLE = "vpn_group_title"; + private static final String VPN_PACKAGE_NAME = "vpn.package.name"; + private static final String VPN_LAUNCH_INTENT = "vpn.action"; + private static final String ADVANCED_VPN_GROUP_KEY = "advanced_vpn_group"; + private static final String ADVANCED_VPN_GROUP_TITLE = "advanced_vpn_group_title"; + private static final String ADVANCED_VPN_PACKAGE_NAME = "advanced.vpn.package.name"; + private static final String ADVANCED_VPN_LAUNCH_INTENT = "advanced.vpn.action"; + + private final Intent mVpnIntent = new Intent().setAction(VPN_LAUNCH_INTENT); + private final Intent mAdvancedVpnIntent = new Intent().setAction(ADVANCED_VPN_LAUNCH_INTENT); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @@ -108,7 +119,7 @@ public class VpnSettingsTest { when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.getVpnPreferenceGroupTitle(mContext)) .thenReturn(VPN_GROUP_TITLE); when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.getAdvancedVpnPackageName()) - .thenReturn(ADVANCED_VPN_GROUP_PACKAGE_NAME); + .thenReturn(ADVANCED_VPN_PACKAGE_NAME); when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.isAdvancedVpnSupported(any())) .thenReturn(true); when(mContext.getPackageManager()).thenReturn(mPackageManager); @@ -122,7 +133,7 @@ public class VpnSettingsTest { public void setShownAdvancedPreferences_hasGeneralVpn_returnsVpnCountAs1() { Set updates = new ArraySet<>(); AppPreference pref = - spy(new AppPreference(mContext, USER_ID_1, FAKE_PACKAGE_NAME)); + spy(new AppPreference(mContext, USER_ID_1, VPN_PACKAGE_NAME)); updates.add(pref); mVpnSettings.setShownAdvancedPreferences(updates); @@ -136,7 +147,7 @@ public class VpnSettingsTest { public void setShownAdvancedPreferences_hasAdvancedVpn_returnsAdvancedVpnCountAs1() { Set updates = new ArraySet<>(); AppPreference pref = - spy(new AppPreference(mContext, USER_ID_1, ADVANCED_VPN_GROUP_PACKAGE_NAME)); + spy(new AppPreference(mContext, USER_ID_1, ADVANCED_VPN_PACKAGE_NAME)); updates.add(pref); mVpnSettings.setShownAdvancedPreferences(updates); @@ -175,7 +186,7 @@ public class VpnSettingsTest { List opEntries = new ArrayList<>(); List apps = new ArrayList<>(); AppOpsManager.PackageOps packageOps = - new AppOpsManager.PackageOps(FAKE_PACKAGE_NAME, uid, opEntries); + new AppOpsManager.PackageOps(VPN_PACKAGE_NAME, uid, opEntries); apps.add(packageOps); when(mAppOpsManager.getPackagesForOps((int[]) any())).thenReturn(apps); when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.isAdvancedVpnSupported(any())) @@ -185,4 +196,90 @@ public class VpnSettingsTest { mFakeFeatureFactory.getAdvancedVpnFeatureProvider(), mAppOpsManager)).isEmpty(); } + + @Test + public void clickVpn_VpnConnected_doesNotStartVpnLaunchIntent() + throws PackageManager.NameNotFoundException { + Set updates = new ArraySet<>(); + AppPreference pref = spy(new AppPreference(mContext, USER_ID_1, VPN_PACKAGE_NAME)); + pref.setState(AppPreference.STATE_CONNECTED); + updates.add(pref); + when(mContext.createPackageContextAsUser(any(), anyInt(), any())).thenReturn(mContext); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.getLaunchIntentForPackage(any())).thenReturn(mVpnIntent); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + doNothing().when(mContext).startActivityAsUser(captor.capture(), any()); + mVpnSettings.setShownPreferences(updates); + + mVpnSettings.onPreferenceClick(pref); + + verify(mContext, never()).startActivityAsUser(any(), any()); + } + + @Test + public void clickVpn_VpnDisconnected_startsVpnLaunchIntent() + throws PackageManager.NameNotFoundException { + Set updates = new ArraySet<>(); + AppPreference pref = spy(new AppPreference(mContext, USER_ID_1, VPN_PACKAGE_NAME)); + pref.setState(AppPreference.STATE_DISCONNECTED); + updates.add(pref); + when(mContext.createPackageContextAsUser(any(), anyInt(), any())).thenReturn(mContext); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.getLaunchIntentForPackage(any())).thenReturn(mVpnIntent); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + doNothing().when(mContext).startActivityAsUser(captor.capture(), any()); + mVpnSettings.setShownPreferences(updates); + + mVpnSettings.onPreferenceClick(pref); + + verify(mContext).startActivityAsUser(captor.capture(), any()); + assertThat(TextUtils.equals(captor.getValue().getAction(), + VPN_LAUNCH_INTENT)).isTrue(); + } + + @Test + public void clickAdvancedVpn_VpnConnectedDisconnectDialogDisabled_startsAppLaunchIntent() + throws PackageManager.NameNotFoundException { + Set updates = new ArraySet<>(); + AppPreference pref = + spy(new AppPreference(mContext, USER_ID_1, ADVANCED_VPN_PACKAGE_NAME)); + pref.setState(AppPreference.STATE_CONNECTED); + updates.add(pref); + when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.isDisconnectDialogEnabled()) + .thenReturn(false); + when(mContext.createPackageContextAsUser(any(), anyInt(), any())).thenReturn(mContext); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.getLaunchIntentForPackage(any())).thenReturn(mAdvancedVpnIntent); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + doNothing().when(mContext).startActivityAsUser(captor.capture(), any()); + mVpnSettings.setShownAdvancedPreferences(updates); + + mVpnSettings.onPreferenceClick(pref); + + verify(mContext).startActivityAsUser(captor.capture(), any()); + assertThat(TextUtils.equals(captor.getValue().getAction(), + ADVANCED_VPN_LAUNCH_INTENT)).isTrue(); + } + + @Test + public void clickAdvancedVpn_VpnConnectedDisconnectDialogEnabled_doesNotStartAppLaunchIntent() + throws PackageManager.NameNotFoundException { + Set updates = new ArraySet<>(); + AppPreference pref = + spy(new AppPreference(mContext, USER_ID_1, ADVANCED_VPN_PACKAGE_NAME)); + pref.setState(AppPreference.STATE_CONNECTED); + updates.add(pref); + when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.isDisconnectDialogEnabled()) + .thenReturn(true); + when(mContext.createPackageContextAsUser(any(), anyInt(), any())).thenReturn(mContext); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.getLaunchIntentForPackage(any())).thenReturn(mAdvancedVpnIntent); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + doNothing().when(mContext).startActivityAsUser(captor.capture(), any()); + mVpnSettings.setShownAdvancedPreferences(updates); + + mVpnSettings.onPreferenceClick(pref); + + verify(mContext, never()).startActivityAsUser(any(), any()); + } } From 2a61b3656b4bea3a6270a43ad1d9eff3d479931c Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Mon, 15 Aug 2022 13:41:44 -0400 Subject: [PATCH 3/6] Update Bridged app link when NLS access changes Test: BridgedAppsLinkPreferenceControllerTest Test: manual; turn an NLS on/off, verify correct disabled state for fields Fixes: 240461761 Change-Id: Ib5a5365f7477c2a8d620ced2af96ace364b292e2 --- .../BridgedAppsLinkPreferenceController.java | 10 +++++++-- .../NotificationAccessDetails.java | 12 ++--------- ...idgedAppsLinkPreferenceControllerTest.java | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceController.java b/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceController.java index 1c787babad2..6a641b30fcc 100644 --- a/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceController.java +++ b/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceController.java @@ -18,10 +18,11 @@ import android.content.Context; import android.os.Build; import android.service.notification.NotificationListenerFilter; +import androidx.preference.Preference; + import com.android.settings.core.BasePreferenceController; import com.android.settings.notification.NotificationBackend; - public class BridgedAppsLinkPreferenceController extends BasePreferenceController { private ComponentName mCn; @@ -61,7 +62,6 @@ public class BridgedAppsLinkPreferenceController extends BasePreferenceControlle if (mTargetSdk > Build.VERSION_CODES.S) { return AVAILABLE; } - mNlf = mNm.getListenerFilter(mCn, mUserId); if (!mNlf.areAllTypesAllowed() || !mNlf.getDisallowedPackages().isEmpty()) { return AVAILABLE; @@ -69,4 +69,10 @@ public class BridgedAppsLinkPreferenceController extends BasePreferenceControlle } return DISABLED_DEPENDENT_SETTING; } + + @Override + public void updateState(Preference pref) { + pref.setEnabled(getAvailabilityStatus() == AVAILABLE); + super.updateState(pref); + } } diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java index da25f17c138..e6feebb92ab 100644 --- a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java +++ b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java @@ -233,11 +233,7 @@ public class NotificationAccessDetails extends DashboardFragment { apc.updateState(screen.findPreference(apc.getPreferenceKey())); getPreferenceControllers().forEach(controllers -> { controllers.forEach(controller -> { - if (controller instanceof TypeFilterPreferenceController) { - TypeFilterPreferenceController tfpc = - (TypeFilterPreferenceController) controller; - tfpc.updateState(screen.findPreference(tfpc.getPreferenceKey())); - } + controller.updateState(screen.findPreference(controller.getPreferenceKey())); }); }); } @@ -249,11 +245,7 @@ public class NotificationAccessDetails extends DashboardFragment { apc.updateState(screen.findPreference(apc.getPreferenceKey())); getPreferenceControllers().forEach(controllers -> { controllers.forEach(controller -> { - if (controller instanceof TypeFilterPreferenceController) { - TypeFilterPreferenceController tfpc = - (TypeFilterPreferenceController) controller; - tfpc.updateState(screen.findPreference(tfpc.getPreferenceKey())); - } + controller.updateState(screen.findPreference(controller.getPreferenceKey())); }); }); } diff --git a/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceControllerTest.java index c5941313946..87998798193 100644 --- a/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceControllerTest.java @@ -31,6 +31,7 @@ import android.content.Context; import android.os.Build; import android.service.notification.NotificationListenerFilter; +import androidx.preference.Preference; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -68,6 +69,11 @@ public class BridgedAppsLinkPreferenceControllerTest { mController.setTargetSdk(Build.VERSION_CODES.CUR_DEVELOPMENT + 1); assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING); + + // disables field + Preference p = new Preference(mContext); + mController.updateState(p); + assertThat(p.isEnabled()).isFalse(); } @Test @@ -77,6 +83,11 @@ public class BridgedAppsLinkPreferenceControllerTest { when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter()); assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING); + + // disables field + Preference p = new Preference(mContext); + mController.updateState(p); + assertThat(p.isEnabled()).isFalse(); } @Test @@ -88,6 +99,11 @@ public class BridgedAppsLinkPreferenceControllerTest { when(mNm.getListenerFilter(mCn, 0)).thenReturn(nlf); assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + + // enables field + Preference p = new Preference(mContext); + mController.updateState(p); + assertThat(p.isEnabled()).isTrue(); } @Test @@ -97,5 +113,10 @@ public class BridgedAppsLinkPreferenceControllerTest { when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter()); assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + + // enables field + Preference p = new Preference(mContext); + mController.updateState(p); + assertThat(p.isEnabled()).isTrue(); } } From d2c372450c0fb078cfb6bccf4fece67dc421768d Mon Sep 17 00:00:00 2001 From: Tsung-Mao Fang Date: Thu, 11 Aug 2022 13:18:23 +0800 Subject: [PATCH 4/6] Wi-Fi panel doesn't need to check permission Prior to this cl, we use #getPackagesForUid() to get a list of calling package names and pick up 1st package name in the list as target calling package. And then go to check the Wi-Fi permission. This implementation is ok for most apps without sharing system uid. However, this may not work if the package is set with sharing system ui. In this case, we get a list of packages and we don't know which one is caller. So, if we decide to choose the 1st package as our calling package, then it could fail to pass permission check since that package could be not a correct calling package. In this cl, we skip permission check for those packages running with system uid. So, it can resolve Wi-Fi Panel problem since Wi-Fi panel runs on settings process(with system uid). Test: 1. adb shell am start -a android.settings.panel.action.WIFI 2. Verify on assistant app and system ui launcher and search app. Bug: 240531998 Change-Id: Ia825853dde2e966e3d390cecfbe1a99f6439d31e Merged-In: Ia825853dde2e966e3d390cecfbe1a99f6439d31e --- res/values/config.xml | 6 ++++++ .../android/settings/wifi/slice/WifiSlice.java | 17 ++++++++++++++++- .../wifi/slice/ContextualWifiSliceTest.java | 1 + .../settings/wifi/slice/WifiSliceTest.java | 4 ++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/res/values/config.xml b/res/values/config.xml index 078c7591771..2f7031560a3 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -621,4 +621,10 @@ false + + + + @string/config_settingsintelligence_package_name + android.uid.system:1000 + diff --git a/src/com/android/settings/wifi/slice/WifiSlice.java b/src/com/android/settings/wifi/slice/WifiSlice.java index 76dfab8491b..8417d8b63a5 100644 --- a/src/com/android/settings/wifi/slice/WifiSlice.java +++ b/src/com/android/settings/wifi/slice/WifiSlice.java @@ -98,7 +98,7 @@ public class WifiSlice implements CustomSliceable { public Slice getSlice() { // If external calling package doesn't have Wi-Fi permission. final boolean isPermissionGranted = - Utils.isSettingsIntelligence(mContext) || isPermissionGranted(mContext); + isCallerExemptUid(mContext) || isPermissionGranted(mContext); final boolean isWifiEnabled = isWifiEnabled(); ListBuilder listBuilder = getListBuilder(isWifiEnabled, null /* wifiSliceItem */, isPermissionGranted); @@ -139,6 +139,21 @@ public class WifiSlice implements CustomSliceable { return listBuilder.build(); } + private boolean isCallerExemptUid(Context context) { + final String[] allowedUidNames = context.getResources().getStringArray( + R.array.config_exempt_wifi_permission_uid_name); + final String uidName = + context.getPackageManager().getNameForUid(Binder.getCallingUid()); + Log.d(TAG, "calling uid name : " + uidName); + + for (String allowedUidName : allowedUidNames) { + if (TextUtils.equals(uidName, allowedUidName)) { + return true; + } + } + return false; + } + private static boolean isPermissionGranted(Context settingsContext) { final int callingUid = Binder.getCallingUid(); final String callingPackage = settingsContext.getPackageManager() diff --git a/tests/robotests/src/com/android/settings/wifi/slice/ContextualWifiSliceTest.java b/tests/robotests/src/com/android/settings/wifi/slice/ContextualWifiSliceTest.java index 52dcb5282da..d9c726ab058 100644 --- a/tests/robotests/src/com/android/settings/wifi/slice/ContextualWifiSliceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/slice/ContextualWifiSliceTest.java @@ -103,6 +103,7 @@ public class ContextualWifiSliceTest { mContext.getString(R.string.config_settingsintelligence_package_name); ShadowBinder.setCallingUid(1); when(mPackageManager.getPackagesForUid(1)).thenReturn(new String[]{siPackageName}); + when(mPackageManager.getNameForUid(1)).thenReturn(siPackageName); ShadowWifiSlice.setWifiPermissible(true); mWifiSlice = new ContextualWifiSlice(mContext); } diff --git a/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java b/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java index 8e42fcd225a..67ab7b49825 100644 --- a/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java @@ -109,6 +109,7 @@ public class WifiSliceTest { mSIPackageName = mContext.getString(R.string.config_settingsintelligence_package_name); ShadowBinder.setCallingUid(USER_ID); when(mPackageManager.getPackagesForUid(USER_ID)).thenReturn(new String[]{mSIPackageName}); + when(mPackageManager.getNameForUid(USER_ID)).thenReturn(mSIPackageName); ShadowWifiSlice.setWifiPermissible(true); mWifiSlice = new WifiSlice(mContext, mWifiRestriction); } @@ -116,6 +117,7 @@ public class WifiSliceTest { @Test public void getWifiSlice_fromSIPackage_shouldHaveTitleAndToggle() { when(mPackageManager.getPackagesForUid(USER_ID)).thenReturn(new String[]{mSIPackageName}); + when(mPackageManager.getNameForUid(USER_ID)).thenReturn(mSIPackageName); ShadowWifiSlice.setWifiPermissible(false); final Slice wifiSlice = mWifiSlice.getSlice(); @@ -131,6 +133,7 @@ public class WifiSliceTest { @Test public void getWifiSlice_notFromSIPackageAndWithWifiPermission_shouldHaveTitleAndToggle() { when(mPackageManager.getPackagesForUid(USER_ID)).thenReturn(new String[]{"com.test"}); + when(mPackageManager.getNameForUid(USER_ID)).thenReturn("com.test"); ShadowWifiSlice.setWifiPermissible(true); final Slice wifiSlice = mWifiSlice.getSlice(); @@ -145,6 +148,7 @@ public class WifiSliceTest { @Test public void getWifiSlice_notFromSIPackageAndWithoutWifiPermission_shouldReturnNoToggle() { when(mPackageManager.getPackagesForUid(USER_ID)).thenReturn(new String[]{"com.test"}); + when(mPackageManager.getNameForUid(USER_ID)).thenReturn("com.test"); ShadowWifiSlice.setWifiPermissible(false); final Slice wifiSlice = mWifiSlice.getSlice(); From 359929732d18d37fcebb50f176197c499fe9ae8f Mon Sep 17 00:00:00 2001 From: Chloris Kuo Date: Tue, 9 Aug 2022 13:30:55 -0700 Subject: [PATCH 5/6] Fix settings crash when enhanced notification shown in search results init NotificationBackend in constructor and set isSliceable to false in search result since the NAS enabling logic is required and is in the parent fragment ConfigureNotificationSettings. Bug: 237251075 Test: test manually on device Change-Id: I9082d6eda27784cf378a0d06304b5fc1e2ae6d7f (cherry picked from commit 26ce9a98f0b4d8bff96e9225abe49466473cf4fc) --- .../notification/ConfigureNotificationSettings.java | 1 - .../NotificationAssistantPreferenceController.java | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/notification/ConfigureNotificationSettings.java b/src/com/android/settings/notification/ConfigureNotificationSettings.java index f888ea7c8e0..19222612f3a 100644 --- a/src/com/android/settings/notification/ConfigureNotificationSettings.java +++ b/src/com/android/settings/notification/ConfigureNotificationSettings.java @@ -107,7 +107,6 @@ public class ConfigureNotificationSettings extends DashboardFragment implements mNotificationAssistantPreferenceController = use(NotificationAssistantPreferenceController.class); mNotificationAssistantPreferenceController.setFragment(this); - mNotificationAssistantPreferenceController.setBackend(new NotificationBackend()); } private static List buildPreferenceControllers(Context context, diff --git a/src/com/android/settings/notification/NotificationAssistantPreferenceController.java b/src/com/android/settings/notification/NotificationAssistantPreferenceController.java index 91031c8e4de..a6179e5306f 100644 --- a/src/com/android/settings/notification/NotificationAssistantPreferenceController.java +++ b/src/com/android/settings/notification/NotificationAssistantPreferenceController.java @@ -44,6 +44,7 @@ public class NotificationAssistantPreferenceController extends TogglePreferenceC public NotificationAssistantPreferenceController(Context context) { super(context, KEY_NAS); mUserManager = UserManager.get(context); + mNotificationBackend = new NotificationBackend(); } @Override @@ -101,4 +102,9 @@ public class NotificationAssistantPreferenceController extends TogglePreferenceC void setBackend(NotificationBackend backend) { mNotificationBackend = backend; } -} \ No newline at end of file + + @Override + public boolean isSliceable() { + return (mFragment != null && mFragment instanceof ConfigureNotificationSettings); + } +} From 5e785a2d99a5d3410c9a8049225e17df4c7790d3 Mon Sep 17 00:00:00 2001 From: Tsung-Mao Fang Date: Thu, 11 Aug 2022 13:18:23 +0800 Subject: [PATCH 6/6] Wi-Fi panel doesn't need to check permission Prior to this cl, we use #getPackagesForUid() to get a list of calling package names and pick up 1st package name in the list as target calling package. And then go to check the Wi-Fi permission. This implementation is ok for most apps without sharing system uid. However, this may not work if the package is set with sharing system ui. In this case, we get a list of packages and we don't know which one is caller. So, if we decide to choose the 1st package as our calling package, then it could fail to pass permission check since that package could be not a correct calling package. In this cl, we skip permission check for those packages running with system uid. So, it can resolve Wi-Fi Panel problem since Wi-Fi panel runs on settings process(with system uid). Test: 1. adb shell am start -a android.settings.panel.action.WIFI 2. Verify on assistant app and system ui launcher and search app. Bug: 240531998 Change-Id: Ia825853dde2e966e3d390cecfbe1a99f6439d31e Merged-In: Ia825853dde2e966e3d390cecfbe1a99f6439d31e --- res/values/config.xml | 6 ++++++ .../android/settings/wifi/slice/WifiSlice.java | 17 ++++++++++++++++- .../wifi/slice/ContextualWifiSliceTest.java | 1 + .../settings/wifi/slice/WifiSliceTest.java | 4 ++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/res/values/config.xml b/res/values/config.xml index c7ef595e3ae..d7528c63bd0 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -617,4 +617,10 @@ false + + + + @string/config_settingsintelligence_package_name + android.uid.system:1000 + diff --git a/src/com/android/settings/wifi/slice/WifiSlice.java b/src/com/android/settings/wifi/slice/WifiSlice.java index 76dfab8491b..8417d8b63a5 100644 --- a/src/com/android/settings/wifi/slice/WifiSlice.java +++ b/src/com/android/settings/wifi/slice/WifiSlice.java @@ -98,7 +98,7 @@ public class WifiSlice implements CustomSliceable { public Slice getSlice() { // If external calling package doesn't have Wi-Fi permission. final boolean isPermissionGranted = - Utils.isSettingsIntelligence(mContext) || isPermissionGranted(mContext); + isCallerExemptUid(mContext) || isPermissionGranted(mContext); final boolean isWifiEnabled = isWifiEnabled(); ListBuilder listBuilder = getListBuilder(isWifiEnabled, null /* wifiSliceItem */, isPermissionGranted); @@ -139,6 +139,21 @@ public class WifiSlice implements CustomSliceable { return listBuilder.build(); } + private boolean isCallerExemptUid(Context context) { + final String[] allowedUidNames = context.getResources().getStringArray( + R.array.config_exempt_wifi_permission_uid_name); + final String uidName = + context.getPackageManager().getNameForUid(Binder.getCallingUid()); + Log.d(TAG, "calling uid name : " + uidName); + + for (String allowedUidName : allowedUidNames) { + if (TextUtils.equals(uidName, allowedUidName)) { + return true; + } + } + return false; + } + private static boolean isPermissionGranted(Context settingsContext) { final int callingUid = Binder.getCallingUid(); final String callingPackage = settingsContext.getPackageManager() diff --git a/tests/robotests/src/com/android/settings/wifi/slice/ContextualWifiSliceTest.java b/tests/robotests/src/com/android/settings/wifi/slice/ContextualWifiSliceTest.java index 52dcb5282da..d9c726ab058 100644 --- a/tests/robotests/src/com/android/settings/wifi/slice/ContextualWifiSliceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/slice/ContextualWifiSliceTest.java @@ -103,6 +103,7 @@ public class ContextualWifiSliceTest { mContext.getString(R.string.config_settingsintelligence_package_name); ShadowBinder.setCallingUid(1); when(mPackageManager.getPackagesForUid(1)).thenReturn(new String[]{siPackageName}); + when(mPackageManager.getNameForUid(1)).thenReturn(siPackageName); ShadowWifiSlice.setWifiPermissible(true); mWifiSlice = new ContextualWifiSlice(mContext); } diff --git a/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java b/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java index 8e42fcd225a..67ab7b49825 100644 --- a/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/slice/WifiSliceTest.java @@ -109,6 +109,7 @@ public class WifiSliceTest { mSIPackageName = mContext.getString(R.string.config_settingsintelligence_package_name); ShadowBinder.setCallingUid(USER_ID); when(mPackageManager.getPackagesForUid(USER_ID)).thenReturn(new String[]{mSIPackageName}); + when(mPackageManager.getNameForUid(USER_ID)).thenReturn(mSIPackageName); ShadowWifiSlice.setWifiPermissible(true); mWifiSlice = new WifiSlice(mContext, mWifiRestriction); } @@ -116,6 +117,7 @@ public class WifiSliceTest { @Test public void getWifiSlice_fromSIPackage_shouldHaveTitleAndToggle() { when(mPackageManager.getPackagesForUid(USER_ID)).thenReturn(new String[]{mSIPackageName}); + when(mPackageManager.getNameForUid(USER_ID)).thenReturn(mSIPackageName); ShadowWifiSlice.setWifiPermissible(false); final Slice wifiSlice = mWifiSlice.getSlice(); @@ -131,6 +133,7 @@ public class WifiSliceTest { @Test public void getWifiSlice_notFromSIPackageAndWithWifiPermission_shouldHaveTitleAndToggle() { when(mPackageManager.getPackagesForUid(USER_ID)).thenReturn(new String[]{"com.test"}); + when(mPackageManager.getNameForUid(USER_ID)).thenReturn("com.test"); ShadowWifiSlice.setWifiPermissible(true); final Slice wifiSlice = mWifiSlice.getSlice(); @@ -145,6 +148,7 @@ public class WifiSliceTest { @Test public void getWifiSlice_notFromSIPackageAndWithoutWifiPermission_shouldReturnNoToggle() { when(mPackageManager.getPackagesForUid(USER_ID)).thenReturn(new String[]{"com.test"}); + when(mPackageManager.getNameForUid(USER_ID)).thenReturn("com.test"); ShadowWifiSlice.setWifiPermissible(false); final Slice wifiSlice = mWifiSlice.getSlice();