Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eb4a22b1 authored by Meng Wang's avatar Meng Wang Committed by Android (Google) Code Review
Browse files

Merge changes Iba187a60,Ic6d7c4c6 into main

* changes:
  Optimize euicc invocations by keeping channel open session ends.
  EuiccSession: tell ApduSender to keep channel open after sending APDU if any session started.
parents d47dba33 7007cb9b
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -831,6 +831,7 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
                        }
                        case CMD_DOWNLOAD_SUBSCRIPTION: {
                            DownloadRequest request = (DownloadRequest) message.obj;
                            EuiccSession.get().startSession(EuiccSession.DOWNLOAD);
                            mEuiccService.downloadSubscription(slotId,
                                    request.mPortIndex,
                                    request.mSubscription,
@@ -845,6 +846,7 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
                                                    .onDownloadComplete(result);
                                                onCommandEnd(callback);
                                            });
                                            EuiccSession.get().endSession(EuiccSession.DOWNLOAD);
                                        }
                                    });
                            break;
+141 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.internal.telephony.euicc;

import android.util.ArraySet;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.uicc.euicc.apdu.ApduSender;
import com.android.telephony.Rlog;

import java.util.Set;

/**
 * A eUICC transaction session aims to optimize multiple back-to-back EuiccPort API calls by only
 * open and close a logical channel once.
 *
 * <p>This class is thread-safe.
 */
public class EuiccSession {
    private static final String TAG = "EuiccSession";

    // **** Well known session IDs, see #startSession() ****
    public static final String DOWNLOAD = "DOWNLOAD";

    @GuardedBy("EuiccSession.class")
    private static EuiccSession sInstance;

    public static synchronized EuiccSession get() {
        if (sInstance == null) {
            sInstance = new EuiccSession();
        }
        return sInstance;
    }

    @GuardedBy("this")
    private final Set<String> mSessions = new ArraySet<>();

    @GuardedBy("this")
    private final Set<ApduSender> mApduSenders = new ArraySet<>();

    /**
     * Marks the start of a eUICC transaction session.
     *
     * <p>A session means a long-open logical channel (see {@link ApduSender}) used to
     * send multiple APDUs for one action e.g. {@link EuiccController#downloadSubscription()}.
     * Those APDUs can be send by one or multiple {@link EuiccCardController} methods.
     *
     * <p>Ideally a session should correespond to one phoneId and hence just one logical channel.
     * But many {@link EuiccCardController} methods uses first available port and is not specific
     * to a phoneId. So EuiccController cannot choose one phoneId to use. Hence a session has to
     * be not specific to phoneId, i.e. for DSDS device both phoneId's will be in a session.
     *
     * <p>If called multiple times with different {@code sessionId}'s, the session is truly closed
     * when the all sessions are ended. See {@link #endSession()}.
     *
     * @param sessionId The session ID.
     */
    public void startSession(String sessionId) {
        if (!Flags.optimizationApduSender()) {
            // Other methods in this class is no-op if no session started.
            // Do not add flag to other methods, so if the flag gets turned off,
            // the session can be ended properly.
            return;
        }
        Rlog.i(TAG, "startSession: " + sessionId);
        synchronized(this) {
            mSessions.add(sessionId);
        }
    }

    /** Returns {@code true} if there is at least one session ongoing. */
    public boolean hasSession() {
        boolean hasSession;
        synchronized(this) {
            hasSession = !mSessions.isEmpty();
        }
        Rlog.i(TAG, "hasSession: " + hasSession);
        return hasSession;
    }

    /**
     * Notes that a logical channel may be opened by the {@code apduSender}, which will
     * be used to close the channel when session ends (see {@link #endSession()}).
     *
     * <p>No-op if no session ongoing (see {@link #hasSession()}).
     *
     * @param apduSender The ApduSender that will open the channel.
     */
    public void noteChannelOpen(ApduSender apduSender) {
        Rlog.i(TAG, "noteChannelOpen: " + apduSender);
        synchronized(this) {
            if (hasSession()) {
                mApduSenders.add(apduSender);
            }
        }
    }

    /**
     * Marks the end of a eUICC transaction session. If this ends the last ongoing session,
     * try to close the logical channel using the noted {@code apduSender}
     * (see {@link #noteChannelOpen()}).
     *
     * @param sessionId The session ID.
     */
    public void endSession(String sessionId) {
        Rlog.i(TAG, "endSession: " + sessionId);
        ApduSender[] apduSenders = new ApduSender[0];
        synchronized(this) {
            boolean sessionRemoved = mSessions.remove(sessionId);
            // sessionRemoved is false if the `sessionId` was never started or there was
            // no session at all i.e. `sessions` is empty. Don't bother invoke `apduSender`.
            if (sessionRemoved && mSessions.isEmpty()) {
                // copy mApduSenders to a local variable so we don't call closeAnyOpenChannel()
                // which can take time in synchronized block.
                apduSenders = mApduSenders.toArray(apduSenders);
                mApduSenders.clear();
            }
        }
        for (ApduSender apduSender : apduSenders) {
            apduSender.closeAnyOpenChannel();
        }
    }

