延續上篇的碼農日常-『Android studio』Android Wifi 搜尋、連線與斷線機制實作(上)

今天要來寫關於Wifi連線&斷線兩個部分(・ω・)b

我當時在研究這個部分的時候,可是花了好大一把勁才爬坑的

因為這是個天坑啊( ´_ゝ`)

 

主要原因是Google在Android9→Android10的過程中有去修改關於這方面的實作

資料->https://developer.android.com/reference/android/net/wifi/WifiManager#reconnect()

截圖 2020-10-02 下午4.53.52

然後也因為Android10還沒有出得很久,因此這方面的資料就不太多QQ

(然後Android11就要出了ORZ)...

 

Okay,說了廢話那麼多不如來看看效果,差很多滴(╯=▃=)╯︵┻━┻

 

Android10的效果(請注意右上角)

 

Android10以下的效果(請注意右上角)

 

效果看懂了嗎(笑)

看懂的話就奉上Github

->https://github.com/thumbb13555/WifiSearchConnectDemo

 

スタート~(-‿◦)

 


 

1. 前提概要

 

上一篇講述的是關於Wifi搜尋以及準備一些介面顯示的部分

也因為這次的內容會跟上篇的內容有很強的接續

因此強烈建議看一看上篇後再來哦(・∀・)

->碼農日常-『Android studio』Android Wifi 搜尋、連線與斷線機制實作(上)

 

 

 

 

好啦其實不看上篇也沒差(笑)

如果你是知道了要怎麼搜尋的朋友的話就直接從下一段開始看吧ʕ•ᴥ•ʔ

 

 

看來你沒跳下一段XD

那就開始來寫今天的內容~

 

首先來先做跟今天的重點比較無關的事

先來補上RecyclerView的點擊事件吧

首先來到RecyclerViewAdapter.java

加入以下粉底白字的內容

 

RecyclerViewAdapter.java

class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {

    List<ScanResult> list = new ArrayList<>();
    OnItemClick onItemClick;

    public void addItem(List<ScanResult> list){
        this.list = list;
        notifyDataSetChanged();
    }

    public OnItemClick getOnItemClick() {
        return onItemClick;
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        private TextView tvSSID,tvBSSID,tvCap,tvFren,tvLevel;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);

            tvSSID = itemView.findViewById(R.id.textView_SSID);
            tvBSSID = itemView.findViewById(R.id.textView_BSSID);
            tvCap = itemView.findViewById(R.id.textView_Cap);
            tvFren = itemView.findViewById(R.id.textView_Frequency);
            tvLevel = itemView.findViewById(R.id.textView_Level);

        }
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.result_item,parent,false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        ScanResult scanResult = list.get(position);
        holder.tvSSID.setText(scanResult.SSID);
        holder.tvBSSID.setText("位址: "+scanResult.BSSID);
        holder.tvCap.setText("加密方式: "+scanResult.capabilities);
        holder.tvFren.setText("訊號頻率: "+scanResult.frequency);
        holder.tvLevel.setText("訊號強度: "+scanResult.level);
        holder.itemView.setOnClickListener(v -> {onItemClick.onItemClick(scanResult);});
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    interface OnItemClick{
        void onItemClick(ScanResult scanResult);
    }


}

 

再來回到MainActivity.java的onCreate()

 

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        /**取得定位權限(搜尋Wifi要記得開定位喔)*/
        /**(中間程式略)*/
//        mAdapter.onItemClick = scanResult -> {};//Lambda 表達式
        mAdapter.onItemClick = onItemClick;//點選RecyclerView中的物件後所做的事
    }

 

然後在onCreate外加入以下內容

/**點擊指定的Wifi後執行連線(Wifi下篇新增內容)*/
private RecyclerViewAdapter.OnItemClick onItemClick = scanResult -> {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        /**Android10(API29↑)以上版本的手機要執行的部分*/
    } else {
        /**Android10(API28↓)以下版本的手機要執行的部分*/
    }

};

 

如果以上程式看不懂的話,可以去參考我寫的這兩篇文章喔

->碼農日常-『Android studio』NumberPicker 配合 Interface (接口)完成一個時間選擇器

->碼農日常-『Android studio』基本RecyclerView 用法-2 基本版下拉更新以及點擊事件

 

寫到這篇,基本的架構就完成了

在此可以執行一下,看看有沒有什麼問題:D

 

此外,我在這次更新中有順便加入RecyclerView點擊後的水波紋效果

在這

 

result_item.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/selectableItemBackground"
    >

 


 

2. 撰寫Android10↑的連線事件

 

來撰寫Android10的連線事件囉(・∀・)

這裏是我當時在做開發時,實作最久的地方ORZ

 

我用的測試開發機是SAMSUNG三星 A7 (Android 10)

然後實作網路連線後,結果一直連線又斷線連線又斷線....

當時解決這個問題解到快要崩潰了

 

*Bug並不可怕,可怕的是有些有有些沒有*

 

對沒錯,然後我換成一樣是SAMSUNG的平板Samsung Galaxy Tab A (Android10)

居然沒有問題正常連線...(Oh 

image

 

寫到這個階段就會覺得我還寧可他閃退(╯=▃=)╯︵┻━┻

 

然後然後...很悲哀的事是這個Android 10的API不相容於往下的版本

所以換言之你的程式就要寫Android以上專用的程式跟以下專用的程式

image

 

哎...路自己選的,接受吧

總之我今天就是來寫我的爬坑記的,往下看吧( ・ὢ・ )

 

先PO以下副程式

/**Android10↑的連線(Wifi下篇新增內容)*/
@RequiresApi(api = Build.VERSION_CODES.Q)
private void connectWifiQ(String ssid, String password) {
    /**Android10以上的手機必須調用WifiNetworkSpecifier*/
    /**官方說明文件:https://developer.android.com/reference/android/net/wifi/WifiManager#reconnect() */
    WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
            .setSsid(ssid)
            .setWpa2Passphrase(password)
            .build();
    NetworkRequest request =
            new NetworkRequest.Builder()
                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                    .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                    .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)
                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
                    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
                    .setNetworkSpecifier(specifier)
                    .build();

    @SuppressLint("ServiceCast")
    ConnectivityManager connectivityManager =(ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);

    mNetwork = new ConnectivityManager.NetworkCallback(){
        @Override
        public void onAvailable(@NonNull Network network) {
            super.onAvailable(network);
            assert connectivityManager != null;
            /**將手機網路綁定到指定Wifi*/
            connectivityManager.bindProcessToNetwork(network);
        }

        @Override
        public void onUnavailable() {
            super.onUnavailable();
            Log.w(TAG, "onUnavailable: 連線失敗");
        }
    };
    connectivityManager.requestNetwork(request,mNetwork);
}

 

基本上,加入以下程式就可以跑了

來簡單聊一下這些在幹嘛

 

首先是上半部

WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
        .setSsid(ssid)
        .setWpa2Passphrase(password)
        .build();
NetworkRequest request =
        new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
                .setNetworkSpecifier(specifier)
                .build();

 

英文應該看得懂吧?上半部WifiNetworkSpecifier是用來建立連線用的

然後NetworkRequest就是顯示連線的方框用的

至於中間加入的...我懶得講,反正全部加入一定不會錯.!(吧)

 

至於如果Wifi是WPA2以外的,在setWpa2Passphrease中也有別的方法可以用,摸索看看吧:D

再來是連線結果回傳的部分,在下半部

 

@SuppressLint("ServiceCast")
ConnectivityManager connectivityManager =(ConnectivityManager)
        getSystemService(Context.CONNECTIVITY_SERVICE);

mNetwork = new ConnectivityManager.NetworkCallback(){
    @Override
    public void onAvailable(@NonNull Network network) {
        super.onAvailable(network);
        assert connectivityManager != null;
        /**將手機網路綁定到指定Wifi*/
        connectivityManager.bindProcessToNetwork(network);
    }

    @Override
    public void onUnavailable() {
        super.onUnavailable();
        Log.w(TAG, "onUnavailable: 連線失敗");
    }
};
connectivityManager.requestNetwork(request,mNetwork);

 

這邊會回傳連線的結果,如果密碼錯誤的話就會跑到onUnavailable

等等程式跑起來可以試試噢

 

然後關於粉底白字的部分...

這行是要將你的網路綁定到手機上去,所以絕對不能忘記加喔ɷ◡ɷ

 

最後再將本副程式加入到上面的版本判斷中,就完成囉

/**點擊指定的Wifi後執行連線(Wifi下篇新增內容)*/
private RecyclerViewAdapter.OnItemClick onItemClick = scanResult -> {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        connectWifiQ(scanResult.SSID, "12345678");
    } else {
       /**Android10(API28↓)以下版本的手機要執行的部分*/
    }

};

 

這時候如果你的手機是Android10↑,你應該就可以順利執行出來囉

 

 


 

2. 撰寫Android10↓的連線事件

 

上面也說了,Android的API中,因為不相容於往下的版本,因此還要手動寫一些其他版本的程式

那麼接下來就是來做這件事了

 

2-1 撰寫廣播事件

 

在寫這個之前,我們必須要來寫Wifi的監聽廣播事件

廣播事件是什麼?簡單來說就是監聽現在這部手機中的一些訊號

像是Wifi連線狀況、藍芽連線狀況或是一些自定義的接收狀況,都可以寫在裡面

 

詳細的部分請參考這篇...喔幹還沒寫(´・д・`)

 

