Android WiFi热点
项目中有需要对WiFi热点进行某些操作,所以记录下一些有关WiFi热点的知识点
开启或者关闭热点
网上的大部分例子,都是通过反射,调用WifiManager
的setWifiApEnabled
方法,来开启或者关闭热点,如:
- Android WiFi开发 (二)Wifi热点
- 三、Android开启wifi热点
//热点的配置类
WifiConfiguration apConfig = new WifiConfiguration();
//配置热点的名称(可以在名字后面加点随机数什么的)
apConfig.SSID = "YRCCONNECTION";
//配置热点的密码
apConfig.preSharedKey="12122112";
//通过反射调用设置热点
Method method = mWifiManager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, Boolean.TYPE);
但上面的方法,貌似只对安卓7.0或7.0以下版本有效,在我Android 11的手机上,并没有调用成功
通过Android系统源码中的WifiTetherSettings
,可以知道,现在启用热点,调用的是ConnectivityManager
中的 startTethering
方法
/**
* Runs tether provisioning for the given type if needed and then starts tethering if
* the check succeeds. If no carrier provisioning is required for tethering, tethering is
* enabled immediately. If provisioning fails, tethering will not be enabled. It also
* schedules tether provisioning re-checks if appropriate.
*
* @param type The type of tethering to start. Must be one of
* {@link ConnectivityManager.TETHERING_WIFI},
* {@link ConnectivityManager.TETHERING_USB}, or
* {@link ConnectivityManager.TETHERING_BLUETOOTH}.
* @param showProvisioningUi a boolean indicating to show the provisioning app UI if there
* is one. This should be true the first time this function is called and also any time
* the user can see this UI. It gives users information from their carrier about the
* check failing and how they can sign up for tethering if possible.
* @param callback an {@link OnStartTetheringCallback} which will be called to notify the caller
* of the result of trying to tether.
* @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
*
* @deprecated Use {@link TetheringManager#startTethering} instead.
* @hide
*/
@SystemApi
@Deprecated
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void startTethering(int type, boolean showProvisioningUi,
final OnStartTetheringCallback callback, Handler handler) {
Objects.requireNonNull(callback, "OnStartTetheringCallback cannot be null.");
final Executor executor = new Executor() {
@Override
public void execute(Runnable command) {
if (handler == null) {
command.run();
} else {
handler.post(command);
}
}
};
final StartTetheringCallback tetheringCallback = new StartTetheringCallback() {
@Override
public void onTetheringStarted() {
callback.onTetheringStarted();
}
@Override
public void onTetheringFailed(final int error) {
callback.onTetheringFailed();
}
};
final TetheringRequest request = new TetheringRequest.Builder(type)
.setShouldShowEntitlementUi(showProvisioningUi).build();
getTetheringManager().startTethering(request, executor, tetheringCallback);
}
这个方法也被标注为@Deprecated
,表示已废弃
提示我们使用TetheringManager#startTethering
TetheringManager
中的startTethering
方法
public void startTethering(@NonNull final TetheringRequest request,
@NonNull final Executor executor, @NonNull final StartTetheringCallback callback) {
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "startTethering caller:" + callerPkg);
final IIntResultListener listener = new IIntResultListener.Stub() {
@Override
public void onResult(final int resultCode) {
executor.execute(() -> {
if (resultCode == TETHER_ERROR_NO_ERROR) {
callback.onTetheringStarted();
} else {
callback.onTetheringFailed(resultCode);
}
});
}
};
getConnector(c -> c.startTethering(request.getParcel(), callerPkg,
getAttributionTag(), listener));
}
所以参考How do I enable/disable hotspot or tethering mode programmatically on Android?
如果Android Version >= 11,则使用
TetheringManager#startTethering
如果Android Version < 11,则使用ConnectivityManager#startTethering
最终,对最新的Android的版本,在网络上最后找到了如下的几个解决方案:
- android开发 控制wifi和热点的开启(适配到 9.1)
- How to turn on/off wifi hotspot programmatically in Android 8.0 (Oreo)
如果直接运行上面的代码,可能会报错,如
java.lang.IllegalArgumentException: dexcache == null (and no default could be found; consider setting the ‘dexmaker.dexcache’ system property)
可能的解决办法是:
1.修改依赖的dexmaker-mockito
版本为2.2.0
2.ProxyBuilder
要调用dexCache()
方法,不要问我为什么
使用的时候需要注意:
1.需要WRITE_SETTINGS
权限,在手机权限里面设置下,打开这个权限
2.是否真的启用了”个人热点“,需要去设置中查看,这个开关是否打开
粗略代码如下,2个方法,大同小异
/** * android8.0以上开启手机热点 */
private void startTethering() {
ConnectivityManager connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
try {
File outputDir = getCodeCacheDir();
Class classOnStartTetheringCallback = Class.forName("android.net.ConnectivityManager$OnStartTetheringCallback");
Method startTethering = connectivityManager.getClass().getDeclaredMethod("startTethering", int.class, boolean.class, classOnStartTetheringCallback);
Object proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).dexCache(outputDir).handler(new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
return null;
}
}).build();
startTethering.invoke(connectivityManager, 0, false, proxy);
} catch (Exception e) {
Log.e(TAG,"打开热点失败");
e.printStackTrace();
}
}
public boolean enableTetheringNew() {
File outputDir = getCodeCacheDir();
Object proxy = new Object();
try {
proxy = ProxyBuilder.forClass(classOnStartTetheringCallback())
.dexCache(outputDir).handler(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "onTetheringStarted":
Log.d(TAG, "onTetheringStarted");
break;
case "onTetheringFailed":
Log.d(TAG, "onTetheringFailed");
break;
default:
ProxyBuilder.callSuper(proxy, method, args);
}
return null;
}
}).build();
} catch (Exception e) {
e.printStackTrace();
}
ConnectivityManager manager = (ConnectivityManager)getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
Method method = null;
try {
method = manager.getClass().getDeclaredMethod("startTethering", int.class, boolean.class, classOnStartTetheringCallback(), Handler.class);
if (method == null) {
Log.e(TAG, "startTetheringMethod is null");
} else {
method.invoke(manager, 0, false, proxy, null);
}
return true;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return false;
}
private Class classOnStartTetheringCallback() {
try {
return Class.forName("android.net.ConnectivityManager$OnStartTetheringCallback");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
在本人oppo Android11的手机上,测试是有效果的
关闭WiFi热点
/**
* android8.0以上关闭手机热点
*/
private void stopTethering() {
ConnectivityManager connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
try {
Method stopTethering = connectivityManager.getClass().getDeclaredMethod("stopTethering", int.class);
stopTethering.invoke(connectivityManager,0);
} catch (Exception e) {
Log.e(TAG,"关闭热点失败");
e.printStackTrace();
}
}
在本人手机上测试也是有效果的
参考文档:
- Android WiFi开发 (三)Wifi热点8.0适配
WiFi热点状态
获取WiFi热点的状态需要使用WifiManager
如,获取WiFi热点的状态:
/**
* Gets the tethered Wi-Fi hotspot enabled state.
* @return One of {@link #WIFI_AP_STATE_DISABLED},
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
* @see #isWifiApEnabled()
*
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
public int getWifiApState() {
try {
return mService.getWifiApEnabledState();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
getWifiApState()
也是系统的API,返回值有5种状态:
-
WIFI_AP_STATE_DISABLING
- 值为10 -
WIFI_AP_STATE_DISABLED
- 值为11 -
WIFI_AP_STATE_ENABLING
- 值为12 -
WIFI_AP_STATE_ENABLED
- 值为13 -
WIFI_AP_STATE_FAILED
- 值为14
通过isWifiApEnabled()
方法,获取热点是否开启
/**
* Return whether tethered Wi-Fi AP is enabled or disabled.
* @return {@code true} if tethered Wi-Fi AP is enabled
* @see #getWifiApState()
*
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
public boolean isWifiApEnabled() {
return getWifiApState() == WIFI_AP_STATE_ENABLED;
}
获取WiFi热点配置
上面参考的例子中有获取热点名称的方法,我测试后发现会发生异常:
java.lang.SecurityException: App not allowed to read or update stored WiFi Ap config (uid = 10488)
网上说法是需要权限
<permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
android:protectionLevel="signature|privileged" />
另外在MyOreoWifiManager中有配置热点的代码,说需要android.permission.TETHER_PRIVILEGED
权限:
/**
* This sets the Wifi SSID and password
* Call this before {@code startTethering} if app is a system/privileged app
* Requires: android.permission.TETHER_PRIVILEGED which is only granted to system apps
*/
public void configureHotspot(String name, String password) {
WifiConfiguration apConfig = new WifiConfiguration();
apConfig.SSID = name;
apConfig.preSharedKey = password;
apConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
try {
Method setConfigMethod = mWifiManager.getClass().getMethod("setWifiApConfiguration", WifiConfiguration.class);
boolean status = (boolean) setConfigMethod.invoke(mWifiManager, apConfig);
Log.d(TAG, "setWifiApConfiguration - success? " + status);
} catch (Exception e) {
Log.e(TAG, "Error in configureHotspot");
e.printStackTrace();
}
}
本人也没有开发系统app的权限,所以有提示如下的异常:
WifiManager
中有关热点配置的方法
public WifiConfiguration getWifiApConfiguration()
获取Wi-Fi AP的配置,注意此API已废弃,建议使用getSoftApConfiguration()
public boolean setWifiApConfiguration(WifiConfiguration wifiConfig)
设置Wi-Fi AP的配置,注意此API已废弃,建议使用setSoftApConfiguration(SoftApConfiguration)
public SoftApConfiguration getSoftApConfiguration()
Gets the Wi-Fi tethered AP Configuration.
/**
* Sets the tethered Wi-Fi AP Configuration.
*
* If the API is called while the tethered soft AP is enabled, the configuration will apply to
* the current soft AP if the new configuration only includes
* {@link SoftApConfiguration.Builder#setMaxNumberOfClients(int)}
* or {@link SoftApConfiguration.Builder#setShutdownTimeoutMillis(long)}
* or {@link SoftApConfiguration.Builder#setClientControlByUserEnabled(boolean)}
* or {@link SoftApConfiguration.Builder#setBlockedClientList(List)}
* or {@link SoftApConfiguration.Builder#setAllowedClientList(List)}
*
* Otherwise, the configuration changes will be applied when the Soft AP is next started
* (the framework will not stop/start the AP).
*
* @param softApConfig A valid SoftApConfiguration specifying the configuration of the SAP.
* @return {@code true} if the operation succeeded, {@code false} otherwise
*
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
public boolean setSoftApConfiguration(@NonNull SoftApConfiguration softApConfig) {
try {
return mService.setSoftApConfiguration(
softApConfig, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
Sets the tethered Wi-Fi AP Configuration
连接到热点的设备信息
网上有通过访问系统文件/proc/net/arp
的方式,来获取连接到热点的设备的ip信息,
可参考:
- Android 开启个人热点时 获取连接人数以及连接上的设备信息
- Android 10版本获取已连接本机热点的ip
在Android 11尝试后,有如下的提示,看来是不再有效了:
java.io.FileNotFoundException: /proc/net/arp: open failed: EACCES (Permission denied)
在java android Q read ip from hotspot /proc/net/arp: open failed: EACCES (Permission denied)处有给出新的方法,尝试之后也行不通
这些命令可以在已root的设备上,或者系统app,可以正常获取
例如,在root的设备上,使用adb shell ip neigh show
,效果如下:
使用
adb shell cat /proc/net/arp
,输出结果如下:
不过,在新版本中,Android又提供了一个SoftApCallback
接口,通过WifiManager#registerSoftApCallback(Executor, SoftApCallback)
设置,参考:
- Android实时获取热点已连接数(谷歌现成API可用,适用于第三方APP和系统APP)
/**
* Base class for soft AP callback. Should be extended by applications and set when calling
* {@link WifiManager#registerSoftApCallback(Executor, SoftApCallback)}.
*
* @hide
*/
@SystemApi
public interface SoftApCallback {
/**
* Called when soft AP state changes.
*
* @param state the new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
* {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
* {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
* @param failureReason reason when in failed state. One of
* {@link #SAP_START_FAILURE_GENERAL},
* {@link #SAP_START_FAILURE_NO_CHANNEL},
* {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION}
*/
default void onStateChanged(@WifiApState int state, @SapStartFailure int failureReason) {}
/**
* Called when the connected clients to soft AP changes.
*
* @param clients the currently connected clients
*/
default void onConnectedClientsChanged(@NonNull List<WifiClient> clients) {}
registerSoftApCallback
方法说明:
/**
* Registers a callback for Soft AP. See {@link SoftApCallback}. Caller will receive the
* following callbacks on registration:
* <ul>
* <li> {@link SoftApCallback#onStateChanged(int, int)}</li>
* <li> {@link SoftApCallback#onConnectedClientsChanged(List<WifiClient>)}</li>
* <li> {@link SoftApCallback#onInfoChanged(SoftApInfo)}</li>
* <li> {@link SoftApCallback#onCapabilityChanged(SoftApCapability)}</li>
* </ul>
* These will be dispatched on registration to provide the caller with the current state
* (and are not an indication of any current change). Note that receiving an immediate
* WIFI_AP_STATE_FAILED value for soft AP state indicates that the latest attempt to start
* soft AP has failed. Caller can unregister a previously registered callback using
* {@link #unregisterSoftApCallback}
* <p>
* Applications should have the
* {@link android.Manifest.permission#NETWORK_SETTINGS NETWORK_SETTINGS} permission. Callers
* without the permission will trigger a {@link java.lang.SecurityException}.
* <p>
*
* @param executor The Executor on whose thread to execute the callbacks of the {@code callback}
* object.
* @param callback Callback for soft AP events
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
public void registerSoftApCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull SoftApCallback callback) {
if (executor == null) throw new IllegalArgumentException("executor cannot be null");
if (callback == null) throw new IllegalArgumentException("callback cannot be null");
Log.v(TAG, "registerSoftApCallback: callback=" + callback + ", executor=" + executor);
Binder binder = new Binder();
try {
mService.registerSoftApCallback(
binder, new SoftApCallbackProxy(executor, callback), callback.hashCode());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
该方法需要android.Manifest.permission#NETWORK_SETTINGS
权限
SoftApConfiguration
相关的源码,可查看Here
Android 10 热点
我们当前开发是基于Android 10,所以记录下相关的内容
WifiManager
在Android10源码中的位置为frameworks/base/wifi/java/android/net/wifi/WifiManager.java
获取热点的设备数量可参考WifiTetherSoftApManager.java
,位于packages/apps/Settings/src/com/android/settings/wifi/tether/WifiTetherSoftApManager.java
/**
* Wrapper for {@link android.net.wifi.WifiManager.SoftApCallback} to pass the robo test
*/
public class WifiTetherSoftApManager {
private WifiManager mWifiManager;
private WifiTetherSoftApCallback mWifiTetherSoftApCallback;
private WifiManager.SoftApCallback mSoftApCallback = new WifiManager.SoftApCallback() {
@Override
public void onStateChanged(int state, int failureReason) {
mWifiTetherSoftApCallback.onStateChanged(state, failureReason);
}
@Override
public void onNumClientsChanged(int numClients) {
mWifiTetherSoftApCallback.onNumClientsChanged(numClients);
}
};
private Handler mHandler;
WifiTetherSoftApManager(WifiManager wifiManager,
WifiTetherSoftApCallback wifiTetherSoftApCallback) {
mWifiManager = wifiManager;
mWifiTetherSoftApCallback = wifiTetherSoftApCallback;
mHandler = new Handler();
}
public void registerSoftApCallback() {
mWifiManager.registerSoftApCallback(mSoftApCallback, mHandler);
}
public void unRegisterSoftApCallback() {
mWifiManager.unregisterSoftApCallback(mSoftApCallback);
}
public interface WifiTetherSoftApCallback {
void onStateChanged(int state, int failureReason);
void onNumClientsChanged(int numClients);
}
}
WifiTetherSettings.java
中有Wifi热点设置的相关内容,位于packages/apps/Settings/src/com/android/settings/wifi/tether/WifiTetherSettings.java
如配置Wifi热点:
@Override
public void onTetherConfigUpdated() {
final WifiConfiguration config = buildNewConfig();
mPasswordPreferenceController.updateVisibility(config.getAuthType());
/**
* 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) {
Log.d("TetheringSettings",
"Wifi AP config changed while enabled, stop and restart");
mRestartWifiApAfterConfigChange = true;
mSwitchBarController.stopTether();
}
mWifiManager.setWifiApConfiguration(config);
}
private WifiConfiguration buildNewConfig() {
final WifiConfiguration config = new WifiConfiguration();
final int securityType = mSecurityPreferenceController.getSecurityType();
config.SSID = mSSIDPreferenceController.getSSID();
config.allowedKeyManagement.set(securityType);
config.preSharedKey = mPasswordPreferenceController.getPasswordValidated(securityType);
config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
config.apBand = mApBandPreferenceController.getBandIndex();
return config;
}