diff --git a/src/com/android/settings/network/SubscriptionUtil.java b/src/com/android/settings/network/SubscriptionUtil.java index 48ff591a43e..41760de6e04 100644 --- a/src/com/android/settings/network/SubscriptionUtil.java +++ b/src/com/android/settings/network/SubscriptionUtil.java @@ -486,7 +486,7 @@ public class SubscriptionUtil { * @param info the subscriptionInfo to check against. * @return true if this subscription should be visible to the API caller. */ - private static boolean isSubscriptionVisible( + public static boolean isSubscriptionVisible( SubscriptionManager subscriptionManager, Context context, SubscriptionInfo info) { if (info == null) return false; // If subscription is NOT grouped opportunistic subscription, it's visible. diff --git a/src/com/android/settings/network/helper/QueryEsimCardId.java b/src/com/android/settings/network/helper/QueryEsimCardId.java new file mode 100644 index 00000000000..dc29c47f133 --- /dev/null +++ b/src/com/android/settings/network/helper/QueryEsimCardId.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 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.helper; + +import android.telephony.TelephonyManager; +import android.telephony.UiccCardInfo; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicIntegerArray; + +/** + * This is a Callable class which queries valid card ID for eSIM + */ +public class QueryEsimCardId implements Callable { + private static final String TAG = "QueryEsimCardId"; + + private TelephonyManager mTelephonyManager; + + /** + * Constructor of class + * @param TelephonyManager + */ + public QueryEsimCardId(TelephonyManager telephonyManager) { + mTelephonyManager = telephonyManager; + } + + /** + * Implementation of Callable + * @return card ID(s) in AtomicIntegerArray + */ + public AtomicIntegerArray call() { + List cardInfos = mTelephonyManager.getUiccCardsInfo(); + if (cardInfos == null) { + return new AtomicIntegerArray(0); + } + return new AtomicIntegerArray(cardInfos.stream() + .filter(Objects::nonNull) + .filter(cardInfo -> (!cardInfo.isRemovable() + && (cardInfo.getCardId() != TelephonyManager.UNSUPPORTED_CARD_ID))) + .mapToInt(UiccCardInfo::getCardId) + .toArray()); + } +} \ No newline at end of file diff --git a/src/com/android/settings/network/helper/QuerySimSlotIndex.java b/src/com/android/settings/network/helper/QuerySimSlotIndex.java new file mode 100644 index 00000000000..b70a148d2e4 --- /dev/null +++ b/src/com/android/settings/network/helper/QuerySimSlotIndex.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 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.helper; + +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.telephony.UiccSlotInfo; + +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicIntegerArray; + +/** + * This is a Callable class which query slot index within device + */ +public class QuerySimSlotIndex implements Callable { + private static final String TAG = "QuerySimSlotIndex"; + + private TelephonyManager mTelephonyManager; + private boolean mDisabledSlotsIncluded; + private boolean mOnlySlotWithSim; + + /** + * Constructor of class + * @param TelephonyManager + * @param disabledSlotsIncluded query both active and inactive slots when true, + * only query active slot when false. + * @param onlySlotWithSim query slot index with SIM available when true, + * include absent ones when false. + */ + public QuerySimSlotIndex(TelephonyManager telephonyManager, + boolean disabledSlotsIncluded, boolean onlySlotWithSim) { + mTelephonyManager = telephonyManager; + mDisabledSlotsIncluded = disabledSlotsIncluded; + mOnlySlotWithSim = onlySlotWithSim; + } + + /** + * Implementation of Callable + * @return slot index in AtomicIntegerArray + */ + public AtomicIntegerArray call() { + UiccSlotInfo [] slotInfo = mTelephonyManager.getUiccSlotsInfo(); + if (slotInfo == null) { + return new AtomicIntegerArray(0); + } + int slotIndexFilter = mOnlySlotWithSim ? 0 : SubscriptionManager.INVALID_SIM_SLOT_INDEX; + return new AtomicIntegerArray(Arrays.stream(slotInfo) + .filter(slot -> filterSlot(slot)) + .mapToInt(slot -> mapToSlotIndex(slot)) + .filter(slotIndex -> (slotIndex >= slotIndexFilter)) + .toArray()); + } + + protected boolean filterSlot(UiccSlotInfo slotInfo) { + if (mDisabledSlotsIncluded) { + return true; + } + if (slotInfo == null) { + return false; + } + return slotInfo.getIsActive(); + } + + protected int mapToSlotIndex(UiccSlotInfo slotInfo) { + if (slotInfo == null) { + return SubscriptionManager.INVALID_SIM_SLOT_INDEX; + } + if (slotInfo.getCardStateInfo() == UiccSlotInfo.CARD_STATE_INFO_ABSENT) { + return SubscriptionManager.INVALID_SIM_SLOT_INDEX; + } + return slotInfo.getLogicalSlotIdx(); + } +} \ No newline at end of file diff --git a/src/com/android/settings/network/helper/SelectableSubscriptions.java b/src/com/android/settings/network/helper/SelectableSubscriptions.java new file mode 100644 index 00000000000..224aa07354d --- /dev/null +++ b/src/com/android/settings/network/helper/SelectableSubscriptions.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2021 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.helper; + +import android.content.Context; +import android.os.ParcelUuid; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; + +import androidx.annotation.Keep; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.network.helper.SubscriptionAnnotation; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * This is a Callable class to query user selectable subscription list. + */ +public class SelectableSubscriptions implements Callable> { + private static final String TAG = "SelectableSubscriptions"; + + private static final ParcelUuid mEmptyUuid = ParcelUuid.fromString("0-0-0-0-0"); + + private Context mContext; + private Supplier> mSubscriptions; + private Predicate mFilter; + + /** + * Constructor of class + * @param context + * @param disabledSlotsIncluded query both active and inactive slots when true, + * only query active slot when false. + */ + public SelectableSubscriptions(Context context, boolean disabledSlotsIncluded) { + mContext = context; + mSubscriptions = disabledSlotsIncluded ? (() -> getAvailableSubInfoList(context)) : + (() -> getActiveSubInfoList(context)); + mFilter = disabledSlotsIncluded ? (subAnno -> subAnno.isExisted()) : + (subAnno -> subAnno.isActive()); + } + + /** + * Implementation of Callable + * @return a list of SubscriptionAnnotation which is user selectable + */ + public List call() { + TelephonyManager telMgr = mContext.getSystemService(TelephonyManager.class); + + try { + // query in background thread + Future eSimCardId = + ThreadUtils.postOnBackgroundThread(new QueryEsimCardId(telMgr)); + + // query in background thread + Future simSlotIndex = + ThreadUtils.postOnBackgroundThread( + new QuerySimSlotIndex(telMgr, true, true)); + + // query in background thread + Future activeSimSlotIndex = + ThreadUtils.postOnBackgroundThread( + new QuerySimSlotIndex(telMgr, false, true)); + + List subInfoList = mSubscriptions.get(); + + // wait for result from background thread + List eSimCardIdList = atomicToList(eSimCardId.get()); + List simSlotIndexList = atomicToList(simSlotIndex.get()); + List activeSimSlotIndexList = atomicToList(activeSimSlotIndex.get()); + + // group by GUID + Map> groupedSubInfoList = + IntStream.range(0, subInfoList.size()) + .mapToObj(subInfoIndex -> + new SubscriptionAnnotation.Builder(subInfoList, subInfoIndex)) + .map(annoBdr -> annoBdr.build(mContext, + eSimCardIdList, simSlotIndexList, activeSimSlotIndexList)) + .filter(mFilter) + .collect(Collectors.groupingBy(subAnno -> getGroupUuid(subAnno))); + + // select best one from subscription(s) within the same group + groupedSubInfoList.replaceAll((uuid, annoList) -> { + if ((uuid == mEmptyUuid) || (annoList.size() <= 1)) { + return annoList; + } + return Collections.singletonList(selectBestFromList(annoList)); + }); + + // build a list of subscriptions (based on the order of slot index) + return groupedSubInfoList.values().stream().flatMap(List::stream) + .sorted(Comparator.comparingInt(anno -> anno.getSubInfo().getSimSlotIndex())) + .collect(Collectors.toList()); + } catch (Exception exception) { + Log.w(TAG, "Fail to request subIdList", exception); + } + return Collections.emptyList(); + } + + protected ParcelUuid getGroupUuid(SubscriptionAnnotation subAnno) { + ParcelUuid groupUuid = subAnno.getSubInfo().getGroupUuid(); + return (groupUuid == null) ? mEmptyUuid : groupUuid; + } + + protected SubscriptionAnnotation selectBestFromList(List annoList) { + Comparator annoSelector = (anno1, anno2) -> { + if (anno1.isDisplayAllowed() != anno2.isDisplayAllowed()) { + return anno1.isDisplayAllowed() ? -1 : 1; + } + if (anno1.isActive() != anno2.isActive()) { + return anno1.isActive() ? -1 : 1; + } + if (anno1.isExisted() != anno2.isExisted()) { + return anno1.isExisted() ? -1 : 1; + } + return 0; + }; + annoSelector = annoSelector + // eSIM in front of pSIM + .thenComparingInt(anno -> -anno.getType()) + // subscription ID in reverse order + .thenComparingInt(anno -> -anno.getSubscriptionId()); + return annoList.stream().sorted(annoSelector).findFirst().orElse(null); + } + + protected List getSubInfoList(Context context, + Function> convertor) { + SubscriptionManager subManager = getSubscriptionManager(context); + return (subManager == null) ? Collections.emptyList() : convertor.apply(subManager); + } + + protected SubscriptionManager getSubscriptionManager(Context context) { + return context.getSystemService(SubscriptionManager.class); + } + + protected List getAvailableSubInfoList(Context context) { + return getSubInfoList(context, SubscriptionManager::getAvailableSubscriptionInfoList); + } + + protected List getActiveSubInfoList(Context context) { + return getSubInfoList(context, SubscriptionManager::getActiveSubscriptionInfoList); + } + + @Keep + @VisibleForTesting + protected static List atomicToList(AtomicIntegerArray atomicIntArray) { + if (atomicIntArray == null) { + return Collections.emptyList(); + } + return IntStream.range(0, atomicIntArray.length()) + .map(idx -> atomicIntArray.get(idx)).boxed() + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/com/android/settings/network/helper/SubscriptionAnnotation.java b/src/com/android/settings/network/helper/SubscriptionAnnotation.java new file mode 100644 index 00000000000..d314474030f --- /dev/null +++ b/src/com/android/settings/network/helper/SubscriptionAnnotation.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2021 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.helper; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.android.settings.network.SubscriptionUtil; + +import java.util.List; + +/** + * This is a class helps providing additional info required by UI + * based on SubscriptionInfo. + */ +public class SubscriptionAnnotation { + private static final String TAG = "SubscriptionAnnotation"; + + private SubscriptionInfo mSubInfo; + private int mOrderWithinList; + private int mType = TYPE_UNKNOWN; + private boolean mIsExisted; + private boolean mIsActive; + private boolean mIsAllowToDisplay; + + public static final int TYPE_UNKNOWN = 0x0; + public static final int TYPE_PSIM = 0x1; + public static final int TYPE_ESIM = 0x2; + + /** + * Builder class for SubscriptionAnnotation + */ + public static class Builder { + + private List mSubInfoList; + private int mIndexWithinList; + + /** + * Constructor of builder + * @param subInfoList list of subscription info + * @param indexWithinList target index within list provided + */ + public Builder(List subInfoList, int indexWithinList) { + mSubInfoList = subInfoList; + mIndexWithinList = indexWithinList; + } + + public SubscriptionAnnotation build(Context context, List eSimCardId, + List simSlotIndex, List activeSimSlotIndex) { + return new SubscriptionAnnotation(mSubInfoList, mIndexWithinList, context, + eSimCardId, simSlotIndex, activeSimSlotIndex); + } + } + + /** + * Constructor of class + */ + protected SubscriptionAnnotation(List subInfoList, int subInfoIndex, + Context context, List eSimCardId, + List simSlotIndex, List activeSimSlotIndexList) { + if ((subInfoIndex < 0) || (subInfoIndex >= subInfoList.size())) { + return; + } + mSubInfo = subInfoList.get(subInfoIndex); + if (mSubInfo == null) { + return; + } + + mOrderWithinList = subInfoIndex; + mType = mSubInfo.isEmbedded() ? TYPE_ESIM : TYPE_PSIM; + if (mType == TYPE_ESIM) { + int cardId = mSubInfo.getCardId(); + mIsExisted = eSimCardId.contains(cardId); + if (mIsExisted) { + mIsActive = activeSimSlotIndexList.contains(mSubInfo.getSimSlotIndex()); + mIsAllowToDisplay = isDisplayAllowed(context); + } + return; + } + + mIsExisted = simSlotIndex.contains(mSubInfo.getSimSlotIndex()); + mIsActive = activeSimSlotIndexList.contains(mSubInfo.getSimSlotIndex()); + if (mIsExisted) { + mIsAllowToDisplay = isDisplayAllowed(context); + } + } + + // the index provided during construction of Builder + public int getOrderingInList() { + return mOrderWithinList; + } + + // type of subscription + public int getType() { + return mType; + } + + // if a subscription is existed within device + public boolean isExisted() { + return mIsExisted; + } + + // if a subscription is currently ON + public boolean isActive() { + return mIsActive; + } + + // if display of subscription is allowed + public boolean isDisplayAllowed() { + return mIsAllowToDisplay; + } + + // the subscription ID + public int getSubscriptionId() { + return (mSubInfo == null) ? SubscriptionManager.INVALID_SUBSCRIPTION_ID : + mSubInfo.getSubscriptionId(); + } + + // the SubscriptionInfo + public SubscriptionInfo getSubInfo() { + return mSubInfo; + } + + private boolean isDisplayAllowed(Context context) { + return SubscriptionUtil.isSubscriptionVisible( + context.getSystemService(SubscriptionManager.class), context, mSubInfo); + } +} \ No newline at end of file diff --git a/src/com/android/settings/network/telephony/MobileNetworkActivity.java b/src/com/android/settings/network/telephony/MobileNetworkActivity.java index f2be37fa985..50164609dd0 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkActivity.java +++ b/src/com/android/settings/network/telephony/MobileNetworkActivity.java @@ -19,6 +19,7 @@ package com.android.settings.network.telephony; import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; import android.app.ActionBar; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserManager; @@ -43,6 +44,8 @@ import com.android.settings.R; import com.android.settings.core.SettingsBaseActivity; import com.android.settings.network.ProxySubscriptionManager; import com.android.settings.network.SubscriptionUtil; +import com.android.settings.network.helper.SelectableSubscriptions; +import com.android.settings.network.helper.SubscriptionAnnotation; import java.util.List; @@ -244,15 +247,21 @@ public class MobileNetworkActivity extends SettingsBaseActivity */ @VisibleForTesting SubscriptionInfo getSubscription() { + List subList = + (new SelectableSubscriptions(this, true)).call(); + SubscriptionAnnotation currentSubInfo = null; if (mCurSubscriptionId != SUB_ID_NULL) { - return getSubscriptionForSubId(mCurSubscriptionId); + currentSubInfo = subList.stream() + .filter(SubscriptionAnnotation::isDisplayAllowed) + .filter(subAnno -> (subAnno.getSubscriptionId() == mCurSubscriptionId)) + .findFirst().orElse(null); } - final List subInfos = getProxySubscriptionManager() - .getActiveSubscriptionsInfo(); - if (CollectionUtils.isEmpty(subInfos)) { - return null; + if (currentSubInfo == null) { + currentSubInfo = subList.stream() + .filter(SubscriptionAnnotation::isDisplayAllowed) + .findFirst().orElse(null); } - return subInfos.get(0); + return (currentSubInfo == null) ? null : currentSubInfo.getSubInfo(); } @VisibleForTesting diff --git a/tests/unit/src/com/android/settings/network/helper/SelectableSubscriptionsTest.java b/tests/unit/src/com/android/settings/network/helper/SelectableSubscriptionsTest.java new file mode 100644 index 00000000000..6bd2c138d9e --- /dev/null +++ b/tests/unit/src/com/android/settings/network/helper/SelectableSubscriptionsTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 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.helper; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.concurrent.atomic.AtomicIntegerArray; + +@RunWith(AndroidJUnit4.class) +public class SelectableSubscriptionsTest { + + @Before + public void setUp() { + } + + + @Test + public void atomicToList_nullInput_getNoneNullEmptyList() { + List result = SelectableSubscriptions.atomicToList(null); + + assertThat(result.size()).isEqualTo(0); + } + + @Test + public void atomicToList_zeroLengthInput_getEmptyList() { + List result = SelectableSubscriptions.atomicToList(new AtomicIntegerArray(0)); + + assertThat(result.size()).isEqualTo(0); + } + + @Test + public void atomicToList_subIdInArray_getList() { + AtomicIntegerArray array = new AtomicIntegerArray(3); + array.set(0, 3); + array.set(1, 7); + array.set(2, 4); + + List result = SelectableSubscriptions.atomicToList(array); + + assertThat(result.size()).isEqualTo(3); + assertThat(result.get(0)).isEqualTo(3); + assertThat(result.get(1)).isEqualTo(7); + assertThat(result.get(2)).isEqualTo(4); + } +}