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