跟Google 學代碼Building Apps with Connectivity & the Cloud【連接無線設備】

說在前面的話:

“WIFI”—工作這麼久以來第一次接觸WIFI的知識

在之前翻譯Google 官方課程所記錄的《Web Apps》《Volley》時,就深感計算機網絡知識的欠缺,在翻譯到這一章的時候更感力不從心

因爲是學習資料看的是英文,很多專業單詞藉助外力(必應詞典,有道,谷歌翻譯)查,結果還是不名所以,模模糊糊的詞義多了,想理解句子含義就更別提了,有的時候學習一篇Goolg的課程,粗略看一遍挺簡單的,翻譯成通順的中文句子,難,將段落翻譯的有條有理,按着作者的行文邏輯將文章複述,更難,更別說消化吸收每一篇知識舉一反三了

從五月份已經開始刷計算機網絡相關書籍,希望捋順一遍計算機網絡的知識,再回過頭來把這裏的章節弄懂。

補充令人慶幸的一點:

還好,代碼是相通的,每次翻譯Google 的教程,各個渠道查,英文句子還是看不懂的,猜,猜不透的,看到代碼部分,就豁然開朗了

嗯,代碼是相通的,一切的疑難,從代碼入手是最快的;但我認爲這個階段慢下來最好,捋順邏輯,跟着Google的示例學習:

  1. 寫代碼的思路(學習任何編程都是從最簡單的入手,比如Volley,先學會發送最簡單的Request,接着使用標準的Request,最後自定義Request時候,有什麼注意事項,跟Google學習使用單例Request等等)

  2. 模塊的設計(分層思想,數據層,控制層,視圖層,每一層如何增刪改查)

分層的思想可以追溯到物理世界計算機網絡系統的設計,大到全球ISP和國家ISP,地區ISP,小到校園,公司,個人等等,爲什麼不提供一層ISP來服務於不同的端系統?網絡層級爲什麼劃分這麼多層?目的都是爲了服務於整個系統不同級別的“端”系統,抽象到軟件編程中,不管MVC MVP,MVVM等分層模式,其實都是爲了寫出”機器執行的快又好,後續的同事能更好的開發和維護“的項目,如果心中有“層”的概念,那麼作爲一個優秀的開發者,不論接手“爛”代碼,還是“好”代碼,都可以遊刃有餘,從容開發和維護了。

想改變世界,先改變自己的適應能力。

本文介紹

本章是翻譯自Google 官方課程 Building Apps with Connectivity & the Cloud 第一節

限於篇幅和文章連續性,本文只翻譯Connecting Devices Wirelessly 連接無線設備篇

本篇文章 的demo爲 NSDChat,是一款使用無線網絡服務所做的聊天室

文末有代碼示例

這裏寫圖片描述

連接無線網絡

這一節所有代碼,最低版本4.1 對應API 16

使用網絡發現服務


這節將包括什麼

  1. 註冊網絡服務
  2. 發現網絡服務
  3. 連接服務
  4. 解除服務

本節目的旨在如何在不同設備連接同一個應用

註冊服務

1. 創建NsdServiceInfo對象,這個對象有什麼用呢 ?

看下段示例:

  public void registerService(int port) {
        // Create the NsdServiceInfo object, and populate it.
        NsdServiceInfo serviceInfo  = new NsdServiceInfo();

        // The name is subject to change based on conflicts
        // with other services advertised on the same network.
        serviceInfo.setServiceName("NsdChat");
        serviceInfo.setServiceType("_http._tcp.");
        serviceInfo.setPort(port);
        ....
    }

通過NsdServiceInfo設置了服務名,服務類型和端口號,這就決定了如何連接此服務

如果我們使用socket編程接口,我們初始該socket的時候可以設置端口

    ```
    public void initializeServerSocket() {
        // Initialize a server socket on the next available port.
        mServerSocket = new ServerSocket(0);

        // Store the chosen port.
        mLocalPort =  mServerSocket.getLocalPort();
        ...
    }

    ```

定義好NsdServiceInfo對象後,接着我們得去實現一個接口,哪個接口呢?

2. 實現RegistrationListenter接口

