最近有一个新的需求,检测设备是否为模拟器,如果是模拟器就禁用某些功能。
你还在为开发中频繁切换环境打包而烦恼吗?快来试试 Environment Switcher 吧!使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。https://github.com/CodeXiaoMai/EnvironmentSwitcher
市面上的模拟器
打开 Google 搜索 “模拟器”,各种模拟器映入眼帘。“逍遥安卓-超强安卓模拟器”、“天天模拟器”、“网易MuMu”、“BlueStacks蓝叠安卓模拟器”、“夜神安卓模拟器”、“海马玩模拟器”、“51模拟器”当然还有功能强大的“Genymotion”……
搜索解决办法
经过上网查找,发现类似的帖子并不是太多,其中经过筛选,发现下面几个通用的解决方案。
方案一:
public boolean isEmulator() {
return ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
.getNetworkOperatorName().toLowerCase().equals("android");
}
方案二:
public boolean isEmulator() {
return Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.toLowerCase().contains("vbox")
|| Build.FINGERPRINT.toLowerCase().contains("test-keys")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.SERIAL.equalsIgnoreCase("unknown")
|| Build.SERIAL.equalsIgnoreCase("android")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|| "google_sdk".equals(Build.PRODUCT);
}
于是把上面两种方案结合起来,就是:
public boolean isEmulator() {
return Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.toLowerCase().contains("vbox")
|| Build.FINGERPRINT.toLowerCase().contains("test-keys")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.SERIAL.equalsIgnoreCase("unknown")
|| Build.SERIAL.equalsIgnoreCase("android")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|| "google_sdk".equals(Build.PRODUCT)
|| ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
.getNetworkOperatorName().toLowerCase().equals("android");
}
测试结果
经过在各个模拟器上测试,发现大多数都是可以检测出来的,只有各别模拟器不可以检测出来,其中包括“夜神安卓模拟器”。经过观察与对比发现,夜神安卓模拟器有一个和其他模拟器以及手机(手头的)不同的地方,就是“Build.SERIAL”是一个16位的字符串,而其他模拟器都是“unknow”或者”android”,真机是 8 位的字符串,哈哈小样被我抓住了吧,于是修改了检测方法。
public boolean isEmulator() {
return Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.toLowerCase().contains("vbox")
|| Build.FINGERPRINT.toLowerCase().contains("test-keys")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.SERIAL.equalsIgnoreCase("unknown")
|| Build.SERIAL.equalsIgnoreCase("android")
|| Build.SERIAL.length() > 8
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|| "google_sdk".equals(Build.PRODUCT)
|| ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
.getNetworkOperatorName().toLowerCase().equals("android");
}
再次检测,成功识别!!
问题再现
由于手头的手机有限,担心将手机识别错误,于是在 weTest 平台抽样对各品牌手机进行测试,果然不出所料,问题出现了。当测试到华为畅享5s的时候,竟然也被识别为模拟器。这下悲剧了,毕竟手机用户还是主要的,可不能错杀好人啊!!!经过观察,发布问题出现在上面自作聪明加的一个判断中 Build.SERIAL.length() > 8
,这个手机的 Build.SERIAL 也是 16 位,这可如何是好???
一个 Crash 让我灵光乍现
App 中有一个跳转到拨号盘的功能,当然在模拟器中无意点到这个按钮的时候,App 居然 Crash 了,这引起了我的注意,加为之前在真机上从来没有出现过问题,于是再次尝试点击这个按钮,它再次如我所料的 Crash 掉了。我实然灵机一动,对啊这是模拟器,不能拨打电话,所以 Crash 了,这不正是解决方案吗?(一不小心一个 Crash 竟然救了我)于是我在其他几个模拟器中也尝试点击这个按钮,结果是大部分都不支持这个操作,而且都是简单粗暴的直接 Crash 。虽然不能 100% 的识别,但大多数还是可以以此来做识别凭证的。
接下来再修改方法,慢着!大多数平板也是不支持拨打电话的,由于手头也是只有一台华为的平板,测试了一下,发现是跳转到保存联系页面,这个至少也不是 Crash,所以算通过了。
最终结果
最终将几种方案整合修改后如下:
public boolean isEmulator() {
String url = "tel:" + "123456";
Intent intent = new Intent();
intent.setData(Uri.parse(url));
intent.setAction(Intent.ACTION_DIAL);
// 是否可以处理跳转到拨号的 Intent
boolean canResolveIntent = intent.resolveActivity(mContext.getPackageManager()) != null;
return Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.toLowerCase().contains("vbox")
|| Build.FINGERPRINT.toLowerCase().contains("test-keys")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.SERIAL.equalsIgnoreCase("unknown")
|| Build.SERIAL.equalsIgnoreCase("android")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|| "google_sdk".equals(Build.PRODUCT)
|| ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
.getNetworkOperatorName().toLowerCase().equals("android")
|| !canResolverIntent;
}
后记
其实,我相信还有更好的方法去检测,比如通过一些硬件特性,或者模拟器不能模拟的其他特性,但目前还没有找到,如果你有好的办法,欢迎分享!!!