diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 21eaaf5fd40..8f3e0b51860 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -136,6 +136,8 @@ android:theme="@style/Theme.Settings.Home" android:launchMode="singleTask"> + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 2bc43e9a70d..3c4de47034d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6990,6 +6990,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 1a9b88508f0..2c5fef66778 100644 --- a/res/xml/mobile_network_settings.xml +++ b/res/xml/mobile_network_settings.xml @@ -52,6 +52,13 @@ android:summary="@string/enhanced_4g_lte_mode_summary" settings:controller="com.android.settings.network.telephony.Enhanced4gLtePreferenceController"/> + + + + 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 protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (FeatureFlagPersistent.isEnabled(this, FeatureFlags.NETWORK_INTERNET_V2)) { setContentView(R.layout.mobile_network_settings_container_v2); } else { @@ -109,6 +135,8 @@ public class MobileNetworkActivity extends SettingsBaseActivity { }); mSubscriptionManager = getSystemService(SubscriptionManager.class); mSubscriptionInfos = mSubscriptionManager.getActiveSubscriptionInfoList(true); + final Intent startIntent = getIntent(); + validate(startIntent); mCurSubscriptionId = savedInstanceState != null ? savedInstanceState.getInt(Settings.EXTRA_SUB_ID, SUB_ID_NULL) : SUB_ID_NULL; @@ -117,8 +145,8 @@ public class MobileNetworkActivity extends SettingsBaseActivity { if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } - updateSubscriptions(savedInstanceState); + maybeShowContactDiscoveryDialog(mCurSubscriptionId); } @Override @@ -250,6 +278,7 @@ public class MobileNetworkActivity extends SettingsBaseActivity { mCurSubscriptionId = subscriptionId; } + @VisibleForTesting private String buildFragmentTag(int subscriptionId) { return MOBILE_SETTINGS_TAG + subscriptionId; } @@ -295,4 +324,60 @@ public class MobileNetworkActivity extends SettingsBaseActivity { mClient.onPhoneChange(); } } + + 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 (SubscriptionManager.INVALID_SUBSCRIPTION_ID == intent.getIntExtra( + Settings.EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID)) { + throw new IllegalArgumentException("Intent with action " + + "SHOW_CAPABILITY_DISCOVERY_OPT_IN must also include the extra " + + "Settings#EXTRA_SUB_ID"); + } + } + } } diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index 004291aa8bc..30ea0c0c056 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -178,6 +178,8 @@ public class MobileNetworkSettings extends RestrictedDashboardFragment { Arrays.asList(wifiCallingPreferenceController, videoCallingPreferenceController)); use(Enhanced4gLtePreferenceController.class).init(mSubId) .addListener(videoCallingPreferenceController); + use(ContactDiscoveryPreferenceController.class).init(getFragmentManager(), mSubId, + getLifecycle()); } @Override diff --git a/src/com/android/settings/network/telephony/MobileNetworkUtils.java b/src/com/android/settings/network/telephony/MobileNetworkUtils.java index 75e0bb12698..ad84e5989ba 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkUtils.java +++ b/src/com/android/settings/network/telephony/MobileNetworkUtils.java @@ -38,7 +38,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; @@ -155,6 +157,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..0370bfa00c6 --- /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.COLUMN_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(); + } +}