Android虐我千百遍,我待Android如初恋。
——————编辑于2017-08-02——————— wifi热点说的是wifiAp相关,所以如果源码开发的话,这个WifiAp算是一个搜索代码的关键字,含义是Wifi Access point,wifi接入点。所以下文中的wifi热点统一用WifiAp代替
既然是要局域网内通信,那就要用到ip地址和端口号了(关于端口号的设定属于开发通信时的问题,是用户自定义的可变的,在我的程序里我规定端口号为80。而ip地址是有规定的,所以只讲关于ip的问题)。ip地址是在Android源码中规定好的,平常所买的路由器的ip地址一般都是192.168.0.1。Android源码中所规定的手机的wifiAp的ip地址为192.168.43.1,这个代码中可以看到
——————编辑于2017-08-03———————
/** * 构建一个默认的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配置需要注意,在修改config时,config会直接设置下去,但是并不会立即生效,必须要重启wifiAp之后才有效。这个可以先拿自己的手机演示确认。
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiConfiguration config = wifiManager.getWifiApConfiguration();
if (config != null) {
config.SSID = mWifiInfoBean.getApSsid();
config.preSharedKey = mWifiInfoBean.getPskKey();
}
当然,你还可以做其他设置,具体的可以参考WifiConfiguration.java源码 到这一步,对于wifiAp的用户名和密码已经设置成功了,此时若手动重启wifiAp后config即可生效。如果你想要立刻生效,那就必须要重启wifiAp了。
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进行config修改时已经涉及到了对于wifiAp的开和关,在进行wifiAp进行开关的过程中需要传入config,如果传入的为null,则沿用上一次的 config,如果上一次的config不存在,则会去加载默认的config。当开启wifiAp时会先去判断wifi的状态,如果wifi处于开启状态则需要关闭WiFi状态,然后开启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代码处于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的设置包括四部分:
所以,如果想要修改wifiApDialog布局相关的可以修改wifi_ap_dialog.xml布局文件。由布局文件也可以看出,Android源码上层中,wifiAp相关的配置 WifiConfiguration包括四部分,用户名、密码 、安全性、频段。
在创建WifiApDialog时会传入一个WifiConfiguration对象,wifiApDialog中显示的WiFiAp信息就是从该config中获取的。在第一次开启wifiAp对象时所获取的config对像是系统默认的配置,当用户进行了修改之后wifiAp的config会被保存到手机,等下次获取到的就是修改后的config。
先来找到创建dialog的地方来看一下config对象,来看一下代码是如何在第一次使用时获取系统默认以及在修改后如何获取用户修改的config的:
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目录下:
WifiApDialog弹窗可以修改WiFi的配置信息,按下确定按钮即可保存,接下来看一下对config的保存设置。 对于dialog的确认按钮的点击事件是在TetherSettings.java中处理的
这段代码做了以下操作
基本上config的设置和获取就这些了。大致分析完成之后,也可以看到WifiAP相关的类主要有这么几个
其他对于config的read&write一律不进行处理。代码目录为: /packages/apps/Settings/src/com/android/settings/wifi/
代码中对这三种模式的开关状态进行了监听以及更新。代码目录为: /frameworks/base/services/core/java/com/android/server/connectivity/Tethering.java
——————编辑于2017-08-16——————— 隔了这么多天,终于有时间更新了,在csdn快两年时间了,一直坚持着,不幸的是我不知道以后还会不会更新csdn,也许以后的文章会出现在别处…比如公众号 wifi设备连接有一个息息相关的类NativeDaemonConnector.java(wifiAp连接源码分析会更新在wifiAp打开源码分析之下)
——————编辑于2017-08-18———————
先大致说一下追的流程:如下,
由上文可知,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
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携带的信息有:
到这里,WifiServiceImpl的任务就完成了,接下里就是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中:
WifiStateMachine继承自StateMachine,该类用于跟踪WiFi的连接状态,所有的事件处理都在这里,所有连接状态的改变也是在这里进行的初始化。Android7.1.1所支持的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的话,需要进行两步的处理,包括
而如果是stop的话,则只需要将wifiAp关闭即可,即调用 mSoftApManager.stop()。接下来就是SoftApManager中的start和stop了
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
start时send的msg为
在接收到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:
经过对以上问题的分析,可以看出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"} }
接下来看一下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不能
//有"\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()]);
}
而stop时send的msg的信息为
对于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); }); });