/*
 * Copyright (C) 2010 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.nfc.tech;

import android.nfc.ErrorCodes;
import android.nfc.FormatException;
import android.nfc.INfcTag;
import android.nfc.NdefMessage;
import android.nfc.Tag;
import android.nfc.TagLostException;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;

import java.io.IOException;

/**
 * Provides access to NDEF content and operations on a {@link Tag}.
 *
 * <p>Acquire a {@link Ndef} object using {@link #get}.
 *
 * <p>NDEF is an NFC Forum data format. The data formats are implemented in
 * {@link android.nfc.NdefMessage} and
 * {@link android.nfc.NdefRecord}. This class provides methods to
 * retrieve and modify the {@link android.nfc.NdefMessage}
 * on a tag.
 *
 * <p>There are currently four NFC Forum standardized tag types that can be
 * formatted to contain NDEF data.
 * <ul>
 * <li>NFC Forum Type 1 Tag ({@link #NFC_FORUM_TYPE_1}), such as the Innovision Topaz
 * <li>NFC Forum Type 2 Tag ({@link #NFC_FORUM_TYPE_2}), such as the NXP MIFARE Ultralight
 * <li>NFC Forum Type 3 Tag ({@link #NFC_FORUM_TYPE_3}), such as Sony Felica
 * <li>NFC Forum Type 4 Tag ({@link #NFC_FORUM_TYPE_4}), such as NXP MIFARE Desfire
 * </ul>
 * It is mandatory for all Android devices with NFC to correctly enumerate
 * {@link Ndef} on NFC Forum Tag Types 1-4, and implement all NDEF operations
 * as defined in this class.
 *
 * <p>Some vendors have their own well defined specifications for storing NDEF data
 * on tags that do not fall into the above categories. Android devices with NFC
 * should enumerate and implement {@link Ndef} under these vendor specifications
 * where possible, but it is not mandatory. {@link #getType} returns a String
 * describing this specification, for example {@link #MIFARE_CLASSIC} is
 * <code>com.nxp.ndef.mifareclassic</code>.
 *
 * <p>Android devices that support MIFARE Classic must also correctly
 * implement {@link Ndef} on MIFARE Classic tags formatted to NDEF.
 *
 * <p>For guaranteed compatibility across all Android devices with NFC, it is
 * recommended to use NFC Forum Types 1-4 in new deployments of NFC tags
 * with NDEF payload. Vendor NDEF formats will not work on all Android devices.
 *
 * <p class="note"><strong>Note:</strong> Methods that perform I/O operations
 * require the {@link android.Manifest.permission#NFC} permission.
 */
public final class Ndef extends BasicTagTechnology {
    private static final String TAG = "NFC";

    /** @hide */
    public static final int NDEF_MODE_READ_ONLY = 1;
    /** @hide */
    public static final int NDEF_MODE_READ_WRITE = 2;
    /** @hide */
    public static final int NDEF_MODE_UNKNOWN = 3;

    /** @hide */
    public static final String EXTRA_NDEF_MSG = "ndefmsg";

    /** @hide */
    public static final String EXTRA_NDEF_MAXLENGTH = "ndefmaxlength";

    /** @hide */
    public static final String EXTRA_NDEF_CARDSTATE = "ndefcardstate";

    /** @hide */
    public static final String EXTRA_NDEF_TYPE = "ndeftype";

    /** @hide */
    public static final int TYPE_OTHER = -1;
    /** @hide */
    public static final int TYPE_1 = 1;
    /** @hide */
    public static final int TYPE_2 = 2;
    /** @hide */
    public static final int TYPE_3 = 3;
    /** @hide */
    public static final int TYPE_4 = 4;
    /** @hide */
    public static final int TYPE_MIFARE_CLASSIC = 101;
    /** @hide */
    public static final int TYPE_ICODE_SLI = 102;

    /** @hide */
    public static final String UNKNOWN = "android.ndef.unknown";

    /** NFC Forum Tag Type 1 */
    public static final String NFC_FORUM_TYPE_1 = "org.nfcforum.ndef.type1";
    /** NFC Forum Tag Type 2 */
    public static final String NFC_FORUM_TYPE_2 = "org.nfcforum.ndef.type2";
    /** NFC Forum Tag Type 3 */
    public static final String NFC_FORUM_TYPE_3 = "org.nfcforum.ndef.type3";
    /** NFC Forum Tag Type 4 */
    public static final String NFC_FORUM_TYPE_4 = "org.nfcforum.ndef.type4";
    /** NDEF on MIFARE Classic */
    public static final String MIFARE_CLASSIC = "com.nxp.ndef.mifareclassic";
    /**
     * NDEF on iCODE SLI
     * @hide
     */
    public static final String ICODE_SLI = "com.nxp.ndef.icodesli";

