八、新的连接 API——安卓波束和 WIFI 直连

安卓冰淇淋三明治–安卓波束采用了设备的 NFC 硬件,而 Wi-Fi Direct 则引入了新的连接 API,允许设备无需使用无线接入点即可相互连接。本章将教我们安卓波束和无线直接接口的用法。

本章涵盖的主题如下:

  • 安卓波束
  • 轴承数据
  • 通过无线网络直接共享数据

安卓波束

有 NFC 硬件的设备可以通过 一起点击共享数据。这可以在安卓波束功能的帮助下完成。它类似于蓝牙,因为我们可以在蓝牙连接中无缝发现和配对。当设备彼此靠近时(不超过几厘米),设备就会连接。用户可以使用 Android Beam 功能共享图片、视频、联系人等。

发送索引消息

在这一节中,我们将实现一个简单的 Android Beam 应用。当两个设备被点击在一起时,该应用将向另一个设备发送图像。安卓冰淇淋三明治推出了三种发送短信的方法。这些方法如下:

  • setndefpusmessage():该方法以一个 NdefMessage 为参数,当设备被连在一起点击时,自动发送到另一个设备 。这通常在消息是静态的并且没有变化时使用。
  • setndefpusmessagecallback():这个 方法是用来创建动态的 NdefMessages 的。当两个设备连在一起时,调用createNdefMessage()方法。
  • setOnNdefPushCompleteCallback():这个 方法设置一个回调,当安卓 Beam 成功时调用。

我们将在示例应用中使用第二种方法。

我们的示例应用的用户界面将包含一个用于显示文本消息的 TextView组件和一个用于显示从另一个设备发送的接收图像的 ImageView组件。布局 XML 代码如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text=""
         />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="14dp"
         />

</RelativeLayout>

现在,我们将逐步实现示例应用的Activity类。Activity类的代码用 onCreate()的方法如下:

public class Chapter9Activity extends Activity implements
 CreateNdefMessageCallback
  {

  NfcAdapter mNfcAdapter;
  TextView mInfoText;
  ImageView imageView;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    imageView = (ImageView) findViewById(R.id.imageView);
    mInfoText = (TextView) findViewById(R.id.textView);
    // Check for available NFC Adapter
       mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());

    if (mNfcAdapter == null) 
    {
      mInfoText = (TextView) findViewById(R.id.textView);
      mInfoText.setText("NFC is not available on this device.");
      finish();
      return;
    }
    // Register callback to set NDEF message
    mNfcAdapter.setNdefPushMessageCallback(this, this);
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
  }
}

在这段代码中可以看到,我们可以检查设备是否提供了NfcAdapter。如果是的话,我们得到一个NfcAdapter的实例。然后,我们调用 setNdefPushMessageCallback()方法,使用NfcAdapter实例设置回调。我们发送Activity类作为回调参数,因为Activity类实现了CreateNdefMessageCallback

为了实现CreateNdefMessageCallback,我们应该覆盖createNdefMessage() 方法,如下代码块所示:

  @Override
  public NdefMessage createNdefMessage(NfcEvent arg0) {

    Bitmap icon =  BitmapFactory.decodeResource(this.getResources(),
        R.drawable.ic_launcher);
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    icon.compress(Bitmap.CompressFormat.PNG, 100, stream);
    byte[] byteArray = stream.toByteArray();

    NdefMessage msg = new NdefMessage(new NdefRecord[] {
 createMimeRecord("application/com.chapter9", byteArray)
 , NdefRecord.createApplicationRecord("com.chapter9")
});
    return msg;
  }
  public NdefRecord createMimeRecord(String mimeType, byte[] payload) {

    byte[] mimeBytes = mimeType.getBytes(Charset.forName("US-ASCII"));
    NdefRecord mimeRecord = new NdefRecord(NdefRecord.TNF_MIME_MEDIA,
        mimeBytes, new byte[0], payload);
    return mimeRecord;
  }

