13.使用NFC传输数据

13.1 问题

你有一个应用程序,需要通过最少的设置实现两台Android设备间小数据包的快速传输。

13.2 解决方案

(API Level 16)
使用NFC(Near Field Communication,近场通信)Beam API。NFC通信起初是在Android 2.3 中加入到SDK中的,在Android 4.0 中做了扩展,包括通过一个名为Android Beam的进程实现设备间短信息的无障碍传输。在Android 4.1中,又对Beam API做了完善,使之在两台设备间的传输方面更加成熟。
Android 4.1 中在此方面一个比较大的补充就是可以通过一些可选的连接实现大数据的传输。在发现设备和建立初始连接方面,NFC表现非常优秀,但它的宽带较窄,对于发送像全彩色图片这样的大数据包效率不是很高。以前,开发人员可以使用NFC在两台设备间建立连接,但是在实际传输文件数据时需要手动选择第二种连接方式,如Wi-Fi直连或蓝牙。在Android4.1中,框架层处理了整个过程,任何应用程序只需要调用一个API就可以通过可用的连接完成大文件的分享。

13.3 实现机制

根据想要推送的内容的大小,有两者机制可以用来在两台设备间传送数据。

1. 使用前台推送进行Beam

如果要使用NFC在设备间发送简单的内容,可以使用前台推送机制来创建一个NfcMessage,它包含一个或多个NfcRecord实例。以下两段代码演示了如何创建一个简单的NfcManager并推送到另一个设备上。

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.nfcbeam">

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".NfcActivity"
            android:label="NfcActivity" 
            android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="application/com.example.androidrecipes.beamtext" />
            </intent-filter>
        </activity>
    </application>

</manifest>

首先需要注意的是,在使用NFC服务时需要android.permission.NFC权限。另外,我们的Activity中添加了一个自定义的<intent-filter>。这样Android就可以知道哪个应用程序应该启动以响应它所收到的内容。

生成一个NFC前台推送的Activity

public class NfcActivity extends Activity implements
        CreateNdefMessageCallback, OnNdefPushCompleteCallback {
    private static final String TAG = "NfcBeam";
    private NfcAdapter mNfcAdapter;
    private TextView mDisplay;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDisplay = new TextView(this);
        setContentView(mDisplay);
        
        // 检查 NFC 适配器是否可用
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (mNfcAdapter == null) {
            mDisplay.setText("NFC is not available on this device.");
        } else {
            // 注册回调来设置 NDEF 消息。这样做可以使Activity处于前台时,
            // NFC 数据推送处于激活状态。
            mNfcAdapter.setNdefPushMessageCallback(this, this);
            // 注册回调来监听消息发送成功
            mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
        }
    }
    
    @Override
    public void onResume() {
        super.onResume();
        // 检查是否是一个Beam启动了这个Activity
        if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
            processIntent(getIntent());
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        //在这之后会调用 onResume 来处理这个Intent
        setIntent(intent);
    }

    void processIntent(Intent intent) {
        Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
        //  Beam 期间只发送了一条消息
        NdefMessage msg = (NdefMessage) rawMsgs[0];
        // 记录 0 包含了 MIME 类型
        mDisplay.setText(new String(msg.getRecords()[0].getPayload()));
    }
    
    @Override
    public NdefMessage createNdefMessage(NfcEvent event) {
        String text = String.format("Sending A Message From Android Recipes at %s",
                DateFormat.getTimeFormat(this).format(new Date()));
        NdefMessage msg = new NdefMessage(NdefRecord.createMime(
                "application/com.example.androidrecipes.beamtext", text.getBytes())
         /**
          * The Android Application Record (AAR) is commented out. When a device
          * receives a push with an AAR in it, the application specified in the AAR
          * is guaranteed to run. The AAR overrides the tag dispatch system.
          * You can add it back in to guarantee that this
          * activity starts when receiving a beamed message. For now, this code
          * uses the tag dispatch system.
          */
          //,NdefRecord.createApplicationRecord("com.examples.nfcbeam")
        );
        return msg;
    }
    
    @Override
    public void onNdefPushComplete(NfcEvent event) {
        //这个回调是在一个绑定线程上执行的,不用在这个方法中直接更新UI
        Log.i(TAG, "Message Sent!");
    }
}

这个示例应用程序针对的是NFC推送的发送和接收,因此在两台设备上都应该安装相同的应用程序:一台负责发送数据,另一台负责接收数据。Activity通过NfcAdapter的setNdefPushMessageCallback()方法将自己注册为可进行前台推送。这次调用中同时做了两件事情。在传输开始时,它告诉NFC服务调用这个Activity来接收它需要发送的信息,同时在Activity处于前台时,会激活NFC推送。另外,还要一个类外的方法叫做setNdefPushMessage(),该方法只接收信息,但不会实现回调。
这个回调方法构造了一个NdefMessage,NdefMessage只包含一条NDFF MIME记录(通过NdefRecord.createMime()方法创建)。MIME记录是一种传递应用程序特定数据的简单方式。createMime()方法包含两个参数,一个用来指定MIME类型和清单中<intent-filter>定义的类型是一样的。
要想推送执行的话,负责发送设备的Activity必须处于前台激活状态,接收的设备也不能是锁屏状态。当用户同时触摸两台设备时,发送设备的屏幕上会显示Android的“Touch to Beam”用户界面,这是再点击一下屏幕就会把消息发送到另一台设备上。一旦接收到消息,接收设备上的应用程序就会启动,并且会触发发送设备的onNdefPushComplete()回调方法。
在负责接收的设备上,会使用ACTION_NDEF_DISCOVERED的Intent来启动Activity,因此我们的示例会检查NdefMessage的Intent并且拆包其中的负载数据,即将byte
数组转换成字符串。这种使用先匹配Intent然后发送NFC数据的方式最为灵活,但是有些时候可能需要显式地调用应用程序。这时候就需要Android Application Record出马了。

