[Settings] Hide subscriptions not existed within device

For non-active subscriptions, the one inserted in slot
but turned off need to be visible to the user. However,
the one un-plugged need to be invisble.

Since SubscriptionUtil#getSelectableSubscriptionInfoList() didn't cover all the cases required. Create this one to fit into the criteria required here.

Bug: 191228344
Test: local
Change-Id: Ia68c23b007164b7520456cb6c7427ca142558b59
This commit is contained in:
Bonian Chen
2021-07-06 14:42:04 +08:00
parent d0748126b0
commit 75f1450bbf
7 changed files with 550 additions and 7 deletions

View File

@@ -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.

View File

@@ -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<AtomicIntegerArray> {
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<UiccCardInfo> 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());
}
}

View File

@@ -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<AtomicIntegerArray> {
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();
}
}

View File

@@ -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<List<SubscriptionAnnotation>> {
private static final String TAG = "SelectableSubscriptions";
private static final ParcelUuid mEmptyUuid = ParcelUuid.fromString("0-0-0-0-0");
private Context mContext;
private Supplier<List<SubscriptionInfo>> mSubscriptions;
private Predicate<SubscriptionAnnotation> 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<SubscriptionAnnotation> call() {
TelephonyManager telMgr = mContext.getSystemService(TelephonyManager.class);
try {
// query in background thread
Future<AtomicIntegerArray> eSimCardId =
ThreadUtils.postOnBackgroundThread(new QueryEsimCardId(telMgr));
// query in background thread
Future<AtomicIntegerArray> simSlotIndex =
ThreadUtils.postOnBackgroundThread(
new QuerySimSlotIndex(telMgr, true, true));
// query in background thread
Future<AtomicIntegerArray> activeSimSlotIndex =
ThreadUtils.postOnBackgroundThread(
new QuerySimSlotIndex(telMgr, false, true));
List<SubscriptionInfo> subInfoList = mSubscriptions.get();
// wait for result from background thread
List<Integer> eSimCardIdList = atomicToList(eSimCardId.get());
List<Integer> simSlotIndexList = atomicToList(simSlotIndex.get());
List<Integer> activeSimSlotIndexList = atomicToList(activeSimSlotIndex.get());
// group by GUID
Map<ParcelUuid, List<SubscriptionAnnotation>> 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<SubscriptionAnnotation> annoList) {
Comparator<SubscriptionAnnotation> 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<SubscriptionInfo> getSubInfoList(Context context,
Function<SubscriptionManager, List<SubscriptionInfo>> 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<SubscriptionInfo> getAvailableSubInfoList(Context context) {
return getSubInfoList(context, SubscriptionManager::getAvailableSubscriptionInfoList);
}
protected List<SubscriptionInfo> getActiveSubInfoList(Context context) {
return getSubInfoList(context, SubscriptionManager::getActiveSubscriptionInfoList);
}
@Keep
@VisibleForTesting
protected static List<Integer> atomicToList(AtomicIntegerArray atomicIntArray) {
if (atomicIntArray == null) {
return Collections.emptyList();
}
return IntStream.range(0, atomicIntArray.length())
.map(idx -> atomicIntArray.get(idx)).boxed()
.collect(Collectors.toList());
}
}

View File

@@ -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<SubscriptionInfo> mSubInfoList;
private int mIndexWithinList;
/**
* Constructor of builder
* @param subInfoList list of subscription info
* @param indexWithinList target index within list provided
*/
public Builder(List<SubscriptionInfo> subInfoList, int indexWithinList) {
mSubInfoList = subInfoList;
mIndexWithinList = indexWithinList;
}
public SubscriptionAnnotation build(Context context, List<Integer> eSimCardId,
List<Integer> simSlotIndex, List<Integer> activeSimSlotIndex) {
return new SubscriptionAnnotation(mSubInfoList, mIndexWithinList, context,
eSimCardId, simSlotIndex, activeSimSlotIndex);
}
}
/**
* Constructor of class
*/
protected SubscriptionAnnotation(List<SubscriptionInfo> subInfoList, int subInfoIndex,
Context context, List<Integer> eSimCardId,
List<Integer> simSlotIndex, List<Integer> 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);
}
}

View File

@@ -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<SubscriptionAnnotation> 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<SubscriptionInfo> 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

View File

@@ -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<Integer> result = SelectableSubscriptions.atomicToList(null);
assertThat(result.size()).isEqualTo(0);
}
@Test
public void atomicToList_zeroLengthInput_getEmptyList() {
List<Integer> 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<Integer> 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);
}
}