Android 檢測裝置是否為模擬器
最近有一個新的需求,檢測裝置是否為模擬器,如果是模擬器就禁用某些功能。
你還在為開發中頻繁切換環境打包而煩惱嗎?快來試試 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;
}
後記
其實,我相信還有更好的方法去檢測,比如通過一些硬體特性,或者模擬器不能模擬的其他特性,但目前還沒有找到,如果你有好的辦法,歡迎分享!!!