然後再撰寫Wifi連線的部分,就可以打包收工了噢

那先來寫廣播(Broadcast)吧

在onCreate外加入私有類別,並繼承BroadcastReceiver

再加入以下程式

 

/**廣播Wifi的所有狀態(Wifi下篇新增內容)*/
private class WifiBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())){
            switch (intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0)){
                case WifiManager.WIFI_STATE_DISABLED:
                    Log.i(TAG, "Wifi關閉中");
                    break;
                case WifiManager.WIFI_STATE_DISABLING:
                    Log.i(TAG, "關閉Wifi中");
                    break;
                case WifiManager.WIFI_STATE_ENABLED:
                    Log.i(TAG, "Wifi使用中");
                    break;
                case WifiManager.WIFI_STATE_ENABLING:
                    Log.i(TAG, "開啟Wifi中");
                    break;
            }
        }else if((WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction()))){
            NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
            assert info != null;
            if (NetworkInfo.State.DISCONNECTED == info.getState()){
                Toast.makeText(context, "Wifi已斷線", Toast.LENGTH_SHORT).show();
            }else if(NetworkInfo.State.CONNECTED == info.getState()){
                Toast.makeText(context, "Wifi已連接", Toast.LENGTH_SHORT).show();
            }else if(NetworkInfo.State.CONNECTING == info.getState()){
                Log.i(TAG, "Wifi連線中");
            }
        }
    }
}

 

