1 /* 2 * Copyright (C) 2018 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.telephony; 18 19 import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; 20 21 import android.app.ActionBar; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.os.Bundle; 25 import android.os.UserManager; 26 import android.provider.Settings; 27 import android.telephony.SubscriptionInfo; 28 import android.telephony.SubscriptionManager; 29 import android.telephony.ims.ImsRcsManager; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.view.View; 33 import android.widget.Toolbar; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.VisibleForTesting; 37 import androidx.fragment.app.Fragment; 38 import androidx.fragment.app.FragmentManager; 39 import androidx.fragment.app.FragmentTransaction; 40 import androidx.lifecycle.Lifecycle; 41 42 import com.android.settings.R; 43 import com.android.settings.core.SettingsBaseActivity; 44 import com.android.settings.network.ProxySubscriptionManager; 45 import com.android.settings.network.SubscriptionUtil; 46 import com.android.settings.network.helper.SelectableSubscriptions; 47 import com.android.settings.network.helper.SubscriptionAnnotation; 48 49 import java.util.List; 50 import java.util.function.Function; 51 52 /** 53 * Activity for displaying MobileNetworkSettings 54 */ 55 public class MobileNetworkActivity extends SettingsBaseActivity 56 implements ProxySubscriptionManager.OnActiveSubscriptionChangedListener { 57 58 private static final String TAG = "MobileNetworkActivity"; 59 @VisibleForTesting 60 static final String MOBILE_SETTINGS_TAG = "mobile_settings:"; 61 @VisibleForTesting 62 static final int SUB_ID_NULL = Integer.MIN_VALUE; 63 64 @VisibleForTesting 65 ProxySubscriptionManager mProxySubscriptionMgr; 66 67 private int mCurSubscriptionId = SUB_ID_NULL; 68 69 // This flag forces subscription information fragment to be re-created. 70 // Otherwise, fragment will be kept when subscription id has not been changed. 71 // 72 // Set initial value to true allows subscription information fragment to be re-created when 73 // Activity re-create occur. 74 private boolean mPendingSubscriptionChange = true; 75 76 @Override onNewIntent(Intent intent)77 protected void onNewIntent(Intent intent) { 78 super.onNewIntent(intent); 79 validate(intent); 80 setIntent(intent); 81 82 int updateSubscriptionIndex = mCurSubscriptionId; 83 if (intent != null) { 84 updateSubscriptionIndex = intent.getIntExtra(Settings.EXTRA_SUB_ID, SUB_ID_NULL); 85 } 86 SubscriptionInfo info = getSubscriptionOrDefault(updateSubscriptionIndex); 87 if (info == null) { 88 Log.d(TAG, "Invalid subId request " + mCurSubscriptionId 89 + " -> " + updateSubscriptionIndex); 90 return; 91 } 92 93 int oldSubId = mCurSubscriptionId; 94 updateSubscriptions(info, null); 95 96 // If the subscription has changed or the new intent doesnt contain the opt in action, 97 // remove the old discovery dialog. If the activity is being recreated, we will see 98 // onCreate -> onNewIntent, so the dialog will first be recreated for the old subscription 99 // and then removed. 100 if (mCurSubscriptionId != oldSubId || !doesIntentContainOptInAction(intent)) { 101 removeContactDiscoveryDialog(oldSubId); 102 } 103 // evaluate showing the new discovery dialog if this intent contains an action to show the 104 // opt-in. 105 if (doesIntentContainOptInAction(intent)) { 106 maybeShowContactDiscoveryDialog(info); 107 } 108 } 109 110 @Override onCreate(Bundle savedInstanceState)111 protected void onCreate(Bundle savedInstanceState) { 112 super.onCreate(savedInstanceState); 113 final UserManager userManager = this.getSystemService(UserManager.class); 114 if (!userManager.isAdminUser()) { 115 this.finish(); 116 return; 117 } 118 119 final Toolbar toolbar = findViewById(R.id.action_bar); 120 toolbar.setVisibility(View.VISIBLE); 121 setActionBar(toolbar); 122 123 final ActionBar actionBar = getActionBar(); 124 if (actionBar != null) { 125 actionBar.setDisplayHomeAsUpEnabled(true); 126 actionBar.setDisplayShowTitleEnabled(true); 127 } 128 129 getProxySubscriptionManager().setLifecycle(getLifecycle()); 130 131 final Intent startIntent = getIntent(); 132 validate(startIntent); 133 mCurSubscriptionId = savedInstanceState != null 134 ? savedInstanceState.getInt(Settings.EXTRA_SUB_ID, SUB_ID_NULL) 135 : ((startIntent != null) 136 ? startIntent.getIntExtra(Settings.EXTRA_SUB_ID, SUB_ID_NULL) 137 : SUB_ID_NULL); 138 // perform registration after mCurSubscriptionId been configured. 139 registerActiveSubscriptionsListener(); 140 141 SubscriptionInfo subscription = getSubscriptionOrDefault(mCurSubscriptionId); 142 if (subscription == null) { 143 Log.d(TAG, "Invalid subId request " + mCurSubscriptionId); 144 tryToFinishActivity(); 145 return; 146 } 147 148 maybeShowContactDiscoveryDialog(subscription); 149 150 updateSubscriptions(subscription, null); 151 } 152 153 @VisibleForTesting getProxySubscriptionManager()154 ProxySubscriptionManager getProxySubscriptionManager() { 155 if (mProxySubscriptionMgr == null) { 156 mProxySubscriptionMgr = ProxySubscriptionManager.getInstance(this); 157 } 158 return mProxySubscriptionMgr; 159 } 160 161 @VisibleForTesting registerActiveSubscriptionsListener()162 void registerActiveSubscriptionsListener() { 163 getProxySubscriptionManager().addActiveSubscriptionsListener(this); 164 } 165 166 /** 167 * Implementation of ProxySubscriptionManager.OnActiveSubscriptionChangedListener 168 */ onChanged()169 public void onChanged() { 170 mPendingSubscriptionChange = false; 171 172 if (mCurSubscriptionId == SUB_ID_NULL) { 173 return; 174 } 175 176 if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { 177 mPendingSubscriptionChange = true; 178 return; 179 } 180 181 SubscriptionInfo subInfo = getSubscription(mCurSubscriptionId, null); 182 if (subInfo != null) { 183 if (mCurSubscriptionId != subInfo.getSubscriptionId()) { 184 // update based on subscription status change 185 removeContactDiscoveryDialog(mCurSubscriptionId); 186 updateSubscriptions(subInfo, null); 187 } 188 return; 189 } 190 191 Log.w(TAG, "subId missing: " + mCurSubscriptionId); 192 193 // When UI is not the active one, avoid from destroy it immediately 194 // but wait until onResume() to see if subscription back online again. 195 // This is to avoid from glitch behavior of subscription which changes 196 // the UI when UI is considered as in the background or only partly 197 // visible. 198 if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { 199 mPendingSubscriptionChange = true; 200 return; 201 } 202 203 // Subscription could be missing 204 tryToFinishActivity(); 205 } 206 runSubscriptionUpdate(Runnable onUpdateRemaining)207 protected void runSubscriptionUpdate(Runnable onUpdateRemaining) { 208 SubscriptionInfo subInfo = getSubscription(mCurSubscriptionId, null); 209 if (subInfo == null) { 210 onUpdateRemaining.run(); 211 tryToFinishActivity(); 212 return; 213 } 214 if (mCurSubscriptionId != subInfo.getSubscriptionId()) { 215 removeContactDiscoveryDialog(mCurSubscriptionId); 216 updateSubscriptions(subInfo, null); 217 } 218 onUpdateRemaining.run(); 219 } 220 tryToFinishActivity()221 protected void tryToFinishActivity() { 222 if ((!isFinishing()) && (!isDestroyed())) { 223 finish(); 224 } 225 } 226 227 @Override onStart()228 protected void onStart() { 229 getProxySubscriptionManager().setLifecycle(getLifecycle()); 230 if (mPendingSubscriptionChange) { 231 mPendingSubscriptionChange = false; 232 runSubscriptionUpdate(() -> super.onStart()); 233 return; 234 } 235 super.onStart(); 236 } 237 238 @Override onResume()239 protected void onResume() { 240 if (mPendingSubscriptionChange) { 241 mPendingSubscriptionChange = false; 242 runSubscriptionUpdate(() -> super.onResume()); 243 return; 244 } 245 super.onResume(); 246 } 247 248 @Override onDestroy()249 protected void onDestroy() { 250 super.onDestroy(); 251 if (mProxySubscriptionMgr == null) { 252 return; 253 } 254 mProxySubscriptionMgr.removeActiveSubscriptionsListener(this); 255 } 256 257 @Override onSaveInstanceState(@onNull Bundle outState)258 protected void onSaveInstanceState(@NonNull Bundle outState) { 259 super.onSaveInstanceState(outState); 260 saveInstanceState(outState); 261 } 262 263 @VisibleForTesting saveInstanceState(@onNull Bundle outState)264 void saveInstanceState(@NonNull Bundle outState) { 265 outState.putInt(Settings.EXTRA_SUB_ID, mCurSubscriptionId); 266 } 267 updateTitleAndNavigation(SubscriptionInfo subscription)268 private void updateTitleAndNavigation(SubscriptionInfo subscription) { 269 // Set the title to the name of the subscription. If we don't have subscription info, the 270 // title will just default to the label for this activity that's already specified in 271 // AndroidManifest.xml. 272 if (subscription != null) { 273 setTitle(SubscriptionUtil.getUniqueSubscriptionDisplayName(subscription, this)); 274 } 275 } 276 277 @VisibleForTesting updateSubscriptions(SubscriptionInfo subscription, Bundle savedInstanceState)278 void updateSubscriptions(SubscriptionInfo subscription, Bundle savedInstanceState) { 279 if (subscription == null) { 280 return; 281 } 282 final int subscriptionIndex = subscription.getSubscriptionId(); 283 284 updateTitleAndNavigation(subscription); 285 if (savedInstanceState == null) { 286 switchFragment(subscription); 287 } 288 289 mCurSubscriptionId = subscriptionIndex; 290 } 291 292 /** 293 * Select one of the subscription as the default subscription. 294 * @param subAnnoList a list of {@link SubscriptionAnnotation} 295 * @return ideally the {@link SubscriptionAnnotation} as expected 296 */ defaultSubscriptionSelection( List<SubscriptionAnnotation> subAnnoList)297 protected SubscriptionAnnotation defaultSubscriptionSelection( 298 List<SubscriptionAnnotation> subAnnoList) { 299 return (subAnnoList == null) ? null : 300 subAnnoList.stream() 301 .filter(SubscriptionAnnotation::isDisplayAllowed) 302 .filter(SubscriptionAnnotation::isActive) 303 .findFirst().orElse(null); 304 } 305 getSubscriptionOrDefault(int subscriptionId)306 protected SubscriptionInfo getSubscriptionOrDefault(int subscriptionId) { 307 return getSubscription(subscriptionId, 308 (subscriptionId != SUB_ID_NULL) ? null : ( 309 subAnnoList -> defaultSubscriptionSelection(subAnnoList) 310 )); 311 } 312 313 /** 314 * Get the current subscription to display. First check whether intent has {@link 315 * Settings#EXTRA_SUB_ID} and if so find the subscription with that id. 316 * If not, select default one based on {@link Function} provided. 317 * 318 * @param preferredSubscriptionId preferred subscription id 319 * @param selectionOfDefault when true current subscription is absent 320 */ 321 @VisibleForTesting getSubscription(int preferredSubscriptionId, Function<List<SubscriptionAnnotation>, SubscriptionAnnotation> selectionOfDefault)322 protected SubscriptionInfo getSubscription(int preferredSubscriptionId, 323 Function<List<SubscriptionAnnotation>, SubscriptionAnnotation> selectionOfDefault) { 324 List<SubscriptionAnnotation> subList = 325 (new SelectableSubscriptions(this, true)).call(); 326 Log.d(TAG, "get subId=" + preferredSubscriptionId + " from " + subList); 327 SubscriptionAnnotation currentSubInfo = subList.stream() 328 .filter(SubscriptionAnnotation::isDisplayAllowed) 329 .filter(subAnno -> (subAnno.getSubscriptionId() == preferredSubscriptionId)) 330 .findFirst().orElse(null); 331 if ((currentSubInfo == null) && (selectionOfDefault != null)) { 332 currentSubInfo = selectionOfDefault.apply(subList); 333 } 334 return (currentSubInfo == null) ? null : currentSubInfo.getSubInfo(); 335 } 336 337 @VisibleForTesting getSubscriptionForSubId(int subId)338 SubscriptionInfo getSubscriptionForSubId(int subId) { 339 return SubscriptionUtil.getAvailableSubscription(this, 340 getProxySubscriptionManager(), subId); 341 } 342 343 @VisibleForTesting switchFragment(SubscriptionInfo subInfo)344 void switchFragment(SubscriptionInfo subInfo) { 345 final FragmentManager fragmentManager = getSupportFragmentManager(); 346 final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); 347 348 final int subId = subInfo.getSubscriptionId(); 349 final Intent intent = getIntent(); 350 final Bundle bundle = new Bundle(); 351 bundle.putInt(Settings.EXTRA_SUB_ID, subId); 352 if (intent != null && Settings.ACTION_MMS_MESSAGE_SETTING.equals(intent.getAction())) { 353 // highlight "mms_message" preference. 354 bundle.putString(EXTRA_FRAGMENT_ARG_KEY, "mms_message"); 355 } 356 357 final String fragmentTag = buildFragmentTag(subId); 358 if (fragmentManager.findFragmentByTag(fragmentTag) != null) { 359 Log.d(TAG, "Construct fragment: " + fragmentTag); 360 } 361 362 final Fragment fragment = new MobileNetworkSettings(); 363 fragment.setArguments(bundle); 364 fragmentTransaction.replace(R.id.content_frame, fragment, fragmentTag); 365 fragmentTransaction.commitAllowingStateLoss(); 366 } 367 removeContactDiscoveryDialog(int subId)368 private void removeContactDiscoveryDialog(int subId) { 369 ContactDiscoveryDialogFragment fragment = getContactDiscoveryFragment(subId); 370 if (fragment != null) { 371 fragment.dismiss(); 372 } 373 } 374 getContactDiscoveryFragment(int subId)375 private ContactDiscoveryDialogFragment getContactDiscoveryFragment(int subId) { 376 // In the case that we are rebuilding this activity after it has been destroyed and 377 // recreated, look up the dialog in the fragment manager. 378 return (ContactDiscoveryDialogFragment) getSupportFragmentManager() 379 .findFragmentByTag(ContactDiscoveryDialogFragment.getFragmentTag(subId)); 380 } 381 maybeShowContactDiscoveryDialog(SubscriptionInfo info)382 private void maybeShowContactDiscoveryDialog(SubscriptionInfo info) { 383 int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; 384 CharSequence carrierName = ""; 385 if (info != null) { 386 subId = info.getSubscriptionId(); 387 carrierName = SubscriptionUtil.getUniqueSubscriptionDisplayName(info, this); 388 } 389 // If this activity was launched using ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN, show the 390 // associated dialog only if the opt-in has not been granted yet. 391 boolean showOptInDialog = doesIntentContainOptInAction(getIntent()) 392 // has the carrier config enabled capability discovery? 393 && MobileNetworkUtils.isContactDiscoveryVisible(this, subId) 394 // has the user already enabled this configuration? 395 && !MobileNetworkUtils.isContactDiscoveryEnabled(this, subId); 396 ContactDiscoveryDialogFragment fragment = getContactDiscoveryFragment(subId); 397 if (showOptInDialog) { 398 if (fragment == null) { 399 fragment = ContactDiscoveryDialogFragment.newInstance(subId, carrierName); 400 } 401 // Only try to show the dialog if it has not already been added, otherwise we may 402 // accidentally add it multiple times, causing multiple dialogs. 403 if (!fragment.isAdded()) { 404 fragment.show(getSupportFragmentManager(), 405 ContactDiscoveryDialogFragment.getFragmentTag(subId)); 406 } 407 } 408 } 409 doesIntentContainOptInAction(Intent intent)410 private boolean doesIntentContainOptInAction(Intent intent) { 411 String intentAction = (intent != null ? intent.getAction() : null); 412 return TextUtils.equals(intentAction, 413 ImsRcsManager.ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN); 414 } 415 validate(Intent intent)416 private void validate(Intent intent) { 417 // Do not allow ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN without a subscription id specified, 418 // since we do not want the user to accidentally turn on capability polling for the wrong 419 // subscription. 420 if (doesIntentContainOptInAction(intent)) { 421 if (SUB_ID_NULL == intent.getIntExtra(Settings.EXTRA_SUB_ID, SUB_ID_NULL)) { 422 throw new IllegalArgumentException("Intent with action " 423 + "SHOW_CAPABILITY_DISCOVERY_OPT_IN must also include the extra " 424 + "Settings#EXTRA_SUB_ID"); 425 } 426 } 427 } 428 429 @VisibleForTesting buildFragmentTag(int subscriptionId)430 String buildFragmentTag(int subscriptionId) { 431 return MOBILE_SETTINGS_TAG + subscriptionId; 432 } 433 } 434