[Settings] Adjusted the API of Settings app

The API of Settings app get changed in order to support large screen.
This is a fix to adopt the change related to this work.

A short brief:
1. Accept ACTION_MAIN for launching MobileNetworkActivity.
2. Support deep-link intent while MobileNetworkActivity in foreground.
3. Avoid from binding MobileNetworkActivity as a single instance.

Bug: 230047450
Bug: 234406562
Bug: 229371407
Test: local & unittest
Change-Id: Ifcb9d4c564839199d998bd503f390f021c6bf3ad
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index ee0743a..c3ab8e2 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -22,8 +22,6 @@
 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.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 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 1a9bdc7..f951501 100644
--- a/src/com/android/settings/SettingsActivity.java
+++ b/src/com/android/settings/SettingsActivity.java
@@ -264,7 +264,10 @@
 
         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 0000000..648ac61
--- /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<Intent, Intent> {
+    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<String> mCachedClassName =
+            new AtomicReference<String>();
+
+    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<Intent, Intent> 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;
+    }
+}