最後,在全域變數內加入這行

WifiBroadcastReceiver wifiBroadcastReceiver = new WifiBroadcastReceiver();

基本上,一個廣播模組就完成了

 

2-2 撰寫連線

 

接下來就要寫連線操作了

由於舊版的連線操作並沒有提供一個連線成功的事件回傳

因此才要使用廣播(´Д`)ヾ

 

那麼就加入以下程式吧

/**Android10↓的連線(Wifi下篇)*/
private void connectWifi(String tagSsid, String tagPassword){
    String ssid = "\""+tagSsid+"\"";
    String password = "\"" + tagPassword + "\"";
    WifiConfiguration conf = new WifiConfiguration();
    conf.allowedProtocols.clear();
    conf.allowedAuthAlgorithms.clear();
    conf.allowedGroupCiphers.clear();
    conf.allowedKeyManagement.clear();
    conf.allowedPairwiseCiphers.clear();
    conf.SSID = ssid;
    conf.preSharedKey = password;
    conf.status = WifiConfiguration.Status.ENABLED;
    conf.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
    conf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
    conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
    conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
    conf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
    conf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
    wifiManager.addNetwork(conf);
    List<WifiConfiguration> list = wifiManager.getConfiguredNetworks();
    for (WifiConfiguration configuration : list){
        if (configuration.SSID != null && conf.SSID.equals(ssid)){
            /**斷開原先的Wifi*/
            wifiManager.disconnect();
            /**連接指定的Wifi*/
            wifiManager.enableNetwork(conf.networkId,true);
            wifiManager.reconnect();
            break;
        }
    }

}

 

其實做的事情跟Android10的做法也沒有真的差太多

就是一樣帶一些設定給Wifi

只是在連線的那個瞬間是使用WifiManager完成

 

邏輯上來說就是設置WifiConfiguration後,再根據其SSID用for-each找出命中註定的那位ʕ→ᴥ←ʔ

找到後斷線、再重連

就這樣XD

 

最後,再回去版本判斷那邊把模組補上吧

private RecyclerViewAdapter.OnItemClick onItemClick = scanResult -> {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        connectWifiQ(scanResult.SSID, "12345678");
    } else {
        IntentFilter filter = new IntentFilter();
        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
        getApplicationContext().registerReceiver(wifiBroadcastReceiver,filter);
        connectWifi(scanResult.SSID, "12345678");
    }

};

 

補充一下

getApplicationContext().registerReceiver(wifiBroadcastReceiver,filter);

這行可以理解為啟動監聽廣播裡面傳出的事件

稍後在斷線的機制中會有關閉監聽的程式介紹

 


 

3. 退出程式後斷開剛才連線到的網路

 

接下來要來寫關於斷線的設計

就斷線來說,我是覺得舊版的就是很簡單暴力

 

就是無論他是哪裡連線的就直接斷開(笑)

而Adnroid10的做法就是從剛才的ConnectivityManager中取得標的,再使之斷線

最後,要實作退出斷線的話,程式可以寫在生命週期的onStop內

不理解的看一看Android的生命週期表吧:)

 

Android fundamentals 02.2: Activity lifecycle and state

 

蛤?你問我這是什麼?

網路很多自己去看啊(╯=▃=)╯︵┻━┻

 

沒啦...我以後有心情的話我會發一篇講....應該啦...

這個網路實在太多人寫了,又是歷久不衰的東西

哪輪得到我這小咖寫啊( ・ὢ・ )

 

那麼我們覆寫一下onStop

截圖 2020-10-02 下午6.24.16

 

咦?不能覆寫?

峨峨,我記得上一篇也有用到onStop齁

那就繼續往下寫就OK囉~

 

請加入以下程式吧

@Override
protected void onStop() {
    super.onStop();
    /**跳出畫面則停止掃描*/
    handler.removeCallbacks(searchWifi);
    /**斷開現在正連線著的Wifi(Wifi下篇新增內容)*/
    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            /**若為Android10的手機,則在此執行斷線*/
            @SuppressLint("ServiceCast")
            ConnectivityManager connectivityManager =(ConnectivityManager)
                    getSystemService(Context.CONNECTIVITY_SERVICE);
            assert connectivityManager != null;
            connectivityManager.unregisterNetworkCallback(mNetwork);
        } else {
            /**非Android10手機,則執行WifiManager的斷線*/
            List<WifiConfiguration> list = wifiManager.getConfiguredNetworks();
            for (WifiConfiguration configuration : list){
                wifiManager.removeNetwork(configuration.networkId);
            }
            wifiManager.disconnect();
            unregisterReceiver(wifiBroadcastReceiver);
        }
    }catch (Exception e){
        Log.i(TAG, "onStop: "+e.toString());
    }


}

 

這樣就是一個Wifi斷線機制了

 


 

結語

 

Wifi系列也算是告一個段落了

當時我在接到此功能需求的時候原本覺得還好

沒想到居然坑~這麼多

真的很想抱頭吶喊

image

 

真的..不要看我輕描淡寫地講述這些程式

背後都是我掉落的頭髮跟肝指數啊(╥_╥)

 

好啦,希望我的文章可以幫忙留住各位攻城屍們的髮根還有肝指數

剛好我發文的時候正是中秋連假~

祝各位中秋節愉快

中秋- 網友創作區/ Meme 梗圖倉庫

arrow
arrow

    碼農日常 發表在 痞客邦 留言(11) 人氣()