這個接口包含了上一步我們註冊成功或失敗的回調

    public void initializeRegistrationListener() {
        mRegistrationListener = new NsdManager.RegistrationListener() {

            @Override
            public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
                // Save the service name.  Android may have changed it in order to
                // resolve a conflict, so update the name you initially requested
                // with the name Android actually used.
                mServiceName = NsdServiceInfo.getServiceName();
            }

            @Override
            public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
                // Registration failed!  Put debugging code here to determine why.
            }

            @Override
            public void onServiceUnregistered(NsdServiceInfo arg0) {
                // Service has been unregistered.  This only happens when you call
                // NsdManager.unregisterService() and pass in this listener.
            }

            @Override
            public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
                // Unregistration failed.  Put debugging code here to determine why.
            }
        };
    }

現在我們已經完成註冊的步驟了(創建NsdService對象,設置註冊成功失敗的監聽)

3. 調用registerService()方法

注意這個方法是 異步的,所以,所有需要執行在 此服務 啓動 之後的操作,都必須在 onServiceRegistered()中

public void registerService(int port) {
    NsdServiceInfo serviceInfo  = new NsdServiceInfo();
    serviceInfo.setServiceName("NsdChat");
    serviceInfo.setServiceType("_http._tcp.");
    serviceInfo.setPort(port);

    mNsdManager = Context.getSystemService(Context.NSD_SERVICE);

    mNsdManager.registerService(
            serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
}

2. 發現網絡上的服務

網絡上運行着各式各樣的服務,想在app中發現那些服務,我們需要監聽服務廣播,去分析哪些服務是可用的,並且過濾掉那些不能正常工作的服務

發現服務 只需兩步:

  1. 設置發現監聽
  2. 加入單例的異步編程接口discoverServices()

1. 設置內部類實現NsdManager.DiscoveryListener

   public void initializeDiscoveryListener() {

        // Instantiate a new DiscoveryListener
        mDiscoveryListener = new NsdManager.DiscoveryListener() {

            //  Called as soon as service discovery begins.
            @Override
            public void onDiscoveryStarted(String regType) {
                Log.d(TAG, "Service discovery started");
            }

            @Override
            public void onServiceFound(NsdServiceInfo service) {
                // A service was found!  Do something with it.
                Log.d(TAG, "Service discovery success" + service);
                if (!service.getServiceType().equals(SERVICE_TYPE)) {
                    // Service type is the string containing the protocol and
                    // transport layer for this service.
                    Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
                } else if (service.getServiceName().equals(mServiceName)) {
                    // The name of the service tells the user what they'd be
                    // connecting to. It could be "Bob's Chat App".
                    Log.d(TAG, "Same machine: " + mServiceName);
                } else if (service.getServiceName().contains("NsdChat")){
                    mNsdManager.resolveService(service, mResolveListener);
                }
            }

            @Override
            public void onServiceLost(NsdServiceInfo service) {
                // When the network service is no longer available.
                // Internal bookkeeping code goes here.
                Log.e(TAG, "service lost" + service);
            }

            @Override
            public void onDiscoveryStopped(String serviceType) {
                Log.i(TAG, "Discovery stopped: " + serviceType);
            }

            @Override
            public void onStartDiscoveryFailed(String serviceType, int errorCode) {
                Log.e(TAG, "Discovery failed: Error code:" + errorCode);
                mNsdManager.stopServiceDiscovery(this);
            }

            @Override
            public void onStopDiscoveryFailed(String serviceType, int errorCode) {
                Log.e(TAG, "Discovery failed: Error code:" + errorCode);
                mNsdManager.stopServiceDiscovery(this);
            }
        };
    }

注意上面的示例在onServiceFound()回調中所做的三次檢查:
1. !service.getServiceType().equals(SERVICE_TYPE)
2. service.getServiceName().equals(mServiceName)
3. service.getServiceName().contains(“NsdChat”)

當設置好監聽後,調用discoverServices(),通過此前應用中所設置的尋找參數:類型,協議,和監聽
mNsdManager.discoverServices(
SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);

3. 連接網絡上的服務

ok,當app發現了網絡上的某個服務,想連接該項服務的時候,

需要使用resolveService()方法,接着實現NsdManager.ResolveListener接口

 public void initializeResolveListener() {
        mResolveListener = new NsdManager.ResolveListener() {

            @Override
            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
                // Called when the resolve fails.  Use the error code to debug.
                Log.e(TAG, "Resolve failed" + errorCode);
            }

            @Override
            public void onServiceResolved(NsdServiceInfo serviceInfo) {
                Log.e(TAG, "Resolve Succeeded. " + serviceInfo);

                if (serviceInfo.getServiceName().equals(mServiceName)) {
                    Log.d(TAG, "Same IP.");
                    return;
                }
                mService = serviceInfo;
                int port = mService.getPort();
                InetAddress host = mService.getHost();
            }
        };
    }