    @VisibleForTesting
    public EuiccSession() {}
}
+291 −117

File changed.

Preview size limit exceeded, changes collapsed.

+178 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.internal.telephony.euicc;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;

import android.app.PendingIntent;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserManager;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.service.euicc.DownloadSubscriptionResult;
import android.service.euicc.EuiccService;
import android.service.euicc.GetDefaultDownloadableSubscriptionListResult;
import android.service.euicc.GetDownloadableSubscriptionMetadataResult;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.UiccAccessRule;
import android.telephony.UiccCardInfo;
import android.telephony.UiccPortInfo;
import android.telephony.euicc.DownloadableSubscription;
import android.telephony.euicc.EuiccInfo;
import android.telephony.euicc.EuiccManager;

import androidx.test.runner.AndroidJUnit4;

import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneFactory;
import com.android.internal.telephony.TelephonyTest;
import com.android.internal.telephony.euicc.EuiccConnector.GetOtaStatusCommandCallback;
import com.android.internal.telephony.euicc.EuiccConnector.OtaStatusChangedCallback;
import com.android.internal.telephony.uicc.euicc.apdu.ApduSender;
import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.uicc.UiccSlot;

import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;

@RunWith(AndroidJUnit4.class)
public class EuiccSessionTest extends TelephonyTest {
    @Rule
    public final TestRule compatChangeRule = new PlatformCompatChangeRule();
    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
    @Rule
    public final MockitoRule rule = MockitoJUnit.rule();

    private static final String SESSION_ID_1 = "SESSION_ID_1";
    private static final String SESSION_ID_2 = "SESSION_ID_2";

    private EuiccSession mEuiccSession;
    @Mock private ApduSender mApduSender;

    @Before
    public void setUp() throws Exception {
        mEuiccSession = new EuiccSession();
    }

    @Test
    @DisableFlags(Flags.FLAG_OPTIMIZATION_APDU_SENDER)
    public void startOneSession_featureDisabled_noop() throws Exception {
        mEuiccSession.startSession(SESSION_ID_1);
        mEuiccSession.noteChannelOpen(mApduSender);

        assertThat(mEuiccSession.hasSession()).isFalse();

        mEuiccSession.endSession(SESSION_ID_1);

        assertThat(mEuiccSession.hasSession()).isFalse();
        verify(mApduSender, never()).closeAnyOpenChannel();
    }

    @Test
    @EnableFlags(Flags.FLAG_OPTIMIZATION_APDU_SENDER)
    public void startOneSession_endSession_hasSession() throws Exception {
        mEuiccSession.startSession(SESSION_ID_1);
        mEuiccSession.noteChannelOpen(mApduSender);

        assertThat(mEuiccSession.hasSession()).isTrue();

        mEuiccSession.endSession(SESSION_ID_2);

        assertThat(mEuiccSession.hasSession()).isTrue();
        verify(mApduSender, never()).closeAnyOpenChannel();

        mEuiccSession.endSession(SESSION_ID_1);

        assertThat(mEuiccSession.hasSession()).isFalse();
        verify(mApduSender).closeAnyOpenChannel();
    }

    @Test
    @EnableFlags(Flags.FLAG_OPTIMIZATION_APDU_SENDER)
    public void startTwoSession_endSession_hasSession() throws Exception {
        mEuiccSession.startSession(SESSION_ID_1);
        mEuiccSession.noteChannelOpen(mApduSender);
        mEuiccSession.startSession(SESSION_ID_2);

        assertThat(mEuiccSession.hasSession()).isTrue();

        mEuiccSession.endSession(SESSION_ID_1);
        verify(mApduSender, never()).closeAnyOpenChannel();

        assertThat(mEuiccSession.hasSession()).isTrue();

        mEuiccSession.endSession(SESSION_ID_2);

        assertThat(mEuiccSession.hasSession()).isFalse();
        verify(mApduSender).closeAnyOpenChannel();
    }

    @Test
    @EnableFlags(Flags.FLAG_OPTIMIZATION_APDU_SENDER)
    public void noteChannelOpen_noSession_noop() throws Exception {
        // noteChannelOpen called without a session started
        mEuiccSession.noteChannelOpen(mApduSender);

        assertThat(mEuiccSession.hasSession()).isFalse();

        mEuiccSession.endSession(SESSION_ID_1);

        assertThat(mEuiccSession.hasSession()).isFalse();
        verify(mApduSender, never()).closeAnyOpenChannel();
    }
}
+195 −14

File changed.

Preview size limit exceeded, changes collapsed.