正如您在这段代码中看到的,我们得到了一个可绘制的,将其转换为位图,然后转换为字节数组。然后我们用两个NdefRecords创建一个NdefMessage。第一条记录包含 mime 类型和字节数组。第一个记录是由createMimeRecord() 法创造的。第二条记录包含安卓应用记录 ( AAR )。安卓应用记录随安卓冰淇淋三明治推出。该记录包含应用的包名,并增加了扫描 NFC 标签时应用将启动的确定性。也就是说,系统首先尝试将意图过滤器和 AAR 匹配在一起以启动活动。如果它们不匹配,则启动与 AAR 匹配的活动。

当活动由安卓波束事件启动时,我们需要处理安卓波束发送的消息。我们在Activity类的 onResume()方法中处理这个消息,如下代码块所示:

  @Override
  public void onResume() {
    super.onResume();
    // Check to see that the Activity started due to an Android Beam
    if (NfcAdapter.ACTION_NDEF_DISCOVERED.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) {

    Parcelable[] rawMsgs = intent

  .getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
    // only one message sent during the beam
 NdefMessage msg = (NdefMessage) rawMsgs[0];
 // record 0 contains the MIME type, record 1 is the AAR
 byte[] bytes = msg.getRecords()[0].getPayload();
    Bitmap bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

    imageView.setImageBitmap(bmp);
  }

在这段代码中可以看到,我们首先检查意图是否为ACTION_NDEF_DISCOVERED。这意味着Activity课程因安卓波束而开始。如果它是由于安卓波束启动的,我们用processIntent() 方法处理意图。我们首先从意图中得到NdefMessage。然后我们获取第一条记录,并使用BitmapFactory将第一条记录中的字节数组转换为位图。请记住,第二个记录是 AAR,我们对它不做任何事情。最后,我们设置 ImageView组件的位图。

申请的AndroidManifest.xml文件应如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.chapter9"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-permission android:name="android.permission.NFC"/>
 <uses-feature android:name="android.hardware.nfc" android:required="false" />

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Chapter9Activity"
            android:label="@string/title_activity_chapter9" >
            <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.chapter9" />
 </intent-filter>
        </activity>
    </application>

</manifest>

正如您在这段代码中看到的,我们需要在AndroidManifest.xml文件中将最小 SDK 设置为 API Level 14 或更高,因为这些 API 在 API Level 14 或更高版本中可用。此外,我们需要设置使用 NFC 的权限。我们还在AndroidManifest.xml中设置了uses功能。该功能被设置为不需要。这意味着我们的应用可以用于不支持 NFC 的设备。最后,我们用application/com.chapter9mimeTypeandroid.nfc.action.NDEF_DISCOVERED创建一个意图过滤器。

当设备使用我们的示例应用发送图像时,屏幕将如下所示:

Beaming NdefMessages

无线直拨

在传统的无线网络中,设备通过无线接入点相互连接。借助 Wi-Fi Direct ,设备无需无线接入点即可相互连接。类似蓝牙,但是速度更快,Wi-Fi Direct 的范围更长。安卓冰淇淋三明治引入了新的无线直接应用编程接口,允许我们使用安卓设备的无线直接属性。

帮助我们寻找和联系同伴的主要班级是 WifiP2pManager班。我们将在查找和连接对等点时使用以下Listener类:

  • WifiP2pManager.ActionListener
  • WifiP2pManager.ChannelListener
  • WifiP2pManager.ConnectionInfoListener
  • WifiP2pManager.PeerListListener

最后,以下意图将帮助我们实现无线直接连接:

  • WIFI_P2P_CONNECTION_CHANGED_ACTION
  • WIFI_P2P_PEERS_CHANGED_ACTION
  • WIFI_P2P_STATE_CHANGED_ACTION
  • WIFI_P2P_THIS_DEVICE_CHANGED_ACTION

在本节中,我们将学习如何在示例应用中使用这些新的无线直接应用接口。

Wi-Fi 直接应用示例

为了使用 Wi-Fi Direct API,我们需要在AndroidManifest.xml中将最低 SDK 版本设置为 API 等级 14 或更高。此外,我们需要一些许可来使用无线直接接口。AndroidManifest.xml 文件应如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.chapter9"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="15" />
 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
 <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
 <uses-permission android:name="android.permission.INTERNET" />
 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Chapter9Activity"
            android:label="@string/title_activity_chapter9" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

我们需要的第一个类是扩展BroadcastReceiver并处理我们之前在 onReceive()方法中列出的意图的类。这个类的构造函数应该如下:

package com.chapter9;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.NetworkInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.net.wifi.p2p.WifiP2pManager.PeerListListener;
import android.widget.Toast;

public class Chapter9WiFiDirectBroadcastReceiver extends BroadcastReceiver {

 private WifiP2pManager manager;
 private Channel channel;
 private Chapter9Activity activity;

    public Chapter9WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel 
channel,
        Chapter9Activity activity) {
        super();
        this.manager = manager;
        this.channel = channel;
        this.activity = activity;
    }
}

