2013年3月22日 星期五

[Android] Android Developer Note Connectivity 4

Wi-Fi Direct (見Android Developer Wi-Fi Direct)

序言:
Wi-Fi Direct 允許Android 4.0(API level 14)以上,
擁有適當的硬體的裝置來直接和其他裝置連接,
透過Wi-Fi而不需中繼的存取點(access point)。
使用這些APIs,你可以探索並連接其他支援Wi-Fi Direct的裝置,
並透過快速的連線跨越比藍芽連線更長的距離。
這對於要在使用者之前分享資料的app很有用,
比如多玩家連線遊戲或者一個照片分享app。

Wi-Fi Direct APIs 是由下面幾個主要組件構成的:
1. 允許你探索、發出請求(request)及連線到其他點的methods,
定義在 WifiP2pManager class裡。
2. 允許你被提醒的WifiP2pManager method calls是否成功的監聽器(Listeners)。
當呼叫的WifiP2pManager methods時,
每個method可以取得一個特定傳入的listener作為參數。
3. 提醒你那些被Wi-Fi Direct framework偵測到的特別的活動(events)的intents,
比如像是放棄(dropped)連線或者一個新發現的peer。

通常我們會一起使用三種APIs。舉例來說,你可以提供一個
WifiP2pManager.ActionListener給一個對discoverPeers()的call,
所以你就可能會被ActionListener.onSuccess()
ctionListener.onFailure()給提醒(notified)。
如果說discoverPeers() method發現peers list已經改變的話,
一個WIFI_P2P_PEERS_CHANGED_ACTION的intent也會廣播出來。

API Overview:
WifiP2pManager  class提供了methods來允許你和裝置上的Wi-Fi硬體互動,
並作一些像是探索和連接別的peers的事情。
下面是可用的actions:
表一. Wi-Fi Direct Methods
MethodDescription
initialize()使用Wi-Fi framework來註冊app。
必須在call任何Wi-Fi Direct method前呼叫。
connect()以指定的設定來和一個裝置開始特定的點對點連線。
cancelConnect()取消所有進行中的點對點群組溝通。
requestConnectInfo()取得一個裝置的連線資訊。
createGroup()以現在的裝置作為群組擁有者來產生一個點對點的群組。
removeGroup()移除現行的點對點的群組。
requestGroupInfo()取得點對點的群組資訊。
discoverPeers()開始對點(peer)的探索。
requestPeers()取得當前已探索到的點的資訊。

WifiP2pManager methods讓你在傳入一個listener,
使得Wi-Fi Direct framework可以提醒你的activity關於一個call的狀態如何。
可用的listener interfaces及對應的WifiP2pManager使用其listener的method calls如下:
表二. Wi-Fi Direct Listeners
Listener interfaceAssociated actions
WifiP2pManager.ActionListenerconnect()cancelConnect(),  
createGroup(),removeGroup(),
and discoverPeers()
WifiP2pManager.ChannelListenerinitialize()
WifiP2pManager.ConnectionInfoListenerrequestConnectInfo()
WifiP2pManager.GroupInfoListenerrequestGroupInfo()
WifiP2pManager.PeerListListenerrequestPeers()

Wi-Fi Direct APIs當特定Wi-Fi Direct events發生時,
會去定義了那些要廣播的intents,
像是新的peer被發現,或者一個裝置的Wi-Fi的狀態改變。
你可以透過作出一個廣播接收器(broadcast receiver),
註冊並在你的app接收處理這些intents:
表三. Wi-Fi Direct Intents
IntentDescription
WIFI_P2P_CONNECTION_CHANGED_ACTION當裝置的Wi-Fi連線狀態改變時進行廣播。
WIFI_P2P_PEERS_CHANGED_ACTION當你呼叫discoverPeers()進行廣播。
通常你會想叫requestPeers()來取得更新過的peer list。
(如果你有在你的app處理這種intent的話)
WIFI_P2P_STATE_CHANGED_ACTION當裝置上的Wi-Fi Direct被打開或關閉時廣播。
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION當裝置上的細節(details)被改變時廣播,
比如裝置的名稱被改變。


