From 0726f263a89e64a13a16679c313fccfc17bf91fd Mon Sep 17 00:00:00 2001 From: Bonian Chen Date: Wed, 7 Jul 2021 23:30:40 +0800 Subject: [PATCH] [Settings] Rollback group UUID merging in SIM settings Within origin design, subscriptions with same group UUID are not merged together. This is a fix which changing grouping by UUID part into a configurable option which allows to be enabled in some other conditions. Bug: 191228344 Test: local Change-Id: I0101f4a51ec2342f059762f0e7d38bb4e93554cf --- .../helper/SelectableSubscriptions.java | 75 ++++------ .../helper/SubscriptionAnnotation.java | 22 ++- .../network/helper/SubscriptionGrouping.java | 96 ++++++++++++ .../helper/SelectableSubscriptionsTest.java | 1 - .../helper/SubscriptionGroupingTest.java | 137 ++++++++++++++++++ 5 files changed, 281 insertions(+), 50 deletions(-) create mode 100644 src/com/android/settings/network/helper/SubscriptionGrouping.java create mode 100644 tests/unit/src/com/android/settings/network/helper/SubscriptionGroupingTest.java diff --git a/src/com/android/settings/network/helper/SelectableSubscriptions.java b/src/com/android/settings/network/helper/SelectableSubscriptions.java index 224aa07354d..436e84c08b7 100644 --- a/src/com/android/settings/network/helper/SelectableSubscriptions.java +++ b/src/com/android/settings/network/helper/SelectableSubscriptions.java @@ -16,7 +16,6 @@ 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; @@ -29,29 +28,38 @@ 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.function.UnaryOperator; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * This is a Callable class to query user selectable subscription list. + * + * Here's example of creating a Callable for retrieving a list of SubscriptionAnnotation + * for active Subscriptions: + * + * List result = (new SelectableSubscriptions(context, false)).call(); + * + * Another example for retrieving a list of SubscriptionAnnotation for all subscriptions + * accessible in another thread. + * + * List result = ExecutorService.submit( + * new SelectableSubscriptions(context, true)).get() */ 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; + private Function, List> mFinisher; /** * Constructor of class @@ -65,6 +73,17 @@ public class SelectableSubscriptions implements Callable getActiveSubInfoList(context)); mFilter = disabledSlotsIncluded ? (subAnno -> subAnno.isExisted()) : (subAnno -> subAnno.isActive()); + mFinisher = annoList -> annoList; + } + + /** + * Add UnaryOperator to be applied to the final result. + * @param finisher a function to be applied to the final result. + */ + public SelectableSubscriptions addFinisher( + UnaryOperator> finisher) { + mFinisher = mFinisher.andThen(finisher); + return this; } /** @@ -96,60 +115,20 @@ public class SelectableSubscriptions implements Callable simSlotIndexList = atomicToList(simSlotIndex.get()); List activeSimSlotIndexList = atomicToList(activeSimSlotIndex.get()); - // group by GUID - Map> groupedSubInfoList = - IntStream.range(0, subInfoList.size()) + // build a list of SubscriptionAnnotation + return 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()); + .collect(Collectors.collectingAndThen(Collectors.toList(), mFinisher)); } 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); diff --git a/src/com/android/settings/network/helper/SubscriptionAnnotation.java b/src/com/android/settings/network/helper/SubscriptionAnnotation.java index d314474030f..fae5b9bbb4b 100644 --- a/src/com/android/settings/network/helper/SubscriptionAnnotation.java +++ b/src/com/android/settings/network/helper/SubscriptionAnnotation.java @@ -16,9 +16,12 @@ 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 androidx.annotation.Keep; +import androidx.annotation.VisibleForTesting; import com.android.settings.network.SubscriptionUtil; @@ -38,6 +41,8 @@ public class SubscriptionAnnotation { private boolean mIsActive; private boolean mIsAllowToDisplay; + public static final ParcelUuid EMPTY_UUID = ParcelUuid.fromString("0-0-0-0-0"); + public static final int TYPE_UNKNOWN = 0x0; public static final int TYPE_PSIM = 0x1; public static final int TYPE_ESIM = 0x2; @@ -70,6 +75,8 @@ public class SubscriptionAnnotation { /** * Constructor of class */ + @Keep + @VisibleForTesting protected SubscriptionAnnotation(List subInfoList, int subInfoIndex, Context context, List eSimCardId, List simSlotIndex, List activeSimSlotIndexList) { @@ -101,37 +108,50 @@ public class SubscriptionAnnotation { } // the index provided during construction of Builder + @Keep public int getOrderingInList() { return mOrderWithinList; } // type of subscription + @Keep public int getType() { return mType; } // if a subscription is existed within device + @Keep public boolean isExisted() { return mIsExisted; } // if a subscription is currently ON + @Keep public boolean isActive() { return mIsActive; } // if display of subscription is allowed + @Keep public boolean isDisplayAllowed() { return mIsAllowToDisplay; } // the subscription ID + @Keep public int getSubscriptionId() { return (mSubInfo == null) ? SubscriptionManager.INVALID_SUBSCRIPTION_ID : mSubInfo.getSubscriptionId(); } + // the grouping UUID + @Keep + public ParcelUuid getGroupUuid() { + return (mSubInfo == null) ? null : mSubInfo.getGroupUuid(); + } + // the SubscriptionInfo + @Keep public SubscriptionInfo getSubInfo() { return mSubInfo; } diff --git a/src/com/android/settings/network/helper/SubscriptionGrouping.java b/src/com/android/settings/network/helper/SubscriptionGrouping.java new file mode 100644 index 00000000000..cfb5ea92650 --- /dev/null +++ b/src/com/android/settings/network/helper/SubscriptionGrouping.java @@ -0,0 +1,96 @@ +/* + * 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.os.ParcelUuid; + +import androidx.annotation.Keep; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.network.helper.SubscriptionAnnotation; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +/** + * A UnaryOperator for converting a list of SubscriptionAnnotation into + * another list of SubscriptionAnnotation based on group UUID. + * Only one SubscriptionAnnotation with entries with same (valid) group UUID would be kept. + * + * Here's an example when applying this operation as a finisher of SelectableSubscriptions: + * + * Callable callable = (new SelectableSubscriptions(context, true)) + * .addFinisher(new SubscriptionGrouping()); + * + * List result = ExecutorService.submit(callable).get() + */ +public class SubscriptionGrouping + implements UnaryOperator> { + + // implementation of UnaryOperator + public List apply(List listOfSubscriptions) { + // group by GUID + Map> groupedSubInfoList = + listOfSubscriptions.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(subAnno -> getGroupUuid(subAnno))); + + // select best one from subscription(s) within the same group + groupedSubInfoList.replaceAll((uuid, annoList) -> { + if ((uuid == SubscriptionAnnotation.EMPTY_UUID) || (annoList.size() <= 1)) { + return annoList; + } + return Collections.singletonList(selectBestFromList(annoList)); + }); + + // build a stream of subscriptions + return groupedSubInfoList.values() + .stream().flatMap(List::stream).collect(Collectors.toList()); + } + + @Keep + @VisibleForTesting + protected ParcelUuid getGroupUuid(SubscriptionAnnotation subAnno) { + ParcelUuid groupUuid = subAnno.getGroupUuid(); + return (groupUuid == null) ? SubscriptionAnnotation.EMPTY_UUID : 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); + } +} diff --git a/tests/unit/src/com/android/settings/network/helper/SelectableSubscriptionsTest.java b/tests/unit/src/com/android/settings/network/helper/SelectableSubscriptionsTest.java index 6bd2c138d9e..04e91222d60 100644 --- a/tests/unit/src/com/android/settings/network/helper/SelectableSubscriptionsTest.java +++ b/tests/unit/src/com/android/settings/network/helper/SelectableSubscriptionsTest.java @@ -33,7 +33,6 @@ public class SelectableSubscriptionsTest { public void setUp() { } - @Test public void atomicToList_nullInput_getNoneNullEmptyList() { List result = SelectableSubscriptions.atomicToList(null); diff --git a/tests/unit/src/com/android/settings/network/helper/SubscriptionGroupingTest.java b/tests/unit/src/com/android/settings/network/helper/SubscriptionGroupingTest.java new file mode 100644 index 00000000000..97bdb7433a7 --- /dev/null +++ b/tests/unit/src/com/android/settings/network/helper/SubscriptionGroupingTest.java @@ -0,0 +1,137 @@ +/* + * 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.ParcelUuid; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.network.helper.SubscriptionAnnotation; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class SubscriptionGroupingTest { + + private ParcelUuid mMockUuid; + + private Context mContext; + private SubscriptionGrouping mTarget; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(ApplicationProvider.getApplicationContext()); + mMockUuid = ParcelUuid.fromString("1-1-1-1-1"); + mTarget = spy(new SubscriptionGrouping()); + } + + @Test + public void apply_multipleEntriesWithSameGroupUuid_onlyOneLeft() { + SubscriptionAnnotation subAnno1 = new TestSubAnnotation(1, + SubscriptionAnnotation.TYPE_ESIM, true, true, mMockUuid); + SubscriptionAnnotation subAnno2 = new TestSubAnnotation(2, + SubscriptionAnnotation.TYPE_PSIM, true, true, mMockUuid); + SubscriptionAnnotation subAnno3 = new TestSubAnnotation(3, + SubscriptionAnnotation.TYPE_ESIM, true, true, mMockUuid); + doReturn(mMockUuid).when(mTarget).getGroupUuid(any()); + + List result = mTarget + .apply(Arrays.asList(subAnno2, subAnno1, subAnno3)); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0)).isEqualTo(subAnno3); + } + + @Test + public void apply_multipleEntriesWithSameGroupUuid_disabledOneIsAvoided() { + SubscriptionAnnotation subAnno1 = new TestSubAnnotation(1, + SubscriptionAnnotation.TYPE_ESIM, true, true, mMockUuid); + SubscriptionAnnotation subAnno2 = new TestSubAnnotation(2, + SubscriptionAnnotation.TYPE_PSIM, true, true, mMockUuid); + SubscriptionAnnotation subAnno3 = new TestSubAnnotation(3, + SubscriptionAnnotation.TYPE_ESIM, false, false, mMockUuid); + doReturn(mMockUuid).when(mTarget).getGroupUuid(any()); + + List result = mTarget + .apply(Arrays.asList(subAnno2, subAnno1, subAnno3)); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0)).isEqualTo(subAnno1); + } + + private class TestSubAnnotation extends SubscriptionAnnotation { + private int mSubId; + private int mSimType; + private boolean mIsActive; + private boolean mIsDisplayAllowed; + private ParcelUuid mUuid; + + private TestSubAnnotation(int subId, int simType, + boolean isActive, boolean isDisplayAllowed, ParcelUuid guuid) { + super(null, -1, null, null, null, null); + mSubId = subId; + mSimType = simType; + mIsActive = isActive; + mIsDisplayAllowed = isDisplayAllowed; + mUuid = guuid; + } + + @Override + public int getSubscriptionId() { + return mSubId; + } + + @Override + public int getType() { + return mSimType; + } + + @Override + public boolean isExisted() { + return true; + } + + @Override + public boolean isActive() { + return mIsActive; + } + + @Override + public boolean isDisplayAllowed() { + return mIsDisplayAllowed; + } + + @Override + public ParcelUuid getGroupUuid() { + return mUuid; + } + } +}