延續上篇的碼農日常-『Android studio』Android Wifi 搜尋、連線與斷線機制實作(上)
今天要來寫關於Wifi連線&斷線兩個部分(・ω・)b
我當時在研究這個部分的時候,可是花了好大一把勁才爬坑的
因為這是個天坑啊( ´_ゝ`)
主要原因是Google在Android9→Android10的過程中有去修改關於這方面的實作
資料->https://developer.android.com/reference/android/net/wifi/WifiManager#reconnect()
然後也因為Android10還沒有出得很久,因此這方面的資料就不太多QQ
(然後Android11就要出了ORZ)...
Okay,說了廢話那麼多不如來看看效果,差很多滴(╯=▃=)╯︵┻━┻
Android10的效果(請注意右上角)
Android10以下的效果(請注意右上角)
效果看懂了嗎(笑)
看懂的話就奉上Github吧
->https://github.com/thumbb13555/WifiSearchConnectDemo
スタート~(-‿◦)
1. 前提概要
上一篇講述的是關於Wifi搜尋以及準備一些介面顯示的部分
也因為這次的內容會跟上篇的內容有很強的接續
因此強烈建議看一看上篇後再來哦(・∀・)
->碼農日常-『Android studio』Android Wifi 搜尋、連線與斷線機制實作(上)
好啦其實不看上篇也沒差(笑)
如果你是知道了要怎麼搜尋的朋友的話就直接從下一段開始看吧ʕ•ᴥ•ʔ
看來你沒跳下一段XD
那就開始來寫今天的內容~
首先來先做跟今天的重點比較無關的事
先來補上RecyclerView的點擊事件吧
首先來到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點擊後的水波紋效果
在這
<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
寫到這個階段就會覺得我還寧可他閃退(╯=▃=)╯︵┻━┻
然後然後...很悲哀的事是這個Android 10的API不相容於往下的版本
所以換言之你的程式就要寫Android以上專用的程式跟以下專用的程式
哎...路自己選的,接受吧
總之我今天就是來寫我的爬坑記的,往下看吧( ・ὢ・ )
先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的生命週期表吧:)
蛤?你問我這是什麼?
網路很多自己去看啊(╯=▃=)╯︵┻━┻
沒啦...我以後有心情的話我會發一篇講....應該啦...
這個網路實在太多人寫了,又是歷久不衰的東西
哪輪得到我這小咖寫啊( ・ὢ・ )
那麼我們覆寫一下onStop吧
咦?不能覆寫?
峨峨,我記得上一篇也有用到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系列也算是告一個段落了
當時我在接到此功能需求的時候原本覺得還好
沒想到居然坑~這麼多
真的很想抱頭吶喊
真的..不要看我輕描淡寫地講述這些程式
背後都是我掉落的頭髮跟肝指數啊(╥_╥)
好啦,希望我的文章可以幫忙留住各位攻城屍們的髮根還有肝指數
剛好我發文的時候正是中秋連假~
祝各位中秋節愉快