1. 程式人生 > >Android 檢測裝置是否為模擬器

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;
}

後記

其實,我相信還有更好的方法去檢測,比如通過一些硬體特性,或者模擬器不能模擬的其他特性,但目前還沒有找到,如果你有好的辦法,歡迎分享!!!