WiFiAp探究实录--功能实现与源码分析

Android虐我千百遍,我待Android如初恋。

——————编辑于2017-08-02——————— wifi热点说的是wifiAp相关,所以如果源码开发的话,这个WifiAp算是一个搜索代码的关键字,含义是Wifi Access point,wifi接入点。所以下文中的wifi热点统一用WifiAp代替

  1. wifiAp打开方式:设置->更多->移动网络共享->便携式wlan热点。
  2. wifiAp打开条件:任何情况下均可。只是有内网外网之分。造成内外网之分的影响条件有sim卡和wifi的连接状态。注意,这里所说的是wifi的连接状态,而不是wifi热点的连接状态
  3. wifiAp开发中用处:可用于局域网内的通信
  4. wifiAp开发中相关问题:
    • 第一,跟WiFiAp相关的有wifiAp的网关Ip,以及ip范围
    • 第二,wifiAp的config:包括初始创建时的defaultvalue:名字(ssid)和密码(preSharedKey),以及后续修改config
    • 第三,wifiAp的enable状态
    • 第四,wifiAp的设备连接列表:一是保证能获取到当前连接设备列表,二是当有设备连接时能够实时的更新
    • 第五,wifiAp的连接限制:包括最大连接数限制,以及黑白名单机制

先就wifiAp的ip进行说明:

既然是要局域网内通信,那就要用到ip地址和端口号了(关于端口号的设定属于开发通信时的问题,是用户自定义的可变的,在我的程序里我规定端口号为80。而ip地址是有规定的,所以只讲关于ip的问题)。ip地址是在Android源码中规定好的,平常所买的路由器的ip地址一般都是192.168.0.1。Android源码中所规定的手机的wifiAp的ip地址为192.168.43.1,这个代码中可以看到

  • 创建wifiAp时的ip:在创建wifiAp时相当于网关ip,/frameworks/opt/net/wifi/service/java/com/android/server/wifi/SoftApManager.java中开启wifiAp时规定了ip地址(Android7.0中在该文件中,如果是其他Android系统可以在WifiStateMachine),具体方法为startThering中
  • wifiAp的ip地址的分配区间:在/frameworks/base/services/core/java/com/android/server/connectivity/Tethering.java中有规定 //usb网络共享的网关是192.168.42.1 // USB is 192.168.42.1 and 255.255.255.0 //wifi便携式热点网关是192.168.43.1 // Wifi is 192.168.43.1 and 255.255.255.0 //蓝牙共享(个人局域网)限制5个 // BT is limited to max default of 5 connections. 192.168.44.1 to 192.168.48.1 // with 255.255.255.0 private String[] mDhcpRange; private static final int TETHER_RETRY_UPSTREAM_LIMIT = 5; private static final String[] DHCP_DEFAULT_RANGE = { "192.168.42.2", "192.168.42.254", "192.168.43.2", "192.168.43.254", "192.168.44.2", "192.168.44.254", "192.168.45.2", "192.168.45.254", "192.168.46.2", "192.168.46.254", "192.168.47.2", "192.168.47.254", "192.168.48.2", "192.168.48.254", }

——————编辑于2017-08-03———————

WifiAp的config分析:

