Error[8]: Undefined offset: 1687, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

Android WiFi — Ap功能实现与源码分析

0. 前言

  WiFi热点通常说的是wifiAp相关,如果是源码开发的话,这个WifiAp算是一个搜索代码的关键字,含义是Wifi Access point,wifi接入点。所以下文中的wifi热点统一用wifiAp代替。

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

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

    // USB is  192.168.42.1 and 255.255.255.0
    // Wifi is 192.168.43.1 and 255.255.255.0
    // BT is limited to max default of 5 connections. 192.168.44.1 to 192.168.48.1
    // with 255.255.255.0

	//usb网络共享的网关是192.168.42.1 
	//wifi便携式热点网关是192.168.43.1 
	//蓝牙共享(个人局域网)限制5个 
	
    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",
    };
2. WifiAp的config分析 2.1 默认的config
 /**
  * 构建一个默认的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;
    }
}
2.2 修改wifiAp的config配置流程

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

3. 开启/关闭WifiAp热点状态流程

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

4. 已连接设备列表 4.1 读取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地址来获取对应的厂商和设备名。当然,方案提供商也许自己会集成这部分工作,所以具体情况具体考虑

4.2 设备列表实时更新

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

4.3 设备连接限制

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

5. 源码分析

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

  关于Android的WiFiAp的源码研究基于andriod 4.4

5.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的设置d出框为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列表进行选择,可选项如下
  3. wifi_password:wifiAp的密码,最大长度限制为63
  4. wifi_ap_band_config:wifiAp的Ap频段
    频段有spinner列表可选,频段可选为2.4g和5g。该array是在WifiApDialog代码中添加的(Android 4.4原生SDK不支持频段选择,需要扩展功能)

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

5.2 WifiConfiguration配置

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

5.2.1 获取wifiConfig

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

    private void initWifiTethering() {
        final Activity activity = getActivity();
        if (mWifiManager == null)
            mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        mWifiConfig = mWifiManager.getWifiApConfiguration();
        mSecurityType = getResources().getStringArray(R.array.wifi_ap_security);

        mCreateNetwork = findPreference(WIFI_AP_SSID_AND_SECURITY);

        if (mWifiConfig == null) {
            final String s = activity.getString(
                    com.android.internal.R.string.wifi_tether_configure_ssid_default);
            mCreateNetwork.setSummary(String.format(activity.getString(CONFIG_SUBTEXT),
                    s, mSecurityType[WifiApDialog.OPEN_INDEX]));
        } else {
            int index = WifiApDialog.getSecurityTypeIndex(mWifiConfig);
            mCreateNetwork.setSummary(String.format(activity.getString(CONFIG_SUBTEXT),
                    mWifiConfig.SSID,
                    mSecurityType[index]));
        }
    }

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


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

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

5.2.2 设置wifiConfig

  WifiApDialogd窗可以修改WiFi的配置信息,按下确定按钮即可保存,接下来看一下对config的保存设置。
  对于dialog的确认按钮的点击事件是在TetherSettings.java中处理的
Androd 4.4修改设置后自动重启AP:

    public void onClick(DialogInterface dialogInterface, int button) {
        if (button == DialogInterface.BUTTON_POSITIVE) {
            mWifiConfig = mDialog.getConfig();
            if (mWifiConfig != null) {
                /**
                 * if soft AP is stopped, bring up
                 * else restart with new config
                 * TODO: update config on a running access point when framework support is added
                 */
                if (mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_ENABLED) {
                    mWifiManager.setWifiApEnabled(null, false);
                    mWifiManager.setWifiApEnabled(mWifiConfig, true);
                } else {
                	mWifiManager.setWifiApEnabled(null, false);
                    mWifiManager.setWifiApConfiguration(mWifiConfig);
                }
                int index = WifiApDialog.getSecurityTypeIndex(mWifiConfig);
                mCreateNetwork.setSummary(String.format(getActivity().getString(CONFIG_SUBTEXT),
                        mWifiConfig.SSID,
                        mSecurityType[index]));
            }
        }
    }

Android 7.1.1 SDK源码:

