diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 44be1ef6df9..a8e0390d4d8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -249,12 +249,14 @@ android:name=".Settings$MobileNetworkActivity" android:label="@string/network_settings_title" android:configChanges="orientation|screenSize|keyboardHidden" - android:launchMode="singleInstance" + android:launchMode="singleTop" android:exported="true"> + + diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index ee0743a5752..c3ab8e25b35 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -22,8 +22,6 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; import android.telephony.ims.ImsRcsManager; import android.text.TextUtils; import android.util.FeatureFlagUtils; @@ -33,8 +31,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.settings.biometrics.face.FaceSettings; import com.android.settings.core.FeatureFlags; import com.android.settings.enterprise.EnterprisePrivacySettings; -import com.android.settings.network.SubscriptionUtil; -import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settings.network.MobileNetworkIntentConverter; import com.android.settings.overlay.FeatureFactory; import com.android.settings.safetycenter.SafetyCenterManagerWrapper; import com.android.settings.security.SecuritySettingsFeatureProvider; @@ -370,41 +367,37 @@ public class Settings extends SettingsActivity { public static class PowerMenuSettingsActivity extends SettingsActivity {} public static class MobileNetworkActivity extends SettingsActivity { + public static final String TAG = "MobileNetworkActivity"; public static final String EXTRA_MMS_MESSAGE = "mms_message"; public static final String EXTRA_SHOW_CAPABILITY_DISCOVERY_OPT_IN = "show_capability_discovery_opt_in"; + private MobileNetworkIntentConverter mIntentConverter; + + /** + * Override of #onNewIntent() requires Activity to have "singleTop" launch mode within + * AndroidManifest.xml + */ @Override - public Intent getIntent() { - final Intent intent = new Intent(super.getIntent()); - int subId = intent.getIntExtra(android.provider.Settings.EXTRA_SUB_ID, - SubscriptionManager.INVALID_SUBSCRIPTION_ID); - SubscriptionInfo subInfo = SubscriptionUtil.getSubscriptionOrDefault( - getApplicationContext(), subId); - CharSequence title = SubscriptionUtil.getUniqueSubscriptionDisplayName( - subInfo, getApplicationContext()); - intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, title); - intent.putExtra(android.provider.Settings.EXTRA_SUB_ID, subId); - if (android.provider.Settings.ACTION_MMS_MESSAGE_SETTING.equals(intent.getAction())) { - // highlight "mms_message" preference. - intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, EXTRA_MMS_MESSAGE); - } + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); - if (doesIntentContainOptInAction(intent)) { - intent.putExtra(EXTRA_SHOW_CAPABILITY_DISCOVERY_OPT_IN, - maybeShowContactDiscoveryDialog(subId)); - } + Log.d(TAG, "Starting onNewIntent"); - return intent; + createUiFromIntent(null /* savedState */, convertIntent(intent)); } - private boolean 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. - return MobileNetworkUtils.isContactDiscoveryVisible(getApplicationContext(), subId) - // has the user already enabled this configuration? - && !MobileNetworkUtils.isContactDiscoveryEnabled( - getApplicationContext(), subId); + @Override + public Intent getIntent() { + return convertIntent(super.getIntent()); + } + + private Intent convertIntent(Intent copyFrom) { + if (mIntentConverter == null) { + mIntentConverter = new MobileNetworkIntentConverter(this); + } + Intent intent = mIntentConverter.apply(copyFrom); + return (intent == null) ? copyFrom : intent; } public static boolean doesIntentContainOptInAction(Intent intent) { diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index 1a9bdc742fe..f9515017406 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -264,7 +264,10 @@ public class SettingsActivity extends SettingsBaseActivity super.onCreate(savedState); Log.d(LOG_TAG, "Starting onCreate"); + createUiFromIntent(savedState, intent); + } + protected void createUiFromIntent(Bundle savedState, Intent intent) { long startTime = System.currentTimeMillis(); final FeatureFactory factory = FeatureFactory.getFactory(this); diff --git a/src/com/android/settings/network/MobileNetworkIntentConverter.java b/src/com/android/settings/network/MobileNetworkIntentConverter.java new file mode 100644 index 00000000000..648ac619d24 --- /dev/null +++ b/src/com/android/settings/network/MobileNetworkIntentConverter.java @@ -0,0 +1,288 @@ +/* + * 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.network; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.os.SystemClock; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.ims.ImsRcsManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.Settings.MobileNetworkActivity; +import com.android.settings.SettingsActivity; +import com.android.settings.network.telephony.MobileNetworkUtils; + +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +/** + * A Java {@link Function} for conversion between {@link Intent} to Settings, + * and within Settings itself. + */ +public class MobileNetworkIntentConverter implements Function { + private static final String TAG = "MobileNetworkIntentConverter"; + + private static final ComponentName sTargetComponent = ComponentName + .createRelative("com.android.settings", + MobileNetworkActivity.class.getTypeName()); + + /** + * These actions has better aligned with definitions within AndroidManifest.xml + */ + private static final String [] sPotentialActions = new String [] { + null, + Intent.ACTION_MAIN, + android.provider.Settings.ACTION_NETWORK_OPERATOR_SETTINGS, + android.provider.Settings.ACTION_DATA_ROAMING_SETTINGS, + android.provider.Settings.ACTION_MMS_MESSAGE_SETTING, + ImsRcsManager.ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN + }; + + private static final String RE_ROUTE_TAG = ":reroute:" + TAG; + private static final AtomicReference mCachedClassName = + new AtomicReference(); + + private final Context mAppContext; + private final ComponentName mComponent; + + /** + * Constructor + * @param activity which receiving {@link Intent} + */ + public MobileNetworkIntentConverter(@NonNull Activity activity) { + mAppContext = activity.getApplicationContext(); + mComponent = activity.getComponentName(); + } + + /** + * API defined by {@link Function}. + * @param fromIntent is the {@link Intent} for convert. + * @return {@link Intent} for sending internally within Settings. + * Return {@code null} when failure. + */ + public Intent apply(Intent fromIntent) { + long startTime = SystemClock.elapsedRealtimeNanos(); + + Intent potentialReqIntent = null; + if (isAttachedToExposedComponents()) { + potentialReqIntent = convertFromDeepLink(fromIntent); + } else if (mayRequireConvert(fromIntent)) { + potentialReqIntent = fromIntent; + } else { + return null; + } + + final Intent reqIntent = potentialReqIntent; + String action = reqIntent.getAction(); + + // Find out the subscription ID of request. + final int subId = extractSubscriptionId(reqIntent); + + // Prepare the arguments Bundle. + Function ops = Function.identity(); + + if (TextUtils.equals(action, + android.provider.Settings.ACTION_NETWORK_OPERATOR_SETTINGS) + || TextUtils.equals(action, + android.provider.Settings.ACTION_DATA_ROAMING_SETTINGS)) { + // Accepted. + ops = ops.andThen(intent -> extractArguments(intent, subId)) + .andThen(args -> rePackIntent(args, reqIntent)) + .andThen(intent -> updateFragment(intent, mAppContext, subId)); + } else if (TextUtils.equals(action, + android.provider.Settings.ACTION_MMS_MESSAGE_SETTING)) { + ops = ops.andThen(intent -> extractArguments(intent, subId)) + .andThen(args -> convertMmsArguments(args)) + .andThen(args -> rePackIntent(args, reqIntent)) + .andThen(intent -> updateFragment(intent, mAppContext, subId)); + } else if (TextUtils.equals(action, + ImsRcsManager.ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN)) { + ops = ops.andThen(intent -> extractArguments(intent, subId)) + .andThen(args -> supportContactDiscoveryDialog(args, mAppContext, subId)) + .andThen(args -> rePackIntent(args, reqIntent)) + .andThen(intent -> updateFragment(intent, mAppContext, subId)); + } else if ((sTargetComponent.compareTo(mComponent) == 0) + && ((action == null) || Intent.ACTION_MAIN.equals(action))) { + Log.d(TAG, "Support default actions direct to this component"); + ops = ops.andThen(intent -> extractArguments(intent, subId)) + .andThen(args -> rePackIntent(args, reqIntent)) + .andThen(intent -> replaceIntentAction(intent)) + .andThen(intent -> updateFragment(intent, mAppContext, subId)); + } else { + return null; + } + + if (!isAttachedToExposedComponents()) { + ops = ops.andThen(intent -> configForReRoute(intent)); + } + + Intent result = ops.apply(reqIntent); + if (result != null) { + long endTime = SystemClock.elapsedRealtimeNanos(); + Log.d(TAG, mComponent.toString() + " intent conversion: " + + (endTime - startTime) + " ns"); + } + return result; + } + + @VisibleForTesting + protected boolean isAttachedToExposedComponents() { + return (sTargetComponent.compareTo(mComponent) == 0); + } + + protected int extractSubscriptionId(Intent reqIntent) { + return reqIntent.getIntExtra(android.provider.Settings.EXTRA_SUB_ID, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + } + + protected Bundle extractArguments(Intent reqIntent, int subId) { + // Duplicate from SettingsActivity#getIntent() + Bundle args = reqIntent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + Bundle result = (args != null) ? new Bundle(args) : new Bundle(); + result.putParcelable("intent", reqIntent); + result.putInt(android.provider.Settings.EXTRA_SUB_ID, subId); + return result; + } + + protected Bundle convertMmsArguments(Bundle args) { + // highlight "mms_message" preference. + args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, + MobileNetworkActivity.EXTRA_MMS_MESSAGE); + return args; + } + + @VisibleForTesting + protected boolean mayShowContactDiscoveryDialog(Context context, 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. + return MobileNetworkUtils.isContactDiscoveryVisible(context, subId) + // has the user already enabled this configuration? + && !MobileNetworkUtils.isContactDiscoveryEnabled(context, subId); + } + + protected Bundle supportContactDiscoveryDialog(Bundle args, Context context, int subId) { + boolean showDialog = mayShowContactDiscoveryDialog(context, subId); + Log.d(TAG, "maybeShowContactDiscoveryDialog subId=" + subId + ", show=" + showDialog); + args.putBoolean(MobileNetworkActivity.EXTRA_SHOW_CAPABILITY_DISCOVERY_OPT_IN, + showDialog); + return args; + } + + protected Intent rePackIntent(Bundle args, Intent reqIntent) { + Intent intent = new Intent(reqIntent); + intent.setComponent(sTargetComponent); + intent.putExtra(android.provider.Settings.EXTRA_SUB_ID, + args.getInt(android.provider.Settings.EXTRA_SUB_ID)); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + return intent; + } + + protected Intent replaceIntentAction(Intent intent) { + intent.setAction(android.provider.Settings.ACTION_NETWORK_OPERATOR_SETTINGS); + return intent; + } + + @VisibleForTesting + protected CharSequence getFragmentTitle(Context context, int subId) { + SubscriptionInfo subInfo = SubscriptionUtil.getSubscriptionOrDefault(context, subId); + return SubscriptionUtil.getUniqueSubscriptionDisplayName(subInfo, context); + } + + protected Intent updateFragment(Intent intent, Context context, int subId) { + if (intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE) == null) { + CharSequence title = getFragmentTitle(context, subId); + if (title != null) { + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, title.toString()); + } + } + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, getFragmentClass(context)); + return intent; + } + + protected String getFragmentClass(Context context) { + String className = mCachedClassName.get(); + if (className != null) { + return className; + } + try { + ActivityInfo ai = context.getPackageManager() + .getActivityInfo(sTargetComponent, PackageManager.GET_META_DATA); + if (ai != null && ai.metaData != null) { + className = ai.metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); + if (className != null) { + mCachedClassName.set(className); + } + return className; + } + } catch (NameNotFoundException nnfe) { + // No recovery + Log.d(TAG, "Cannot get Metadata for: " + sTargetComponent.toString()); + } + return null; + } + + protected Intent configForReRoute(Intent intent) { + if (intent.hasExtra(RE_ROUTE_TAG)) { + Log.d(TAG, "Skip re-routed intent " + intent); + return null; + } + return intent.putExtra(RE_ROUTE_TAG, intent.getAction()) + .setComponent(null); + } + + protected static boolean mayRequireConvert(Intent intent) { + if (intent == null) { + return false; + } + final String action = intent.getAction(); + return Arrays.stream(sPotentialActions).anyMatch(potentialAction -> + TextUtils.equals(action, potentialAction) + ); + } + + protected Intent convertFromDeepLink(Intent intent) { + if (intent == null) { + return null; + } + if (!TextUtils.equals(intent.getAction(), + android.provider.Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)) { + return intent; + } + try { + return Intent.parseUri(intent.getStringExtra( + android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI), + Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException exception) { + Log.d(TAG, "Intent URI corrupted", exception); + } + return null; + } +} diff --git a/tests/unit/src/com/android/settings/network/MobileNetworkIntentConverterTest.java b/tests/unit/src/com/android/settings/network/MobileNetworkIntentConverterTest.java new file mode 100644 index 00000000000..1800156fc42 --- /dev/null +++ b/tests/unit/src/com/android/settings/network/MobileNetworkIntentConverterTest.java @@ -0,0 +1,192 @@ +/* + * 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.network; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.telephony.ims.ImsRcsManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.Settings.MobileNetworkActivity; +import com.android.settings.SettingsActivity; +import com.android.settings.Utils; +import com.android.settings.network.telephony.MobileNetworkSettings; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class MobileNetworkIntentConverterTest { + + private static final String ACTIONS_ALLOWED [] = { + Intent.ACTION_MAIN, + Settings.ACTION_NETWORK_OPERATOR_SETTINGS, + Settings.ACTION_DATA_ROAMING_SETTINGS, + Settings.ACTION_MMS_MESSAGE_SETTING, + ImsRcsManager.ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN + }; + + private static final int TEST_SUBSCRIPTION_ID = 3; + + private static final CharSequence TEST_TITLE_CHAR_SEQUENCE = + "Test Title".subSequence(0, 10); + + @Mock + private Activity mActivity; + + private MobileNetworkIntentConverter mIntentConverter; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Context context = spy(ApplicationProvider.getApplicationContext()); + ComponentName componentName = ComponentName.createRelative(Utils.SETTINGS_PACKAGE_NAME, + MobileNetworkActivity.class.getTypeName()); + + doReturn(context).when(mActivity).getApplicationContext(); + doReturn(componentName).when(mActivity).getComponentName(); + + mIntentConverter = new MobileNetworkIntentConverter(mActivity) { + @Override + protected boolean isAttachedToExposedComponents() { + return false; + } + }; + } + + @Test + public void converter_returnNull_whenNotInterested() { + Intent intent = new Intent(Intent.ACTION_USER_INITIALIZE); + assertThat(mIntentConverter.apply(intent)).isEqualTo(null); + } + + @Test + public void converter_acceptableIntent_whenInterested() { + Arrays.stream(ACTIONS_ALLOWED).forEach(action -> { + Intent intent = new Intent(action); + assertThat(mIntentConverter.apply(intent)).isNotEqualTo(null); + }); + } + + @Test + public void convertSubscriptionId_fromIntentExtra_copyToBundleArgument() { + Intent intent = new Intent(ACTIONS_ALLOWED[0]); + intent.putExtra(Settings.EXTRA_SUB_ID, TEST_SUBSCRIPTION_ID); + + Intent convertedIntent = mIntentConverter.apply(intent); + + Bundle args = convertedIntent.getBundleExtra( + SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + assertThat(args.getInt(Settings.EXTRA_SUB_ID)).isEqualTo(TEST_SUBSCRIPTION_ID); + + int intExtra = convertedIntent.getIntExtra( + Settings.EXTRA_SUB_ID, TEST_SUBSCRIPTION_ID - 1); + assertThat(intExtra).isEqualTo(TEST_SUBSCRIPTION_ID); + } + + @Test + public void supportMms_addExtra_whenIntentForMms() { + Intent intent = new Intent(Settings.ACTION_MMS_MESSAGE_SETTING); + + Intent convertedIntent = mIntentConverter.apply(intent); + + Bundle args = convertedIntent.getBundleExtra( + SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + assertThat(args).isNotEqualTo(null); + assertThat(args.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY)).isEqualTo( + MobileNetworkActivity.EXTRA_MMS_MESSAGE); + } + + @Test + public void supportContacts_addExtra_whenIntentForContacts() { + Intent intent = new Intent(ImsRcsManager.ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN); + MobileNetworkIntentConverter converter = new MobileNetworkIntentConverter(mActivity) { + @Override + protected boolean isAttachedToExposedComponents() { + return false; + } + @Override + protected boolean mayShowContactDiscoveryDialog(Context context, int subId) { + return true; + } + }; + + Intent convertedIntent = converter.apply(intent); + + Bundle args = convertedIntent.getBundleExtra( + SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + assertThat(args).isNotEqualTo(null); + assertThat(args.getBoolean(MobileNetworkActivity.EXTRA_SHOW_CAPABILITY_DISCOVERY_OPT_IN)) + .isEqualTo(true); + } + + @Test + public void convertFormat_forSettings_fragmentPresentation() { + MobileNetworkIntentConverter converter = new MobileNetworkIntentConverter(mActivity) { + @Override + protected boolean isAttachedToExposedComponents() { + return false; + } + @Override + protected CharSequence getFragmentTitle(Context context, int subId) { + return TEST_TITLE_CHAR_SEQUENCE; + } + }; + + Intent intent = new Intent(ACTIONS_ALLOWED[0]); + + Intent convertedIntent = converter.apply(intent); + + assertThat(convertedIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE)) + .isEqualTo(TEST_TITLE_CHAR_SEQUENCE.toString()); + assertThat(convertedIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(MobileNetworkSettings.class.getTypeName()); + } + + @Test + public void convertFormat_deepLink_unwrapIntent() { + MobileNetworkIntentConverter converter = new MobileNetworkIntentConverter(mActivity) { + @Override + protected boolean isAttachedToExposedComponents() { + return true; + } + }; + + Intent intent = new Intent(ACTIONS_ALLOWED[0]); + String intentUri = intent.toUri(Intent.URI_INTENT_SCHEME); + + Intent deepLinkIntent = new Intent(Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY); + deepLinkIntent.putExtra(Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI, intentUri); + + assertThat(converter.apply(deepLinkIntent)).isNotEqualTo(null); + } +}