默认的config:

  • 代码位置 在恢复出厂设置后打开WifiAp,初始的wifiAp的名称是一定的,但是wifiAp的密码是随机,这个可以自行测试,实现代码位于一个叫做 WifiApConfigStore.java的文件中,文件路径为/frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiApConfigStore.java
  • 代码实现
  /**   * 构建一个默认的wifiAp,加密类型是WPA2,密码随机   *    * We are changing the Wifi Ap configuration storage from secure settings to a   * flat file accessible only by the system. A WPA2 based default configuration   * will keep the device secure after the update.   */   private WifiConfiguration getDefaultApConfiguration() {         WifiConfiguration config = new WifiConfiguration();         //wifiAp的ssid         config.SSID = mContext.getResources().getString(                 R.string.wifi_tether_configure_ssid_default);           //wifiAp的加密方式         config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);         //随机生成uuid         String randomUUID = UUID.randomUUID().toString();         //first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx         config.preSharedKey = randomUUID.substring(0, 8) + randomUUID.substring(9, 13);         return config;     } }
 

修改wifiAp的config配置

如果想要修改wifiAp的config配置需要注意,在修改config时,config会直接设置下去,但是并不会立即生效,必须要重启wifiAp之后才有效。这个可以先拿自己的手机演示确认。

  • 首先获取到wifiManager对象
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
  • 然后获取到config对象
  WifiConfiguration config = wifiManager.getWifiApConfiguration();
  • 有了config之后,就可以对参数进行设置了,比如设置用户名和密码
if (config != null) {
           config.SSID = mWifiInfoBean.getApSsid();
           config.preSharedKey = mWifiInfoBean.getPskKey();
   }

当然,你还可以做其他设置,具体的可以参考WifiConfiguration.java源码 到这一步,对于wifiAp的用户名和密码已经设置成功了,此时若手动重启wifiAp后config即可生效。如果你想要立刻生效,那就必须要重启wifiAp了。

  • 重启wifiAp,将所设置的config设置进去,并重启热点,流程是首先判断WiFi热点是否处于开启状态,如果是,则重启wifiAp。如果当前wifiAp不处于开启状态,则只需要把config设置下去即可
if (wifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_ENABLED) {
   //如果wifiAp处于开启状态,则关闭并重启
  wifiManager.setWifiApEnabled(null, false);
  return wifiManager.setWifiApEnabled(config, true);
} else {
   //如果wifiAp不处于开启状态,则只需要将config设置下去
    return wifiManager.setWifiApConfiguration(config);
 }

——————编辑于2017-08-04———————

开启/关闭WifiAp热点状态

在对wifiAp进行config修改时已经涉及到了对于wifiAp的开和关,在进行wifiAp进行开关的过程中需要传入config,如果传入的为null,则沿用上一次的 config,如果上一次的config不存在,则会去加载默认的config。当开启wifiAp时会先去判断wifi的状态,如果wifi处于开启状态则需要关闭WiFi状态,然后开启wifiAp。

  • 获取WiFimanager对象(参考上文)
  • 判断目前wififAp的开关状态,如果处于开启状态,则不进行任何操作。当然,如果你想自己设置config,那么就照着上文中配置config的步骤来 /** * if wifiap is enabled */ if (wifiManager.isWifiApEnabled()) { return true; }
  • 判断wifi的状态,如果处于开启状态,则关闭wifi状态 /** * Disable Wifi if enabling tethering */ int wifiState = wifiManager.getWifiState(); if (enable && ((wifiState == WifiManager.WIFI_STATE_ENABLING) || (wifiState == WifiManager.WIFI_STATE_ENABLED))) { wifiManager.setWifiEnabled(false); }
  • 接下来就可以调用wifiAp开启的方法了 wifiManager.setWifiApEnabled(null, enable);

已连接设备列表

读取wifiAp的已连接设备列表

这个很纠结,关于wifiAp的这些东西不存在什么jni接口,只能是通过读文件或者是监听广播来和底层通信。Android源码中提供了一个读取已连接设别列表的方法——读取特定文件“/proc/net/arp” 来获取已连接设备信息。 代码如下:

  File file = new File("/proc/net/arp");
     try {
       reader = new BufferedReader(new FileReader(file));
       String line;
       while ((line = reader.readLine()) != null) {
          String[] tokens = line.split("[ ]+");
          if (tokens.length < 6 || tokens[3].length() < 8) {
               continue;
           }
           //角标为3是mac地址,角标为0是ip地址 ,设备名是根据mac来获取
       }
   } catch (IOException e) {
      e.printStackTrace();
   } finally {
      if (reader != null) {
        try {
          reader.close();
       } catch (IOException e) {
          e.printStackTrace();
       }
    }

该文件包含的数据有sscanf(buf, “%s 0x%x 0x%x %s %s %s\n”, ip, &h_type, &flag, hw_addr, mask, dev ) 也就是说tokens 长度为6,可以看到包含已连接设备的ip和addr,但是设备名却没有说明,这个需要自己根据mac地址来获取对应的厂商和设备名。当然,方案提供商也许自己会集成这部分工作,所以具体情况具体考虑

设备列表实时更新

这个目前Android源码中也没提供任何解决方案,如果是系统开发的,可以在设备连接时加个广播,当有设备连接成功后发送广播,然后上层应用可以通过监听广播来实时更新设备列表。

设备连接限制

设备连接限制包括最大连接数,以及黑白名单。我只能说目前上层是没有直接可以调用的接口来实现。目前大致只能通过调用adb shell命令来实现了。(如果平台支持的话)

——————编辑于2017-08-09———————

源码探究

文中上半部分介绍了wifiAp相关的功能开发,接下来就从源码的角度出发,分析为什么我们可以用这种方式来实现wifiAp的功能 关于Android的WiFiAp的源码研究基于andriod7.1.1

WifiAp始于UI

wifiAp代码处于package/apps/Settings中,wifiAp开启的入口在 /packages/apps/Settings/src/com/android/settings/TetherSettings.java 中的onCreateDialog。在TetherSettings中包括蓝牙热点,WiFi热点,usb热点的相关问题。

  @Override
    public Dialog onCreateDialog(int id) {
        if (id == DIALOG_AP_SETTINGS) {
            final Activity activity = getActivity();
            //开启WiFiAp的设置
            mDialog = new WifiApDialog(activity, this, mWifiConfig);
            return mDialog;
        }
        return null;
    }

wifiAp的设置弹出框为WifiAPDialog,目录为: /packages/apps/Settings/src/com/android/settings/wifi/WifiApDialog.java WiFiAp的设置框所加载的xml布局文件为wifi_ap_dialog.xml。wifiAp的设置包括四部分:

  1. wifi_ssid:wifiAp的名称:输入长度最大限制为32个字符
  2. wifi_security:wifiAp的安全性:提供spinner列表进行选择,可选项如下
  1. wifi_password:wifiAp的密码,最大长度限制为63
  2. wifi_ap_band_config:wifiAp的Ap频段 频段有spinner列表可选,频段可选为2.4g和5g。该array是在WifiApDialog代码中添加的

所以,如果想要修改wifiApDialog布局相关的可以修改wifi_ap_dialog.xml布局文件。由布局文件也可以看出,Android源码上层中,wifiAp相关的配置 WifiConfiguration包括四部分,用户名、密码 、安全性、频段。

WifiConfiguration配置

在创建WifiApDialog时会传入一个WifiConfiguration对象,wifiApDialog中显示的WiFiAp信息就是从该config中获取的。在第一次开启wifiAp对象时所获取的config对像是系统默认的配置,当用户进行了修改之后wifiAp的config会被保存到手机,等下次获取到的就是修改后的config。

获取wifiConfig

先来找到创建dialog的地方来看一下config对象,来看一下代码是如何在第一次使用时获取系统默认以及在修改后如何获取用户修改的config的:

wifiAp的config对象是在TetherSettings的initWifiTethering的方法中获取的。可以看到,mWifiConfig对像通过wifiManager调用getWifiApConfiguration()来获取,当然,源码设计有套路,manager只是client的一个中转站,真正的还是找的是service,所以找到WifiServiceImpl.java,紧接着是WifiStateMachine.java,一级一级的都是调用,最终的实现在WifiApConfigStore.java文件中,该文件包含了系统默认的config以及用户设置的config.

从这个代码可以看出两个信息

  • 第一,wifiAp的config信息存储在文件中
  • 第二,优先加载文件中存储的config信息。其中loadApConfiguration用于从文件中加载wifiAp的配置信息,如果所加载的config为null—-即表示用户未对wifiAp进行过信息设置,则会去调用getDefaultApConfiguration来获取系统的默认设置,并且将获取到的config写入到存储wifiAp的config的文件

总结来说就是当wifiManager想要获取config时,会先加载文件中所保存的config信息,如果config信息从未进行过保存,则获取默认的config,并且将config写入到文件中去。 config文件保存目录在wifiApConfigStore中已经声明了,位于data目录下:

设置wifiConfig

WifiApDialog弹窗可以修改WiFi的配置信息,按下确定按钮即可保存,接下来看一下对config的保存设置。 对于dialog的确认按钮的点击事件是在TetherSettings.java中处理的

这段代码做了以下操作

  • 获取到dialog中填写的用户名、密码、加密方式、频段这些WiFiap的config: mDialog.getConfig()
  • 如果获取到的config不为null,则将wifiAp的config保存起来: mWifiManager.setWifiApConfiguration(mWifiConfig),和get时类似,该方法是一路往下调用 WifiManager->WifiServiceImpl->WifiStateMachine->WifiApConfigStore,最终的实现就是在WifiApConfigStore中进行将config写入到文件。config是要下一次开启wifiAp时才会生效,所以此时如果wifiAp处于开启状态,则关闭wifiAp。注意,从这里也可以看到,源码的实现是在修改wifiAp的config之后,会将ap关闭,并不会自动重启。在这里关闭wifiAp调用的是ConnectivityManager的实例方法:mCm.stopTethering(int type),该方法经过ConnectivityManager->ConnectivityService->Tethering.java,最终是在Tethering中的stopTethering进行实现

基本上config的设置和获取就这些了。大致分析完成之后,也可以看到WifiAP相关的类主要有这么几个

  1. WifiApDialog.java:用户交互界面,直观呈现出wifiAp的配置信息,提供用户修改config的ui交互,继承自AlertDialog,在构造该dialog对象时会传入 DialogInterface.OnClickListener和WifiConfiguration,所以也可以看出按钮点击事件的处理以及所显示的config内容信息都是在创建dialog时获取的,所以总结下来,该类其实就做了两件事
    • 把所获取的config加载出来
    • 提供编辑框供user编辑

    其他对于config的read&write一律不进行处理。代码目录为: /packages/apps/Settings/src/com/android/settings/wifi/

  2. TetherSettings.java:用户交互界面,呈现手机所支持的便携式热点的开关交互,代码目录为: /packages/apps/Settings/src/com/android/settings/TetherSettings.java
  3. Tethering.java:逻辑实现类,该类中拥有很多业务处理逻辑来支持Android设备作为BT\USB\WIFI作为网关,即设备作为便携式热点代码的业务逻辑实现。该类中包含网络共享和便携式热点信息,即
    • bluetooth_tethering:蓝牙网络共享,涉及到BluetoothPan协议
    • usb_tethering:usb网络共享,涉及到设备连接usb时状态切换,即是否是充当大容量存储设备
    • wifiAp便携式热点

    代码中对这三种模式的开关状态进行了监听以及更新。代码目录为: /frameworks/base/services/core/java/com/android/server/connectivity/Tethering.java

  4. WifiManager.java :该类提供了管理wifi连接的主要的api接口,这里所说的wifi连接包括WiFiAp和WiFi。三方应用开发者在对wifiap进行相关的操作时可以调用wifiManager类下的接口。developer需要注意的是在获取wifiManager对象时必是要应用程序的context,以防止memory leaks内存泄漏。代码目录为: /frameworks/base/wifi/java/android/net/wifi/WifiManager.java .wifimanager相关的有以下几种情况
    • list of configured network:已经配置过的网络列表,即手机中以保存的 WiFi列表,对已经配置过的wifi可以进行增删改查的操作viewed、update、modify
    • current active wifi:当前正在运行的WiFi,即可用WiFi列表。列表中的wifi接入点access point 可以连接或者是断开连接
    • result of access point scans:wifi接入点扫描结果,包含足够的信息来决定连接哪一个WiFi热点
    • 定义了当wifi状态发生改变时所要发送的广播
  5. WifiServiceImpl.java :作为一个binder代理形式的存在,衔接binder的client和server,主要是中间人的作用,该类不对三方应用开发者开放,不存在sdk中。代码目录为: /frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiServiceImpl.java
  6. WifiStateMachine.java :顾名思义,状态机,用来监测WiFi的各种连接状态。代码目录为: /frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
  7. WifiApConfigStore.java :这个也很显然,用于wifiAp的config信息的存取即 reading&writing,大部分的代码在文中已经分析过,所以不再分析。代码目录为: /frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiApConfigStore.java

wifiAp设备连接

——————编辑于2017-08-16——————— 隔了这么多天,终于有时间更新了,在csdn快两年时间了,一直坚持着,不幸的是我不知道以后还会不会更新csdn,也许以后的文章会出现在别处…比如公众号 wifi设备连接有一个息息相关的类NativeDaemonConnector.java(wifiAp连接源码分析会更新在wifiAp打开源码分析之下)

——————编辑于2017-08-18———————

wifiAp打开源码流程

先大致说一下追的流程:如下,

  1. WifiManager.java中setWifiApEnabled调用service方法
  2. WifiServiceImpl.java中setWifiApEnabled借助controller发送message,msg.what=com.android.server.wifi.WifiController.CMD_SET_AP;
  3. WifiController.java中ApStaDisabledState的processMessage去处理CMD_SET_AP的msg,并触发mWifiStateMachine.setHostApRunning
  4. WifiStateMachine.java.java中发送msg,msg.what=CMD_START_AP,并在该类中的SoftApState的enter的方法中处理msg:调用 mSoftApManager.start(config);
  5. SoftApManager.java中IdleState的processmessage处理,调用startSoftAp、紧接着调用 mNmService.startAccessPoint
  6. NetworkManagementService.java中 executeOrLogWithMessage执行开启wifiap的命令

WifiManager

由上文可知,WifiManager是Android源码提供给应用开发者使用的,提供API接口。如果上层应用想要打开wifiAp,那么就需要调用wifiManager的api—–>setWifiApEnabled(),那么该方法具体做了什么呢??

 /**
    *利用传入的config开启接入点即WiFiap.如果无线已经处于ap模式,那么就更新         
    *该config,开启ap模式,禁用sta模式 
    *该方法对于三方应用时hide的,属于系统api
     */
    @SystemApi
    public boolean setWifiApEnabled(WifiConfiguration wifiConfig, 
       boolean enabled) {
        try {
            mService.setWifiApEnabled(wifiConfig, enabled);
            return true;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

既然是走的service,那就找到service

WifiServiceImpl

 IWifiManager mService;

可以看到这里用到了binder机制,service中方法实际实现是在继承字WifiManager.Stub的类中,所以找到所需要的类:

public class WifiServiceImpl extends IWifiManager.Stub 

也就是说service所对应的代理类为WifiServiceImpl,所以去看该类中的具体方法实现

public void setWifiApEnabled(WifiConfiguration wifiConfig,  
   boolean enabled) {
     //检查是否有android.Manifest.permission.CHANGE_WIFI_STATE权限
        enforceChangePermission();
     //检查是否有android.Manifest.permission.TETHER_PRIVILEGED权限     
       ConnectivityManager.enforceTetherChangePermission(mContext);
       //判断是否允许修改
        if (mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING)) {
            throw new SecurityException("DISALLOW_CONFIG_TETHERING is enabled for this user.");
        }
        // null wifiConfig is a meaningful input for CMD_SET_AP
        //当config为null时,wifiAp会使用上一次ordefault的config,所以
        //config为null是有意义的,isValid是根据config中传入的wifiAp的加
        //密方式来进行判断是否有效的。即如果要配置config的加密方式,那么一
        //定要配置有效,否则无法开启ap
        if (wifiConfig == null || isValid(wifiConfig)) {
        //发送message给mWifiController
            mWifiController.obtainMessage(CMD_SET_AP, enabled ? 1 : 0, 0, wifiConfig).sendToTarget();
        } else {
            Slog.e(TAG, "Invalid WifiConfiguration");
        }
    }

先是进行一系列的权限判断,在允许的条件下发送msg,可以看到,所发送的message携带的信息有:

  • msg.what : CMD_SET_AP
  • msg.arg1:如果是开启ap则为1,如果是关闭ap则为0
  • msg.arg2:传入为0
  • msg.obj:WifiConfiguration对象

到这里,WifiServiceImpl的任务就完成了,接下里就是WifiController来处理了

WifiController

WifiController继承与StateMachine状态机,用来管理各种操作(airplane,WiFi hotspot)在wifiStateMachine中的on/off状态。 既然是状态机,那么会有一个特点,一旦注册了状态处理,那么就会按照所添加的状态类去顺序执行。 在StateMachine中有一个方法,叫做addState,用于添加状态:

//添加一个state
 public void addState(State state) {}
 ........
 //添加一个state,并且是从fromState执行过后,再执行toState
public void addState(State fromState, State toState) {}

状态机默认的是线性模型,即按照add(State)的顺序执行,但如果使用了 addState(fromState, toState),那么就相当与指明了状态机的执行顺序。 关于状态机的介绍就是后话了,接下来看接受到msg后的wifiController的处理:wifiController总结起来就做了两件事

  • 保存wifiAp的开关状态:mSettingsStore.setWifiSavedState(WifiSettingsStore.WIFI_DISABLED); 该代码用于向settingsdb中存储wifiAp的状态,所存储的字段为: Settings.Global.WIFI_SAVED_STATE
  • 并执行wifiAp的开关操作:mWifiStateMachine.setHostApRunning((WifiConfiguration) msg.obj, true); 该代码用于进行wifiAp的开关操作,调用的是WifiStateMachine的setHostApRunning方法,并将wifiConfiguration传给WifiStateMachine,当然这里如果是要关闭ap传入的boolean值为false

所以可以看到wifiController只是起一个当状态改变时传递msg的作用,接下来进入到WifiStateMachine中:

WifiStateMachine

WifiStateMachine继承自StateMachine,该类用于跟踪WiFi的连接状态,所有的事件处理都在这里,所有连接状态的改变也是在这里进行的初始化。Android7.1.1所支持的WiFi操作包括三种:

  1. Clients:设备作为客户端连接其他wifi
  2. p2p:wifi直连
  3. softAp: wifi热点

目前WiFiStateMachine用于处理wifi作为Clients以及WiFi作为softAp,而p2p则交由WifiP2pService进行处理。 接下来直接进去到setHostApRunning方法:

public void setHostApRunning(WifiConfiguration wifiConfig, boolean enable) {
    if (enable) {
         sendMessage(CMD_START_AP, wifiConfig);
    } else {
        sendMessage(CMD_STOP_AP);
    }
 }

很明显,该方法也是sendmsg,只不过这个msg是在WifiStateMachine这个类中自己处理的,可以看到从此时开始,start/stop wifiAp的msg.what开始不同,而不是仅仅依靠boolean值来区分,因为如果是start的话,需要进行两步的处理,包括

  1. 加载softAp的hal:setupDriverForSoftAp
  2. 在保证hal加载完成的情况下将要进行的操作以及config传递给softAp,让其开始wifiAp: mSoftApManager.start(config)

而如果是stop的话,则只需要将wifiAp关闭即可,即调用 mSoftApManager.stop()。接下来就是SoftApManager中的start和stop了

SoftApManager

start和stop对比分析

    /**
     * 利用传入的config对象开启ap
     * @param config AP configuration
    */
 public void start(WifiConfiguration config) {
     mStateMachine.sendMessage(SoftApStateMachine.CMD_START, config);
  }

    /**
     * 停止ap
     */
  public void stop() {
     mStateMachine.sendMessage(SoftApStateMachine.CMD_STOP);
  }

可以看到SoftApManager的start和stop只是send了msg

startAp

start时send的msg为

  1. msg.what : SoftApStateMachine.CMD_START
  2. msg.arg1:config对象,这个值就是从wifiApDialog传过来的,在传递过程中如果有为null的情况就加载默认的或者文件中存储的,具体可参见上文。但是从传到这里开始,config就不会再做任何的修改,所以如果config有为null的情况,则返回

在接收到CMD_START这个msg之后,SoftApManager最终会在startSoftAp方法中进行处理:

 private int startSoftAp(WifiConfiguration config) {
    if (config == null) {
       Log.e(TAG, "Unable to start soft AP without configuration");
       return ERROR_GENERIC;
     }
     WifiConfiguration localConfig = new WifiConfiguration(config);
     int result = ApConfigUtil.updateApChannelConfig(
           mWifiNative, mCountryCode, mAllowed2GChannels, localConfig);
     if (result != SUCCESS) {
        Log.e(TAG, "Failed to update AP band and channel");
        return result;
     }
        /* 创建国家代码 */
     if (mCountryCode != null) {
        /* 当ap的频段被设置成5G时,必须设置contry code*/
        if (!mWifiNative.setCountryCodeHal(mCountryCode.toUpperCase(Locale.ROOT))&& config.apBand == WifiConfiguration.AP_BAND_5GHZ) {
           Log.e(TAG, "Failed to set country code, required for setting up " + "soft ap in 5GHz");
           return ERROR_GENERIC;
          }
       }
        /* 当wifiAp的驱动层配置好之后就可以创建wifiAp了*/
        try {
            mNmService.startAccessPoint(localConfig, mInterfaceName);
       } catch (Exception e) {
            Log.e(TAG, "Exception in starting soft AP: " + e);
            return ERROR_GENERIC;
        }

       Log.d(TAG, "Soft AP is started");

        return SUCCESS;
    }

开启wifiAp接着会去调用 mNmService.startAccessPoint:方法的实现在NetworkManagementService.java中,内容如下

@Override
public void startAccessPoint(WifiConfiguration wifiConfig, String wlanIface) {        
   mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
   Object[] args;
   String logMsg = "startAccessPoint Error setting up softap";
   try {
       if (wifiConfig == null) {
          args = new Object[] {"set", wlanIface};
       } else {
          args = new Object[] {"set", wlanIface, wifiConfig.SSID,
                   "broadcast",Integer.toString(wifiConfig.apChannel),getSecurityType(wifiConfig), new SensitiveArg(wifiConfig.preSharedKey)};
       }
       //设置wifiConfig
       executeOrLogWithMessage(SOFT_AP_COMMAND, args, NetdResponseCode.SoftapStatusResult,SOFT_AP_COMMAND_SUCCESS, logMsg);
       logMsg = "startAccessPoint Error starting softap";
       args = new Object[] {"startap"};
       //startap开启ap
       executeOrLogWithMessage(SOFT_AP_COMMAND, args, NetdResponseCode.SoftapStatusResult,SOFT_AP_COMMAND_SUCCESS, logMsg);
        } catch (NativeDaemonConnectorException e) {
            throw e.rethrowAsParcelableException();
        }
    }

该方法首先是拼接command字符串,并调用方法去执行命令,executeOrLogWithMessage方法是NetworkManagementService的private方法,其实就是利用NativeDaemonConnector这个runnable对象来执行command命令

private void executeOrLogWithMessage(String command, Object[] args,int expectedResponseCode, String expectedResponseMessage, String logMsg) throws NativeDaemonConnectorException {
 //返回执行结果
   NativeDaemonEvent event = mConnector.execute(command, args);
     if (event.getCode() != expectedResponseCode || !event.getMessage().equals(expectedResponseMessage)) {
        //当执行失败时
        Log.e(TAG, logMsg + ": event = " + event);
     }
}

可以看到,构造了个NativeDaemonConnector–mConnector用于执行命令,先看命令执行的传入参数arguments:

  1. String command : SOFT_AP_COMMAND = “softap” :要执行的command
  2. Object[] args:command的附加参数。这个命令稍微有一点复杂,会根据所传入的config是否为null而有所不同: if (wifiConfig == null) { args = new Object[] {"set", wlanIface}; } else { args = new Object[] {"set", wlanIface, wifiConfig.SSID,"broadcast", Integer.toString(wifiConfig.apChannel), getSecurityType(wifiConfig), new SensitiveArg(wifiConfig.preSharedKey)}; } 第一个问题,wlanIface的取值: 首先这里的wlanIface是在构建softApManager对象时借助WifiNative对象获取的,WifiNative中获取wlanIface的地方位于: private static WifiNative wlanNativeInterface = new WifiNative(SystemProperties.get("wifi.interface", "wlan0"), true); 很显然,wlanIface的值取决于属性字段wifi.interface的对应值,可以看到,如果未定义即默认取值为wlan0,源码中设置的也是wlan0. 第二个问题,preSharedKey:指的是wifiAp的密码,之所以列出来是因为源码用一层类SensitiveArg将他包装了起来,该类的作用就是告诉开发者:该字段属于敏感内容,禁止使用log打印出来,该类所重写的toString方法也是将构造时传入的obj对象转换成string输出。 第三个问题,apChannel:定义如下 /** * The channel which AP resides on,currently, US only * 2G 1-11 * 5G 36,40,44,48,149,153,157,161,165 * 0 - find a random available channel according to the apBand * @hide */ public int apChannel = 0; channel是根据wifiConfig所配置的频段(2.4g或者是5.0,默认是2.4g)来决定的 第四个问题,securitytype:wifiAp的安全保密类型:

经过对以上问题的分析,可以看出args的取值如下: if(config == null){ args = new Object[] {"set","wlan0"}; }else{ //eg:args = new Object[] {"set","wlan0","MyWifiAp","broadcast","0", //"open","12345678"} args = new Object[] {"set","wlan0","YourNetwork name","broadcast","your network apchannel","your network security type","your network password"} }

  1. int expectedResponseCode:执行结果期望值(int),即执行成功时的响应。NetdResponseCode.SoftapStatusResult = 214:
  2. String expectedResponseMessage :执行结果期望值(string类型) SOFT_AP_COMMAND_SUCCESS = “Ok”
  3. String logMsg:如果命令执行失败,会打印该log:String logMsg = "startAccessPoint Error setting up softap";

接下来看一下execute命令的对象—–mConnector对象: 在NetworkManagementService的构造时会构造mConnector对象

mConnector = new NativeDaemonConnector(new NetdCallbackReceiver(), socket, 10, NETD_TAG,160,wl,FgThread.get().getLooper());

传入参数有7个

  • INativeDaemonConnectorCallbacks callbacks执行结果回调
  • String socket:关于命令的执行都是借助socket的输出流进行处理的,在创建 networkManagerService时会声明,值为:String NETD_SERVICE_NAME = "netd";
  • int responseQueueSize:响应队列的大小(message queue,looper)
  • String logTag
  • int maxLogSize
  • PowerManager.WakeLock wl:在这里,传入的值为null,因为不再需要唤醒锁。
  • Looper looper:使消息借助handler循环处理

接下里就是execute方法,最终会是去调用NativeDeamonConnector中的 executeForList(long timeoutMs, String cmd, Object… args)方法进行处理,如下,可以看到executeForList方法会返回一个event的列表,而execute方法只返回列表的第一个event元素

public NativeDaemonEvent[] executeForList(long timeout, String cmd, Object... args) throws NativeDaemonConnectorException {
   //记录下开始execute的时间
    final long startTime = SystemClock.elapsedRealtime();
    //初始化一个NativeDeamonEvent列表对象
    final ArrayList<NativeDaemonEvent> events = Lists.newArrayList();
    //初始化两个sb
    final StringBuilder rawBuilder = new StringBuilder();
    final StringBuilder logBuilder = new StringBuilder();
    //序列号,因为消息队列最大允许有10个,该序列号是在当前序列号的基础上加1
    final int sequenceNumber = mSequenceNumber.incrementAndGet();
    //makeCommand用于将sequenceNumber、cmd
    //args拼接到rawBuilder和logBuilder(如果arg是sensitivearg则用别的
    //字符串代替),首先会判断command是否符合要求,第一command不能
    //有"\0",第二command必须要与argument分开处理即避开args,即cmd不能有
    //" "
    makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args);
    final String rawCmd = rawBuilder.toString();
    final String logCmd = logBuilder.toString();
    log("SND -> {" + logCmd + "}");
    synchronized (mDaemonLock) {
      //根据上文中所述,wifiap上层与底层基本上是命令或者是文件存储的形式进行交互
      //所以在这里借助传入的socket获取到os
      if (mOutputStream == null) {
      //os为null时抛出异常
     throw new NativeDaemonConnectorException("missing output stream");
       } else {
           try {                   
           //开始往输出流中写cmd,编码格式为UTF_8
          mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8));
           } catch (IOException e) {
           //在写cmd时发生io异常
               throw new NativeDaemonConnectorException("problem sending command", e);
            }
        }
      }
      NativeDaemonEvent event = null;
     do {
        event = mResponseQueue.remove(sequenceNumber, timeout, logCmd);
        if (event == null) {
         //在经过了DEFAULT_TIMEOUT:1分钟之后,处理仍未成功,则抛出timeout异常
            loge("timed-out waiting for response to " + logCmd);
            throw new NativeDaemonTimeoutException(logCmd, event);
        }
        log("RMV <- {" + event + "}");
        //将处理成功的event添加到arraylist中
        events.add(event);
        //while的判断条件是event处理之后的返回码处于[100,200)之间
     } while (event.isClassContinue());
     //记录下cmd处理结束的时间
     final long endTime = SystemClock.elapsedRealtime();
     //WARN_EXECUTE_DELAY_MS为5秒,如果cmd的处理时间超过5秒则发出处理时间过长
     //的log警告
     if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {
        loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");
      }
      //如果event的返回码取值返回为[500,600),则抛出参数请求异常,即客户端异常
     if (event.isClassClientError()) {
        throw new NativeDaemonArgumentException(logCmd, event);
     }
     //如果event的返回码取值为[400,500),则失败,服务器处理异常
     if (event.isClassServerError()) {
        throw new NativeDaemonFailureException(logCmd, event);
     }
     //只有event返回码在[200,300)之间,才表示请求成功
     return events.toArray(new NativeDaemonEvent[events.size()]);
 }

stopAp

而stop时send的msg的信息为

  • msg.what : SoftApStateMachine.CMD_STOP

对于msg的处理也是在SoftApManager中,

private void stopSoftAp() {
     try {
          mNmService.stopAccessPoint(mInterfaceName);
  } catch (Exception e) {
            Log.e(TAG, "Exception in stopping soft AP: " + e);
            return;
 }
        Log.d(TAG, "Soft AP is stopped");
  }

同样,也是调用NetworkManagerMentService中的方法进行处理,分析基本类似startAccessPonint,传入的cmd与start一致,只不过arguments不同,stop时传入的args为:

Object[] args = {"stopap"};

请求错误时的logmsg为:

 String logMsg = "stopAccessPoint Error stopping softap";

执行命令后要去重新加载wififirmware,即切换了wifi的模式到sta.(wifi总共有三种模式ap,sta,p2p)

             $(function () {                 $('pre.prettyprint code').each(function () {                     var lines = $(this).text().split('\n').length;                     var $numbering = $('<ul/>').addClass('pre-numbering').hide();                     $(this).addClass('has-numbering').parent().append($numbering);                     for (i = 1; i <= lines; i++) {                         $numbering.append($('<li/>').text(i));                     };                     $numbering.fadeIn(1700);                 });             });         

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员宝库

PHP 面试知识梳理

算法与数据结构 BTree和B+tree BTree B树是为了磁盘或者其他存储设备而设计的一种多叉平衡查找树,相对于二叉树,B树的每个内节点有多个分支,即多叉...

42512
来自专栏Seebug漏洞平台

D-Link DIR-605L 拒绝服务错误报告 (CVE-2017-9675)

原文:http://hypercrux.com/bug-report/2017/06/19/DIR605L-DoS-BugReport/ 译者:Serene ...

3796
来自专栏王亚昌的专栏

Linux进程同步机制-Futex

引子 在编译2.6内核的时候,你会在编译选项中看到[*] Enable futex support这一项,上网查,有的资料会告诉你"不选这个内核不一定能正确的运...

8281
来自专栏信安之路

ring3层恶意代码实例汇总

之前一期我们学习了 IAT 的基本结构,相信大家对 C++ 有了一个基本的认识,这一期放点干货,我把 ring3 层恶意代码常用的编程技术给大家整理了一下,所有...

1380
来自专栏orientlu

FreeRTOS 软定时器实现

考虑平台硬件定时器个数限制的, FreeRTOS 通过一个 Daemon 任务(启动调度器时自动创建)管理软定时器, 满足用户定时需求. Daemon 任务会在...

1682
来自专栏Seebug漏洞平台

Mirai源码分析

1. 背景概述 最近的德国断网事件让Mirai恶意程序再次跃入公众的视线,相对而言,目前的IoT领域对于恶意程序还是一片蓝海,因此吸引了越来越多的人开始涉足这趟...

4917
来自专栏Java Web

Java I/O不迷茫,一文为你导航!

学习过计算机相关课程的童鞋应该都知道,I/O 即输入Input/ 输出Output的缩写,最容易让人联想到的就是屏幕这样的输出设备以及键盘鼠标这一类的输入设备,...

1182
来自专栏腾讯Bugly的专栏

《Android 创建线程源码与OOM分析》

| 导语 企鹅FM近几个版本的外网Crash出现很多OutOfMemory(以下简称OOM)问题,Crash的堆栈都在Thread::start方法上。该文详细...

1K5
来自专栏蓝天

Linux系统面面观 PROC文件系统详细介绍

什么是proc文件系统? proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。用户和应用...

1182
来自专栏Golang语言社区

Golang工程经验(上)

作为一个C/C++的开发者而言,开启Golang语言开发之路是很容易的,从语法、语义上的理解到工程开发,都能够快速熟悉起来;相比C、C++,Golang语言更简...

5882

扫码关注云+社区

领取腾讯云代金券