这段代码做了以下 *** 作

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

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

    1. 把所获取的config加载出来
    2. 提供编辑框供user编辑

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

  2. TetherSettings.java:用户交互界面,呈现手机所支持的便携式热点的开关交互,代码目录为:
    /packages/apps/Settings/src/com/android/settings/TetherSettings.java

  3. Tethering.java:逻辑实现类,该类中拥有很多业务处理逻辑来支持Android设备作为BT\USB\WIFI作为网关,即设备作为便携式热点代码的业务逻辑实现。该类中包含网络共享和便携式热点信息,即

    1. bluetooth_tethering:蓝牙网络共享,涉及到BluetoothPan协议
    2. usb_tethering:usb网络共享,涉及到设备连接usb时状态切换,即是否是充当大容量存储设备
    3. 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相关的有以下几种情况

    1. list of configured network:已经配置过的网络列表,即手机中以保存的 WiFi列表,对已经配置过的wifi可以进行增删改查的 *** 作viewed、update、modify
    2. current active wifi:当前正在运行的WiFi,即可用WiFi列表。列表中的wifi接入点access point 可以连接或者是断开连接
    3. result of access point scans:wifi接入点扫描结果,包含足够的信息来决定连接哪一个WiFi热点
    4. 定义了当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

5.3 wifiAp设备连接

  wifi设备连接有一个息息相关的类NativeDaemonConnector.java,具体了解可以参考Android NativeDaemonConnector源码解析。

5.4 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中发送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的命令
5.4.1 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

5.4.2 WifiService
IWifiManager mService;

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

public final class WifiService extends IWifiManager.Stub 

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

/**
 * see {@link android.net.wifi.WifiManager#setWifiApEnabled(WifiConfiguration, boolean)}
 * @param wifiConfig SSID, security and channel details as
 *        part of WifiConfiguration
 * @param enabled true to enable and false to disable
 */
 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携带的信息有:

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

5.4.3 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总结起来就做了两件事

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

5.4.4 WifiStateMachine

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

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

    /**
     * TODO: doc
     */
    public void setHostApRunning(WifiConfiguration wifiConfig, boolean enable) {
        if (enable) {
            sendMessage(CMD_START_AP, wifiConfig);
        } else {
            sendMessage(CMD_STOP_AP);
        }
    }

  很明显,该方法也是sendmsg,只不过这个msg是在WifiStateMachine这个类中自己处理的,此时Android 4.4与Android 7.1.1又有所区别。

Android 4.4: 开启AP:判断是否loaddriver,若为true,setWifiApState enable,之后状态转换transitionTo(mSoftApStartingState);

在SoftApStartingState中若为CMD_START_AP,则会调用startSoftApWithConfig(config);

startSoftApWithConfig函数实现如下:

同理,关闭ap:

从定义我们可以看出mUntetheringState为 SoftApStartedState 类型

继续找到SoftApStartedState类

Android 7.1.1: 可以看到从此时开始,start/stop wifiAp的msg.what开始不同,而不是仅仅依靠boolean值来区分,因为如果是start的话,需要进行两步的处理,包括

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