一旦連接上該服務,我們的應用將接受到服務的信息,包括端口號和IP地址

應用關閉的時候解除服務

當應用關閉或退出至後臺的時候,我們就需要停止NSD功能。不關閉?ok,那應用將一直去發現符合要求的服務並嘗試連接,費電不說,關鍵是手機燙啊!

預覽整個activity生命週期,並在onstart和onstop中插入服務相關的廣播:

    //In your application's Activity

        @Override
        protected void onPause() {
            if (mNsdHelper != null) {
                mNsdHelper.tearDown();
            }
            super.onPause();
        }

        @Override
        protected void onResume() {
            super.onResume();
            if (mNsdHelper != null) {
                mNsdHelper.registerService(mConnection.getLocalPort());
                mNsdHelper.discoverServices();
            }
        }

        @Override
        protected void onDestroy() {
            mNsdHelper.tearDown();
            mConnection.tearDown();
            super.onDestroy();
        }

        // NsdHelper's tearDown method
            public void tearDown() {
            mNsdManager.unregisterService(mRegistrationListener);
            mNsdManager.stopServiceDiscovery(mDiscoveryListener);
        }

使用WI-FI創建P2P連接

這一節將瞭解:

  1. 如何設置權限
  2. 設置廣播接收者和Peer-to-Peer管理器
  3. 初始化Peer發現
  4. 拿Peers的列表
  5. 連接Peer

1. 設置應用權限

爲了使用WI-FI P2P功能,需要添加如下權限:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.android.nsdchat"
        ...

        <uses-permission
            android:required="true"
            android:name="android.permission.ACCESS_WIFI_STATE"/>
        <uses-permission
            android:required="true"
            android:name="android.permission.CHANGE_WIFI_STATE"/>
        <uses-permission
            android:required="true"
            android:name="android.permission.INTERNET"/>
        ...

2. 設置廣播接收者和文件管理器

  1. 爲了使用WI-FI P2P功能,還需要監聽廣播事件

    WIFI_P2P_STATE_CHANGED_ACTION 標明WI-FI P2P 功能是否可用

    WIFI_P2P_PEERS_CHANGED_ACTION 標明文件列表已經改變

    WIFI_P2P_CONNECTION_CHANGED_ACTION 標明WI-FI P2P連接已經改變

    WIFI_P2P_THIS_DEVICE_CHANGED_ACTION 標明設備配置已經改變

  2. 接着只需在IntentFilter中加入這些行爲,就可以過濾到其他不需要的行爲,接收我們感興趣的Intent了

    private final IntentFilter intentFilter = new IntentFilter();
        ...
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            //  Indicates a change in the Wi-Fi P2P status.
            intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
    
            // Indicates a change in the list of available peers.
            intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
    
            // Indicates the state of Wi-Fi P2P connectivity has changed.
            intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
    
            // Indicates this device's details have changed.
            intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
    
            ...
        }
  3. 獲得WifiP2pManager對象

    在oncreate()方法的最後,初始化WifiP2pManager對象,接着獲取WifiP2pManager.Channel對象,通過Channel可以連接WI-FI P2P 服務

        @Override
    
        Channel mChannel;
    
        public void onCreate(Bundle savedInstanceState) {
            ....
            mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
            mChannel = mManager.initialize(this, getMainLooper(), null);
        }
  4. 創建BroadcastReceiver

    創建廣播可以監聽WI-FI P2P的狀態, 當WI-FI 的狀態發生改變時,我們在onReceive()中做相應的處理:

            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
                    // Determine if Wifi P2P mode is enabled or not, alert
                    // the Activity.
                    int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
                    if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
                        activity.setIsWifiP2pEnabled(true);
                    } else {
                        activity.setIsWifiP2pEnabled(false);
                    }
                } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
    
                    // The peer list has changed!  We should probably do something about
                    // that.
    
                } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
    
                    // Connection state changed!  We should probably do something about
                    // that.
    
                } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
                    DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager()
                            .findFragmentById(R.id.frag_list);
                    fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra(
                            WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));
    
                }
            }
  5. 註冊和取消廣播接收

    在onResume()和onPause()中分別註冊和取消廣播

            /** register the BroadcastReceiver with the intent values to be matched */
            @Override
            public void onResume() {
                super.onResume();
                receiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this);
                registerReceiver(receiver, intentFilter);
            }
    
            @Override
            public void onPause() {
                super.onPause();
                unregisterReceiver(receiver);
            }

