深度剖析WiFi的SSID问题

        最近在研究字符编码问题,正好想到WiFi SSID的问题,今天就来说说这个SSID的编码和显示问题,尤其是大家一直关心的各种操作系统对中文SSID的显示乱码问题。

        首先我们先简单聊下编码,编码最基本的就是UTF-8,UTF-16, UTF-32这些都属于Unicode的范畴,也就是国际编码可以针对全世界任何一个国家的文字和符号进行统一格式编码,具体格式就不再赘述,自行百度。而我国针对汉字和相关符号又有一套自己的编码,最典型的就是GB2312,这个是按照分区编码规则来实现的,具体自行百度。本文重点是研究针对多种编码的WiFi SSID系统如何处理字符编解码问题。

         IEEE802.11中针对SSID一项的描述,其实就是32个字节,也没有明确规定这个32个字节需要什么编码方式,所以理论是这32个字节我们可以放入任意值的二进制数据,但是考虑到windows/linux/mac系统的编码问题,第一首选当然是UTF-8编码这是最通用的编码,也是可以被所有的系统识别和解析,最终显示。其他如GB2312等编码也是可以的,而英文字母具备天生的优势,UTF-8编码和ASCII针对26个字符和其他符号,即十进制32-127的数值都可以直接ASCII吗显示。如果SSID内容的某个/多个的字节内容不在32-127之内,那必定是包含非英文字母和符号了,那就需要进行有条件的判断,这个非英文字符是UTF-8编码呢还是非UTF-8编码,这个关键点是解决如何解码SSID内容最关键之处,因为如果获取不到SSID非英文内容的编码方式,就无法解码,最终的结果可能是系统显示乱码,而导致用户无法连接,因为系统本身识别出错了。

        下面说说Android系统针对SSID识别问题,从Android系统的framework源代码看,其实系统默认就是把SSID的内容统一当作UTF-8编码处理了,后面当然是按照UTF-8格式去解码为UTF-16编码。如果默认SSID是UTF-8编码,那系统可以正确显示WiFi AP的SSID内容,如果是非UTF-8编码或是GB2312之类的,那就导致UI显示乱码了,这也正是很多网友疑惑的地方,包括很多Android的开发人员,那问题的本质是什么? 答案就是安卓系统没有对SSID的原始内容做编码格式判断,如果增加这一个判断的步骤,那所有问题都可以迎刃而解。这个方案是针对Android系统framework内部开发人员而言的。

        Android针对SSID的解码在frameworks/base/wifi/java/android/net/wifi/WifiSsid.java文件中的WifiSsid类成员函数toString()实现的,默认是把SSID当UTF-8编码进行解码并准换成UTF-16的,问题的关键就在这里,那这里要解决的问题就是上述的方法,也就是在转换前,增加对SSID内容的编码识别和确认而不是直接按UTF-8处理。

        既然有方案了,那采用什么样的功能函数来处理此类问题呢?有没有现成的函数/库/API供参考?还是要自己写Java代码来实现判断数据的编码方式呢? 思前顾后也没有想出方案来吧,而百度一下看到的网友的分享也有类似的实现,但是都没有一个标准的处理方法,那到底有没有呢,当然有了,而且量身定制。

        既然是要完成编码识别任务,当然是找通用的Java库,而International Components for Unicode组织发布的Java/C++类库可以很好的解决这个问题,具体介绍参考ICU官网,那安卓呢?我们讨论的是安卓系统,安卓参考API官网,打不开请自行科学上网解决。Android从6.0开始(API 23),引进的ICU的API函数,但是针对字符识别的API是隐藏的,也就是官网没有针对这类函数的解释,因为不是针对普通APP开发者开放的,而我们这里讨论的是Android系统framework开发人员,所以针对这类开发人员这个API类函数的调用属于系统级调用,拥有足够高的权限来实现这个内部编码识别和解析功能。

     Android在6.0以后引入的jar包名字为: android.icu, 而6.0以下版本需要自行集成com.ibm.icu包来实现,具体的代码在android/external/icu目录,里面有C++/Java库的源代码,这个icu就是针对字符处理的库函数,下面我们来描述如何实现。

        在上述的WifiSsid.java的补丁文件:

diff --git a/wifi/java/android/net/wifi/WifiSsid.java b/wifi/java/android/net/wifi/WifiSsid.java
index c53cd3c6454..1ae5b102a09 100644
--- a/wifi/java/android/net/wifi/WifiSsid.java
+++ b/wifi/java/android/net/wifi/WifiSsid.java
@@ -28,6 +28,8 @@ import java.nio.charset.CoderResult;
 import java.nio.charset.CodingErrorAction;
 import java.util.Locale;
 