2. Android Application Record

应用程序可以在一个NdefMessage中添加一个额外的NdefRecord,它可以引导Android在接收设备上调用一个指定的包名,要想在之前的示例中实现它,我们只需要像下面这样简单地修改一下CreateNdefMessageCallback方法:

    @Override
    public NdefMessage createNdefMessage(NfcEvent event) {
        String text = String.format("Sending A Message From Android Recipes at %s",
                DateFormat.getTimeFormat(this).format(new Date()));
        NdefMessage msg = new NdefMessage(NdefRecord.createMime(
                "application/com.example.androidrecipes.beamtext", text.getBytes())
         /**
          * The Android Application Record (AAR) is commented out. When a device
          * receives a push with an AAR in it, the application specified in the AAR
          * is guaranteed to run. The AAR overrides the tag dispatch system.
          * You can add it back in to guarantee that this
          * activity starts when receiving a beamed message. For now, this code
          * uses the tag dispatch system.
          */
          //,NdefRecord.createApplicationRecord("com.examples.nfcbeam")
        );
        return msg;
    }

加了NdefRecord.createApplicationRecord()这个额外的参数后,现在可以保证推送消息就只会启动我们我们的com.examples.nfcbeam包。消息的第一天记录还是原来的文本信息,所以我们对接收的信息的拆包过程不要变。

3.Beam较大的数据

在本节开头,已提到了
最后不要使用NFC发送大内容块。尽管如此,Android Beam还是有能力处理大内容块的。以下三段代码演示了使用Beam来发送大型图片文件。
AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.nfcbeam" >

    <uses-permission android:name="android.permission.NFC" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/NfcBeam" >
        <activity
            android:name=".BeamActivity"
            android:label="BeamActivity" 
            android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <data android:mimeType="image/*" />
            </intent-filter>
        </activity>
    </application>

</manifest>

res/layout/main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Select Image"
        android:onClick="onSelectClick" />
    <TextView
        android:id="@+id/text_uri"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <ImageView
        android:id="@+id/image_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="center" />
</LinearLayout>

传送一个大图片的Activity

public class BeamActivity extends Activity implements CreateBeamUrisCallback, OnNdefPushCompleteCallback {
    private static final String TAG = "NfcBeam";
    private static final int PICK_IMAGE = 100;
    
    private NfcAdapter mNfcAdapter;
    private Uri mSelectedImage;
    
    private TextView mUriName;
    private ImageView mPreviewImage;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mUriName = (TextView) findViewById(R.id.text_uri);
        mPreviewImage = (ImageView) findViewById(R.id.image_preview);
        
        // 检查 NFC 适配器是否可用
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (mNfcAdapter == null) {
            mUriName.setText("NFC is not available on this device.");
        } else {
            // 注册回调来设置NDFF 消息
            mNfcAdapter.setBeamPushUrisCallback(this, this);
            // 注册回调来监听消息发送成功
            mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
        }
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == PICK_IMAGE && resultCode == RESULT_OK && data != null) {
            mUriName.setText( data.getData().toString() );
            mSelectedImage = data.getData();
        }
    }
    
    @Override
    public void onResume() {
        super.onResume();
        // Check to see that the Activity started due to an Android Beam
        if (Intent.ACTION_VIEW.equals(getIntent().getAction())) {
            processIntent(getIntent());
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        // onResume gets called after this to handle the intent
        setIntent(intent);
    }

    void processIntent(Intent intent) {
        Uri data = intent.getData();
        if(data != null) {
            mPreviewImage.setImageURI(data);
        } else {
            mUriName.setText("Received Invalid Image Uri");
        }
    }
    
    public void onSelectClick(View v) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        startActivityForResult(intent, PICK_IMAGE);
    }

    @Override
    public Uri[] createBeamUris(NfcEvent event) {
        if (mSelectedImage == null) {
            return null;
        }
        return new Uri[] {mSelectedImage};
    }
    
    @Override
    public void onNdefPushComplete(NfcEvent event) {
//这个回调是在一个绑定线程上执行的,不要在这个地方中直接更新UI。
//这里最好告诉用户不需要再把手机放在一起了
        Log.i(TAG, "Push Complete!");
    }
}

这个示例使用了CreateBeamUrisCallback,它允许应用程序构造一个Uri实例的数组,这些Uri指向你要发送的内容。Android首先会通过NFC建立一个初始连接,然后再寻找一种合适的连接方式(如蓝牙或Wi-Fi)来完成大文件的传输。
在本例中,接收设备上的数据是通过系统标准的Intent.ACTION_VIEW action 启动的,因此没有必要在两台设备上都加载应用程序。尽管如此,我们的应用程序还是对ACTION_VIEW进行了过滤,这样的话如果接收设备愿意,可以使用它来浏览接收的图片。
这里会要求用户从他的设备上选择了一张图片来传送,一旦选定后,图片的Uri会显示出来。一旦用户点击设备到另一台设备,屏幕同样会显示“Touch to Beam”用户界面,当再次点击屏幕时传输就开始了。

在传输过程中有关NFC的部分完成后,就会在发送设备上调用onNdefPushComplete()方法。这时,传递过程就转移到其他类型的连接上,因此用户也就不需要再把手机放在一起了。
在传输文件中,接收设备会在系统窗口的顶部显示进度通知,当传输完成时,用户可以点击该通知来浏览内容。如果选择我们的应用程序作为内容浏览器,图片就会显示在应用程序的ImageView中。为你的应用程序注册这种通用的Intent可能有一个缺点,就是设备上所有的应用程序都可以用你的应用程序浏览图片,所以定义过滤器时要谨慎。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容