Creating a Broadcast Receiver for Wi-Fi Direct Intents
一個廣播接收器允許你藉由Android系統來接收intents,
以讓你的app可以對你感興趣的events作回應。
基本來製作一個處理Wi-Fi Direct的intents的廣播接受器的步驟如下:
1. 作一個繼承 BroadcastReceiver class的class。在建構子(constructor)裡,
你大概會想要有參數給WifiP2pManagerWifiP2pManager.Channel
以及廣播接收器將註冊的activity。這允許了廣播接收器傳送更新給activity,
以及和Wi-Fi硬體連接,和一個溝通的channel(如果需要的話)。
2. 在廣播接收器裡,在onReceive()裡檢查你有興趣的intents。
依照所接收到的intent,帶出任何可能的actions。
舉例來說,如果廣播接收器接收一個WIFI_P2P_PEERS_CHANGED_ACTION的intent,
你可以呼叫requestPeers() method,來取得一個現在探索到的peer list。

下面的code展示了如何去作出一個典型的廣播接收器。
廣播接收器拿取WifiP2pManager物件以及activity作為參數,
並在接收到一個intent時,用這兩個classes來適當地帶出需要的動作。

/**
 * A BroadcastReceiver that notifies of important Wi-Fi p2p events.
 */
public class WiFiDirectBroadcastReceiver extends BroadcastReceiver {

    private WifiP2pManager mManager;
    private Channel mChannel;
    private MyWiFiActivity mActivity;

    public WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel channel,
            MyWifiActivity activity) {
        super();
        this.mManager = manager;
        this.mChannel = channel;
        this.mActivity = activity;
    }

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

        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
            // Check to see if Wi-Fi is enabled and notify appropriate activity
        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
            // Call WifiP2pManager.requestPeers() to get a list of current peers
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
            // Respond to new connection or disconnections
        } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
            // Respond to this device's wifi state changing
        }
    }
}


Creating a Wi-Fi Direct Application
作出一個Wi-Fi Direct的app包含了作出並註冊一個廣播接收器給你的app、
探索peers、連接到一個peer,以及傳輸資料到一個peer。
下面的區塊描述如何去做這件事情。

Initial setup:
在使用Wi-Fi Direct APIs之前,
你必須確保你的app可以存取能支援Wi-Fi Direct protocol的硬體。
如果Wi-Fi Direct被支援的話,你可以取得一個WifiP2pManager實例,
作出並註冊你的廣播接收器並且開始使用Wi-Fi Direct APIs。

1. 要求使用裝置上的Wi-Fi硬體的權限,
並在Android manifest裡聲明你的app要有正確的最小SDK版本。

<uses-sdk android:minSdkVersion="14" />
<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" />


2. 檢查Wi-Fi Direct是否被打開並被支援。一個好的檢查的地方是在你的廣播接收器裡。
(當它接收到WIFI_P2P_STATE_CHANGED_ACTION intent時)
提醒你的activity注意Wi-Fi Direct狀態並且相對應地去處理:

@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 is enabled
        } else {
            // Wi-Fi Direct is not enabled
        }
    }
    ...
}


3. 在你的activity裡的 onCreate() method裡,取得一個WifiP2pManager的實例,
並且用Wi-Fi Direct framework呼叫initialize()來註冊你的app。
這個method回傳了一個WifiP2pManager.Channel
用來連接你的app到Wi-Fi Direct framework。你也應該作一個廣播接收器的實例,
使用 WifiP2pManagerWifiP2pManager.Channel物件、以及一個activity的參照。
這允許你的廣播接收器來提醒你的activity感興趣的events並從而作更新。
它也讓你可以操作裝置的Wi-Fi狀態,如果必要的話:

WifiP2pManager mManager;
Channel mChannel;
BroadcastReceiver mReceiver;
...
@Override
protected void onCreate(Bundle savedInstanceState){
    ...
    mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
    mChannel = mManager.initialize(this, getMainLooper(), null);
    mReceiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this);
    ...
}

4. 製作一個intent filter並加上你廣播接收器有檢查的那些intents:

