/* * 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. */ /* * Copyright (c) 2015-2017, The Linux Foundation. All rights reserved. */ /* * Contributed by: Giesecke & Devrient GmbH. */ package android.se.omapi; import android.annotation.BroadcastBehavior; import android.annotation.NonNull; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import java.util.HashMap; import java.util.concurrent.Executor; /** * The SEService realises the communication to available Secure Elements on the * device. This is the entry point of this API. It is used to connect to the * infrastructure and get access to a list of Secure Element Readers. * * @see SIMalliance Open Mobile API v3.0 */ public final class SEService { /** * Error code used with ServiceSpecificException. * Thrown if there was an error communicating with the Secure Element. * * @hide */ public static final int IO_ERROR = 1; /** * Error code used with ServiceSpecificException. * Thrown if AID cannot be selected or is not available when opening * a logical channel. * * @hide */ public static final int NO_SUCH_ELEMENT_ERROR = 2; /** * Interface to send call-backs to the application when the service is connected. */ public interface OnConnectedListener { /** * Called by the framework when the service is connected. */ void onConnected(); } /** * Broadcast Action: Intent to notify if the secure element state is changed. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) @BroadcastBehavior(registeredOnly = true, protectedBroadcast = true) public static final String ACTION_SECURE_ELEMENT_STATE_CHANGED = "android.se.omapi.action.SECURE_ELEMENT_STATE_CHANGED"; /** * Mandatory extra containing the reader name of the state changed secure element. * * @see Reader#getName() */ public static final String EXTRA_READER_NAME = "android.se.omapi.extra.READER_NAME"; /** * Mandatory extra containing the connected state of the state changed secure element. * * True if the secure element is connected correctly, false otherwise. */ public static final String EXTRA_READER_STATE = "android.se.omapi.extra.READER_STATE"; /** * Listener object that allows the notification of the caller if this * SEService could be bound to the backend. */ private class SEListener extends ISecureElementListener.Stub { public OnConnectedListener mListener = null; public Executor mExecutor = null; @Override public IBinder asBinder() { return this; } public void onConnected() { if (mListener != null && mExecutor != null) { mExecutor.execute(new Runnable() { @Override public void run() { mListener.onConnected(); } }); } } @Override public String getInterfaceHash() { return ISecureElementListener.HASH; } @Override public int getInterfaceVersion() { return ISecureElementListener.VERSION; } } private SEListener mSEListener = new SEListener(); private static final String TAG = "OMAPI.SEService"; private static final String UICC_TERMINAL = "SIM"; private final Object mLock = new Object(); /** The client context (e.g. activity). */ private final Context mContext; /** The backend system. */ private volatile ISecureElementService mSecureElementService; /** * Class for interacting with the main interface of the backend. */ private ServiceConnection mConnection; /** * Collection of available readers */ private final HashMap mReaders = new HashMap(); /** * Establishes a new connection that can be used to connect to all the * Secure Elements available in the system. The connection process can be * quite long, so it happens in an asynchronous way. It is usable only if * the specified listener is called or if isConnected() returns * true.
* The call-back object passed as a parameter will have its * onConnected() method called when the connection actually happen. * * @param context * the context of the calling application. Cannot be * null. * @param listener * a OnConnectedListener object. * @param executor * an Executor which will be used when invoking the callback. */ public SEService(@NonNull Context context, @NonNull Executor executor, @NonNull OnConnectedListener listener) { if (context == null || listener == null || executor == null) { throw new NullPointerException("Arguments must not be null"); } mContext = context; mSEListener.mListener = listener; mSEListener.mExecutor = executor; mConnection = new ServiceConnection() { public synchronized void onServiceConnected( ComponentName className, IBinder service) { mSecureElementService = ISecureElementService.Stub.asInterface(service); if (mSEListener != null) { mSEListener.onConnected(); } Log.i(TAG, "Service onServiceConnected"); } public void onServiceDisconnected(ComponentName className) { mSecureElementService = null; Log.i(TAG, "Service onServiceDisconnected"); } }; Intent intent = new Intent(ISecureElementService.class.getName()); intent.setClassName("com.android.se", "com.android.se.SecureElementService"); boolean bindingSuccessful = mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); if (bindingSuccessful) { Log.i(TAG, "bindService successful"); } } /** * Tells whether or not the service is connected. * * @return true if the service is connected. */ public boolean isConnected() { return mSecureElementService != null; } /** * Returns an array of available Secure Element readers. * There must be no duplicated objects in the returned list. * All available readers shall be listed even if no card is inserted. * * @return An array of Readers. If there are no readers the returned array * is of length 0. */ public @NonNull Reader[] getReaders() { loadReaders(); return mReaders.values().toArray(new Reader[0]); } /** * Obtain a UICC Reader instance with specific slot number from the SecureElementService * * @param slotNumber The index of the uicc slot. The index starts from 1. * @throws IllegalArgumentException if the reader object corresponding to the uiccSlotNumber * is not exist. * @return A Reader object for this uicc slot. */ public @NonNull Reader getUiccReader(int slotNumber) { if (slotNumber < 1) { throw new IllegalArgumentException("slotNumber should be larger than 0"); } loadReaders(); String readerName = UICC_TERMINAL + slotNumber; Reader reader = mReaders.get(readerName); if (reader == null) { throw new IllegalArgumentException("Reader:" + readerName + " doesn't exist"); } return reader; } /** * Releases all Secure Elements resources allocated by this SEService * (including any binding to an underlying service). * As a result isConnected() will return false after shutdown() was called. * After this method call, the SEService object is not connected. * This method should be called when connection to the Secure Element is not needed * or in the termination method of the calling application * (or part of this application) which is bound to this SEService. */ public void shutdown() { synchronized (mLock) { if (mSecureElementService != null) { for (Reader reader : mReaders.values()) { try { reader.closeSessions(); } catch (Exception ignore) { } } } try { mContext.unbindService(mConnection); } catch (IllegalArgumentException e) { // Do nothing and fail silently since an error here indicates // that binding never succeeded in the first place. } mSecureElementService = null; } } /** * Returns the version of the OpenMobile API specification this * implementation is based on. * * @return String containing the OpenMobile API version (e.g. "3.0"). */ public @NonNull String getVersion() { return "3.3"; } @NonNull ISecureElementListener getListener() { return mSEListener; } /** * Obtain a Reader instance from the SecureElementService */ private @NonNull ISecureElementReader getReader(String name) { try { return mSecureElementService.getReader(name); } catch (RemoteException e) { throw new IllegalStateException(e.getMessage()); } } /** * Load available Secure Element Readers */ private void loadReaders() { if (mSecureElementService == null) { throw new IllegalStateException("service not connected to system"); } String[] readerNames; try { readerNames = mSecureElementService.getReaders(); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } for (String readerName : readerNames) { if (mReaders.get(readerName) == null) { try { mReaders.put(readerName, new Reader(this, readerName, getReader(readerName))); } catch (Exception e) { Log.e(TAG, "Error adding Reader: " + readerName, e); } } } } }