AndroidのNFC読み取りを特定のActivityが立ち上がっている時のみ行う


表題の通りです。

- [NFC Basics | Android Developers](https://developer.android.com/guide/topics/connectivity/nfc/nfc.html)
- [Advanced NFC | Android Developers](https://developer.android.com/guide/topics/connectivity/nfc/advanced-nfc.html)

こちらの2つをさっくりまとめた記事になります。

## とりあえず端末が読み込んだらアプリを起動させる

直近でNFCを読み取り機能を搭載したアプリを開発することになりました。
AndroidでのNFC読み取りは、ハードウェアがNFCを読み込んだタイミングで暗黙のIntentを投げるので、それを受け取るように設定する必要があります。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest package="io.github.yamacraft.app.nfc"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- NFCの利用許可、2つ目は「NFCがない端末はインストールさせない」設定 -->
    <uses-permission android:name="android.permission.NFC" />
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".view.activity.MainActivity">
            <!-- NFCの読み取りからの立ち上げを設定 -->
			 <intent-filter>
			     <action android:name="android.nfc.action.TECH_DISCOVERED"/>
			 </intent-filter>
			
			 <!-- 対象となるNFCフォーマットを指定 -->
			 <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
			     android:resource="@xml/nfc_tech_filter" />
        </activity>
    </application>

</manifest>
```

```xml:nfc_tech_filter.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
        <tech>android.nfc.tech.NfcA</tech>
        <tech>android.nfc.tech.NfcB</tech>
        <tech>android.nfc.tech.NfcF</tech>
        <tech>android.nfc.tech.NfcV</tech>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>
```

こうすることで、各種フォーマットのNFCが端末で読み込まれた時にアプリ（Activity）が呼び出されるようになります。読み込んだNFCの情報は`getIntent()`によって取得します。


```java:MainActivity.java
Intent intent = getIntent();
if (intent != null && NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) {
    // ここで読み取り処理
}
```

しかしこの設定ではアプリが立ち上がっていない状態でも端末がNFCを読み込んだ瞬間にアプリを呼び出してしまうため、作るアプリの用途によっては不便なことになってしまいます。

### 特定のActivityでのみ読み取りを実施

では特定のActivityでのみ読み取り機能を立ち上げたい場合どうするかと言うと、Foreground Dispatch Systemと呼ばれる機能を使います。

おおざっぱに解説すると「Intent-filterを動的に設定しつつ、自身のIntentの受け取りを最優先にする」仕組みです。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest package="io.github.yamacraft.app.nfc"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- NFCの利用許可、2つ目は「NFCがない端末はインストールさせない」設定 -->
    <uses-permission android:name="android.permission.NFC" />
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".view.activity.MainActivity">
            <!-- ここでNFC読み取りからの立ち上げ設定を「しない」 -->
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

			 <!-- 対象となるNFCフォーマットを指定 -->
			 <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
			     android:resource="@xml/nfc_tech_filter" />
        </activity>
    </application>

</manifest>
```

```java:MainActivity.java

private NfcAdapter mNfcAdapter;
private PendingIntent mPendingIntent;
private IntentFilter[] mIntentFilters;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Foreground Dispatch Systemの設定
    mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
    Intent intent = new Intent(getApplicationContext(), getClass());
    intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
    mPendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

    IntentFilter intentFilter = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
    mIntentFilters = new IntentFilter[]{intentFilter};
}

@Override
protected void onResume() {
    super.onResume();
    mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, mIntentFilters, null);
}

@Override
protected void onPause() {
    super.onPause();
    mNfcAdapter.disableForegroundDispatch(this);
}

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    if (intent != null && NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) {
        // ここで読み取り処理
    }
}
```

ActivityをSingleTopで立ち上げるため、onCreate()ではなくonNewIntent()で処理をする必要があります。ご注意ください。

現場からは以上です。