3. 初始化文件發現服務

通過discoverPeers()可以開啓 查找附近支持WI-FI P2P的設備

discoverPeers()裏有倆重要的參數:

  1. 上一步創建的WifiP2pManager.Channel
  2. WifiP2pManager.ActionListener 系統級回調,發現成功與否

       mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
    
                @Override
                public void onSuccess() {
                    // Code for when the discovery initiation is successful goes here.
                    // No services have actually been discovered yet, so this method
                    // can often be left blank.  Code for peer discovery goes in the
                    // onReceive method, detailed below.
                }
    
                @Override
                public void onFailure(int reasonCode) {
                    // Code for when the discovery initiation fails goes here.
                    // Alert the user that something went wrong.
                }
        });

通過上述兩步只是初始化了文件發現服務,discoverPeers()方法啓動發現進程,並且會立刻得到返回結果,

獲得文件列表

1. 第一步實現WifiP2pManager.PeerListListener 接口,它可以提供已連接的WI-FI
P2P 信息:

                 private List peers = new ArrayList();
                    ...

                    private PeerListListener peerListListener = new PeerListListener() {
                        @Override
                        public void onPeersAvailable(WifiP2pDeviceList peerList) {

                            // Out with the old, in with the new.
                            peers.clear();
                            peers.addAll(peerList.getDeviceList());

                            // If an AdapterView is backed by this data, notify it
                            // of the change.  For instance, if you have a ListView of available
                            // peers, trigger an update.
                            ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();
                            if (peers.size() == 0) {
                                Log.d(WiFiDirectActivity.TAG, "No devices found");
                                return;
                            }
                        }
                    }

2. 第二步當接收到匹配WIFI_P2P_PEERS_CHANGED_ACTION 行爲的Intent後,在廣播的onReceiver()中調用用requestPeers(),

  public void onReceive(Context context, Intent intent) {
        ...
        else 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, peerListListener);
            }
            Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
        }...
    }

連接一個網絡

爲了連接一個同級網絡,創建WifiP2pConfig 對象,並且複製數據至WifiP2pDevice (你想連接到哪個設備),接着調用connect()方法

      @Override
        public void connect() {
            // Picking the first device found on the network.
            WifiP2pDevice device = peers.get(0);

            WifiP2pConfig config = new WifiP2pConfig();
            config.deviceAddress = device.deviceAddress;
            config.wps.setup = WpsInfo.PBC;

            mManager.connect(mChannel, config, new ActionListener() {

                @Override
                public void onSuccess() {
                    // WiFiDirectBroadcastReceiver will notify us. Ignore for now.
                }

                @Override
                public void onFailure(int reason) {
                    Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
                            Toast.LENGTH_SHORT).show();
                }
            });
        }

如果想監聽連接狀態,需要實現 WifiP2pManager.ConnectionInfoListener,在onConnectionInfoAvailable()中將返回WI-FI P2P連接狀態

   @Override
    public void onConnectionInfoAvailable(final WifiP2pInfo info) {

        // InetAddress from WifiP2pInfo struct.
        InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress());

        // After the group negotiation, we can determine the group owner.
        if (info.groupFormed && info.isGroupOwner) {
            // Do whatever tasks are specific to the group owner.
            // One common case is creating a server thread and accepting
            // incoming connections.
        } else if (info.groupFormed) {
            // The other device acts as the client. In this case,
            // you'll want to create a client thread that connects to the group
            // owner.
        }
    }

現在返回到onReceive()方法中,當接收到匹配 WIFI_P2P_CONNECTION_CHANGED_ACTION的Intent,調requestConnectionInfo(),這是異步的調用,所以結果將被上一部設置的連接監聽(ConnectionInfoListener)所接收;

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

            if (mManager == null) {
                return;
            }

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

            if (networkInfo.isConnected()) {

                // We are connected with the other device, request connection
                // info to find group owner IP

                mManager.requestConnectionInfo(mChannel, connectionListener);
            }
            ...

使用WIFI P2P 發現服務

這節將涉及:

  1. 設置Mainfest文件
  2. 加入本地服務
  3. 發現附近的服務

在前面兩節,我們已經學會如何發現服務並連接本地網絡,然後,使用WI-FI P2P發現服務允許你發現附近的設備,不需要連接網絡,這樣就可以不需要網絡從而連接兩個服務。

設置Manifest文件

