From 66c709844aa8a2db7c3501093f650333c6b644f3 Mon Sep 17 00:00:00 2001 From: Brad Ebinger Date: Thu, 27 Feb 2020 19:14:15 -0800 Subject: [PATCH] Add new DialogFragment and Controller for capability discovery opt-in Adds a new controller to monitor the capability discovery opt-in setting as well as a new DialogFragment, which displays a dialog providing the user with more information before they enable the setting. Also removes multiple updateSubscriptions() happening when the activity is first created from onStart() and onChanged() callbacks. Bug: 111305845 Test: manual Change-Id: I70821964bc618c3c389c9039cd7f5028e34c7ebb --- AndroidManifest.xml | 2 + res/values/strings.xml | 13 ++ res/xml/mobile_network_settings.xml | 7 + .../ContactDiscoveryDialogFragment.java | 104 +++++++++++ .../ContactDiscoveryPreferenceController.java | 136 ++++++++++++++ .../telephony/MobileNetworkActivity.java | 91 +++++++++- .../telephony/MobileNetworkSettings.java | 2 + .../network/telephony/MobileNetworkUtils.java | 76 ++++++++ .../ContactDiscoveryDialogFragmentTest.java | 89 ++++++++++ ...tactDiscoveryPreferenceControllerTest.java | 168 ++++++++++++++++++ 10 files changed, 681 insertions(+), 7 deletions(-) create mode 100644 src/com/android/settings/network/telephony/ContactDiscoveryDialogFragment.java create mode 100644 src/com/android/settings/network/telephony/ContactDiscoveryPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/network/telephony/ContactDiscoveryDialogFragmentTest.java create mode 100644 tests/robotests/src/com/android/settings/network/telephony/ContactDiscoveryPreferenceControllerTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b4c88bae84b..dc220f2b038 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -137,6 +137,8 @@ android:theme="@style/Theme.Settings.Home" android:launchMode="singleTask"> + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 81c76e4a6f5..c029df25e61 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7204,6 +7204,19 @@ Use LTE services to improve voice and other communications (recommended) Use 4G services to improve voice and other communications (recommended) + + Contact discovery + + Allows your carrier to discover which calling features your contacts support. + + Enable contact discovery? + + Enabling this feature will allow your carrier access to phone numbers in your contacts in order to discover which calling features they support. Preferred network type diff --git a/res/xml/mobile_network_settings.xml b/res/xml/mobile_network_settings.xml index 732967d2c6d..17445135d0e 100644 --- a/res/xml/mobile_network_settings.xml +++ b/res/xml/mobile_network_settings.xml @@ -116,6 +116,13 @@ settings:keywords="@string/keywords_enhance_4g_lte" settings:controller="com.android.settings.network.telephony.Enhanced4gAdvancedCallingPreferenceController"/> + + onNewIntent, so the dialog will first be recreated for the old subscription + // and then removed. + if (updateSubscriptionIndex != oldSubId || !doesIntentContainOptInAction(intent)) { + removeContactDiscoveryDialog(oldSubId); + } + // evaluate showing the new discovery dialog if this intent contains an action to show the + // opt-in. + if (doesIntentContainOptInAction(intent)) { + maybeShowContactDiscoveryDialog(updateSubscriptionIndex); + } } @Override @@ -91,6 +107,7 @@ public class MobileNetworkActivity extends SettingsBaseActivity mProxySubscriptionMgr.addActiveSubscriptionsListener(this); final Intent startIntent = getIntent(); + validate(startIntent); mCurSubscriptionId = savedInstanceState != null ? savedInstanceState.getInt(Settings.EXTRA_SUB_ID, SUB_ID_NULL) : ((startIntent != null) @@ -99,20 +116,29 @@ public class MobileNetworkActivity extends SettingsBaseActivity final SubscriptionInfo subscription = getSubscription(); updateTitleAndNavigation(subscription); + maybeShowContactDiscoveryDialog(mCurSubscriptionId); } /** * Implementation of ProxySubscriptionManager.OnActiveSubscriptionChangedListener */ public void onChanged() { - updateSubscriptions(getSubscription()); + SubscriptionInfo info = getSubscription(); + int oldSubIndex = mCurSubscriptionId; + int subIndex = info.getSubscriptionId(); + updateSubscriptions(info); + // Remove the dialog if the subscription associated with this activity changes. + if (subIndex != oldSubIndex) { + removeContactDiscoveryDialog(oldSubIndex); + } } @Override protected void onStart() { mProxySubscriptionMgr.setLifecycle(getLifecycle()); super.onStart(); - updateSubscriptions(getSubscription()); + // updateSubscriptions doesn't need to be called, onChanged will always be called after we + // register a listener. } @Override @@ -193,12 +219,63 @@ public class MobileNetworkActivity extends SettingsBaseActivity fragmentTransaction.commit(); } + private void removeContactDiscoveryDialog(int subId) { + ContactDiscoveryDialogFragment fragment = getContactDiscoveryFragment(subId); + if (fragment != null) { + fragment.dismiss(); + } + } + + private ContactDiscoveryDialogFragment getContactDiscoveryFragment(int subId) { + // In the case that we are rebuilding this activity after it has been destroyed and + // recreated, look up the dialog in the fragment manager. + return (ContactDiscoveryDialogFragment) getSupportFragmentManager() + .findFragmentByTag(ContactDiscoveryDialogFragment.getFragmentTag(subId)); + } + + private void maybeShowContactDiscoveryDialog(int subId) { + // If this activity was launched using ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN, show the + // associated dialog only if the opt-in has not been granted yet. + boolean showOptInDialog = doesIntentContainOptInAction(getIntent()) + // has the carrier config enabled capability discovery? + && MobileNetworkUtils.isContactDiscoveryVisible(this, subId) + // has the user already enabled this configuration? + && !MobileNetworkUtils.isContactDiscoveryEnabled(this, subId); + ContactDiscoveryDialogFragment fragment = getContactDiscoveryFragment(subId); + if (showOptInDialog) { + if (fragment == null) { + fragment = ContactDiscoveryDialogFragment.newInstance(subId); + } + // Only try to show the dialog if it has not already been added, otherwise we may + // accidentally add it multiple times, causing multiple dialogs. + if (!fragment.isAdded()) { + fragment.show(getSupportFragmentManager(), + ContactDiscoveryDialogFragment.getFragmentTag(subId)); + } + } + } + + private boolean doesIntentContainOptInAction(Intent intent) { + String intentAction = (intent != null ? intent.getAction() : null); + return TextUtils.equals(intentAction, + ImsRcsManager.ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN); + } + + private void validate(Intent intent) { + // Do not allow ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN without a subscription id specified, + // since we do not want the user to accidentally turn on capability polling for the wrong + // subscription. + if (doesIntentContainOptInAction(intent)) { + if (SUB_ID_NULL == intent.getIntExtra(Settings.EXTRA_SUB_ID, SUB_ID_NULL)) { + throw new IllegalArgumentException("Intent with action " + + "SHOW_CAPABILITY_DISCOVERY_OPT_IN must also include the extra " + + "Settings#EXTRA_SUB_ID"); + } + } + } + @VisibleForTesting String buildFragmentTag(int subscriptionId) { return MOBILE_SETTINGS_TAG + subscriptionId; } - - private boolean isSubscriptionChanged(int subscriptionId) { - return (subscriptionId == SUB_ID_NULL) || (subscriptionId != mCurSubscriptionId); - } } diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index e5ba96ab469..ae60dafd7c6 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -177,6 +177,8 @@ public class MobileNetworkSettings extends RestrictedDashboardFragment { .addListener(videoCallingPreferenceController); use(Enhanced4gAdvancedCallingPreferenceController.class).init(mSubId) .addListener(videoCallingPreferenceController); + use(ContactDiscoveryPreferenceController.class).init(getParentFragmentManager(), mSubId, + getLifecycle()); } @Override diff --git a/src/com/android/settings/network/telephony/MobileNetworkUtils.java b/src/com/android/settings/network/telephony/MobileNetworkUtils.java index 1e3cd428588..b505c2fddcf 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkUtils.java +++ b/src/com/android/settings/network/telephony/MobileNetworkUtils.java @@ -47,7 +47,9 @@ import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.telephony.euicc.EuiccManager; +import android.telephony.ims.ImsRcsManager; import android.telephony.ims.ProvisioningManager; +import android.telephony.ims.RcsUceAdapter; import android.telephony.ims.feature.ImsFeature; import android.telephony.ims.feature.MmTelFeature; import android.telephony.ims.stub.ImsRegistrationImplBase; @@ -163,6 +165,80 @@ public class MobileNetworkUtils { return isWifiCallingEnabled; } + /** + * @return The current user setting for whether or not contact discovery is enabled for the + * subscription id specified. + * @see RcsUceAdapter#isUceSettingEnabled() + */ + public static boolean isContactDiscoveryEnabled(Context context, int subId) { + android.telephony.ims.ImsManager imsManager = + context.getSystemService(android.telephony.ims.ImsManager.class); + return isContactDiscoveryEnabled(imsManager, subId); + } + + /** + * @return The current user setting for whether or not contact discovery is enabled for the + * subscription id specified. + * @see RcsUceAdapter#isUceSettingEnabled() + */ + public static boolean isContactDiscoveryEnabled(android.telephony.ims.ImsManager imsManager, + int subId) { + ImsRcsManager manager = getImsRcsManager(imsManager, subId); + if (manager == null) return false; + RcsUceAdapter adapter = manager.getUceAdapter(); + try { + return adapter.isUceSettingEnabled(); + } catch (android.telephony.ims.ImsException e) { + Log.w(TAG, "UCE service is not available: " + e.getMessage()); + } + return false; + } + + /** + * Set the new user setting to enable or disable contact discovery through RCS UCE. + * @see RcsUceAdapter#setUceSettingEnabled(boolean) + */ + public static void setContactDiscoveryEnabled(android.telephony.ims.ImsManager imsManager, + int subId, boolean isEnabled) { + ImsRcsManager manager = getImsRcsManager(imsManager, subId); + if (manager == null) return; + RcsUceAdapter adapter = manager.getUceAdapter(); + try { + adapter.setUceSettingEnabled(isEnabled); + } catch (android.telephony.ims.ImsException e) { + Log.w(TAG, "UCE service is not available: " + e.getMessage()); + } + } + + /** + * @return The ImsRcsManager associated with the subscription specified. + */ + private static ImsRcsManager getImsRcsManager(android.telephony.ims.ImsManager imsManager, + int subId) { + if (imsManager == null) return null; + try { + return imsManager.getImsRcsManager(subId); + } catch (Exception e) { + Log.w(TAG, "Could not resolve ImsRcsManager: " + e.getMessage()); + } + return null; + } + + /** + * @return true if contact discovery is available for the subscription specified and the option + * should be shown to the user, false if the option should be hidden. + */ + public static boolean isContactDiscoveryVisible(Context context, int subId) { + CarrierConfigManager carrierConfigManager = context.getSystemService( + CarrierConfigManager.class); + if (carrierConfigManager == null) { + Log.w(TAG, "isContactDiscoveryVisible: Could not resolve carrier config"); + return false; + } + PersistableBundle bundle = carrierConfigManager.getConfigForSubId(subId); + return bundle.getBoolean(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, false /*default*/); + } + @VisibleForTesting static Intent buildPhoneAccountConfigureIntent( Context context, PhoneAccountHandle accountHandle) { diff --git a/tests/robotests/src/com/android/settings/network/telephony/ContactDiscoveryDialogFragmentTest.java b/tests/robotests/src/com/android/settings/network/telephony/ContactDiscoveryDialogFragmentTest.java new file mode 100644 index 00000000000..17c121a19b2 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/telephony/ContactDiscoveryDialogFragmentTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 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.network.telephony; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +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 android.content.DialogInterface; +import android.telephony.ims.ImsManager; +import android.telephony.ims.ImsRcsManager; +import android.telephony.ims.RcsUceAdapter; +import android.widget.Button; + +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.Robolectric; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ContactDiscoveryDialogFragmentTest { + private static final int TEST_SUB_ID = 2; + + @Mock private ImsManager mImsManager; + @Mock private ImsRcsManager mImsRcsManager; + @Mock private RcsUceAdapter mRcsUceAdapter; + + private ContactDiscoveryDialogFragment mDialogFragmentUT; + private FragmentActivity mActivity; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mActivity = Robolectric.buildActivity(FragmentActivity.class).setup().get(); + mDialogFragmentUT = spy(ContactDiscoveryDialogFragment.newInstance(TEST_SUB_ID)); + doReturn(mImsManager).when(mDialogFragmentUT).getImsManager(any()); + doReturn(mImsRcsManager).when(mImsManager).getImsRcsManager(TEST_SUB_ID); + doReturn(mRcsUceAdapter).when(mImsRcsManager).getUceAdapter(); + } + + @Test + public void testCancelDoesNothing() throws Exception { + final AlertDialog dialog = startDialog(); + final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + assertThat(negativeButton).isNotNull(); + negativeButton.performClick(); + verify(mRcsUceAdapter, never()).setUceSettingEnabled(any()); + } + + @Test + public void testOkEnablesDiscovery() throws Exception { + final AlertDialog dialog = startDialog(); + final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + assertThat(positiveButton).isNotNull(); + positiveButton.performClick(); + verify(mRcsUceAdapter).setUceSettingEnabled(true /*isEnabled*/); + } + + private AlertDialog startDialog() { + mDialogFragmentUT.show(mActivity.getSupportFragmentManager(), null); + return ShadowAlertDialogCompat.getLatestAlertDialog(); + } +} diff --git a/tests/robotests/src/com/android/settings/network/telephony/ContactDiscoveryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/telephony/ContactDiscoveryPreferenceControllerTest.java new file mode 100644 index 00000000000..45a4563f55e --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/telephony/ContactDiscoveryPreferenceControllerTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2020 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.network.telephony; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.PersistableBundle; +import android.provider.Telephony; +import android.telephony.CarrierConfigManager; +import android.telephony.ims.ImsManager; +import android.telephony.ims.ImsRcsManager; +import android.telephony.ims.RcsUceAdapter; + +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.SwitchPreference; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ContactDiscoveryPreferenceControllerTest { + + private static final int TEST_SUB_ID = 2; + private static final Uri UCE_URI = Uri.withAppendedPath(Telephony.SimInfo.CONTENT_URI, + Telephony.SimInfo.IMS_RCS_UCE_ENABLED); + + @Mock private ImsManager mImsManager; + @Mock private ImsRcsManager mImsRcsManager; + @Mock private RcsUceAdapter mRcsUceAdapter; + @Mock private CarrierConfigManager mCarrierConfigManager; + @Mock private ContentResolver mContentResolver; + @Mock private FragmentManager mFragmentManager; + @Mock private FragmentTransaction mFragmentTransaction; + + private Context mContext; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private ContactDiscoveryPreferenceController mPreferenceControllerUT; + private SwitchPreference mSwitchPreferenceUT; + private PersistableBundle mCarrierConfig = new PersistableBundle(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + mContext = spy(RuntimeEnvironment.application); + doReturn(mImsManager).when(mContext).getSystemService(ImsManager.class); + doReturn(mImsRcsManager).when(mImsManager).getImsRcsManager(anyInt()); + doReturn(mRcsUceAdapter).when(mImsRcsManager).getUceAdapter(); + doReturn(mCarrierConfigManager).when(mContext).getSystemService(CarrierConfigManager.class); + doReturn(mCarrierConfig).when(mCarrierConfigManager).getConfigForSubId(eq(TEST_SUB_ID)); + // Start all tests with presence being disabled. + setRcsPresenceConfig(false); + doReturn(mContentResolver).when(mContext).getContentResolver(); + doReturn(mFragmentTransaction).when(mFragmentManager).beginTransaction(); + + mPreferenceControllerUT = new ContactDiscoveryPreferenceController(mContext, + "ContactDiscovery"); + mPreferenceControllerUT.init(mFragmentManager, TEST_SUB_ID, mLifecycle); + mSwitchPreferenceUT = spy(new SwitchPreference(mContext)); + mSwitchPreferenceUT.setKey(mPreferenceControllerUT.getPreferenceKey()); + mPreferenceControllerUT.preference = mSwitchPreferenceUT; + } + + @Test + public void testGetAvailabilityStatus() { + assertEquals("Availability status should not be available.", CONDITIONALLY_UNAVAILABLE, + mPreferenceControllerUT.getAvailabilityStatus(TEST_SUB_ID)); + setRcsPresenceConfig(true); + assertEquals("Availability status should available.", AVAILABLE, + mPreferenceControllerUT.getAvailabilityStatus(TEST_SUB_ID)); + } + + @Test + public void testIsChecked() throws Exception { + doReturn(false).when(mRcsUceAdapter).isUceSettingEnabled(); + assertFalse(mPreferenceControllerUT.isChecked()); + + doReturn(true).when(mRcsUceAdapter).isUceSettingEnabled(); + assertTrue(mPreferenceControllerUT.isChecked()); + } + + @Test + public void testRegisterObserver() { + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + verify(mContentResolver).registerContentObserver(eq(UCE_URI), anyBoolean(), any()); + + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); + verify(mContentResolver).unregisterContentObserver(any()); + } + + @Test + public void testContentObserverChanged() throws Exception { + assertFalse(mSwitchPreferenceUT.isChecked()); + ContentObserver observer = getUceChangeObserver(); + assertNotNull(observer); + + doReturn(true).when(mRcsUceAdapter).isUceSettingEnabled(); + observer.onChange(false, UCE_URI); + assertTrue(mSwitchPreferenceUT.isChecked()); + } + + @Test + public void testSetChecked() throws Exception { + // Verify a dialog is shown when the switch is enabled (but the switch is not enabled). + assertFalse(mPreferenceControllerUT.setChecked(true /*isChecked*/)); + verify(mFragmentTransaction).add(any(), anyString()); + // Verify content discovery is disabled when the user disables it. + assertTrue(mPreferenceControllerUT.setChecked(false /*isChecked*/)); + verify(mRcsUceAdapter).setUceSettingEnabled(false); + } + + private void setRcsPresenceConfig(boolean isEnabled) { + mCarrierConfig.putBoolean(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, isEnabled); + } + + private ContentObserver getUceChangeObserver() { + ArgumentCaptor observerCaptor = + ArgumentCaptor.forClass(ContentObserver.class); + mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + verify(mContentResolver).registerContentObserver(eq(UCE_URI), anyBoolean(), + observerCaptor.capture()); + return observerCaptor.getValue(); + } +}