IntentFilter mIntentFilter;
...
@Override
protected void onCreate(Bundle savedInstanceState){
    ...
    mIntentFilter = new IntentFilter();
    mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
    mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
    mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
    mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
    ...
}

5. 在onResume() method裡註冊廣播接收器,並在onPause() method裡反註冊:

/* register the broadcast receiver with the intent values to be matched */
@Override
protected void onResume() {
    super.onResume();
    registerReceiver(mReceiver, mIntentFilter);
}
/* unregister the broadcast receiver */
@Override
protected void onPause() {
    super.onPause();
    unregisterReceiver(mReceiver);
}

當你必須要取得一個 WifiP2pManager.Channel並且設定廣播接收器時,
你的app可以做出Wi-Fi Direct method的呼叫並接收Wi-Fi Direct intents。
你現在可以透過呼叫WifiP2pManager裡的methods實作app的部分並使用Wi-Fi Direct功能。
接下來的段落描述了如何去做一些常見的動作,
像是探索和連接peers。

Discovering peers
要探索可以連接的peers,呼叫discoverPeers()來偵測範圍內可用的peers。
呼叫這個function是不同步的,且成功或失敗會以 onSuccess() onFailure()你的app溝通。
(如果你做了一個 WifiP2pManager.ActionListener的話)
onSuccess() method只會提醒你探索的process成功,
而不會提供任何實際的發現的peer的資訊。

mManager.discoverPeers(channel, new WifiP2pManager.ActionListener() {
    @Override
    public void onSuccess() {
        ...
    }

    @Override
    public void onFailure(int reasonCode) {
        ...
    }
});

如果探索的process成功並偵測到peers的話,
系統會廣播WIFI_P2P_PEERS_CHANGED_ACTION intent,
由此你可以在廣播接收器監聽來接收peer list。
當你的app取得WIFI_P2P_PEERS_CHANGED_ACTION intent時,
你可以使用requestPeers()來request一組發現的peers的列表。
下面展示設定方法:

PeerListListener myPeerListListener;
...
if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

    // request available peers from the wifi p2p manager. This is an
    // asynchronous call and the calling activity is notified with a
    // callback on PeerListListener.onPeersAvailable()
    if (mManager != null) {
        mManager.requestPeers(mChannel, myPeerListListener);
    }
}

requestPeers() method也是不同步的,
並且當一個peers的列表可用時,可以用 onPeersAvailable()提醒你的activity。
(被定義在WifiP2pManager.PeerListListener interface裡)
onPeersAvailable() method提供你一個WifiP2pDeviceList
你可以透過iteration來尋找你想要連接的peer。

Connecting to peers
當你已經找到你想連接的裝置(在取得可能的peers的list後),
呼叫connect() method來連線到其他裝置。
這個method call需要一個包含欲連線的裝置資訊的WifiP2pConfig物件。
你可以透過WifiP2pManager.ActionListener來被提醒連線成功或失敗。
下面的codes展示如何去對希望的裝置作連線:

//obtain a peer from the WifiP2pDeviceList
WifiP2pDevice device;
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = device.deviceAddress;
mManager.connect(mChannel, config, new ActionListener() {

    @Override
    public void onSuccess() {
        //success logic
    }

    @Override
    public void onFailure(int reason) {
        //failure logic
    }
});

Transferring data
一旦連線被建立,你可以使用sockets在裝置之間傳輸資料。
基本的傳輸資料的步驟如下:
1. 製作一個ServerSocket。這個socket等候從client來的特定port的連線,
且直到連線發生前block,在背景的thread也是這麼做。
2. 製作一個client Socket。client使用IP位址和server socket的port,
來連到server裝置。
3. 從client傳送資料到server。當client socket成功地連接到server socket時,
你可以以byte串流(streams)來將資料從client傳到server。
4. server socket等待一個client連線(以accept() method)。
這個呼叫會block,直到一個client連線,所以呼叫這個也是另一個thread。
當一個連線發生時,server裝置可以從client來接收資料。
用這個資料帶出其他的動作,比如說存到一個檔案裡,
或者將其展示給使用者。

