读取 NFC 标签得到空 UID

Reading NFC Tag gets null UID

我正在开发一个使用 NfcAMifareUltralightNdef 技术的 NFC reader 应用程序。当我尝试使用贴纸标签时它工作正常,我的意思是它可以正确读取序列号。当使用另一种类型的标签时,根据 NFC 工具,它似乎涵盖了相同的技术,但是 getByteArrayExtra returns null 即使标签具有有效的序列号。

另一方面,我遇到问题的最后一类标签确实包含 NDEF 消息。

简而言之,这些标签的序列号用于识别经过入口设备的用户。

我对此很困惑,欢迎任何帮助。代码如下:

主要活动:

package org.bogdan.learning.gantopendemo;

import org.bogdan.learning.gantopendemo.util.SystemUiHider;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.AssetManager;
import android.media.MediaPlayer;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.MifareUltralight;
import android.nfc.tech.Ndef;
import android.nfc.tech.NfcA;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;


/**
 * An example full-screen activity that shows and hides the system UI (i.e.
 * status bar and navigation/system bar) with user interaction.
 *
 * @see SystemUiHider
 */
public class MainActivity extends Activity {
    /**
     * Whether or not the system UI should be auto-hidden after
     * {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
     */
    private static final boolean AUTO_HIDE = true;

    /**
     * If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after
     * user interaction before hiding the system UI.
     */
    private static final int AUTO_HIDE_DELAY_MILLIS = 3000;

    /**
     * If set, will toggle the system UI visibility upon interaction. Otherwise,
     * will show the system UI visibility upon interaction.
     */
    private static final boolean TOGGLE_ON_CLICK = true;

    /**
     * The flags to pass to {@link SystemUiHider#getInstance}.
     */
    private static final int HIDER_FLAGS = SystemUiHider.FLAG_HIDE_NAVIGATION;

    /**
     * The instance of the {@link SystemUiHider} for this activity.
     */
    private SystemUiHider mSystemUiHider;

    // NFC Related variables go here, namely the adapter and helpers

    public static final String MIME_TEXT_PLAIN = "text/plain";
    public static final String TAG = "EventTAG";
    private NfcAdapter mNfcAdapter; // Used for accessing the NFC Hardware

    // SQLite Variables go here
    private EventTagDataSource dataSource;

    // wait interval before screen is redrawn
    private int miliseconds = 6000;