爲了使用WI-FI P2P功能,加入如下權限:

     <uses-permission
            android:required="true"
            android:name="android.permission.ACCESS_WIFI_STATE"/>
        <uses-permission
            android:required="true"
            android:name="android.permission.CHANGE_WIFI_STATE"/>
        <uses-permission
            android:required="true"
            android:name="android.permission.INTERNET"/>

加入本地服務

爲了提供本地服務,還需要註冊發現服務,一旦註冊了發現服務,系統將自動響應來自文件的發現請求

現在來創建一個本地服務:

  1. 創建WifiP2pServiceInfo 對象
  2. 計算關於服務的信息
  3. 調用 addLocalService()方法去註冊本地服務
   private void startRegistration() {
            //  Create a string map containing information about your service.
            Map record = new HashMap();
            record.put("listenport", String.valueOf(SERVER_PORT));
            record.put("buddyname", "John Doe" + (int) (Math.random() * 1000));
            record.put("available", "visible");

            // Service information.  Pass it an instance name, service type
            // _protocol._transportlayer , and the map containing
            // information other devices will want once they connect to this one.
            WifiP2pDnsSdServiceInfo serviceInfo =
                    WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record);

            // Add the local service, sending the service info, network channel,
            // and listener that will be used to indicate success or failure of
            // the request.
            mManager.addLocalService(channel, serviceInfo, new ActionListener() {
                @Override
                public void onSuccess() {
                    // Command successful! Code isn't necessarily needed here,
                    // Unless you want to update the UI or add logging statements.
                }

                @Override
                public void onFailure(int arg0) {
                    // Command failed.  Check for P2P_UNSUPPORTED, ERROR, or BUSY
                }
            });
        }

發現附近的服務