    private final int mMaxNdefSize;
    private final int mCardState;
    private final NdefMessage mNdefMsg;
    private final int mNdefType;

    /**
     * Get an instance of {@link Ndef} for the given tag.
     *
     * <p>Returns null if {@link Ndef} was not enumerated in {@link Tag#getTechList}.
     * This indicates the tag is not NDEF formatted, or that this tag
     * is NDEF formatted but under a vendor specification that this Android
     * device does not implement.
     *
     * <p>Does not cause any RF activity and does not block.
     *
     * @param tag an NDEF compatible tag
     * @return Ndef object
     */
    public static Ndef get(Tag tag) {
        if (!tag.hasTech(TagTechnology.NDEF)) return null;
        try {
            return new Ndef(tag);
        } catch (RemoteException e) {
            return null;
        }
    }

    /**
     * Internal constructor, to be used by NfcAdapter
     * @hide
     */
    public Ndef(Tag tag) throws RemoteException {
        super(tag, TagTechnology.NDEF);
        Bundle extras = tag.getTechExtras(TagTechnology.NDEF);
        if (extras != null) {
            mMaxNdefSize = extras.getInt(EXTRA_NDEF_MAXLENGTH);
            mCardState = extras.getInt(EXTRA_NDEF_CARDSTATE);
            mNdefMsg = extras.getParcelable(EXTRA_NDEF_MSG, android.nfc.NdefMessage.class);
            mNdefType = extras.getInt(EXTRA_NDEF_TYPE);
        } else {
            throw new NullPointerException("NDEF tech extras are null.");
        }

    }

    /**
     * Get the {@link NdefMessage} that was read from the tag at discovery time.
     *
     * <p>If the NDEF Message is modified by an I/O operation then it
     * will not be updated here, this function only returns what was discovered
     * when the tag entered the field.
     * <p>Note that this method may return null if the tag was in the
     * INITIALIZED state as defined by NFC Forum, as in this state the
     * tag is formatted to support NDEF but does not contain a message yet.
     * <p>Does not cause any RF activity and does not block.
     * @return NDEF Message read from the tag at discovery time, can be null
     */
    public NdefMessage getCachedNdefMessage() {
        return mNdefMsg;
    }

    /**
     * Get the NDEF tag type.
     *
     * <p>Returns one of {@link #NFC_FORUM_TYPE_1}, {@link #NFC_FORUM_TYPE_2},
     * {@link #NFC_FORUM_TYPE_3}, {@link #NFC_FORUM_TYPE_4},
     * {@link #MIFARE_CLASSIC} or another NDEF tag type that has not yet been
     * formalized in this Android API.
     *
     * <p>Does not cause any RF activity and does not block.
     *
     * @return a string representing the NDEF tag type
     */
    public String getType() {
        switch (mNdefType) {
            case TYPE_1:
                return NFC_FORUM_TYPE_1;
            case TYPE_2:
                return NFC_FORUM_TYPE_2;
            case TYPE_3:
                return NFC_FORUM_TYPE_3;
            case TYPE_4:
                return NFC_FORUM_TYPE_4;
            case TYPE_MIFARE_CLASSIC:
                return MIFARE_CLASSIC;
            case TYPE_ICODE_SLI:
                return ICODE_SLI;
            default:
                return UNKNOWN;
        }
    }

    /**
     * Get the maximum NDEF message size in bytes.
     *
     * <p>Does not cause any RF activity and does not block.
     *
     * @return size in bytes
     */
    public int getMaxSize() {
        return mMaxNdefSize;
    }

    /**
     * Determine if the tag is writable.
     *
     * <p>NFC Forum tags can be in read-only or read-write states.
     *
     * <p>Does not cause any RF activity and does not block.
     *
     * <p>Requires {@link android.Manifest.permission#NFC} permission.
     *
     * @return true if the tag is writable
     */
    public boolean isWritable() {
        return (mCardState == NDEF_MODE_READ_WRITE);
    }