    // Sound related activities
    private MediaPlayer mediaPlayer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_gant_open);

        final View controlsView = findViewById(R.id.fullscreen_content_controls);
        final View contentView = findViewById(R.id.fullscreen_content);

        /**
         * Open the database connection
         */
        dataSource = new EventTagDataSource(this);

        try {
            dataSource.open();
        } catch (SQLException exp) {
            Log.d("SQLExp", "SQL Exception ocurred", exp);
        }


        // return the default NFC adapter

        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);

        // is NFC Enabled or not=
        if (mNfcAdapter == null) {
            finish();
            return;
        }

        // is the NFC Reader enabled?
        if (!mNfcAdapter.isEnabled()) {
            Toast.makeText(this, "NFC Reader is not enabled. Please enable and try again", Toast.LENGTH_LONG).show();
        }

        // Set up an instance of SystemUiHider to control the system UI for
        // this activity.
        mSystemUiHider = SystemUiHider.getInstance(this, contentView, HIDER_FLAGS);
        mSystemUiHider.setup();
        mSystemUiHider
                .setOnVisibilityChangeListener(new SystemUiHider.OnVisibilityChangeListener() {
                    // Cached values.
                    int mControlsHeight;
                    int mShortAnimTime;

                    @Override
                    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
                    public void onVisibilityChange(boolean visible) {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
                            // If the ViewPropertyAnimator API is available
                            // (Honeycomb MR2 and later), use it to animate the
                            // in-layout UI controls at the bottom of the
                            // screen.
                            if (mControlsHeight == 0) {
                                mControlsHeight = controlsView.getHeight();
                            }
                            if (mShortAnimTime == 0) {
                                mShortAnimTime = getResources().getInteger(
                                        android.R.integer.config_shortAnimTime);
                            }
                            controlsView.animate()
                                    .translationY(visible ? 0 : mControlsHeight)
                                    .setDuration(mShortAnimTime);
                        } else {
                            // If the ViewPropertyAnimator APIs aren't
                            // available, simply show or hide the in-layout UI
                            // controls.
                            controlsView.setVisibility(visible ? View.VISIBLE : View.GONE);
                        }

                        if (visible && AUTO_HIDE) {
                            // Schedule a hide().
                            delayedHide(AUTO_HIDE_DELAY_MILLIS);
                        }
                    }
                });

        // Set up the user interaction to manually show or hide the system UI.
        contentView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (TOGGLE_ON_CLICK) {
                    mSystemUiHider.toggle();
                } else {
                    mSystemUiHider.show();
                }
            }
        });

        // Upon interacting with UI controls, delay any scheduled hide()
        // operations to prevent the jarring behavior of controls going away
        // while interacting with the UI.
        findViewById(R.id.dummy_button).setOnTouchListener(mDelayHideTouchListener);
        findViewById(R.id.dummy_button).setOnLongClickListener(new View.OnLongClickListener() {
            public boolean onLongClick(View view) {
                //Toast.makeText(getApplicationContext(), "Long Click Detected", Toast.LENGTH_LONG).show();
                launchRingDialog(view);
                return false;
            }
        });
        handleIntent(getIntent());
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);

        // Trigger the initial hide() shortly after the activity has been
        // created, to briefly hint to the user that UI controls
        // are available.
        delayedHide(100);
    }


    /**
     * Touch listener to use for in-layout UI controls to delay hiding the
     * system UI. This is to prevent the jarring behavior of controls going away
     * while interacting with activity UI.
     */
    View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            if (AUTO_HIDE) {
                delayedHide(AUTO_HIDE_DELAY_MILLIS);
            }
            return false;
        }
    };

    Handler mHideHandler = new Handler();
    Runnable mHideRunnable = new Runnable() {
        @Override
        public void run() {
            mSystemUiHider.hide();
        }
    };

    /**
     * Schedules a call to hide() in [delay] milliseconds, canceling any
     * previously scheduled calls.
     */
    private void delayedHide(int delayMillis) {
        mHideHandler.removeCallbacks(mHideRunnable);
        mHideHandler.postDelayed(mHideRunnable, delayMillis);
    }

    /**
     * onResume - when application returns from standby it reopens database connection
     * and sets up foreground dispatch
     */
    @Override
    protected void onResume() {
        try {
            dataSource.open();
        } catch (SQLException exp) {
            Log.d("SQLExp", "SQL Exception ocurred", exp);
        }
        super.onResume();
        setupForegroundDispatch(this, mNfcAdapter);
    }

    /**
     * onPause - closes database connection, stops foreground despatch and sets application in
     * standby mode.
     */
    @Override
    protected void onPause() {
        dataSource.close();
        stopForegroundDispatch(this, mNfcAdapter);
        super.onPause();
    }

    /**
     * creates a new intent and calles handleIntent(intent) to deal with it.
     *
     * @param intent
     */
    @Override
    protected void onNewIntent(Intent intent) {
        handleIntent(intent);
    }

    /**
     *
     */
    public String uidByteToHexString(byte[] uid) {
        int iCounter, jCounter, in;

        String[] hex = {
                "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"
        };
        String out = "";

        for (jCounter = 0; jCounter < uid.length; jCounter++) {
            in = uid[jCounter] & 0xff;
            iCounter = (in >> 4) & 0x0f;
            out += hex[iCounter];
            iCounter = in & 0x0f;
            out += hex[iCounter];
        }

        return out;

    }

    /**
     * Handles intent within the application (such as NFC Tag detected)
     *
     * @param intent
     */
    private void handleIntent(Intent intent) {

        String action = intent.getAction();

        if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
            Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
            new NdefReaderTask().execute(tag);
        }
        /*else if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
            Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
            new NdefReaderTask().execute(tag);
        }*/
    }

    /**
     * Method: loadTagsFromFile
     *
     * @param - none
     * @return - void
     * @description - Loads a set of NFC Tags from a given CSV file into the SQLite Database
     */
    public void loadTagsFromFile(String fileName) {

        AssetManager manager = getApplicationContext().getAssets();
        InputStream inStream = null;

        try {
            inStream = manager.open(fileName);
        } catch (IOException exp) {
            exp.printStackTrace();
        }

        // open a buffered reader and start reading from the inputStream
        BufferedReader buffer = new BufferedReader(new InputStreamReader(inStream));
        String line = ""; //current line in our CSV file

        // open a transcation to start mass inserting data
        dataSource.database.beginTransaction();

        try {
            while ((line = buffer.readLine()) != null) {
                String[] columns = line.split(";");

                if (columns.length != 4) {
                    Log.d("CSVParser", "Skipping malformed CSV format");
                    continue;
                }

                dataSource.createEventTag(columns[1].trim(), columns[2].trim(), columns[3]);
            }
        } catch (IOException exp) {
            exp.printStackTrace();
        }

        dataSource.database.setTransactionSuccessful();
        dataSource.database.endTransaction();
    }

    /**
     * Method: launchRingDialog
     * Description: Opens the Ring Dialog and loads the CSV File into the Database
     *
     * @param view
     */
    public void launchRingDialog(View view) {
        final ProgressDialog ringProgressDialog = ProgressDialog.show(this, "Loading NFC Tags ...", "Loading records ...", true);

        ringProgressDialog.setCancelable(true);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //Thread.sleep(10000);
                    loadTagsFromFile("nfccodes.csv");

                } catch (Exception e) {
                    Log.d("CSVLoader", "CSV Loader Exception occurred", e);
                }
                ringProgressDialog.dismiss();
            }
        }).start();
    }

    /**
     * Sets up Foreground Dispatch system for the activity and NfcAdapter required
     *
     * @param activity
     * @param adapter
     */
    public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) {

        final Intent intent = new Intent(activity.getApplicationContext(), activity.getClass());
        intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

        final PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0);

        IntentFilter[] filters = new IntentFilter[1];
        String[][] techList = new String[][]{ new String[] {MifareUltralight.class.getName()},
                                              new String[] {NfcA.class.getName()},
                                              new String[] {Ndef.class.getName()}
                                            };

        filters[0] = new IntentFilter();
        filters[0].addAction(NfcAdapter.ACTION_TECH_DISCOVERED);
        filters[0].addAction(NfcAdapter.ACTION_NDEF_DISCOVERED);
        filters[0].addAction(NfcAdapter.ACTION_TAG_DISCOVERED);


        filters[0].addCategory(Intent.CATEGORY_DEFAULT);

        try {
            filters[0].addDataType(MIME_TEXT_PLAIN);
        } catch (IntentFilter.MalformedMimeTypeException e) {
            throw new RuntimeException("Check your mime type");
        }

        adapter.enableForegroundDispatch(activity, pendingIntent, filters, techList);
    }

    /**
     * Stops the Foreground Dispatch system for specified activity and nfc adapter.
     *
     * @param activity
     * @param adapter
     */
    public static void stopForegroundDispatch(final Activity activity, NfcAdapter adapter) {
        adapter.disableForegroundDispatch(activity);
    }

    /**
     * Internal class used to create an NFC Reading Task, triggered when an NFC card is brought
     * close or in contact to the device's NFC reading system.
     */
    private class NdefReaderTask extends AsyncTask<Tag, Void, String> {

        /**
         * Overriden abstract method that performs the async task of reading the task.
         *
         * @param params
         * @return - the contents of the Tag as String
         */

        @Override
        protected String doInBackground(Tag... params) {
            Tag tag = params[0];
            byte[] uid = getIntent().getByteArrayExtra(NfcAdapter.EXTRA_ID);

            String serialNumber = uidByteToHexString(uid);

            if (serialNumber != null && !serialNumber.isEmpty()) {
                return serialNumber;
            }
            return null;
        }

        /**
         * Reads a NdefRecord and returns the string calculated from the tag's payload taken over as a
         * byte array
         *
         * @param record
         * @return String - the string representation of the NFC Tag's payload
         * @throws UnsupportedEncodingException
         */
        private String readText(NdefRecord record) throws UnsupportedEncodingException {
            byte[] payload = record.getPayload();

            String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16";
            int languageCodeLength = payload[0] & 0063;

            return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding);
        }


        /**
         * Overriden method - based on the result (Tag's code) it shows the correct screen,
         * either OK or FAILURE screen, plays a sound and returns app to default screen.
         * <p/>
         * This method is executed after the AsyncTask is finished.
         *
         * @param result
         */
        @Override
        protected void onPostExecute(String result) {
            if (result != null) {
                // Do SOMETHING HERE
                // find the tag which has the same code as result
                EventTag currentTag = dataSource.getEventTag(result);
                if (currentTag != null) {
                    //Toast.makeText(getApplicationContext(), "Welcome " + currentTag.getFullName() + " you are using tag: " + currentTag.getNfcCode(), Toast.LENGTH_LONG).show();
                    findViewById(R.id.fullscreen_content).setBackgroundResource(R.drawable.bggreen);
                    ((TextView) findViewById(R.id.fullscreen_content)).setText(getResources().getString(R.string.txt_tag_ok) + " " + currentTag.getFullName());

                    // create a media player or better just play the music
                    mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.gantsound_ok);
                    mediaPlayer.start();

                } else {
                    //Toast.makeText(getApplicationContext(), "Tag with code: " + result + " not found in database", Toast.LENGTH_LONG).show();
                    findViewById(R.id.fullscreen_content).setBackgroundResource(R.drawable.bgred);
                    ((TextView) findViewById(R.id.fullscreen_content)).setText(getResources().getString(R.string.txt_tag_notok));

                    // create a media player or better just play the music
                    mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.error);
                    mediaPlayer.start();

                }

                // use a handler here

                final Handler handler = new Handler();

                final Runnable runnable = new Runnable() {
                    public void run() {
                        findViewById(R.id.fullscreen_content).setBackgroundResource(R.drawable.bgblue);
                        ((TextView) findViewById(R.id.fullscreen_content)).setText(getResources().getString(R.string.dummy_content));

                        handler.postDelayed(this, miliseconds);
                    }
                };
                handler.postDelayed(runnable, miliseconds);
            }
            else {

                //Toast.makeText(getApplicationContext(), "Tag with code: " + result + " not found in database", Toast.LENGTH_LONG).show();
                findViewById(R.id.fullscreen_content).setBackgroundResource(R.drawable.bgred);
                ((TextView) findViewById(R.id.fullscreen_content)).setText(getResources().getString(R.string.txt_tag_notok));

                // create a media player or better just play the music
                mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.error);
                mediaPlayer.start();

                // use a handler here
                final Handler handler = new Handler();

                final Runnable runnable = new Runnable() {
                    public void run() {
                        findViewById(R.id.fullscreen_content).setBackgroundResource(R.drawable.bgblue);
                        ((TextView) findViewById(R.id.fullscreen_content)).setText(getResources().getString(R.string.dummy_content));

                        handler.postDelayed(this, miliseconds);
                    }
                };
                handler.postDelayed(runnable, miliseconds);
            }
        }
    }
}