5.4.5 SoftApManager(Android 7.1.1)

  因为Android 4.4 为公司代码,涉及许多定制改动,因此这里改为Android 7.1.1 的流程继续细致分析,后面大体流程类似。

  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)};
    }
    
    1. wlanIface的取值: 首先这里的wlanIface是在构建softApManager对象时借助WifiNative对象获取的,WifiNative中获取wlanIface的地方位于:

      private static WifiNative wlanNativeInterface = new WifiNative(SystemProperties.get("wifi.interface", "wlan0"), true);
      

      很显然,wlanIface的值取决于属性字段wifi.interface的对应值,可以看到,如果未定义即默认取值为wlan0,源码中设置的也是wlan0.

    2. preSharedKey:指的是wifiAp的密码,之所以列出来是因为源码用一层类SensitiveArg将他包装了起来,该类的作用就是告诉开发者:该字段属于敏感内容,禁止使用log打印出来,该类所重写的toString方法也是将构造时传入的obj对象转换成string输出。

    3. 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)来决定的

    4. 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"}  
      }
      
  3. int expectedResponseCode:执行结果期望值(int),即执行成功时的响应。NetdResponseCode.SoftapStatusResult = 214:

  4. String expectedResponseMessage :执行结果期望值(string类型)
    SOFT_AP_COMMAND_SUCCESS = “Ok”

  5. 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个

  接下里就是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不能
    //有"//" "",第二command必须要与argument分开处理即避开args,即cmd不能有
    makeCommand
    (,rawBuilder, logBuilder, sequenceNumber, cmd) args;final
    String = rawCmd . rawBuildertoString();final
    String = logCmd . logBuildertoString();log
    (+"SND -> {" + logCmd "}" );synchronized
    ( )mDaemonLock//根据上文中所述,wifiap上层与底层基本上是命令或者是文件存储的形式进行交互 {
      //所以在这里借助传入的socket获取到os
      if
      ( ==mOutputStream null )//os为null时抛出异常 {
      throw
     new NativeDaemonConnectorException ("missing output stream");}
       else try {
           //开始往输出流中写cmd,编码格式为UTF_8 {                   
           .
          mOutputStreamwrite(.rawCmdgetBytes(StandardCharsets.)UTF_8);}
           catch ( IOException) e//在写cmd时发生io异常 {
           throw
               new NativeDaemonConnectorException ("problem sending command",) e;}
            }
        }
      NativeDaemonEvent
      = event null ;do
     = {
        event . mResponseQueueremove(,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中
        .
        eventsadd()event;//while的判断条件是event处理之后的返回码处于[100,200)之间
        }
     while ( .eventisClassContinue());//记录下cmd处理结束的时间
     final
     long = endTime SystemClock .elapsedRealtime();//WARN_EXECUTE_DELAY_MS为5秒,如果cmd的处理时间超过5秒则发出处理时间过长
     //的log警告
     if
     ( -endTime ) startTime > WARN_EXECUTE_DELAY_MSloge {
        ("NDC Command {"+ + logCmd "} took too long (" + ( -endTime ) startTime+ "ms)" );}
      //如果event的返回码取值返回为[500,600),则抛出参数请求异常,即客户端异常
      if
     ( .eventisClassClientError())throw {
        new NativeDaemonArgumentException (,logCmd) event;}
     //如果event的返回码取值为[400,500),则失败,服务器处理异常
     if
     ( .eventisClassServerError())throw {
        new NativeDaemonFailureException (,logCmd) event;}
     //只有event返回码在[200,300)之间,才表示请求成功
     return
     . eventstoArray(newNativeDaemonEvent [.eventssize()]);}
 
  • msg.what : SoftApStateMachine.CMD_STOP
  • stopAp

      而stop时send的msg的信息为

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

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

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

    []= args "stopap" {};String
    

      请求错误时的logmsg为:

    = logMsg "stopAccessPoint Error stopping softap" ;[+++]
    

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

    )
    File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
    File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
    File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
    Android WiFi —softAP流程分析_java_内存溢出

    Android WiFi —softAP流程分析

    Android WiFi —softAP流程分析,第1张

    Android WiFi — Ap功能实现与源码分析
    • 0. 前言
    • 1. wifiAp的ip
    • 2. WifiAp的config分析
      • 2.1 默认的config
      • 2.2 修改wifiAp的config配置流程
    • 3. 开启/关闭WifiAp热点状态流程
    • 4. 已连接设备列表
      • 4.1 读取wifiAp的已连接设备列表
      • 4.2 设备列表实时更新
      • 4.3 设备连接限制
    • 5. 源码分析
      • 5.1 WifiAp始于UI
      • 5.2 WifiConfiguration配置
        • 5.2.1 获取wifiConfig
        • 5.2.2 设置wifiConfig
      • 5.3 wifiAp设备连接
      • 5.4 wifiAp打开流程
        • 5.4.1 WifiManager
        • 5.4.2 WifiService
        • 5.4.3 WifiController
        • 5.4.4 WifiStateMachine
        • 5.4.5 SoftApManager(Android 7.1.1)

    0. 前言

      WiFi热点通常说的是wifiAp相关,如果是源码开发的话,这个WifiAp算是一个搜索代码的关键字,含义是Wifi Access point,wifi接入点。所以下文中的wifi热点统一用wifiAp代替。

    1. wifiAp打开方式: 设置 → 更多 → 移动网络共享 → 便携式wlan热点。
    2. wifiAp打开条件: 任何情况下均可。只是有内网外网之分。造成内外网之分的影响条件有sim卡和wifi的连接状态。注意,这里所说的是wifi的连接状态,而不是wifi热点的连接状态
    3. wifiAp开发中用处: 可用于局域网内的通信
    4. wifiAp开发中相关问题:
      1. 跟WiFiAp相关的有wifiAp的网关Ip,以及ip范围
      2. wifiAp的config:包括初始创建时的defaultvalue:名字(ssid)和密码(preSharedKey),以及后续修改config
      3. wifiAp的enable状态
      4. wifiAp的设备连接列表:一是保证能获取到当前连接设备列表,二是当有设备连接时能够实时的更新
      5. wifiAp的连接限制:包括最大连接数限制,以及黑白名单机制
    1. 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地址(Android 7.0中在该文件中,如果是其他Android系统(Android 4.4)可以在WifiStateMachine),具体方法在startThering中
    • wifiAp的ip地址的分配区间:在/frameworks/base/services/core/java/com/android/server/connectivity/Tethering.java中有规定
        // USB is  192.168.42.1 and 255.255.255.0
        // Wifi is 192.168.43.1 and 255.255.255.0
        // BT is limited to max default of 5 connections. 192.168.44.1 to 192.168.48.1
        // with 255.255.255.0
    
    	//usb网络共享的网关是192.168.42.1 
    	//wifi便携式热点网关是192.168.43.1 
    	//蓝牙共享(个人局域网)限制5个 
    	
        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",
        };
    
    2. WifiAp的config分析 2.1 默认的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;
        }
    }
    
    2.2 修改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);
      }
      
    3. 开启/关闭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);
      
    4. 已连接设备列表 4.1 读取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地址来获取对应的厂商和设备名。当然,方案提供商也许自己会集成这部分工作,所以具体情况具体考虑

    4.2 设备列表实时更新

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

    4.3 设备连接限制

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

    5. 源码分析

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

      关于Android的WiFiAp的源码研究基于andriod 4.4

    5.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的设置d出框为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列表进行选择,可选项如下
    3. wifi_password:wifiAp的密码,最大长度限制为63
    4. wifi_ap_band_config:wifiAp的Ap频段
      频段有spinner列表可选,频段可选为2.4g和5g。该array是在WifiApDialog代码中添加的(Android 4.4原生SDK不支持频段选择,需要扩展功能)

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

    5.2 WifiConfiguration配置

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

    5.2.1 获取wifiConfig

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

        private void initWifiTethering() {
            final Activity activity = getActivity();
            if (mWifiManager == null)
                mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
            mWifiConfig = mWifiManager.getWifiApConfiguration();
            mSecurityType = getResources().getStringArray(R.array.wifi_ap_security);
    
            mCreateNetwork = findPreference(WIFI_AP_SSID_AND_SECURITY);
    
            if (mWifiConfig == null) {
                final String s = activity.getString(
                        com.android.internal.R.string.wifi_tether_configure_ssid_default);
                mCreateNetwork.setSummary(String.format(activity.getString(CONFIG_SUBTEXT),
                        s, mSecurityType[WifiApDialog.OPEN_INDEX]));
            } else {
                int index = WifiApDialog.getSecurityTypeIndex(mWifiConfig);
                mCreateNetwork.setSummary(String.format(activity.getString(CONFIG_SUBTEXT),
                        mWifiConfig.SSID,
                        mSecurityType[index]));
            }
        }
    

      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目录下:

    5.2.2 设置wifiConfig

      WifiApDialogd窗可以修改WiFi的配置信息,按下确定按钮即可保存,接下来看一下对config的保存设置。
      对于dialog的确认按钮的点击事件是在TetherSettings.java中处理的
    Androd 4.4修改设置后自动重启AP:

        public void onClick(DialogInterface dialogInterface, int button) {
            if (button == DialogInterface.BUTTON_POSITIVE) {
                mWifiConfig = mDialog.getConfig();
                if (mWifiConfig != null) {
                    /**
                     * if soft AP is stopped, bring up
                     * else restart with new config
                     * TODO: update config on a running access point when framework support is added
                     */
                    if (mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_ENABLED) {
                        mWifiManager.setWifiApEnabled(null, false);
                        mWifiManager.setWifiApEnabled(mWifiConfig, true);
                    } else {
                    	mWifiManager.setWifiApEnabled(null, false);
                        mWifiManager.setWifiApConfiguration(mWifiConfig);
                    }
                    int index = WifiApDialog.getSecurityTypeIndex(mWifiConfig);
                    mCreateNetwork.setSummary(String.format(getActivity().getString(CONFIG_SUBTEXT),
                            mWifiConfig.SSID,
                            mSecurityType[index]));
                }
            }
        }
    

    Android 7.1.1 SDK源码:

    这段代码做了以下 *** 作

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

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

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

      1. 把所获取的config加载出来
      2. 提供编辑框供user编辑

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

    2. TetherSettings.java:用户交互界面,呈现手机所支持的便携式热点的开关交互,代码目录为:
      /packages/apps/Settings/src/com/android/settings/TetherSettings.java

    3. Tethering.java:逻辑实现类,该类中拥有很多业务处理逻辑来支持Android设备作为BT\USB\WIFI作为网关,即设备作为便携式热点代码的业务逻辑实现。该类中包含网络共享和便携式热点信息,即

      1. bluetooth_tethering:蓝牙网络共享,涉及到BluetoothPan协议
      2. usb_tethering:usb网络共享,涉及到设备连接usb时状态切换,即是否是充当大容量存储设备
      3. 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相关的有以下几种情况

      1. list of configured network:已经配置过的网络列表,即手机中以保存的 WiFi列表,对已经配置过的wifi可以进行增删改查的 *** 作viewed、update、modify
      2. current active wifi:当前正在运行的WiFi,即可用WiFi列表。列表中的wifi接入点access point 可以连接或者是断开连接
      3. result of access point scans:wifi接入点扫描结果,包含足够的信息来决定连接哪一个WiFi热点
      4. 定义了当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

    5.3 wifiAp设备连接

      wifi设备连接有一个息息相关的类NativeDaemonConnector.java,具体了解可以参考Android NativeDaemonConnector源码解析。

    5.4 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中发送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的命令
    5.4.1 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

    5.4.2 WifiService
    IWifiManager mService;
    

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

    public final class WifiService extends IWifiManager.Stub 
    

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

    /**
     * see {@link android.net.wifi.WifiManager#setWifiApEnabled(WifiConfiguration, boolean)}
     * @param wifiConfig SSID, security and channel details as
     *        part of WifiConfiguration
     * @param enabled true to enable and false to disable
     */
     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来处理了

    5.4.3 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中

    5.4.4 WifiStateMachine

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

    • Clients:设备作为客户端连接其他wifi
    • p2p:wifi直连
    • softAp: wifi热点

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

        /**
         * TODO: doc
         */
        public void setHostApRunning(WifiConfiguration wifiConfig, boolean enable) {
            if (enable) {
                sendMessage(CMD_START_AP, wifiConfig);
            } else {
                sendMessage(CMD_STOP_AP);
            }
        }
    

      很明显,该方法也是sendmsg,只不过这个msg是在WifiStateMachine这个类中自己处理的,此时Android 4.4与Android 7.1.1又有所区别。

    Android 4.4: 开启AP:判断是否loaddriver,若为true,setWifiApState enable,之后状态转换transitionTo(mSoftApStartingState);

    在SoftApStartingState中若为CMD_START_AP,则会调用startSoftApWithConfig(config);

    startSoftApWithConfig函数实现如下:

    同理,关闭ap:

    从定义我们可以看出mUntetheringState为 SoftApStartedState 类型

    继续找到SoftApStartedState类

    Android 7.1.1: 可以看到从此时开始,start/stop wifiAp的msg.what开始不同,而不是仅仅依靠boolean值来区分,因为如果是start的话,需要进行两步的处理,包括

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

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

    5.4.5 SoftApManager(Android 7.1.1)

      因为Android 4.4 为公司代码,涉及许多定制改动,因此这里改为Android 7.1.1 的流程继续细致分析,后面大体流程类似。

      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)};
      }
      
      1. wlanIface的取值: 首先这里的wlanIface是在构建softApManager对象时借助WifiNative对象获取的,WifiNative中获取wlanIface的地方位于:

        private static WifiNative wlanNativeInterface = new WifiNative(SystemProperties.get("wifi.interface", "wlan0"), true);
        

        很显然,wlanIface的值取决于属性字段wifi.interface的对应值,可以看到,如果未定义即默认取值为wlan0,源码中设置的也是wlan0.

      2. preSharedKey:指的是wifiAp的密码,之所以列出来是因为源码用一层类SensitiveArg将他包装了起来,该类的作用就是告诉开发者:该字段属于敏感内容,禁止使用log打印出来,该类所重写的toString方法也是将构造时传入的obj对象转换成string输出。

      3. 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)来决定的

      4. 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"}  
        }
        
    3. int expectedResponseCode:执行结果期望值(int),即执行成功时的响应。NetdResponseCode.SoftapStatusResult = 214:

    4. String expectedResponseMessage :执行结果期望值(string类型)
      SOFT_AP_COMMAND_SUCCESS = “Ok”

    5. 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不能
        //有"//" "",第二command必须要与argument分开处理即避开args,即cmd不能有
        makeCommand
        (,rawBuilder, logBuilder, sequenceNumber, cmd) args;final
        String = rawCmd . rawBuildertoString();final
        String = logCmd . logBuildertoString();log
        (+"SND -> {" + logCmd "}" );synchronized
        ( )mDaemonLock//根据上文中所述,wifiap上层与底层基本上是命令或者是文件存储的形式进行交互 {
          //所以在这里借助传入的socket获取到os
          if
          ( ==mOutputStream null )//os为null时抛出异常 {
          throw
         new NativeDaemonConnectorException ("missing output stream");}
           else try {
               //开始往输出流中写cmd,编码格式为UTF_8 {                   
               .
              mOutputStreamwrite(.rawCmdgetBytes(StandardCharsets.)UTF_8);}
               catch ( IOException) e//在写cmd时发生io异常 {
               throw
                   new NativeDaemonConnectorException ("problem sending command",) e;}
                }
            }
          NativeDaemonEvent
          = event null ;do
         = {
            event . mResponseQueueremove(,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中
            .
            eventsadd()event;//while的判断条件是event处理之后的返回码处于[100,200)之间
            }
         while ( .eventisClassContinue());//记录下cmd处理结束的时间
         final
         long = endTime SystemClock .elapsedRealtime();//WARN_EXECUTE_DELAY_MS为5秒,如果cmd的处理时间超过5秒则发出处理时间过长
         //的log警告
         if
         ( -endTime ) startTime > WARN_EXECUTE_DELAY_MSloge {
            ("NDC Command {"+ + logCmd "} took too long (" + ( -endTime ) startTime+ "ms)" );}
          //如果event的返回码取值返回为[500,600),则抛出参数请求异常,即客户端异常
          if
         ( .eventisClassClientError())throw {
            new NativeDaemonArgumentException (,logCmd) event;}
         //如果event的返回码取值为[400,500),则失败,服务器处理异常
         if
         ( .eventisClassServerError())throw {
            new NativeDaemonFailureException (,logCmd) event;}
         //只有event返回码在[200,300)之间,才表示请求成功
         return
         . eventstoArray(newNativeDaemonEvent [.eventssize()]);}
     
  • msg.what : SoftApStateMachine.CMD_STOP
  • stopAp

      而stop时send的msg的信息为

      private

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

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

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

    []= args "stopap" {};String
    

      请求错误时的logmsg为:

    = logMsg "stopAccessPoint Error stopping softap" ;
    

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

    欢迎分享,转载请注明来源:内存溢出

    原文地址: http://outofmemory.cn/langs/788222.html

    (0)
    打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
    上一篇 2022-05-05
    下一篇 2022-05-05

    发表评论

    登录后才能评论

    评论列表(0条)

    保存