Android使用回調方法通知客戶端哪些是可用的服務,所以首先得設置這些回調接口:

  1. WifiP2pManager.DnsSdTxtRecordListener

        final HashMap<String, String> buddies = new HashMap<String, String>();
        ...
        private void discoverService() {
            DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() {
                @Override
                /* Callback includes:
                 * fullDomain: full domain name: e.g "printer._ipp._tcp.local."
                 * record: TXT record dta as a map of key/value pairs.
                 * device: The device running the advertised service.
                 */
    
                public void onDnsSdTxtRecordAvailable(
                        String fullDomain, Map record, WifiP2pDevice device) {
                        Log.d(TAG, "DnsSdTxtRecord available -" + record.toString());
                        buddies.put(device.deviceAddress, record.get("buddyname"));
                    }
                };
            ...
        }
  2. 爲了獲得服務信息,創建WifiP2pManager.DnsSdServiceResponseListener接口

        private void discoverService() {
        ...
    
            DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() {
                @Override
                public void onDnsSdServiceAvailable(String instanceName, String registrationType,
                        WifiP2pDevice resourceType) {
    
                        // Update the device name with the human-friendly version from
                        // the DnsTxtRecord, assuming one arrived.
                        resourceType.deviceName = buddies
                                .containsKey(resourceType.deviceAddress) ? buddies
                                .get(resourceType.deviceAddress) : resourceType.deviceName;
    
                        // Add to the custom adapter defined specifically for showing
                        // wifi devices.
                        WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager()
                                .findFragmentById(R.id.frag_peerlist);
                        WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment
                                .getListAdapter());
    
                        adapter.add(resourceType);
                        adapter.notifyDataSetChanged();
                        Log.d(TAG, "onBonjourServiceAvailable " + instanceName);
                }
            };
    
            mManager.setDnsSdResponseListeners(channel, servListener, txtListener);
            ...
        }
  3. 現在創建服務請求並且調用addServiceRequest(),這個方法需要傳遞一個監聽接口 ActionListener

         serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
                mManager.addServiceRequest(channel,
                        serviceRequest,
                        new ActionListener() {
                            @Override
                            public void onSuccess() {
                                // Success!
                            }
    
                            @Override
                            public void onFailure(int code) {
                                // Command failed.  Check for P2P_UNSUPPORTED, ERROR, or BUSY
                            }
                        });
  4. 最後調用discoverServices()

       mManager.discoverServices(channel, new ActionListener() {
    
                    @Override
                    public void onSuccess() {
                        // Success!
                    }
    
                    @Override
                    public void onFailure(int code) {
                        // Command failed.  Check for P2P_UNSUPPORTED, ERROR, or BUSY
                        if (code == WifiP2pManager.P2P_UNSUPPORTED) {
                            Log.d(TAG, "P2P isn't supported on this device.");
                        else if(...)
                            ...
                    }
                });

全文代碼

1. ChatConnection

ChatConnection 是最核心的地方,有兩個內部類ChatServer服務端和ChatClient客戶端,作用是初始化NsdServiceInfo信息,刷新客戶端的聊天界面

public class ChatConnection {

    private Handler mUpdateHandler;
    private ChatServer mChatServer;
    private ChatClient mChatClient;

    private static final String TAG = "ChatConnection";

    private Socket mSocket;
    private int mPort = -1;

    public ChatConnection(Handler handler) {
        mUpdateHandler = handler;
        mChatServer = new ChatServer(handler);
    }

    public void tearDown() {
        mChatServer.tearDown();
        mChatClient.tearDown();
    }

    public void connectToServer(InetAddress address, int port) {
        mChatClient = new ChatClient(address, port);
    }

    public void sendMessage(String msg) {
        if (mChatClient != null) {
            mChatClient.sendMessage(msg);
        }
    }

    public int getLocalPort() {
        return mPort;
    }

    public void setLocalPort(int port) {
        mPort = port;
    }


    public synchronized void updateMessages(String msg, boolean local) {
        Log.e(TAG, "Updating message: " + msg);

        if (local) {
            msg = "me: " + msg;
        } else {
            msg = "them: " + msg;
        }

        Bundle messageBundle = new Bundle();
        messageBundle.putString("msg", msg);

        Message message = new Message();
        message.setData(messageBundle);
        mUpdateHandler.sendMessage(message);

    }

    private synchronized void setSocket(Socket socket) {
        Log.d(TAG, "setSocket being called.");
        if (socket == null) {
            Log.d(TAG, "Setting a null socket.");
        }
        if (mSocket != null) {
            if (mSocket.isConnected()) {
                try {
                    mSocket.close();
                } catch (IOException e) {
                    // TODO(alexlucas): Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        mSocket = socket;
    }

    private Socket getSocket() {
        return mSocket;
    }

    private class ChatServer {
        ServerSocket mServerSocket = null;
        Thread mThread = null;

        public ChatServer(Handler handler) {
            mThread = new Thread(new ServerThread());
            mThread.start();
        }

        public void tearDown() {
            mThread.interrupt();
            try {
                mServerSocket.close();
            } catch (IOException ioe) {
                Log.e(TAG, "Error when closing server socket.");
            }
        }

        class ServerThread implements Runnable {

            @Override
            public void run() {

                try {
                    // Since discovery will happen via Nsd, we don't need to care which port is
                    // used.  Just grab an available one  and advertise it via Nsd.
                    mServerSocket = new ServerSocket(0);
                    setLocalPort(mServerSocket.getLocalPort());

                    while (!Thread.currentThread().isInterrupted()) {
                        Log.d(TAG, "ServerSocket Created, awaiting connection");
                        setSocket(mServerSocket.accept());
                        Log.d(TAG, "Connected.");
                        if (mChatClient == null) {
                            int port = mSocket.getPort();
                            InetAddress address = mSocket.getInetAddress();
                            connectToServer(address, port);
                        }
                    }
                } catch (IOException e) {
                    Log.e(TAG, "Error creating ServerSocket: ", e);
                    e.printStackTrace();
                }
            }
        }
    }

    private class ChatClient {

        private InetAddress mAddress;
        private int PORT;

        private final String CLIENT_TAG = "ChatClient";

        private Thread mSendThread;
        private Thread mRecThread;

        public ChatClient(InetAddress address, int port) {

            Log.d(CLIENT_TAG, "Creating chatClient");
            this.mAddress = address;
            this.PORT = port;

            mSendThread = new Thread(new SendingThread());
            mSendThread.start();
        }

        class SendingThread implements Runnable {

            BlockingQueue<String> mMessageQueue;
            private int QUEUE_CAPACITY = 10;

            public SendingThread() {
                mMessageQueue = new ArrayBlockingQueue<String>(QUEUE_CAPACITY);
            }

            @Override
            public void run() {
                try {
                    if (getSocket() == null) {
                        setSocket(new Socket(mAddress, PORT));
                        Log.d(CLIENT_TAG, "Client-side socket initialized.");

                    } else {
                        Log.d(CLIENT_TAG, "Socket already initialized. skipping!");
                    }

                    mRecThread = new Thread(new ReceivingThread());
                    mRecThread.start();

                } catch (UnknownHostException e) {
                    Log.d(CLIENT_TAG, "Initializing socket failed, UHE", e);
                } catch (IOException e) {
                    Log.d(CLIENT_TAG, "Initializing socket failed, IOE.", e);
                }

                while (true) {
                    try {
                        String msg = mMessageQueue.take();
                        sendMessage(msg);
                    } catch (InterruptedException ie) {
                        Log.d(CLIENT_TAG, "Message sending loop interrupted, exiting");
                    }
                }
            }
        }

        class ReceivingThread implements Runnable {

            @Override
            public void run() {

                BufferedReader input;
                try {
                    input = new BufferedReader(new InputStreamReader(
                            mSocket.getInputStream()));
                    while (!Thread.currentThread().isInterrupted()) {

                        String messageStr = null;
                        messageStr = input.readLine();
                        if (messageStr != null) {
                            Log.d(CLIENT_TAG, "Read from the stream: " + messageStr);
                            updateMessages(messageStr, false);
                        } else {
                            Log.d(CLIENT_TAG, "The nulls! The nulls!");
                            break;
                        }
                    }
                    input.close();

                } catch (IOException e) {
                    Log.e(CLIENT_TAG, "Server loop error: ", e);
                }
            }
        }

        public void tearDown() {
            try {
                getSocket().close();
            } catch (IOException ioe) {
                Log.e(CLIENT_TAG, "Error when closing server socket.");
            }
        }

        public void sendMessage(String msg) {
            try {
                Socket socket = getSocket();
                if (socket == null) {
                    Log.d(CLIENT_TAG, "Socket is null, wtf?");
                } else if (socket.getOutputStream() == null) {
                    Log.d(CLIENT_TAG, "Socket output stream is null, wtf?");
                }

                PrintWriter out = new PrintWriter(
                        new BufferedWriter(
                                new OutputStreamWriter(getSocket().getOutputStream())), true);
                out.println(msg);
                out.flush();
                updateMessages(msg, true);
            } catch (UnknownHostException e) {
                Log.d(CLIENT_TAG, "Unknown Host", e);
            } catch (IOException e) {
                Log.d(CLIENT_TAG, "I/O Exception", e);
            } catch (Exception e) {
                Log.d(CLIENT_TAG, "Error3", e);
            }
            Log.d(CLIENT_TAG, "Client sent message: " + msg);
        }
    }
}

2. NsdHelper

NsdHelper是這個聊天室app 中第二重要的類

它的作用是設置監聽器,提供接口給Activity交給用戶控制:中斷服務,啓動服務,發現服務,連接服務等

public class NsdHelper {

    Context mContext;

    NsdManager mNsdManager;
    NsdManager.ResolveListener mResolveListener;
    NsdManager.DiscoveryListener mDiscoveryListener;
    NsdManager.RegistrationListener mRegistrationListener;

    public static final String SERVICE_TYPE = "_http._tcp.";

    public static final String TAG = "NsdHelper";
    public String mServiceName = "NsdChat";

    NsdServiceInfo mService;

    public NsdHelper(Context context) {
        mContext = context;
        mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
    }

    public void initializeNsd() {
        initializeResolveListener();
        initializeDiscoveryListener();
        initializeRegistrationListener();

//        mNsdManager.init(mContext.getMainLooper(), this);

    }

    public void initializeDiscoveryListener() {
        mDiscoveryListener = new NsdManager.DiscoveryListener() {

            @Override
            public void onDiscoveryStarted(String regType) {
                Log.d(TAG, "Service discovery started");
            }

            @Override
            public void onServiceFound(NsdServiceInfo service) {
                Log.d(TAG, "Service discovery success" + service);
                if (!service.getServiceType().equals(SERVICE_TYPE)) {
                    Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
                } else if (service.getServiceName().equals(mServiceName)) {
                    Log.d(TAG, "Same machine: " + mServiceName);
                } else if (service.getServiceName().contains(mServiceName)){
                    mNsdManager.resolveService(service, mResolveListener);
                }
            }

            @Override
            public void onServiceLost(NsdServiceInfo service) {
                Log.e(TAG, "service lost" + service);
                if (mService == service) {
                    mService = null;
                }
            }

            @Override
            public void onDiscoveryStopped(String serviceType) {
                Log.i(TAG, "Discovery stopped: " + serviceType);        
            }

            @Override
            public void onStartDiscoveryFailed(String serviceType, int errorCode) {
                Log.e(TAG, "Discovery failed: Error code:" + errorCode);
                mNsdManager.stopServiceDiscovery(this);
            }

            @Override
            public void onStopDiscoveryFailed(String serviceType, int errorCode) {
                Log.e(TAG, "Discovery failed: Error code:" + errorCode);
                mNsdManager.stopServiceDiscovery(this);
            }
        };
    }

    public void initializeResolveListener() {
        mResolveListener = new NsdManager.ResolveListener() {

            @Override
            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
                Log.e(TAG, "Resolve failed" + errorCode);
            }

            @Override
            public void onServiceResolved(NsdServiceInfo serviceInfo) {
                Log.e(TAG, "Resolve Succeeded. " + serviceInfo);

                if (serviceInfo.getServiceName().equals(mServiceName)) {
                    Log.d(TAG, "Same IP.");
                    return;
                }
                mService = serviceInfo;
            }
        };
    }

    public void initializeRegistrationListener() {
        mRegistrationListener = new NsdManager.RegistrationListener() {

            @Override
            public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
                mServiceName = NsdServiceInfo.getServiceName();
            }

            @Override
            public void onRegistrationFailed(NsdServiceInfo arg0, int arg1) {
            }

            @Override
            public void onServiceUnregistered(NsdServiceInfo arg0) {
            }

            @Override
            public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            }

        };
    }

    public void registerService(int port) {
        NsdServiceInfo serviceInfo  = new NsdServiceInfo();
        serviceInfo.setPort(port);
        serviceInfo.setServiceName(mServiceName);
        serviceInfo.setServiceType(SERVICE_TYPE);

        mNsdManager.registerService(
                serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);

    }

    public void discoverServices() {
        mNsdManager.discoverServices(
                SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
    }

    public void stopDiscovery() {
        mNsdManager.stopServiceDiscovery(mDiscoveryListener);
    }

    public NsdServiceInfo getChosenServiceInfo() {
        return mService;
    }

    public void tearDown() {
        mNsdManager.unregisterService(mRegistrationListener);
    }
}

NsdActivity

響應用戶的操作,根據activity的生命週期做出一些響應

public class NsdChatActivity extends AppCompatActivity {

    NsdHelper mNsdHelper;

    private TextView mStatusView;
    private Handler mUpdateHandler;

    public static final String TAG = "NsdChat";

    ChatConnection mConnection;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mStatusView = (TextView) findViewById(R.id.status);

        mUpdateHandler = new Handler() {
                @Override
            public void handleMessage(Message msg) {
                String chatLine = msg.getData().getString("msg");
                addChatLine(chatLine);
            }
        };

        mConnection = new ChatConnection(mUpdateHandler);

        mNsdHelper = new NsdHelper(this);
        mNsdHelper.initializeNsd();

    }

    public void clickAdvertise(View v) {
        // Register service
        if(mConnection.getLocalPort() > -1) {
            mNsdHelper.registerService(mConnection.getLocalPort());
        } else {
            Log.d(TAG, "ServerSocket isn't bound.");
        }
    }

    public void clickDiscover(View v) {
        mNsdHelper.discoverServices();
    }

    public void clickConnect(View v) {
        NsdServiceInfo service = mNsdHelper.getChosenServiceInfo();
        if (service != null) {
            Log.d(TAG, "Connecting.");
            mConnection.connectToServer(service.getHost(),
                    service.getPort());
        } else {
            Log.d(TAG, "No service to connect to!");
        }
    }

    public void clickSend(View v) {
        EditText messageView = (EditText) this.findViewById(R.id.chatInput);
        if (messageView != null) {
            String messageString = messageView.getText().toString();
            if (!messageString.isEmpty()) {
                mConnection.sendMessage(messageString);
            }
            messageView.setText("");
        }
    }

    public void addChatLine(String line) {
        mStatusView.append("\n" + line);
    }

    @Override
    protected void onPause() {
        if (mNsdHelper != null) {
            mNsdHelper.stopDiscovery();
        }
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mNsdHelper != null) {
            mNsdHelper.discoverServices();
        }
    }

    @Override
    protected void onDestroy() {
        mNsdHelper.tearDown();
        mConnection.tearDown();
        super.onDestroy();
    }
}

參考

  1. 這裏寫鏈接內容
  2. demo下載

結尾

這一篇翻譯所爛的程度和之前《 OpenGL ES 》有一拼,但還是硬着頭皮翻譯了6-7成的樣子,以博客的形式寫出,意味着終歸要回來審視,這一遍翻譯得不夠好,下一遍,下下遍的時候是不是能更好一些,這是我更加需要反思的地方

寫在結尾,勉勵自己!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章