申请清单:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="my app and package" >

    <!-- Application permissions go here, mainly NFC -->
    <uses-permission android:name="android.permission.NFC" />
    <users-feature android:name="android.hardware.nfc" android:required="true"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|keyboardHidden|screenSize"
            android:label="@string/app_name"
            android:theme="@style/FullscreenTheme" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!-- NFC Intent Filters -->
            <intent-filter>
                <action android:name="android.nfc.action.TECH_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.nfc.action.TAG_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>

            <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
                android:resource="@xml/nfc_tech_filter" />
        </activity>
    </application>

</manifest>

我的技术列表:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcA</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

您的 AsyncTask 使用

byte[] uid = getIntent().getByteArrayExtra(NfcAdapter.EXTRA_ID);

获取被扫描标签的防冲突标识符(UID)

这里有问题的部分是getIntent()。这将使用使用 setIntent() 设置的最后一个意图(或者如果设置的意图从未像您的代码中那样使用,它将使用启动您的 activity 的意图)。因此,如果您使用其启动器图标启动 activity,getIntent() 将 return 启动器意图而不是 NFC 发现意图。因此,不会有一个名为 NfcAdapter.EXTRA_ID.

的额外意图

所以你可以做的是将整个意图而不只是标签对象传递给 AsyncTask:

