前言: WiFi热点设置页面的安全性选项在Android 4.x上有“无”、“WPA PSK”、“WPA2 PSK”三个选项,在Android 5.0(含)之后去掉了WPA PSK选项(部分手机厂家会修改ROM,有些手机4.4就没有这个选项了,安全性选项下拉选项是在packages/apps/Settings/res/values/arrays.xml这个文件的wifi_ap_security数组中定义的),当安全性为“无”时连接热点不需要密码,其他两种都是要输入密码才能连接的。本文将讲解用代码自动创建、跳转系统热点设置页手动创建两种方式创建热点,以及当targetSdkVersion设为23以上时如何处理权限问题。
热点的安全性选项对应的Java类是在WifiConfiguration.java中定义的(源码路径:/frameworks/base/wifi/java/android/net/wifi/WifiConfiguration.java,API 25,不同的API内容有差异)
1 public static class KeyMgmt { 2 private KeyMgmt() { } 3 4 /** WPA is not used; plaintext or static WEP could be used. */ 5 public static final int NONE = 0; 6 /** WPA pre-shared key (requires {@code preSharedKey} to be specified). */ 7 public static final int WPA_PSK = 1; 8 /** WPA using EAP authentication. Generally used with an external authentication server. */ 9 public static final int WPA_EAP = 2; 10 /** IEEE 802.1X using EAP authentication and (optionally) dynamically 11 * generated WEP keys. */ 12 public static final int IEEE8021X = 3; 13 14 /** WPA2 pre-shared key for use with soft access point 15 * (requires {@code preSharedKey} to be specified). 16 * @hide 17 */ 18 @SystemApi 19 public static final int WPA2_PSK = 4; 20 /** 21 * Hotspot 2.0 r2 OSEN: 22 * @hide 23 */ 24 public static final int OSEN = 5; 25 26 public static final String varName = "key_mgmt"; 27 28 public static final String[] strings = { "NONE", "WPA_PSK", "WPA_EAP", "IEEE8021X", 29 "WPA2_PSK", "OSEN" }; 30 }
可以看到,无、WPA PSK、WPA2 PSK这三个选项分别对应到KeyMgmt.NONE、KeyMgmt.WPA_PSK、KeyMgmt.WPA2_PSK,最后一个WPA2_PSK添加了@SystemApi标记,我们不可以直接访问的,怎么创建这三种形式的热点呢?
- 编码实现
添加需要的权限
Android并没有公开创建WiFi热点的API,所以我们只能通过反射的方式实现,创建安全性为“无”的代码如下:
1 /** 2 * 自定义wifi热点 3 * 4 * @param enabled 开启or关闭 5 * @return 6 */ 7 private boolean setWifiApEnabled(boolean enabled) { 8 boolean result = false; 9 WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); 10 if (enabled) { 11 //wifi和热点不能同时打开,所以打开热点的时候需要关闭wifi 12 if (wifiManager.isWifiEnabled()) { 13 wifiManager.setWifiEnabled(false); 14 } 15 } 16 try { 17 //热点的配置类 18 WifiConfiguration apConfig = new WifiConfiguration(); 19 //配置热点的名称 20 apConfig.SSID = "ap_test"; 21 //配置热点的密码,至少八位 22 apConfig.preSharedKey = ""; 23 //配置热点安全性选项 24 apConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); 25 //通过反射调用设置热点 26 Method method = wifiManager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, boolean.class); 27 //返回热点打开状态 28 result = (Boolean) method.invoke(wifiManager, apConfig, enabled); 29 if (!result) { 30 Toast.makeText(this, "热点创建失败,请手动创建!", Toast.LENGTH_SHORT).show(); 31 openAPUI(); 32 } 33 } catch (Exception e) { 34 Toast.makeText(this, "热点创建失败,请手动创建!", Toast.LENGTH_SHORT).show(); 35 openAPUI(); 36 } 37 return result; 38 }
创建安全性为“WPA PSK”的热点需要修改22、24行,需要注意的是:创建热点后,在不支持WPA_PSK的手机上(Android 4.3以上版本)打开系统热点页面,“安全性”将显示为“无”,看不到密码文本框,但是连接此类热点还是需要输入密码的。
1 //配置热点的密码,至少八位 2 apConfig.preSharedKey = "12345678"; 3 //配置热点安全性 4 apConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
创建安全性选项为WPA2 PSK只需将上面代码中第4行改为
apConfig.allowedKeyManagement.set(4);
直接将值指定为4,原因上面讲过,这里还有另外一个坑:在MIUI系统上,WPA2 PSK这个值为6,如果在MIUI上运行需要设为6才可以,我们可以判断一下手机ROM的版本,根据不同的ROM设为4或6。(后面有另外一种判断方式)
1 /** 2 * 判断是否为MIUI系统,参考http://blog.csdn.net/xx326664162/article/details/52438706 3 * 4 * @return 5 */ 6 public static boolean isMIUI() { 7 try { 8 String KEY_MIUI_VERSION_CODE = "ro.miui.ui.version.code"; 9 String KEY_MIUI_VERSION_NAME = "ro.miui.ui.version.name"; 10 String KEY_MIUI_INTERNAL_STORAGE = "ro.miui.internal.storage"; 11 Properties prop = new Properties(); 12 prop.load(new FileInputStream(new File(Environment.getRootDirectory(), "build.prop"))); 13 14 return prop.getProperty(KEY_MIUI_VERSION_CODE, null) != null 15 || prop.getProperty(KEY_MIUI_VERSION_NAME, null) != null 16 || prop.getProperty(KEY_MIUI_INTERNAL_STORAGE, null) != null; 17 } catch (IOException e) { 18 return false; 19 } 20 }
apConfig.allowedKeyManagement.set(isMIUI() ? 6 : 4);
至于MIUI为什么设为6就无从得知了,我尝试在MIUI论坛发帖询问原因,帖子一直没有审核通过,由此可见使用系统非公开API是有一定风险的。
20170914更新
经过研究,可以从WifiConfiguration.KeyMgmt.strings这个数组中读取所有的安全性选项(MIUI的安全性选项为NONE, WPA_PSK, WPA_EAP, IEEE8021X, WAPI_PSK, WAPI_CERT, WPA2_PSK, OSEN,多了WAPI_PSK, WAPI_CERT这两个),我们只要从中找到WPA2_PSK的索引值就可以了,不需要根据ROM类型判断,所以上述代码可以修改为
1 int indexOfWPA2_PSK = 4; 2 //从WifiConfiguration.KeyMgmt数组中查找WPA2_PSK的值 3 for (int i = 0; i < WifiConfiguration.KeyMgmt.strings.length; i++) { 4 if (WifiConfiguration.KeyMgmt.strings[i].equals("WPA2_PSK")) { 5 indexOfWPA2_PSK = i; 6 break; 7 } 8 } 9 apConfig.allowedKeyManagement.set(indexOfWPA2_PSK);
如果自动创建热点失败,我们也可以跳转系统设置页让用户手动创建。 Android同样没有提供跳转热点设置页面的Intent,我们可以打开热点设置页面后,通过使用adb shell actives去查找相应的APP包名、类,打开系统热点设置页的代码如下:
/** * 打开网络共享与热点设置页面 */ private void openAPUI() { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //打开网络共享与热点设置页面 ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$TetherSettingsActivity"); intent.setComponent(comp); startActivity(intent); }
- 读取热点配置信息
直接贴代码吧,判断热点是否打开可以参考源码中的isWifiApEnabled()方法
1 /** 2 * 读取热点配置信息 3 */ 4 private void getWiFiAPConfig() { 5 try { 6 WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); 7 Method method = wifiManager.getClass().getMethod("getWifiApConfiguration"); 8 WifiConfiguration apConfig = (WifiConfiguration) method.invoke(wifiManager); 9 if (apConfig == null) { 10 tvInfo.setText("未配置热点"); 11 return; 12 } 13 tvInfo.setText(String.format("热点名称:%s\r\n", apConfig.SSID)); 14 tvInfo.append(String.format("密码:%s\n", apConfig.preSharedKey)); 15 //使用apConfig.allowedKeyManagement.toString()返回{0}这样的格式,需要截取中间的具体数值 16 //下面几种写法都可以 17 //int index = Integer.valueOf(apConfig.allowedKeyManagement.toString().substring(1, 2)); 18 //int index = Integer.valueOf(String.valueOf(apConfig.allowedKeyManagement.toString().charAt(1))); 19 //int index = Integer.valueOf(apConfig.allowedKeyManagement.toString().charAt(1)+""); 20 int index = apConfig.allowedKeyManagement.toString().charAt(1) - '0'; 21 //从KeyMgmt数组中取出对应的文本 22 String apType = WifiConfiguration.KeyMgmt.strings[index]; 23 tvInfo.append(String.format(Locale.getDefault(), "WifiConfiguration.KeyMgmt:%s\r\n", Arrays.toString(WifiConfiguration.KeyMgmt.strings))); 24 tvInfo.append(String.format(Locale.getDefault(), "安全性:%d,%s\r\n", index, apType)); 25 isOpen = isWifiApEnabled(); 26 tvInfo.append("是否已开启:" + isOpen); 27 } catch (NoSuchMethodException e) { 28 e.printStackTrace(); 29 } catch (IllegalAccessException e) { 30 e.printStackTrace(); 31 } catch (InvocationTargetException e) { 32 e.printStackTrace(); 33 } 34 }
系统会自动创建一个默认的热点,如果我们不需要自定义热点参数的话,可以直接读取系统的热点参数,请参考源码中的switchWifiApEnabled(boolean enabled)方法
- Android 6.0权限适配
如果APP的targetSdkVersion为23以上,需要在清单文件中添加android.permission.WRITE_SETTINGS权限,并在运行时申请,跳转系统设置页由用户开启。
1 private boolean isHasPermissions() { 2 boolean result = false; 3 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 4 if (!Settings.System.canWrite(this)) { 5 Toast.makeText(this, "打开热点需要启用“修改系统设置”权限,请手动开启", Toast.LENGTH_SHORT).show(); 6 7 //清单文件中需要android.permission.WRITE_SETTINGS,否则打开的设置页面开关是灰色的 8 Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS); 9 intent.setData(Uri.parse("package:" + this.getPackageName())); 10 //判断系统能否处理,部分ROM无此action,如魅族Flyme 11 if (intent.resolveActivity(getPackageManager()) != null) { 12 startActivity(intent); 13 } else { 14 //打开应用详情页 15 intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 16 intent.setData(Uri.parse("package:" + this.getPackageName())); 17 if (intent.resolveActivity(getPackageManager()) != null) { 18 startActivity(intent); 19 } 20 } 21 } else { 22 result = true; 23 } 24 } else { 25 result = true; 26 } 27 return result; 28 }
项目源码:https://github.com/fly263/APDemo
PS:布局文件使用了ConstraintLayout,如果没有安装,需要在Android Studio中点击File-Settings-System Settings-Android SDK,切换到SDK Tools标签页,下载ConstraintLayout for Android、Solver for ConstraintLayout两个支持库