    /**
     * Read the current {@link android.nfc.NdefMessage} on this tag.
     *
     * <p>This always reads the current NDEF Message stored on the tag.
     *
     * <p>Note that this method may return null if the tag was in the
     * INITIALIZED state as defined by NFC Forum, as in that state the
     * tag is formatted to support NDEF but does not contain a message yet.
     *
     * <p>This is an I/O operation and will block until complete. It must
     * not be called from the main application thread. A blocked call will be canceled with
     * {@link IOException} if {@link #close} is called from another thread.
     *
     * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.
     *
     * @return the NDEF Message, can be null
     * @throws TagLostException if the tag leaves the field
     * @throws IOException if there is an I/O failure, or the operation is canceled
     * @throws FormatException if the NDEF Message on the tag is malformed
     * @throws SecurityException if the tag object is reused after the tag has left the field
     */
    public NdefMessage getNdefMessage() throws IOException, FormatException {
        checkConnected();

        try {
            INfcTag tagService = mTag.getTagService();
            if (tagService == null) {
                throw new IOException("Mock tags don't support this operation.");
            }
            int serviceHandle = mTag.getServiceHandle();
            if (tagService.isNdef(serviceHandle)) {
                NdefMessage msg = tagService.ndefRead(serviceHandle);
                if (msg == null && !tagService.isPresent(serviceHandle)) {
                    throw new TagLostException();
                }
                return msg;
            } else if (!tagService.isPresent(serviceHandle)) {
                throw new TagLostException();
            } else {
                return null;
            }
        } catch (RemoteException e) {
            Log.e(TAG, "NFC service dead", e);
            return null;
        }
    }

    /**
     * Overwrite the {@link NdefMessage} on this tag.
     *
     * <p>This is an I/O operation and will block until complete. It must
     * not be called from the main application thread. A blocked call will be canceled with
     * {@link IOException} if {@link #close} is called from another thread.
     *
     * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.
     *
     * @param msg the NDEF Message to write, must not be null
     * @throws TagLostException if the tag leaves the field
     * @throws IOException if there is an I/O failure, or the operation is canceled
     * @throws FormatException if the NDEF Message to write is malformed
     * @throws SecurityException if the tag object is reused after the tag has left the field
     */
    public void writeNdefMessage(NdefMessage msg) throws IOException, FormatException {
        checkConnected();

        try {
            INfcTag tagService = mTag.getTagService();
            if (tagService == null) {
                throw new IOException("Mock tags don't support this operation.");
            }
            int serviceHandle = mTag.getServiceHandle();
            if (tagService.isNdef(serviceHandle)) {
                int errorCode = tagService.ndefWrite(serviceHandle, msg);
                switch (errorCode) {
                    case ErrorCodes.SUCCESS:
                        break;
                    case ErrorCodes.ERROR_IO:
                        throw new IOException();
                    case ErrorCodes.ERROR_INVALID_PARAM:
                        throw new FormatException();
                    default:
                        // Should not happen
                        throw new IOException();
                }
            }
            else {
                throw new IOException("Tag is not ndef");
            }
        } catch (RemoteException e) {
            Log.e(TAG, "NFC service dead", e);
        }
    }

    /**
     * Indicates whether a tag can be made read-only with {@link #makeReadOnly()}.
     *
     * <p>Does not cause any RF activity and does not block.
     *
     * @return true if it is possible to make this tag read-only
     * @throws SecurityException if the tag object is reused after the tag has left the field
     */
    public boolean canMakeReadOnly() {
        INfcTag tagService = mTag.getTagService();
        if (tagService == null) {
            return false;
        }
        try {
            return tagService.canMakeReadOnly(mNdefType);
        } catch (RemoteException e) {
            Log.e(TAG, "NFC service dead", e);
            return false;
        }
    }

    /**
     * Make a tag read-only.
     *
     * <p>This sets the CC field to indicate the tag is read-only,
     * and where possible permanently sets the lock bits to prevent
     * any further modification of the memory.
     * <p>This is a one-way process and cannot be reverted!
     *
     * <p>This is an I/O operation and will block until complete. It must
     * not be called from the main application thread. A blocked call will be canceled with
     * {@link IOException} if {@link #close} is called from another thread.
     *
     * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.
     *
     * @return true on success, false if it is not possible to make this tag read-only
     * @throws TagLostException if the tag leaves the field
     * @throws IOException if there is an I/O failure, or the operation is canceled
     * @throws SecurityException if the tag object is reused after the tag has left the field
     */
    public boolean makeReadOnly() throws IOException {
        checkConnected();

        try {
            INfcTag tagService = mTag.getTagService();
            if (tagService == null) {
                return false;
            }
            if (tagService.isNdef(mTag.getServiceHandle())) {
                int errorCode = tagService.ndefMakeReadOnly(mTag.getServiceHandle());
                switch (errorCode) {
                    case ErrorCodes.SUCCESS:
                        return true;
                    case ErrorCodes.ERROR_IO:
                        throw new IOException();
                    case ErrorCodes.ERROR_INVALID_PARAM:
                        return false;
                    default:
                        // Should not happen
                        throw new IOException();
                }
           }
           else {
               throw new IOException("Tag is not ndef");
           }
        } catch (RemoteException e) {
            Log.e(TAG, "NFC service dead", e);
            return false;
        }
    }
}