private void handleIntent(Intent intent) {
    String action = intent.getAction();

    if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) ||
        NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
        new NdefReaderTask().execute(intent);
    }
}

并相应地调整您的 AsyncTask

private class NdefReaderTask extends AsyncTask<Intent, Void, String> {
    @Override
    protected String doInBackground(Intent... params) {
        Intent intent = params[0];
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        byte[] uid = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);

        // ... //
    }

一些进一步的改进

  1. 您的清单中不需要后备意图过滤器 TAG_DISCOVERED。只有当设备上没有应用程序注册与您的标签匹配的更具体的 NFC 事件时,才会触发。由于您已经注册了更具体的事件类型(NDEF_DISCOVEREDTECH_DISCOVERED),因此此 intent 过滤器不会用于您的标签。所以你可以删除

    <intent-filter>
        <action android:name="android.nfc.action.TAG_DISCOVERED" />
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
    
  2. TECH_DISCOVERED过滤器不使用任何类别,所以你也可以这样写:

    <intent-filter>
        <action android:name="android.nfc.action.TECH_DISCOVERED" />
    </intent-filter>
    
  3. 所有 MIFARE Ultralight 标签都是 NFC-A 标签,因此在技术过滤器文件中使用 MifareUltralight + NfcA 是多余的。你可以简单地使用

    <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
        <tech-list>
            <tech>android.nfc.tech.Ndef</tech>
        </tech-list>
        <tech-list>
            <tech>android.nfc.tech.NfcA</tech>
        </tech-list>
    </resources>
    
  4. 您的前台调度设置注册您的 activity 以接收数据类型 "text/plain" 全部的所有 NDEF_DISCOVERED 事件TECH_DISCOVERED 标签技术 (MifareUltralight OR NfcA OR Ndef), 用于所有 NFC 发现事件 (TAG_DISCOVERED)。这是多余的,您还可以简单地注册所有 NFC 发现事件:

    public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) {
        final Intent intent = new Intent(activity, activity.getClass());
        intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
    
        final PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
    
        adapter.enableForegroundDispatch(activity, pendingIntent, null, null);
    }
    
  5. 当您简化前台调度以在任何标记上触发时,Android 将向您的应用传递一个 TAG_DDISCOVERED 意图。所以你必须相应地更新 handleIntent() 方法:

    private void handleIntent(Intent 意图) { 字符串操作 = intent.getAction();

        if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action) ||
            NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) ||
            NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
            new NdefReaderTask().execute(intent);
        }
    }
    
  6. 如果您的应用仅检测标签,而您的 activity 在前台 运行,则您的应用中不需要任何 *_DISCOVERED 过滤器清单。