正如您在这段代码中所看到的,我们将ChannelWifiP2pManagerActivity类作为参数传递给了构造函数,因为我们稍后将在onReceive()方法中需要它们。我们需要覆盖BroadcastReceiveronReceive()方法,如下面的代码块所示:

@Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {

            int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);

            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
                // Wifi Direct mode is enabled
              Toast.makeText(activity, "wifi direct is enabled",Toast.LENGTH_LONG).show();
            } else {
              // Wifi Direct mode is disabled
              Toast.makeText(activity, "wifi direct is disabled",Toast.LENGTH_LONG).show();
            }

        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) 
        {

            // request peers from the wifi p2p manager
            if (manager != null) {
                manager.requestPeers(channel, (PeerListListener) activity);
            }

        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

            if (manager == null) {
                return;
            }

            NetworkInfo networkInfo = (NetworkInfo) intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

            if (networkInfo.isConnected()) {

                // request connection info
                manager.requestConnectionInfo(channel, activity);
            } else {
                // It's a disconnect

            }
        } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {

        }
    }

在这种方法中,我们处理接收到的意图。首先,我们检查意图是否为WIFI_P2P_STATE_CHANGED_ACTION。当启用或禁用无线直接连接时,会收到此意图。我们从意向接收无线直接状态,并根据无线直接状态采取行动。

其次,我们检查意图是否为 WIFI_P2P_PEERS_CHANGED_ACTION。这个意图是在调用WifiP2pManager类的 discoverPeers()方法时收到的。当我们收到WIFI_P2P_PEERS_CHANGED_ACTION意向时,我们从Wifi2P2pManager类的requestPeers() 方法中获得同行列表。

接下来,我们检查接收到的意图是否为WIFI_P2P_CONNECTION_CHANGED_ACTION。当无线网络连接改变时,接收到该意图。当我们收到WIFI_P2P_CONNECTION_CHANGED_ACTION意向时,我们处理连接或断开。我们首先从意图中得到NetworkInfo来理解是否存在连接或断开。如果是连接,我们称之为WifiP2pManagerrequestConnectionInfo()法进行连接。

最后,我们检查意图是否为 WIFI_P2P_THIS_DEVICE_CHANGED_ACTION。当设备细节发生变化时,我们会收到此意向。我们不会为此做任何事。

对于这个应用,我们有一个简单的用户界面;带有两个按钮的布局。第一个按钮是查找,第二个按钮是连接对等点。布局的 XML 代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/buttonFind"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="find" />

    <Button
        android:id="@+id/buttonConnect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="connect" />

</LinearLayout>

用户界面如下图所示:

Sample Wi-Fi Direct application

最后,我们需要实现这个应用的Activity类。Activity类的代码应该如下:

package com.chapter9;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.IntentFilter;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pDevice;
import android.net.wifi.p2p.WifiP2pDeviceList;
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.net.wifi.p2p.WifiP2pManager.ChannelListener;
import android.net.wifi.p2p.WifiP2pManager.ConnectionInfoListener;
import android.net.wifi.p2p.WifiP2pManager.PeerListListener;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
public class Chapter9Activity extends Activity implements 
ChannelListener,OnClickListener,PeerListListener,ConnectionInfoListener {

    private WifiP2pManager manager;
    private final IntentFilter intentFilter = new IntentFilter();
    private Channel channel;
    private BroadcastReceiver receiver = null;
    private Button buttonFind;
    private Button buttonConnect;
    private WifiP2pDevice device;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
 channel = manager.initialize(this, getMainLooper(), null);

 intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
 intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
 intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
 intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);

        receiver = new Chapter9WiFiDirectBroadcastReceiver(manager, channel, this);
        registerReceiver(receiver, intentFilter);

        this.buttonConnect = (Button) this.findViewById(R.id.buttonConnect);
        this.buttonConnect.setOnClickListener(this);

        this.buttonFind = (Button)this.findViewById(R.id.buttonFind);
        this.buttonFind.setOnClickListener(this);
    }
}

