blob: fba6e737bbc7f0dee4cf77a22e73821c3c5ebdd9 [file] [log] [blame]
/*
* Copyright (C) 2022 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 android.carrierapi.cts.targetprep;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.Manifest;
import android.app.UiAutomation;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.UiccSlotMapping;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.UiccUtil.ApduCommand;
import com.android.compatibility.common.util.UiccUtil.ApduResponse;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
class ApduScriptUtil {
private static final String TAG = "ApduScriptUtil";
private static final long SET_SIM_POWER_TIMEOUT_SECONDS = 30;
// TelephonyManager constants are @hide, so manually copy them here
private static final int CARD_POWER_DOWN = 0;
private static final int CARD_POWER_UP = 1;
private static final int CARD_POWER_UP_PASS_THROUGH = 2;
private static Context getContext() {
return InstrumentationRegistry.getInstrumentation().getTargetContext();
}
/**
* Executes an APDU script over the basic channel.
*
* <p>The sequence of events is as follows:
*
* <ol>
* <li>Power the SIM card (as specified by {@code subId}) down
* <li>Power the SIM card back up in pass-through mode (see {@link
* TelephonyManager#CARD_POWER_UP_PASS_THROUGH})
* <li>Transmit {@code apdus} over the basic channel to the SIM
* <li>Power the SIM card down
* <li>Power the SIM card back up
* </ol>
*
* <p>If any of the response statuses from the SIM are not {@code 9000} or {@code 91xx}, that is
* considered an error and an exception will be thrown, terminating the script execution. {@code
* 61xx} statuses are handled internally.
*
* <p>NOTE: {@code subId} must correspond to an active SIM.
*/
public static void runApduScript(int subId, List<ApduCommand> apdus)
throws InterruptedException {
SubscriptionInfo sub =
getContext()
.getSystemService(SubscriptionManager.class)
.getActiveSubscriptionInfo(subId);
assertThat(sub).isNotNull();
assertThat(sub.getSimSlotIndex()).isNotEqualTo(SubscriptionManager.INVALID_SIM_SLOT_INDEX);
int logicalSlotId = sub.getSimSlotIndex();
// We need a physical slot ID + port to send APDU to in the case when we power the SIM up in
// pass-through mode, which will result in a temporary lack of SubscriptionInfo until we
// restore it to the normal power mode.
int physicalSlotId = -1;
int portIndex = -1;
Collection<UiccSlotMapping> slotMappings =
ShellIdentityUtils.invokeMethodWithShellPermissions(
getContext().getSystemService(TelephonyManager.class),
TelephonyManager::getSimSlotMapping,
Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
for (UiccSlotMapping slotMapping : slotMappings) {
if (slotMapping.getLogicalSlotIndex() == logicalSlotId) {
physicalSlotId = slotMapping.getPhysicalSlotIndex();
portIndex = slotMapping.getPortIndex();
break;
}
}
if (physicalSlotId == -1 || portIndex == -1) {
throw new IllegalStateException(
"Unable to determine physical slot + port from logical slot: " + logicalSlotId);
}
try {
// Note: this may wipe out subId, so we need to use the slot/port-based APDU method
// while in pass-through mode.
rebootSimCard(logicalSlotId, CARD_POWER_UP_PASS_THROUGH);
sendApdus(physicalSlotId, portIndex, apdus);
} finally {
// Even if rebootSimCard failed midway through (leaving the SIM in POWER_DOWN) or timed
// out waiting for the right SIM state after rebooting in POWER_UP_PASS_THROUGH, we try
// to bring things back to the normal POWER_UP state to avoid breaking other suites.
rebootSimCard(logicalSlotId, CARD_POWER_UP);
}
}
/**
* Powers the SIM card down, waits for it to become ABSENT, then powers it back up in {@code
* targetPowerState} and waits for it to become PRESENT.
*/
private static void rebootSimCard(int logicalSlotId, int targetPowerState)
throws InterruptedException {
setSimPowerAndWaitForCardState(
logicalSlotId, CARD_POWER_DOWN, TelephonyManager.SIM_STATE_ABSENT);
setSimPowerAndWaitForCardState(
logicalSlotId, targetPowerState, TelephonyManager.SIM_STATE_PRESENT);
}
private static void setSimPowerAndWaitForCardState(
int logicalSlotId, int targetPowerState, int targetSimState)
throws InterruptedException {
// A small little state machine:
// 1. Call setSimPower(targetPowerState)
// 2. Wait for callback passed to setSimPower to complete, fail if not SUCCESS
// 3. Wait for SIM state broadcast to match targetSimState
// TODO(b/229790522) figure out a cleaner expression here.
AtomicInteger powerResult = new AtomicInteger(Integer.MIN_VALUE);
CountDownLatch powerLatch = new CountDownLatch(1);
CountDownLatch cardStateLatch = new CountDownLatch(1);
BroadcastReceiver cardStateReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED.equals(
intent.getAction())) {
return;
}
int slotId =
intent.getIntExtra(
SubscriptionManager.EXTRA_SLOT_INDEX,
SubscriptionManager.INVALID_SIM_SLOT_INDEX);
if (slotId != logicalSlotId) return;
int simState =
intent.getIntExtra(
TelephonyManager.EXTRA_SIM_STATE,
TelephonyManager.SIM_STATE_UNKNOWN);
if (simState == targetSimState) {
if (powerLatch.getCount() == 0) {
cardStateLatch.countDown();
} else {
Log.w(
TAG,
"Received SIM state "
+ simState
+ " prior to setSimPowerState callback");
}
} else {
Log.d(TAG, "Unwanted SIM state: " + simState);
}
}
};
// Since we need to listen to a broadcast that requires READ_PRIVILEGED_PHONE_STATE at
// onReceive time, just take all the permissions we need for all our component API calls and
// drop them at the end.
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
try {
uiAutomation.adoptShellPermissionIdentity(
Manifest.permission.MODIFY_PHONE_STATE,
Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
getContext()
.registerReceiver(
cardStateReceiver,
new IntentFilter(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED));
Log.i(
TAG,
"Setting SIM " + logicalSlotId + " power state to " + targetPowerState + "...");
getContext()
.getSystemService(TelephonyManager.class)
.setSimPowerStateForSlot(
logicalSlotId,
targetPowerState,
Runnable::run,
result -> {
powerResult.set(result);
powerLatch.countDown();
});
if (!powerLatch.await(SET_SIM_POWER_TIMEOUT_SECONDS, SECONDS)) {
throw new IllegalStateException(
"Failed to receive SIM power result within "
+ SET_SIM_POWER_TIMEOUT_SECONDS
+ " seconds");
} else if (powerResult.get() != TelephonyManager.SET_SIM_POWER_STATE_SUCCESS) {
throw new IllegalStateException(
"Unexpected SIM power result: " + powerResult.get());
}
// Once the RIL request completes successfully, wait for the SIM to move to the desired
// state (from the broadcast).
Log.i(TAG, "Waiting for SIM " + logicalSlotId + " to become " + targetSimState + "...");
if (!cardStateLatch.await(SET_SIM_POWER_TIMEOUT_SECONDS, SECONDS)) {
throw new IllegalStateException(
"Failed to receive SIM state "
+ targetSimState
+ " within "
+ SET_SIM_POWER_TIMEOUT_SECONDS
+ " seconds");
}
} finally {
getContext().unregisterReceiver(cardStateReceiver);
uiAutomation.dropShellPermissionIdentity();
}
}
private static void sendApdus(int physicalSlotId, int portIndex, List<ApduCommand> apdus) {
TelephonyManager telMan = getContext().getSystemService(TelephonyManager.class);
for (int lineNum = 0; lineNum < apdus.size(); ++lineNum) {
ApduCommand apdu = apdus.get(lineNum);
Log.i(TAG, "APDU #" + (lineNum + 1) + ": " + apdu);
// Format: data=response[0,len-4), sw1=response[len-4,len-2), sw2=response[len-2,len)
String response =
ShellIdentityUtils.invokeMethodWithShellPermissions(
telMan,
tm ->
tm.iccTransmitApduBasicChannelByPort(
physicalSlotId,
portIndex,
apdu.cla,
apdu.ins,
apdu.p1,
apdu.p2,
apdu.p3,
apdu.data),
Manifest.permission.MODIFY_PHONE_STATE);
if (response == null || response.length() < 4) {
Log.e(TAG, " response=" + response + " (unexpected)");
throw new IllegalStateException(
"Unexpected APDU response on line " + (lineNum + 1) + ": " + response);
}
StringBuilder responseBuilder = new StringBuilder();
responseBuilder.append(response.substring(0, response.length() - 4));
String lastStatusWords = response.substring(response.length() - 4);
// If we got a 61xx status, send repeated GET RESPONSE commands until we get a different
// status word back.
while (ApduResponse.SW1_MORE_RESPONSE.equals(lastStatusWords.substring(0, 2))) {
int moreResponseLength = Integer.parseInt(lastStatusWords.substring(2), 16);
Log.i(TAG, " fetching " + moreResponseLength + " bytes of data...");
response =
ShellIdentityUtils.invokeMethodWithShellPermissions(
telMan,
tm ->
tm.iccTransmitApduBasicChannelByPort(
physicalSlotId,
portIndex,
// Use unencrypted class byte when getting more data
apdu.cla & ~4,
ApduCommand.INS_GET_RESPONSE,
0,
0,
moreResponseLength,
""),
Manifest.permission.MODIFY_PHONE_STATE);
if (response == null || response.length() < 4) {
Log.e(
TAG,
" response="
+ response
+ " (unexpected), partialResponse="
+ responseBuilder.toString()
+ " (incomplete)");
throw new IllegalStateException(
"Unexpected APDU response on line " + (lineNum + 1) + ": " + response);
}
responseBuilder.append(response.substring(0, response.length() - 4));
lastStatusWords = response.substring(response.length() - 4);
}
// Now check the final status after we've gotten all the data coming out of the SIM.
String fullResponse = responseBuilder.toString();
if (ApduResponse.SW1_SW2_OK.equals(lastStatusWords)
|| ApduResponse.SW1_OK_PROACTIVE_COMMAND.equals(
lastStatusWords.substring(0, 2))) {
// 9000 is standard "ok" status, and 91xx is "ok with pending proactive command"
Log.i(TAG, " response=" + fullResponse + ", statusWords=" + lastStatusWords);
} else {
// Anything else is considered a fatal error; stop the script and fail this
// precondition.
Log.e(
TAG,
" response="
+ fullResponse
+ ", statusWords="
+ lastStatusWords
+ " (unexpected)");
throw new IllegalStateException(
"Unexpected APDU response on line " + (lineNum + 1) + ": " + fullResponse);
}
}
}
}