/* * 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.android.settings.Utils.SETTINGS_PACKAGE_NAME; 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(SETTINGS_PACKAGE_NAME, MobileNetworkActivity.class.getTypeName()); private static final String INTENT_TRAMPOLINE = "android.settings.SEARCH_RESULT_TRAMPOLINE"; /** * 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, INTENT_TRAMPOLINE }; 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) || TextUtils.equals(action, INTENT_TRAMPOLINE)) { // 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; } }