/* * Copyright (C) 2017 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.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import android.Manifest; import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.service.euicc.EuiccService; import android.service.euicc.GetDefaultDownloadableSubscriptionListResult; import android.service.euicc.GetDownloadableSubscriptionMetadataResult; import android.service.euicc.GetEuiccProfileInfoListResult; import android.service.euicc.IDeleteSubscriptionCallback; import android.service.euicc.IDownloadSubscriptionCallback; import android.service.euicc.IEraseSubscriptionsCallback; import android.service.euicc.IEuiccService; import android.service.euicc.IGetDefaultDownloadableSubscriptionListCallback; import android.service.euicc.IGetDownloadableSubscriptionMetadataCallback; import android.service.euicc.IGetEidCallback; import android.service.euicc.IGetEuiccInfoCallback; import android.service.euicc.IGetEuiccProfileInfoListCallback; import android.service.euicc.IRetainSubscriptionsForFactoryResetCallback; import android.service.euicc.ISwitchToSubscriptionCallback; import android.service.euicc.IUpdateSubscriptionNicknameCallback; import android.telephony.SubscriptionManager; import android.telephony.euicc.DownloadableSubscription; import android.telephony.euicc.EuiccInfo; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.util.IState; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.List; import java.util.Objects; import java.util.Set; /** * State machine which maintains the binding to the EuiccService implementation and issues commands. * *
Keeps track of the highest-priority EuiccService implementation to use. When a command comes * in, brings up a binding to that service, issues the command, and lingers the binding as long as * more commands are coming in. The binding is dropped after an idle timeout. */ public class EuiccConnector extends StateMachine implements ServiceConnection { private static final String TAG = "EuiccConnector"; /** * Maximum amount of time to wait for a connection to be established after bindService returns * true or onServiceDisconnected is called (and no package change has occurred which should * force us to reestablish the binding). */ private static final int BIND_TIMEOUT_MILLIS = 30000; /** * Maximum amount of idle time to hold the binding while in {@link ConnectedState}. After this, * the binding is dropped to free up memory as the EuiccService is not expected to be used * frequently as part of ongoing device operation. */ @VisibleForTesting static final int LINGER_TIMEOUT_MILLIS = 60000; /** * Command indicating that a package change has occurred. * *
{@link Message#obj} is an optional package name. If set, this package has changed in a * way that will permanently sever any open bindings, and if we're bound to it, the binding must * be forcefully reestablished. */ private static final int CMD_PACKAGE_CHANGE = 1; /** Command indicating that {@link #BIND_TIMEOUT_MILLIS} has been reached. */ private static final int CMD_CONNECT_TIMEOUT = 2; /** Command indicating that {@link #LINGER_TIMEOUT_MILLIS} has been reached. */ private static final int CMD_LINGER_TIMEOUT = 3; /** * Command indicating that the service has connected. * *
{@link Message#obj} is the connected {@link IEuiccService} implementation. */ private static final int CMD_SERVICE_CONNECTED = 4; /** Command indicating that the service has disconnected. */ private static final int CMD_SERVICE_DISCONNECTED = 5; /** * Command indicating that a command has completed and the callback should be executed. * *
{@link Message#obj} is a {@link Runnable} which will trigger the callback.
*/
private static final int CMD_COMMAND_COMPLETE = 6;
// Commands corresponding with EuiccService APIs. Keep isEuiccCommand in sync with any changes.
private static final int CMD_GET_EID = 100;
private static final int CMD_GET_DOWNLOADABLE_SUBSCRIPTION_METADATA = 101;
private static final int CMD_DOWNLOAD_SUBSCRIPTION = 102;
private static final int CMD_GET_EUICC_PROFILE_INFO_LIST = 103;
private static final int CMD_GET_DEFAULT_DOWNLOADABLE_SUBSCRIPTION_LIST = 104;
private static final int CMD_GET_EUICC_INFO = 105;
private static final int CMD_DELETE_SUBSCRIPTION = 106;
private static final int CMD_SWITCH_TO_SUBSCRIPTION = 107;
private static final int CMD_UPDATE_SUBSCRIPTION_NICKNAME = 108;
private static final int CMD_ERASE_SUBSCRIPTIONS = 109;
private static final int CMD_RETAIN_SUBSCRIPTIONS = 110;
private static boolean isEuiccCommand(int what) {
return what >= CMD_GET_EID;
}
/** Flags to use when querying PackageManager for Euicc component implementations. */
private static final int EUICC_QUERY_FLAGS =
PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DEBUG_TRIAGED_MISSING
| PackageManager.GET_RESOLVED_FILTER;
/**
* Return the activity info of the activity to start for the given intent, or null if none
* was found.
*/
public static ActivityInfo findBestActivity(PackageManager packageManager, Intent intent) {
List All incoming commands will be rejected through
* {@link BaseEuiccCommandCallback#onEuiccServiceUnavailable()}.
*
* Package state changes will lead to transitions between {@link UnavailableState} and
* {@link AvailableState} depending on whether an EuiccService becomes unavailable or
* available.
*/
private class UnavailableState extends State {
@Override
public boolean processMessage(Message message) {
if (message.what == CMD_PACKAGE_CHANGE) {
mSelectedComponent = findBestComponent();
if (mSelectedComponent != null) {
transitionTo(mAvailableState);
} else if (getCurrentState() != mUnavailableState) {
transitionTo(mUnavailableState);
}
return HANDLED;
} else if (isEuiccCommand(message.what)) {
BaseEuiccCommandCallback callback = getCallback(message);
callback.onEuiccServiceUnavailable();
return HANDLED;
}
return NOT_HANDLED;
}
}
/**
* State in which a EuiccService is available, but no binding is established or in the process
* of being established.
*
* If a command is received, this state will defer the message and enter {@link BindingState}
* to bring up the binding.
*/
private class AvailableState extends State {
@Override
public boolean processMessage(Message message) {
if (isEuiccCommand(message.what)) {
deferMessage(message);
transitionTo(mBindingState);
return HANDLED;
}
return NOT_HANDLED;
}
}
/**
* State in which we are binding to the current EuiccService.
*
* This is a transient state. If bindService returns true, we enter {@link DisconnectedState}
* while waiting for the binding to be established. If it returns false, we move back to
* {@link AvailableState}.
*
* Any received messages will be deferred.
*/
private class BindingState extends State {
@Override
public void enter() {
if (createBinding()) {
transitionTo(mDisconnectedState);
} else {
// createBinding() should generally not return false since we've already performed
// Intent resolution, but it's always possible that the package state changes
// asynchronously. Transition to available for now, and if the package state has
// changed, we'll process that event and move to mUnavailableState as needed.
transitionTo(mAvailableState);
}
}
@Override
public boolean processMessage(Message message) {
deferMessage(message);
return HANDLED;
}
}
/**
* State in which a binding is established, but not currently connected.
*
* We wait up to {@link #BIND_TIMEOUT_MILLIS} for the binding to establish. If it doesn't,
* we go back to {@link AvailableState} to try again.
*
* Package state changes will cause us to unbind and move to {@link BindingState} to
* reestablish the binding if the selected component has changed or if a forced rebind is
* necessary.
*
* Any received commands will be deferred.
*/
private class DisconnectedState extends State {
@Override
public void enter() {
sendMessageDelayed(CMD_CONNECT_TIMEOUT, BIND_TIMEOUT_MILLIS);
}
@Override
public boolean processMessage(Message message) {
if (message.what == CMD_SERVICE_CONNECTED) {
mEuiccService = (IEuiccService) message.obj;
transitionTo(mConnectedState);
return HANDLED;
} else if (message.what == CMD_PACKAGE_CHANGE) {
ServiceInfo bestComponent = findBestComponent();
String affectedPackage = (String) message.obj;
boolean isSameComponent;
if (bestComponent == null) {
isSameComponent = mSelectedComponent != null;
} else {
isSameComponent = mSelectedComponent == null
|| Objects.equals(
bestComponent.getComponentName(),
mSelectedComponent.getComponentName());
}
boolean forceRebind = bestComponent != null
&& Objects.equals(bestComponent.packageName, affectedPackage);
if (!isSameComponent || forceRebind) {
unbind();
mSelectedComponent = bestComponent;
if (mSelectedComponent == null) {
transitionTo(mUnavailableState);
} else {
transitionTo(mBindingState);
}
}
return HANDLED;
} else if (message.what == CMD_CONNECT_TIMEOUT) {
transitionTo(mAvailableState);
return HANDLED;
} else if (isEuiccCommand(message.what)) {
deferMessage(message);
return HANDLED;
}
return NOT_HANDLED;
}
}
/**
* State in which the binding is connected.
*
* Commands will be processed as long as we're in this state. We wait up to
* {@link #LINGER_TIMEOUT_MILLIS} between commands; if this timeout is reached, we will drop the
* binding until the next command is received.
*/
private class ConnectedState extends State {
@Override
public void enter() {
removeMessages(CMD_CONNECT_TIMEOUT);
sendMessageDelayed(CMD_LINGER_TIMEOUT, LINGER_TIMEOUT_MILLIS);
}
@Override
public boolean processMessage(Message message) {
if (message.what == CMD_SERVICE_DISCONNECTED) {
mEuiccService = null;
transitionTo(mDisconnectedState);
return HANDLED;
} else if (message.what == CMD_LINGER_TIMEOUT) {
unbind();
transitionTo(mAvailableState);
return HANDLED;
} else if (message.what == CMD_COMMAND_COMPLETE) {
Runnable runnable = (Runnable) message.obj;
runnable.run();
return HANDLED;
} else if (isEuiccCommand(message.what)) {
final BaseEuiccCommandCallback callback = getCallback(message);
onCommandStart(callback);
// TODO(b/36260308): Plumb through an actual SIM slot ID.
int slotId = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
try {
switch (message.what) {
case CMD_GET_EID: {
mEuiccService.getEid(slotId,
new IGetEidCallback.Stub() {
@Override
public void onSuccess(String eid) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((GetEidCommandCallback) callback)
.onGetEidComplete(eid);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_GET_DOWNLOADABLE_SUBSCRIPTION_METADATA: {
GetMetadataRequest request = (GetMetadataRequest) message.obj;
mEuiccService.getDownloadableSubscriptionMetadata(slotId,
request.mSubscription,
request.mForceDeactivateSim,
new IGetDownloadableSubscriptionMetadataCallback.Stub() {
@Override
public void onComplete(
GetDownloadableSubscriptionMetadataResult result) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((GetMetadataCommandCallback) callback)
.onGetMetadataComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_DOWNLOAD_SUBSCRIPTION: {
DownloadRequest request = (DownloadRequest) message.obj;
mEuiccService.downloadSubscription(slotId,
request.mSubscription,
request.mSwitchAfterDownload,
request.mForceDeactivateSim,
new IDownloadSubscriptionCallback.Stub() {
@Override
public void onComplete(int result) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((DownloadCommandCallback) callback)
.onDownloadComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_GET_EUICC_PROFILE_INFO_LIST: {
mEuiccService.getEuiccProfileInfoList(slotId,
new IGetEuiccProfileInfoListCallback.Stub() {
@Override
public void onComplete(
GetEuiccProfileInfoListResult result) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((GetEuiccProfileInfoListCommandCallback) callback)
.onListComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_GET_DEFAULT_DOWNLOADABLE_SUBSCRIPTION_LIST: {
GetDefaultListRequest request = (GetDefaultListRequest) message.obj;
mEuiccService.getDefaultDownloadableSubscriptionList(slotId,
request.mForceDeactivateSim,
new IGetDefaultDownloadableSubscriptionListCallback.Stub() {
@Override
public void onComplete(
GetDefaultDownloadableSubscriptionListResult result
) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((GetDefaultListCommandCallback) callback)
.onGetDefaultListComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_GET_EUICC_INFO: {
mEuiccService.getEuiccInfo(slotId,
new IGetEuiccInfoCallback.Stub() {
@Override
public void onSuccess(EuiccInfo euiccInfo) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((GetEuiccInfoCommandCallback) callback)
.onGetEuiccInfoComplete(euiccInfo);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_DELETE_SUBSCRIPTION: {
DeleteRequest request = (DeleteRequest) message.obj;
mEuiccService.deleteSubscription(slotId, request.mIccid,
new IDeleteSubscriptionCallback.Stub() {
@Override
public void onComplete(int result) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((DeleteCommandCallback) callback)
.onDeleteComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_SWITCH_TO_SUBSCRIPTION: {
SwitchRequest request = (SwitchRequest) message.obj;
mEuiccService.switchToSubscription(slotId, request.mIccid,
request.mForceDeactivateSim,
new ISwitchToSubscriptionCallback.Stub() {
@Override
public void onComplete(int result) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((SwitchCommandCallback) callback)
.onSwitchComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_UPDATE_SUBSCRIPTION_NICKNAME: {
UpdateNicknameRequest request = (UpdateNicknameRequest) message.obj;
mEuiccService.updateSubscriptionNickname(slotId, request.mIccid,
request.mNickname,
new IUpdateSubscriptionNicknameCallback.Stub() {
@Override
public void onComplete(int result) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((UpdateNicknameCommandCallback) callback)
.onUpdateNicknameComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_ERASE_SUBSCRIPTIONS: {
mEuiccService.eraseSubscriptions(slotId,
new IEraseSubscriptionsCallback.Stub() {
@Override
public void onComplete(int result) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((EraseCommandCallback) callback)
.onEraseComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
case CMD_RETAIN_SUBSCRIPTIONS: {
mEuiccService.retainSubscriptionsForFactoryReset(slotId,
new IRetainSubscriptionsForFactoryResetCallback.Stub() {
@Override
public void onComplete(int result) {
sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
((RetainSubscriptionsCommandCallback) callback)
.onRetainSubscriptionsComplete(result);
onCommandEnd(callback);
});
}
});
break;
}
default: {
Log.wtf(TAG, "Unimplemented eUICC command: " + message.what);
callback.onEuiccServiceUnavailable();
onCommandEnd(callback);
return HANDLED;
}
}
} catch (Exception e) {
// If this is a RemoteException, we expect to be disconnected soon. For other
// exceptions, this is a bug in the EuiccService implementation, but we must
// not let it crash the phone process.
Log.w(TAG, "Exception making binder call to EuiccService", e);
callback.onEuiccServiceUnavailable();
onCommandEnd(callback);
}
return HANDLED;
}
return NOT_HANDLED;
}
@Override
public void exit() {
removeMessages(CMD_LINGER_TIMEOUT);
// Dispatch callbacks for all in-flight commands; they will no longer succeed. (The
// remote process cannot possibly trigger a callback at this stage because the
// connection has dropped).
for (BaseEuiccCommandCallback callback : mActiveCommandCallbacks) {
callback.onEuiccServiceUnavailable();
}
mActiveCommandCallbacks.clear();
}
}
private static BaseEuiccCommandCallback getCallback(Message message) {
switch (message.what) {
case CMD_GET_EID:
case CMD_GET_EUICC_PROFILE_INFO_LIST:
case CMD_GET_EUICC_INFO:
case CMD_ERASE_SUBSCRIPTIONS:
case CMD_RETAIN_SUBSCRIPTIONS:
return (BaseEuiccCommandCallback) message.obj;
case CMD_GET_DOWNLOADABLE_SUBSCRIPTION_METADATA:
return ((GetMetadataRequest) message.obj).mCallback;
case CMD_DOWNLOAD_SUBSCRIPTION:
return ((DownloadRequest) message.obj).mCallback;
case CMD_GET_DEFAULT_DOWNLOADABLE_SUBSCRIPTION_LIST:
return ((GetDefaultListRequest) message.obj).mCallback;
case CMD_DELETE_SUBSCRIPTION:
return ((DeleteRequest) message.obj).mCallback;
case CMD_SWITCH_TO_SUBSCRIPTION:
return ((SwitchRequest) message.obj).mCallback;
case CMD_UPDATE_SUBSCRIPTION_NICKNAME:
return ((UpdateNicknameRequest) message.obj).mCallback;
default:
throw new IllegalArgumentException("Unsupported message: " + message.what);
}
}
/** Call this at the beginning of the execution of any command. */
private void onCommandStart(BaseEuiccCommandCallback callback) {
mActiveCommandCallbacks.add(callback);
removeMessages(CMD_LINGER_TIMEOUT);
}
/** Call this at the end of execution of any command (whether or not it succeeded). */
private void onCommandEnd(BaseEuiccCommandCallback callback) {
if (!mActiveCommandCallbacks.remove(callback)) {
Log.wtf(TAG, "Callback already removed from mActiveCommandCallbacks");
}
if (mActiveCommandCallbacks.isEmpty()) {
sendMessageDelayed(CMD_LINGER_TIMEOUT, LINGER_TIMEOUT_MILLIS);
}
}
/** Return the service info of the EuiccService to bind to, or null if none were found. */
@Nullable
private ServiceInfo findBestComponent() {
return (ServiceInfo) findBestComponent(mPm);
}
/**
* Bring up a binding to the currently-selected component.
*
* Returns true if we've successfully bound to the service.
*/
private boolean createBinding() {
if (mSelectedComponent == null) {
Log.wtf(TAG, "Attempting to create binding but no component is selected");
return false;
}
Intent intent = new Intent(EuiccService.EUICC_SERVICE_INTERFACE);
intent.setComponent(mSelectedComponent.getComponentName());
// We bind this as a foreground service because it is operating directly on the SIM, and we
// do not want it subjected to power-savings restrictions while doing so.
return mContext.bindService(intent, this,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE);
}
private void unbind() {
mEuiccService = null;
mContext.unbindService(this);
}
private static ComponentInfo findBestComponent(
PackageManager packageManager, List