目前尚未完成实施。我们将逐步添加必要的方法。

在这段代码中可以看到,我们的Activity类实现了各种Listeners来处理 Wi-Fi Direct 事件。ConnectionInfoListener用于连接信息可用时的回拨。 PeerListListener用于对等列表可用时的回拨。ChannelListener用于通道丢失时的回调。

我们创建一个意图过滤器,并添加我们将在扩展BroadcastReceiver的类的onReceive()方法中检查的意图。

我们通过调用 initialize()方法来初始化 WifiP2pManager类。这将向无线网络注册我们的应用。

  1. 我们需要覆盖 onChannelDisconnected()方法,因为我们实现了ChannelListener,如下面的代码块所示:

    java @Override public void onChannelDisconnected() { //handle the channel lost event }

  2. We need to implement the onPeersAvailable() method because we implemented PeerListListener, as shown in the following code block:

    ```java @Override public void onPeersAvailable(WifiP2pDeviceList peerList) {

    for (WifiP2pDevice device : peerList.getDeviceList()) {
      this.device = device;
      break;
    }
    

    } ```

    我们用这种方法得到可用的peerList。我们得到第一个装置,打破for循环。我们需要连接设备。

  3. We need to implement the onConnectionInfoAvailable() method because we implemented ConnectionInfoListener, as shown in the following code block:

    ```java @Override public void onConnectionInfoAvailable(WifiP2pInfo info) { String infoname = info.groupOwnerAddress.toString();

    } ```

    这是我们获取连接信息、连接并向对等方发送数据的地方。例如,可以在这里执行传输文件的AsyncTask

  4. 我们需要对按钮执行 onClick()方法:

    ```java @Override public void onClick(View v) { if(v == buttonConnect) { connect(this.device); } else if(v == buttonFind) { find(); }

    } ```

find()connect()的方法如下:

public void connect(WifiP2pDevice device)
  {
    WifiP2pConfig config = new WifiP2pConfig();
    if(device != null)
    {
      config.deviceAddress = device.deviceAddress;
      manager.connect(channel, config, new ActionListener() {

          @Override
          public void onSuccess() {

            //success
          }

          @Override
          public void onFailure(int reason) {
            //fail
          }
      });
  }
    else
    {
      Toast.makeText(Chapter9Activity.this, "Couldn't connect, device is not found",

                Toast.LENGTH_SHORT).show();
    }
  }  
       public void find()
  {
    manager.discoverPeers(channel, new WifiP2pManager.ActionListener() 
       {

            @Override
            public void onSuccess() {
                Toast.makeText(Chapter9Activity.this, "Finding Peers",
                        Toast.LENGTH_SHORT).show();
       }

            @Override
            public void onFailure(int reasonCode) 
           {
                Toast.makeText(Chapter9Activity.this, "Couldnt find peers ",
                        Toast.LENGTH_SHORT).show();
            }
        });
  }

当点击查找按钮时,我们调用WifiP2pManagerdiscoverPeers()方法来发现可用的对等体。你会记得,调用 discoverPeers()方法会导致BroadcastReceiver收到WIFI_P2P_PEERS_CHANGED_ACTION意图。然后我们将在BroadcastReceiver中请求对等列表。

点击连接按钮,我们使用设备信息调用WifiP2pManagerconnect()方法。这将启动与指定设备的对等连接。

介绍无线直接应用接口的示例应用就是用这些方法完成的。

总结

在这一章中,我们首先学习了 Android 的 Android Beam 特性。借助此功能,设备可以使用 NFC 硬件发送数据。我们实现了一个示例 Android Beam 应用,并学习了如何使用 Android Beam APIs。其次,我们学习了什么是 Wi-Fi Direct,以及如何使用 Wi-Fi Direct API。