1 /* 2 * Copyright (C) 2024 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 package com.android.internal.telephony.euicc; 17 18 import android.annotation.Nullable; 19 import android.content.Context; 20 import android.util.ArraySet; 21 22 import com.android.internal.annotations.GuardedBy; 23 import com.android.internal.annotations.VisibleForTesting; 24 import com.android.internal.telephony.flags.Flags; 25 import com.android.internal.telephony.uicc.euicc.apdu.ApduSender; 26 import com.android.telephony.Rlog; 27 28 import java.util.Set; 29 30 /** 31 * A eUICC transaction session aims to optimize multiple back-to-back EuiccPort API calls by only 32 * open and close a logical channel once. 33 * 34 * <p>This class is thread-safe. 35 */ 36 public class EuiccSession { 37 private static final String TAG = "EuiccSession"; 38 39 // **** Well known session IDs, see #startSession() **** 40 public static final String DOWNLOAD = "DOWNLOAD"; 41 42 @GuardedBy("EuiccSession.class") 43 private static EuiccSession sInstance; 44 get(Context context)45 public static synchronized EuiccSession get(Context context) { 46 if (sInstance == null) { 47 sInstance = new EuiccSession(context); 48 } 49 return sInstance; 50 } 51 52 @GuardedBy("this") 53 private final Set<String> mSessions = new ArraySet<>(); 54 55 @GuardedBy("this") 56 private final Set<ApduSender> mApduSenders = new ArraySet<>(); 57 private final Context mContext; 58 59 /** 60 * Returns true if the ApduSender optimization is enabled i.e. a logical channel is opened 61 * and kept open for multiple APDU commands within one session. 62 * 63 * This is gated by both an aconfig flag and a device-specific flag. 64 */ optimizeApduSender()65 private boolean optimizeApduSender() { 66 return Flags.optimizationApduSender() && mContext.getResources().getBoolean( 67 com.android.internal.R.bool.euicc_optimize_apdu_sender); 68 } 69 70 /** 71 * Marks the start of a eUICC transaction session. 72 * 73 * <p>A session means a long-open logical channel (see {@link ApduSender}) used to 74 * send multiple APDUs for one action e.g. {@link EuiccController#downloadSubscription()}. 75 * Those APDUs can be send by one or multiple {@link EuiccCardController} methods. 76 * 77 * <p>Ideally a session should correespond to one phoneId and hence just one logical channel. 78 * But many {@link EuiccCardController} methods uses first available port and is not specific 79 * to a phoneId. So EuiccController cannot choose one phoneId to use. Hence a session has to 80 * be not specific to phoneId, i.e. for DSDS device both phoneId's will be in a session. 81 * 82 * <p>If called multiple times with different {@code sessionId}'s, the session is truly closed 83 * when the all sessions are ended. See {@link #endSession()}. 84 * 85 * @param sessionId The session ID. 86 */ startSession(String sessionId)87 public void startSession(String sessionId) { 88 if (!optimizeApduSender()) { 89 // Other methods in this class is no-op if no session started. 90 // Do not add flag to other methods, so if the flag gets turned off, 91 // the session can be ended properly. 92 return; 93 } 94 Rlog.i(TAG, "startSession: " + sessionId); 95 synchronized(this) { 96 mSessions.add(sessionId); 97 } 98 } 99 100 /** Returns {@code true} if there is at least one session ongoing. */ hasSession()101 public boolean hasSession() { 102 boolean hasSession = hasSessionInternal(); 103 Rlog.i(TAG, "hasSession: " + hasSession); 104 return hasSession; 105 } 106 107 // The bare metal implementation of hasSession() without logging. hasSessionInternal()108 private boolean hasSessionInternal() { 109 synchronized(this) { 110 return !mSessions.isEmpty(); 111 } 112 } 113 114 /** 115 * Notes that a logical channel may be opened by the {@code apduSender}, which will 116 * be used to close the channel when session ends (see {@link #endSession()}). 117 * 118 * <p>No-op if no session ongoing (see {@link #hasSession()}). 119 * 120 * @param apduSender The ApduSender that will open the channel. 121 */ noteChannelOpen(ApduSender apduSender)122 public void noteChannelOpen(ApduSender apduSender) { 123 Rlog.i(TAG, "noteChannelOpen: " + apduSender); 124 synchronized(this) { 125 if (hasSessionInternal()) { 126 mApduSenders.add(apduSender); 127 } 128 } 129 } 130 131 /** 132 * Marks the end of a eUICC transaction session. If this ends the last ongoing session, 133 * try to close the logical channel using the noted {@code apduSender}s 134 * (see {@link #noteChannelOpen()}). 135 * 136 * @param sessionId The session ID. 137 */ endSession(String sessionId)138 public void endSession(String sessionId) { 139 Rlog.i(TAG, "endSession: " + sessionId); 140 endSessionInternal(sessionId); 141 } 142 143 /** 144 * Marks the end of all eUICC transaction sessions and close the logical 145 * channels using the noted {@code apduSender}s 146 * (see {@link #noteChannelOpen()}). 147 * 148 * <p>This is useful in global cleanup e.g. when EuiccService 149 * implementation app crashes and indivial {@link #endSession()} calls 150 * won't happen in {@link EuiccConnector}. 151 */ endAllSessions()152 public void endAllSessions() { 153 Rlog.i(TAG, "endAllSessions"); 154 endSessionInternal(null); 155 } 156 157 // The implementation of endSession(sessionId) or endAllSessions() when the sessionId is null, 158 // without logging. endSessionInternal(@ullable String sessionId)159 private void endSessionInternal(@Nullable String sessionId) { 160 ApduSender[] apduSenders = new ApduSender[0]; 161 synchronized(this) { 162 boolean sessionRemoved = removeOrClear(mSessions, sessionId); 163 // 1. sessionRemoved is false if the `sessionId` was never started or there was 164 // no session. Don't bother invoke `apduSender`. 165 // 2. If some session is removed, and as a result there is no more session, we 166 // can clsoe channels. 167 if (sessionRemoved && !hasSessionInternal()) { 168 // copy mApduSenders to a local variable so we don't call closeAnyOpenChannel() 169 // which can take time in synchronized block. 170 apduSenders = mApduSenders.toArray(apduSenders); 171 mApduSenders.clear(); 172 } 173 } 174 for (ApduSender apduSender : apduSenders) { 175 apduSender.closeAnyOpenChannel(); 176 } 177 } 178 179 /** 180 * Removes the given element from the set. If the element is null, clears the set. 181 * 182 * @return true if the set changed as a result of the call 183 */ removeOrClear(Set<String> collection, @Nullable String element)184 private static boolean removeOrClear(Set<String> collection, @Nullable String element) { 185 if (element == null) { 186 boolean collectionChanged = !collection.isEmpty(); 187 collection.clear(); 188 return collectionChanged; 189 } else { 190 return collection.remove(element); 191 } 192 } 193 194 @VisibleForTesting EuiccSession(Context context)195 public EuiccSession(Context context) { 196 mContext = context; 197 } 198 } 199