1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.network; 18 19 import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; 20 21 import android.app.Activity; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ActivityInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.os.Bundle; 29 import android.os.SystemClock; 30 import android.telephony.SubscriptionInfo; 31 import android.telephony.SubscriptionManager; 32 import android.telephony.ims.ImsRcsManager; 33 import android.text.TextUtils; 34 import android.util.Log; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.VisibleForTesting; 38 39 import com.android.settings.Settings.MobileNetworkActivity; 40 import com.android.settings.SettingsActivity; 41 import com.android.settings.network.telephony.MobileNetworkUtils; 42 43 import java.net.URISyntaxException; 44 import java.util.Arrays; 45 import java.util.concurrent.atomic.AtomicReference; 46 import java.util.function.Function; 47 48 /** 49 * A Java {@link Function} for conversion between {@link Intent} to Settings, 50 * and within Settings itself. 51 */ 52 public class MobileNetworkIntentConverter implements Function<Intent, Intent> { 53 private static final String TAG = "MobileNetworkIntentConverter"; 54 55 private static final ComponentName sTargetComponent = ComponentName 56 .createRelative(SETTINGS_PACKAGE_NAME, 57 MobileNetworkActivity.class.getTypeName()); 58 private static final String INTENT_TRAMPOLINE = "android.settings.SEARCH_RESULT_TRAMPOLINE"; 59 /** 60 * These actions has better aligned with definitions within AndroidManifest.xml 61 */ 62 private static final String[] sPotentialActions = new String[]{ 63 null, 64 Intent.ACTION_MAIN, 65 android.provider.Settings.ACTION_NETWORK_OPERATOR_SETTINGS, 66 android.provider.Settings.ACTION_DATA_ROAMING_SETTINGS, 67 android.provider.Settings.ACTION_MMS_MESSAGE_SETTING, 68 ImsRcsManager.ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN, 69 INTENT_TRAMPOLINE 70 }; 71 72 private static final String RE_ROUTE_TAG = ":reroute:" + TAG; 73 private static final AtomicReference<String> mCachedClassName = 74 new AtomicReference<String>(); 75 76 private final Context mAppContext; 77 private final ComponentName mComponent; 78 79 /** 80 * Constructor 81 * @param activity which receiving {@link Intent} 82 */ MobileNetworkIntentConverter(@onNull Activity activity)83 public MobileNetworkIntentConverter(@NonNull Activity activity) { 84 mAppContext = activity.getApplicationContext(); 85 mComponent = activity.getComponentName(); 86 } 87 88 /** 89 * API defined by {@link Function}. 90 * @param fromIntent is the {@link Intent} for convert. 91 * @return {@link Intent} for sending internally within Settings. 92 * Return {@code null} when failure. 93 */ apply(Intent fromIntent)94 public Intent apply(Intent fromIntent) { 95 long startTime = SystemClock.elapsedRealtimeNanos(); 96 97 Intent potentialReqIntent = null; 98 if (isAttachedToExposedComponents()) { 99 potentialReqIntent = convertFromDeepLink(fromIntent); 100 } else if (mayRequireConvert(fromIntent)) { 101 potentialReqIntent = fromIntent; 102 } else { 103 return null; 104 } 105 106 final Intent reqIntent = potentialReqIntent; 107 String action = reqIntent.getAction(); 108 109 // Find out the subscription ID of request. 110 final int subId = extractSubscriptionId(reqIntent); 111 112 // Prepare the arguments Bundle. 113 Function<Intent, Intent> ops = Function.identity(); 114 115 if (TextUtils.equals(action, 116 android.provider.Settings.ACTION_NETWORK_OPERATOR_SETTINGS) 117 || TextUtils.equals(action, 118 android.provider.Settings.ACTION_DATA_ROAMING_SETTINGS) 119 || TextUtils.equals(action, INTENT_TRAMPOLINE)) { 120 // Accepted. 121 ops = ops.andThen(intent -> extractArguments(intent, subId)) 122 .andThen(args -> rePackIntent(args, reqIntent)) 123 .andThen(intent -> updateFragment(intent, mAppContext, subId)); 124 } else if (TextUtils.equals(action, 125 android.provider.Settings.ACTION_MMS_MESSAGE_SETTING)) { 126 ops = ops.andThen(intent -> extractArguments(intent, subId)) 127 .andThen(args -> convertMmsArguments(args)) 128 .andThen(args -> rePackIntent(args, reqIntent)) 129 .andThen(intent -> updateFragment(intent, mAppContext, subId)); 130 } else if (TextUtils.equals(action, 131 ImsRcsManager.ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN)) { 132 ops = ops.andThen(intent -> extractArguments(intent, subId)) 133 .andThen(args -> supportContactDiscoveryDialog(args, mAppContext, subId)) 134 .andThen(args -> rePackIntent(args, reqIntent)) 135 .andThen(intent -> updateFragment(intent, mAppContext, subId)); 136 } else if ((sTargetComponent.compareTo(mComponent) == 0) 137 && ((action == null) || Intent.ACTION_MAIN.equals(action))) { 138 Log.d(TAG, "Support default actions direct to this component"); 139 ops = ops.andThen(intent -> extractArguments(intent, subId)) 140 .andThen(args -> rePackIntent(args, reqIntent)) 141 .andThen(intent -> replaceIntentAction(intent)) 142 .andThen(intent -> updateFragment(intent, mAppContext, subId)); 143 } else { 144 return null; 145 } 146 147 if (!isAttachedToExposedComponents()) { 148 ops = ops.andThen(intent -> configForReRoute(intent)); 149 } 150 151 Intent result = ops.apply(reqIntent); 152 if (result != null) { 153 long endTime = SystemClock.elapsedRealtimeNanos(); 154 Log.d(TAG, mComponent.toString() + " intent conversion: " 155 + (endTime - startTime) + " ns"); 156 } 157 return result; 158 } 159 160 @VisibleForTesting isAttachedToExposedComponents()161 protected boolean isAttachedToExposedComponents() { 162 return (sTargetComponent.compareTo(mComponent) == 0); 163 } 164 extractSubscriptionId(Intent reqIntent)165 protected int extractSubscriptionId(Intent reqIntent) { 166 return reqIntent.getIntExtra(android.provider.Settings.EXTRA_SUB_ID, 167 SubscriptionManager.INVALID_SUBSCRIPTION_ID); 168 } 169 extractArguments(Intent reqIntent, int subId)170 protected Bundle extractArguments(Intent reqIntent, int subId) { 171 // Duplicate from SettingsActivity#getIntent() 172 Bundle args = reqIntent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); 173 Bundle result = (args != null) ? new Bundle(args) : new Bundle(); 174 result.putParcelable("intent", reqIntent); 175 result.putInt(android.provider.Settings.EXTRA_SUB_ID, subId); 176 return result; 177 } 178 convertMmsArguments(Bundle args)179 protected Bundle convertMmsArguments(Bundle args) { 180 // highlight "mms_message" preference. 181 args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, 182 MobileNetworkActivity.EXTRA_MMS_MESSAGE); 183 return args; 184 } 185 186 @VisibleForTesting mayShowContactDiscoveryDialog(Context context, int subId)187 protected boolean mayShowContactDiscoveryDialog(Context context, int subId) { 188 // If this activity was launched using ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN, show the 189 // associated dialog only if the opt-in has not been granted yet. 190 return MobileNetworkUtils.isContactDiscoveryVisible(context, subId) 191 // has the user already enabled this configuration? 192 && !MobileNetworkUtils.isContactDiscoveryEnabled(context, subId); 193 } 194 supportContactDiscoveryDialog(Bundle args, Context context, int subId)195 protected Bundle supportContactDiscoveryDialog(Bundle args, Context context, int subId) { 196 boolean showDialog = mayShowContactDiscoveryDialog(context, subId); 197 Log.d(TAG, "maybeShowContactDiscoveryDialog subId=" + subId + ", show=" + showDialog); 198 args.putBoolean(MobileNetworkActivity.EXTRA_SHOW_CAPABILITY_DISCOVERY_OPT_IN, 199 showDialog); 200 return args; 201 } 202 rePackIntent(Bundle args, Intent reqIntent)203 protected Intent rePackIntent(Bundle args, Intent reqIntent) { 204 Intent intent = new Intent(reqIntent); 205 intent.setComponent(sTargetComponent); 206 intent.putExtra(android.provider.Settings.EXTRA_SUB_ID, 207 args.getInt(android.provider.Settings.EXTRA_SUB_ID)); 208 intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); 209 return intent; 210 } 211 replaceIntentAction(Intent intent)212 protected Intent replaceIntentAction(Intent intent) { 213 intent.setAction(android.provider.Settings.ACTION_NETWORK_OPERATOR_SETTINGS); 214 return intent; 215 } 216 217 @VisibleForTesting getFragmentTitle(Context context, int subId)218 protected CharSequence getFragmentTitle(Context context, int subId) { 219 SubscriptionInfo subInfo = SubscriptionUtil.getSubscriptionOrDefault(context, subId); 220 return SubscriptionUtil.getUniqueSubscriptionDisplayName(subInfo, context); 221 } 222 updateFragment(Intent intent, Context context, int subId)223 protected Intent updateFragment(Intent intent, Context context, int subId) { 224 if (intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE) == null) { 225 CharSequence title = getFragmentTitle(context, subId); 226 if (title != null) { 227 intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, title.toString()); 228 } 229 } 230 intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, getFragmentClass(context)); 231 return intent; 232 } 233 getFragmentClass(Context context)234 protected String getFragmentClass(Context context) { 235 String className = mCachedClassName.get(); 236 if (className != null) { 237 return className; 238 } 239 try { 240 ActivityInfo ai = context.getPackageManager() 241 .getActivityInfo(sTargetComponent, PackageManager.GET_META_DATA); 242 if (ai != null && ai.metaData != null) { 243 className = ai.metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); 244 if (className != null) { 245 mCachedClassName.set(className); 246 } 247 return className; 248 } 249 } catch (NameNotFoundException nnfe) { 250 // No recovery 251 Log.d(TAG, "Cannot get Metadata for: " + sTargetComponent.toString()); 252 } 253 return null; 254 } 255 configForReRoute(Intent intent)256 protected Intent configForReRoute(Intent intent) { 257 if (intent.hasExtra(RE_ROUTE_TAG)) { 258 Log.d(TAG, "Skip re-routed intent " + intent); 259 return null; 260 } 261 return intent.putExtra(RE_ROUTE_TAG, intent.getAction()) 262 .setComponent(null); 263 } 264 mayRequireConvert(Intent intent)265 protected static boolean mayRequireConvert(Intent intent) { 266 if (intent == null) { 267 return false; 268 } 269 final String action = intent.getAction(); 270 return Arrays.stream(sPotentialActions).anyMatch(potentialAction -> 271 TextUtils.equals(action, potentialAction) 272 ); 273 } 274 convertFromDeepLink(Intent intent)275 protected Intent convertFromDeepLink(Intent intent) { 276 if (intent == null) { 277 return null; 278 } 279 if (!TextUtils.equals(intent.getAction(), 280 android.provider.Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)) { 281 return intent; 282 } 283 try { 284 return Intent.parseUri(intent.getStringExtra( 285 android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI), 286 Intent.URI_INTENT_SCHEME); 287 } catch (URISyntaxException exception) { 288 Log.d(TAG, "Intent URI corrupted", exception); 289 } 290 return null; 291 } 292 } 293