+import android.icu.text.CharsetMatch;
+import android.icu.text.CharsetDetector;
 /**
  * Stores SSID octets and handles conversion.
  *
@@ -167,7 +169,12 @@ public class WifiSsid implements Parcelable {
         // behavior of returning empty string for this case.
         if (octets.size() <= 0 || isArrayAllZeroes(ssidBytes)) return "";
         // TODO: Handle conversion to other charsets upon failure
-        Charset charset = Charset.forName("UTF-8");
+        CharsetDetector encode_detect = new CharsetDetector();
+        CharsetMatch m;
+        encode_detect.setText(ssidBytes);
+        m =encode_detect.detect();
+        String char_encode = m.getName();
+        Charset charset = Charset.forName(char_encode);
         CharsetDecoder decoder = charset.newDecoder()
                 .onMalformedInput(CodingErrorAction.REPLACE)
                 .onUnmappableCharacter(CodingErrorAction.REPLACE);

       上述补丁是把默认的UTF-8编码取消,引入CharsetDetector()类来对原始SSID数据进行编码数据检测并得到char_encode, 再使用char_encode构造Charset类,最后实现对SSID数据的定向解码,即SSID原始数据什么编码格式,就按照检测到的格式进行解码,这样就实现了对WiFi 原始SSID的自适应解析,当然这个自适应的解析包括了所有可能的编码方式,而GB2312只是其中一个编码方式而已,这个补丁的关键解决自动判断编码方式,再按照检测出的编码方式进行解码。

        编码自适应的解码我们解决了,但是由于SSID被转换,Java类中有关SSID的所有信息都是转后的UTF-16,而中间层wpa_supplicant里面保存的SSID可是原始的数据啊,这样等于的Java层和中间协议层内容不一致,这样的最终结果可能是WiFi如果有密码的,那可能无法连接,需要对上下做对应的处理。

        那就需要对frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiConfigStore.java进行修改,这个函数涉及到Android Framework对APP层WiFi网络的新增连接和保存已连接的AP网络信息等,这里直接把补丁贴出来。

diff --git a/service/java/com/android/server/wifi/WifiConfigStore.java b/service/java/com/android/server/wifi/WifiConfigStore.java
index 25ab4493..fe810f25 100644
--- a/service/java/com/android/server/wifi/WifiConfigStore.java
+++ b/service/java/com/android/server/wifi/WifiConfigStore.java
@@ -24,6 +24,7 @@ import android.net.wifi.WifiEnterpriseConfig;
 import android.net.wifi.WifiSsid;
 import android.net.wifi.WpsInfo;
 import android.net.wifi.WpsResult;
+import android.net.wifi.ScanResult;
 import android.os.FileObserver;
 import android.os.Process;
 import android.security.Credentials;
@@ -47,6 +48,7 @@ import java.io.FileReader;
 import java.io.IOException;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
+import java.nio.charset.Charset;
 import java.security.PrivateKey;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
@@ -61,6 +63,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import android.icu.text.CharsetMatch;
+import android.icu.text.CharsetDetector;
+
 /**
  * This class provides the API's to save/load/modify network configurations from a persistent
  * config database.
@@ -170,14 +175,35 @@ public class WifiConfigStore {
         return TextUtils.join(" ", valueSet);
     }
 
+    private static Charset getSSIDCharSet(WifiConfiguration config) {
+        ArrayList scanResults = mWifiNative.getScanResults();
+        String  charsetName = "";
+        for (int i = 0; i < scanResults.size(); ++i) {
+            ScanResult result = scanResults.get(i).getScanResult();
+            if(result.BSSID.equals(config.BSSID)) {
+                byte[] raw_ssid = result.wifiSsid.getOctets();
+                CharsetDetector encode_detect = new CharsetDetector();
+                CharsetMatch m;
+                encode_detect.setText(raw_ssid);
+                m =encode_detect.detect();
+                charsetName = m.getName();  
+            }
+        }
+
+        if(i >= scanResults.size() && charsetName.isEmpty()) {
+            charsetName = "UTF-8";
+        }
+
+        return Charset.forName(charsetName);
+    }
     /*
      * Convert string to Hexadecimal before passing to wifi native layer
      * In native function "doCommand()" have trouble in converting Unicode character string to UTF8
      * conversion to hex is required because SSIDs can have space characters in them;
      * and that can confuses the supplicant because it uses space charaters as delimiters
      */
-    private static String encodeSSID(String str) {
-        return Utils.toHex(removeDoubleQuotes(str).getBytes(StandardCharsets.UTF_8));
+    private static String encodeSSID(String str, Charset charset) {
+        return Utils.toHex(removeDoubleQuotes(str).getBytes(charset));
     }
 
     // Certificate and private key management for EnterpriseConfig
@@ -609,10 +635,12 @@ public class WifiConfigStore {
             return false;
         }
         if (VDBG) localLog("saveNetwork: " + netId);
+
+        Charset charset = getSSIDCharSet(config);
         if (config.SSID != null && !mWifiNative.setNetworkVariable(
                 netId,
                 WifiConfiguration.ssidVarName,
-                encodeSSID(config.SSID))) {
+                encodeSSID(config.SSID, charset))) {
             loge("failed to set SSID: " + config.SSID);
             return false;
         }
@@ -922,8 +950,10 @@ public class WifiConfigStore {
             return false;
         }
         if (VDBG) localLog("setNetworkSSID: " + config.networkId);
+
+        Charset charset = getSSIDCharSet(config);
         if (!mWifiNative.setNetworkVariable(config.networkId, WifiConfiguration.ssidVarName,
-                encodeSSID(ssid))) {
+                encodeSSID(ssid, charset))) {
             loge("Set SSID of network in wpa_supplicant failed on " + config.networkId);
             return false;
         }

       上面的补丁显示针对即将要保存的网络,先检测原始SSID的编码方式,然后把安卓现有的数据,也就是转换后UTF-16的数据再以原始SSID编码的方式转换回去,同时转换成16进制ASCII字符方式写入到wpa_supplicant, 这样就保持了对原始SSID数据的更新,而不是未经转换或是默认UTF-8的编码方式写入wpa_supplicant, 这样wpa_supplicant中的ssid保存的内容永远都是原始数据,而我们无需为支持GB2312等中文格式修改wpa_supplicant, 这样保证以最小的改动实现目的。

        由于本人手头无设备验证测试,按照核心本质进行修改,希望有条件的开发人员能帮忙按照修改验证和测试以GB2312/GBK等非UTF-8编码的AP-SSID进行测试。

你可能感兴趣的:(C,C++,Framework)