下面的範例是從Wi-Fi Direct Demo sample修改來的,
展示了如何作出client-server socket溝通並用服務(service)從client傳輸JPEG圖檔到server。
如果要看完整的範例就請去找 Wi-Fi Direct Demo的sample來編譯執行吧!

public static class FileServerAsyncTask extends AsyncTask {

    private Context context;
    private TextView statusText;

    public FileServerAsyncTask(Context context, View statusText) {
        this.context = context;
        this.statusText = (TextView) statusText;
    }

    @Override
    protected String doInBackground(Void... params) {
        try {

            /**
             * Create a server socket and wait for client connections. This
             * call blocks until a connection is accepted from a client
             */
            ServerSocket serverSocket = new ServerSocket(8888);
            Socket client = serverSocket.accept();

            /**
             * If this code is reached, a client has connected and transferred data
             * Save the input stream from the client as a JPEG file
             */
            final File f = new File(Environment.getExternalStorageDirectory() + "/"
                    + context.getPackageName() + "/wifip2pshared-" + System.currentTimeMillis()
                    + ".jpg");

            File dirs = new File(f.getParent());
            if (!dirs.exists())
                dirs.mkdirs();
            f.createNewFile();
            InputStream inputstream = client.getInputStream();
            copyFile(inputstream, new FileOutputStream(f));
            serverSocket.close();
            return f.getAbsolutePath();
        } catch (IOException e) {
            Log.e(WiFiDirectActivity.TAG, e.getMessage());
            return null;
        }
    }

    /**
     * Start activity that can handle the JPEG image
     */
    @Override
    protected void onPostExecute(String result) {
        if (result != null) {
            statusText.setText("File copied - " + result);
            Intent intent = new Intent();
            intent.setAction(android.content.Intent.ACTION_VIEW);
            intent.setDataAndType(Uri.parse("file://" + result), "image/*");
            context.startActivity(intent);
        }
    }
}


在client端,用client socket連線到server socket,並傳輸資料。
這個範例傳輸一個client裝置上檔案系統的JPEG檔案。

Context context = this.getApplicationContext();
String host;
int port;
int len;
Socket socket = new Socket();
byte buf[]  = new byte[1024];
...
try {
    /**
     * Create a client socket with the host,
     * port, and timeout information.
     */
    socket.bind(null);
    socket.connect((new InetSocketAddress(host, port)), 500);

    /**
     * Create a byte stream from a JPEG file and pipe it to the output stream
     * of the socket. This data will be retrieved by the server device.
     */
    OutputStream outputStream = socket.getOutputStream();
    ContentResolver cr = context.getContentResolver();
    InputStream inputStream = null;
    inputStream = cr.openInputStream(Uri.parse("path/to/picture.jpg"));
    while ((len = inputStream.read(buf)) != -1) {
        outputStream.write(buf, 0, len);
    }
    outputStream.close();
    inputStream.close();
} catch (FileNotFoundException e) {
    //catch logic
} catch (IOException e) {
    //catch logic
}
/**
 * Clean up any open sockets when done
 * transferring or if an exception occurred.
 */
finally {
    if (socket != null) {
        if (socket.isConnected()) {
            try {
                socket.close();
            } catch (IOException e) {
                //catch logic
            }
        }
    }
}


4 則留言:

  1. 可以跟你要這個程式的壓縮檔嗎?
    我想放到手機上測試。

    回覆刪除
    回覆
    1. 您好,範例的部分請下載Android developer的API Demos~

      刪除
  2. 您好,請問:如果我寫一個賓果程式,在不架設AP server的情況下想行資料庫數據或檔案互相即時傳輸,透過此API就可以完成,對嗎?

    回覆刪除
    回覆
    1. David您好:
      的確是這樣子沒有錯,像是對岸的一些音樂撥放程式裡,
      就有使用Wi-Fi Direct的方式進行傳輸音樂檔案的功能。
      只要能夠讓兩點相配對到就可以進行資料的傳輸。
      不過如果是想要有一個主體的Server來管理很多個Client的數據連線的話,
      可能還是要弄一個固定的位址來讓其他透過TCP來連結傳輸,會比較恰當。

      刪除