不说废话,直接上代码:
public String getWifiMac() {
String wifiMac = "";
try {
WifiManager wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE);
wifiMac = wifi.getConnectionInfo().getMacAddress();
Log.d(TAG, "wifiMac: " + wifiMac);
} catch (Exception e) {
e.printStackTrace();
}
return wifiMac;
}
那么问题来了,这份代码在Android5.1上跑的好好的,在Android8.1上,拿到的却是一个固定地址02:00:00:00:00:00。
估计是Android新特性,不让第三方应用拿MAC地址了。我们阅读源码,证实一下。
frameworks/base/wifi/java/android/net/wifi/WifiManager.java
public WifiInfo getConnectionInfo() {
try {
//这里没做啥事,只是封装了一下,调到服务端去了。
return mService.getConnectionInfo(getContext().getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiServiceImpl.java
@Override
public WifiInfo getConnectionInfo(String callingPackage) {
enforceAccessPermission();//权限检查
mLog.info("getConnectionInfo uid=%").c(Binder.getCallingUid()).flush();
/*
* Make sure we have the latest information, by sending
* a status request to the supplicant.
*/
//继续往下,调WifiStateMachine
return mWifiStateMachine.syncRequestConnectionInfo(callingPackage);
}
//要声明android.permission.ACCESS_WIFI_STATE权限
private void enforceAccessPermission() {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_WIFI_STATE,
"WifiService");
}
frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
public WifiInfo syncRequestConnectionInfo(String callingPackage) {
int uid = Binder.getCallingUid();
WifiInfo result = new WifiInfo(mWifiInfo);// new了一个WifiInfo实例
if (uid == Process.myUid()) return result;
boolean hideBssidAndSsid = true;
//这里把MacAddress设成了一个默认值,这个值的内容,就是02:00:00:00:00:00
result.setMacAddress(WifiInfo.DEFAULT_MAC_ADDRESS);
IPackageManager packageManager = AppGlobals.getPackageManager();
try {
if (packageManager.checkUidPermission(Manifest.permission.LOCAL_MAC_ADDRESS,
uid) == PackageManager.PERMISSION_GRANTED) {
//这里又设置了MacAddress,原因就在这里,
//第三方应用过不了这个权限检查,代码走不到这里,所以拿到的是02:00:00:00:00:00
result.setMacAddress(mWifiInfo.getMacAddress());
}
final WifiConfiguration currentWifiConfiguration = getCurrentWifiConfiguration();
if (mWifiPermissionsUtil.canAccessFullConnectionInfo(
currentWifiConfiguration,
callingPackage,
uid,
Build.VERSION_CODES.O)) {
hideBssidAndSsid = false;
}
} catch (RemoteException e) {
Log.e(TAG, "Error checking receiver permission", e);
} catch (SecurityException e) {
Log.e(TAG, "Security exception checking receiver permission", e);
}
if (hideBssidAndSsid) {
result.setBSSID(WifiInfo.DEFAULT_MAC_ADDRESS);
result.setSSID(WifiSsid.createFromHex(null));
}
return result;
}
看下这个WifiInfo.DEFAULT_MAC_ADDRESS是什么:
frameworks/base/wifi/java/android/net/wifi/WifiInfo.java
/**
* Default MAC address reported to a client that does not have the
* android.permission.LOCAL_MAC_ADDRESS permission.
*
* @hide
*/
public static final String DEFAULT_MAC_ADDRESS = "02:00:00:00:00:00";
源码的注释非常清晰,你没有声明"android.permission.LOCAL_MAC_ADDRESS"这个权限,不按规矩来,那就给个DEFAULT_MAC_ADDRESS 忽悠一下吧!
既然是没权限,那简单,加上就行了。
然而,我在测试应用的AndroidManifest.xml加了权限,再测试,还是不行,这就尴尬了。
继续分析,看下这个权限是个啥东西,为啥加了还不行?
frameworks/base/core/res/AndroidManifest.xml
<!-- @SystemApi Allows applications to read the local WiFi and Bluetooth MAC address.
@hide -->
<permission android:name="android.permission.LOCAL_MAC_ADDRESS"
android:protectionLevel="signature|privileged" />
这个权限的protectionLevel是"signature|privileged",这个目的就很明确了,就是不让第三方应用拿MAC地址,除非你用系统签名。
如果客户说,我同样的代码,在A平台可以,为啥B平台就不行了,都是你们的产品啊!
那就改吧,把权限检查去掉,就OK了。
--- a/code/frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
+++ b/code/frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
@@ -1897,10 +1897,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
IPackageManager packageManager = AppGlobals.getPackageManager();
try {
- if (packageManager.checkUidPermission(Manifest.permission.LOCAL_MAC_ADDRESS,
- uid) == PackageManager.PERMISSION_GRANTED) {
+// if (packageManager.checkUidPermission(Manifest.permission.LOCAL_MAC_ADDRESS,
+// uid) == PackageManager.PERMISSION_GRANTED) {
result.setMacAddress(mWifiInfo.getMacAddress());
- }
+// }
final WifiConfiguration currentWifiConfiguration = getCurrentWifiConfiguration();
if (mWifiPermissionsUtil.canAccessFullConnectionInfo(
currentWifiConfiguration,
再看看Android5.1为啥没问题。调用流程都是一样的,不多说,直接看关键代码。
frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
/**
* Get status information for the current connection, if any.
* @return a {@link WifiInfo} object containing information about the current connection
*
*/
public WifiInfo syncRequestConnectionInfo() {
return mWifiInfo;
}
没有权限检查,直接把mWifiInfo丢回去,你再getMacAddress就OK了。
需要注意的是,如果WIFI没有打开过,可能拿不到MAC地址(取决于wifi模组),就是说要Supplicant跑起来,MAC才会传上来,这里就不深究了。
frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
@Override
public boolean processMessage(Message message) {
logStateAndMessage(message, getClass().getSimpleName());
switch(message.what) {
case WifiMonitor.SUP_CONNECTION_EVENT:
if (DBG) log("Supplicant connection established");
setWifiState(WIFI_STATE_ENABLED);
//... ...省略部分代码
mWifiInfo.setMacAddress(mWifiNative.getMacAddress());
应用代码:
public String getBlueToothMac() {
String btMac = "";
try {
BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
btMac = manager.getAdapter().getAddress();
Log.d(TAG, "btMac: " + btMac);
} catch (Exception e) {
e.printStackTrace();
}
return btMac;
}
同Wifi一样,这个代码在Android8.1上运行,也是拿到固定地址02:00:00:00:00:00。
下面分析源码。
frameworks/base/core/java/android/bluetooth/BluetoothManager.java
/**
* Get the default BLUETOOTH Adapter for this device.
*
* @return the default BLUETOOTH Adapter
*/
public BluetoothAdapter getAdapter() {
return mAdapter;//返回BluetoothAdapter 实例
}
frameworks/base/core/java/android/bluetooth/BluetoothAdapter.java
/**
* Returns the hardware address of the local Bluetooth adapter.
* For example, "00:11:22:AA:BB:CC".
*
* @return Bluetooth hardware address as string
*/
@RequiresPermission(Manifest.permission.BLUETOOTH) //需要“android.permission.BLUETOOTH”权限
public String getAddress() {
try {
return mManagerService.getAddress(); //调服务端的方法
} catch (RemoteException e) {Log.e(TAG, "", e);}
return null;
}
frameworks/base/services/core/java/com/android/server/BluetoothManagerService.java
public String getAddress() {
//定义:private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH;
mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM,
"Need BLUETOOTH permission");
if ((Binder.getCallingUid() != Process.SYSTEM_UID) &&
(!checkIfCallerIsForegroundUser())) {
Slog.w(TAG,"getAddress(): not allowed for non-active and non system user");
return null;
}
//这个权限检查跟wifi一样,如果没有“android.permission.LOCAL_MAC_ADDRESS”,就返回默认地址
if (mContext.checkCallingOrSelfPermission(Manifest.permission.LOCAL_MAC_ADDRESS)
!= PackageManager.PERMISSION_GRANTED) {
return BluetoothAdapter.DEFAULT_MAC_ADDRESS;
}
try {
mBluetoothLock.readLock().lock();
if (mBluetooth != null) return mBluetooth.getAddress();
} catch (RemoteException e) {
Slog.e(TAG, "getAddress(): Unable to retrieve address remotely. Returning cached address", e);
} finally {
mBluetoothLock.readLock().unlock();
}
// mAddress is accessed from outside.
// It is alright without a lock. Here, bluetooth is off, no other thread is
// changing mAddress
return mAddress;
}
跟wifi一个套路,跟着代码流程走一遍就行了,
Android没有提供获取以太网MAC地址的API,但有一个API可以间接实现。
public String getEthernetMac() {
String ethMac = "";
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET);
if (info != null) {
ethMac = info.getExtraInfo();//这个ExtraInfo就是以太网的mac地址
Log.d(TAG, "ethernet mac = " + ethMac);
} else {
Log.e(TAG, "info is null !");
}
return ethMac;
}
为什么“getExtraInfo”就能拿到以太网的MAC地址?我们继续往下看。
frameworks/base/core/java/android/net/NetworkInfo.java
/**
* Report the extra information about the network state, if any was
* provided by the lower networking layers.
* @return the extra information, or null if not available
*/
public String getExtraInfo() {
synchronized (this) {
return mExtraInfo;
}
}
/**
* Set the extraInfo field.
* @param extraInfo an optional {@code String} providing addditional network state
* information passed up from the lower networking layers.
* @hide
*/
public void setExtraInfo(String extraInfo) {
synchronized (this) {
this.mExtraInfo = extraInfo;
}
}
这个“getExtraInfo”的本意是“Report the extra information about the network state”,具体是什么信息,取决于底层。
setExtraInfo添加了哪些附加信息?
如上图,
Ethernet是mHwAddr,
Wifi是mWifiInfo.getSSID(),
telephony是mApnSetting.apn
所以,对于以太网而言,getExtraInfo就能取出MAC地址。
如果不改Android源码,而是由应用端来解决这个问题,有没有办法?
直接读节点,也是可行的。
wifi的节点:/sys/class/net/wlan0/address
ethernet的节点:/sys/class/net/eth0/address
Bt没有找到节点。
写一下获取wifi mac地址的代码,ethernet类似。
public String getWifiMacFromNode() {
String wifiMac = "";
RandomAccessFile f = null;
try {
f = new RandomAccessFile("/sys/class/net/wlan0/address", "r");
f.seek(0);
wifiMac = f.readLine().trim();
f.close();
Log.d(TAG, "getWifiMacFromNode "+wifiMac);
return wifiMac;
} catch (FileNotFoundException e) {
e.printStackTrace();
return wifiMac;
} catch (IOException e) {
e.printStackTrace();
return wifiMac;
} finally {
if (f != null) {
try {
f.close();
f = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
其他方法,反射调用Android的API,同时绕过权限检查,应该也是可行的。