/*
 * 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 <a href="http://simalliance.org">SIMalliance Open Mobile API  v3.0</a>
 */
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<String, Reader> mReaders = new HashMap<String, Reader>();

    /**
     * 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
     * <code>true</code>. <br>
     * 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
     *            <code>null</code>.
     * @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 <code>true</code> 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);
                }
            }
        }
    }
}
