framgia님의 Android Emulator Detector [https://github.com/framgia/android-emulator-detector] 를 약간 수정해서 안드로이드 에뮬레이터를 판별하는 클래스를 만들어서 공유하려한다.
지원 Emulator
테스트가 끝난 Emulator들은 다음과 같다!
- LD Player
- NOX
- Bluestacks
- Genymotion
- MEmu
- KoPlayer
- GameLoop
- NetEase MuMu Player
- Andy
Telephony 정보를 받지않고도 거를 수 있는데, 이건 Architecture가 x86이나 i686 기반으로 되어있으면 걸러버리기 때문이다. 하지만 이때문에 인텔 칩셋을 사용하는 휴대폰은 실제 디바이스임에도 불구하고 걸려버리는 수가있긴하다. (ㅠ_ㅠ)
문제가 있는 Device
- Lenovo K80
- Asys Zenfone Series
val arch = System.getProperty("os.arch") arch.contains("x86") || arch.contains("i686")
그외 특이점이있다면 특정 Player에서 사용하는 스토어가 Device 내에 깔려있다면 에뮬레이터로 처리해버린다는 것도 있다.
mListPackageName.add("com.google.android.launcher.layouts.genymotion"); mListPackageName.add("com.bluestacks"); mListPackageName.add("com.bignox.app"); mListPackageName.add("com.vphone.launcher"); mListPackageName.add("com.microvirt.tools"); mListPackageName.add("com.microvirt.download"); mListPackageName.add("com.cyanogenmod.filemanager"); mListPackageName.add("com.mumu.store");
EmulatorDetector
EmulatorDetector의 풀코드는 아래와 같다.
import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Build; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import androidx.core.content.ContextCompat; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public final class EmulatorDetector { public interface OnEmulatorDetectorListener { void onResult(boolean isEmulator); } private static final String[] PHONE_NUMBERS = { "15555215554", "15555215556", "15555215558", "15555215560", "15555215562", "15555215564", "15555215566", "15555215568", "15555215570", "15555215572", "15555215574", "15555215576", "15555215578", "15555215580", "15555215582", "15555215584" }; private static final String[] DEVICE_IDS = { "000000000000000", "e21833235b6eef10", "012345678912345" }; private static final String[] IMSI_IDS = { "310260000000000" }; private static final String[] GENY_FILES = { "/dev/socket/genyd", "/dev/socket/baseband_genyd" }; private static final String[] QEMU_DRIVERS = {"goldfish"}; private static final String[] PIPES = { "/dev/socket/qemud", "/dev/qemu_pipe" }; private static final String[] X86_FILES = { "ueventd.android_x86.rc", "x86.prop", "ueventd.ttVM_x86.rc", "init.ttVM_x86.rc", "fstab.ttVM_x86", "fstab.vbox86", "init.vbox86.rc", "ueventd.vbox86.rc" }; private static final String[] ANDY_FILES = { "fstab.andy", "ueventd.andy.rc" }; private static final String[] NOX_FILES = { "fstab.nox", "init.nox.rc", "ueventd.nox.rc" }; private static final Property[] PROPERTIES = { new Property("init.svc.qemud", null), new Property("init.svc.qemu-props", null), new Property("qemu.hw.mainkeys", null), new Property("qemu.sf.fake_camera", null), new Property("qemu.sf.lcd_density", null), new Property("ro.bootloader", "unknown"), new Property("ro.bootmode", "unknown"), new Property("ro.hardware", "goldfish"), new Property("ro.kernel.android.qemud", null), new Property("ro.kernel.qemu.gles", null), new Property("ro.kernel.qemu", "1"), new Property("ro.product.device", "generic"), new Property("ro.product.model", "sdk"), new Property("ro.product.name", "sdk"), new Property("ro.serialno", null) }; private static final String IP = "10.0.2.15"; private static final int MIN_PROPERTIES_THRESHOLD = 0x5; private final Context mContext; private boolean isDebug = false; private boolean isTelephony = false; private boolean isCheckPackage = true; private List<String> mListPackageName = new ArrayList<>(); @SuppressLint("StaticFieldLeak") //Since we use application context now this won't leak memory anymore. This is only to please Lint private static EmulatorDetector mEmulatorDetector; public static EmulatorDetector with(Context pContext) { if (pContext == null) { throw new IllegalArgumentException("Context must not be null."); } if (mEmulatorDetector == null) mEmulatorDetector = new EmulatorDetector(pContext.getApplicationContext()); return mEmulatorDetector; } private EmulatorDetector(Context pContext) { mContext = pContext; mListPackageName.add("com.google.android.launcher.layouts.genymotion"); mListPackageName.add("com.bluestacks"); mListPackageName.add("com.bignox.app"); mListPackageName.add("com.vphone.launcher"); mListPackageName.add("com.microvirt.tools"); mListPackageName.add("com.microvirt.download"); mListPackageName.add("com.cyanogenmod.filemanager"); mListPackageName.add("com.mumu.store"); } public void detect(final OnEmulatorDetectorListener pOnEmulatorDetectorListener) { new Thread(new Runnable() { @Override public void run() { boolean isEmulator = detect(); log("This System is Emulator: " + isEmulator); if (pOnEmulatorDetectorListener != null) { pOnEmulatorDetectorListener.onResult(isEmulator); } } }).start(); } private boolean detect() { boolean result = false; // Check Basic if (!result) { result = checkBasic(); } // Check Advanced if (!result) { result = checkAdvanced(); } // Check Package Name if (!result) { result = checkPackageName(); } return result; } public boolean checkBasic() { String architecture = System.getProperty("os.arch"); boolean result = Build.FINGERPRINT.startsWith("generic") || architecture.contains("x86") || architecture.contains("i686") || Build.MODEL.contains("google_sdk") || Build.MODEL.toLowerCase().contains("droid4x") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") || Build.HARDWARE.equals("goldfish") || Build.HARDWARE.equals("vbox86") || Build.HARDWARE.toLowerCase().contains("nox") || Build.PRODUCT.equals("sdk") || Build.PRODUCT.equals("google_sdk") || Build.PRODUCT.equals("sdk_x86") || Build.PRODUCT.equals("vbox86p") || Build.PRODUCT.toLowerCase().contains("windroye") || Build.PRODUCT.toLowerCase().contains("nox") || Build.BRAND.toLowerCase().contains("windroy") || Build.BOARD.toLowerCase().contains("nox") || Build.BOOTLOADER.toLowerCase().contains("nox") || Build.SERIAL.toLowerCase().contains("nox") || Build.MANUFACTURER.contains("Genymotion"); if (result) return true; result |= Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"); if (result) return true; result |= "google_sdk".equals(Build.PRODUCT); return result; } public boolean checkAdvanced() { boolean result = checkTelephony() || checkFiles(GENY_FILES, "Geny") || checkFiles(ANDY_FILES, "Andy") || checkFiles(NOX_FILES, "Nox") || checkQEmuDrivers() || checkFiles(PIPES, "Pipes") || checkIp() || (checkQEmuProps() && checkFiles(X86_FILES, "X86")); return result; } public boolean checkPackageName() { if (!isCheckPackage || mListPackageName.isEmpty()) { return false; } final PackageManager packageManager = mContext.getPackageManager(); for (final String pkgName : mListPackageName) { final Intent tryIntent = packageManager.getLaunchIntentForPackage(pkgName); if (tryIntent != null) { final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(tryIntent, PackageManager.MATCH_DEFAULT_ONLY); if (!resolveInfos.isEmpty()) { return true; } } } return false; } private boolean checkTelephony() { if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED && this.isTelephony && isSupportTelePhony()) { return checkPhoneNumber() || checkDeviceId() || checkImsi() || checkOperatorNameAndroid(); } return false; } private boolean checkPhoneNumber() { TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); try { @SuppressLint({"HardwareIds", "MissingPermission"}) String phoneNumber = telephonyManager.getLine1Number(); for (String number : PHONE_NUMBERS) { if (number.equalsIgnoreCase(phoneNumber)) { log(" check phone number is detected"); return true; } } } catch (Exception e) { log("No permission to detect access of Line1Number"); } return false; } private boolean checkDeviceId() { TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); try { @SuppressLint({"HardwareIds", "MissingPermission"}) String deviceId = telephonyManager.getDeviceId(); for (String known_deviceId : DEVICE_IDS) { if (known_deviceId.equalsIgnoreCase(deviceId)) { log("Check device id is detected"); return true; } } } catch (Exception e) { log("No permission to detect access of DeviceId"); } return false; } private boolean checkImsi() { TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); try { @SuppressLint({"HardwareIds", "MissingPermission"}) String imsi = telephonyManager.getSubscriberId(); for (String known_imsi : IMSI_IDS) { if (known_imsi.equalsIgnoreCase(imsi)) { log("Check imsi is detected"); return true; } } } catch (Exception e) { log("No permission to detect access of SubscriberId"); } return false; } private boolean checkOperatorNameAndroid() { String operatorName = ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE)).getNetworkOperatorName(); if (operatorName.equalsIgnoreCase("android")) { log("Check operator name android is detected"); return true; } return false; } private boolean checkQEmuDrivers() { for (File drivers_file : new File[]{new File("/proc/tty/drivers"), new File("/proc/cpuinfo")}) { if (drivers_file.exists() && drivers_file.canRead()) { byte[] data = new byte[1024]; try { InputStream is = new FileInputStream(drivers_file); is.read(data); is.close(); } catch (Exception exception) { exception.printStackTrace(); } String driver_data = new String(data); for (String known_qemu_driver : QEMU_DRIVERS) { if (driver_data.contains(known_qemu_driver)) { log("Check QEmuDrivers is detected"); return true; } } } } return false; } private boolean checkFiles(String[] targets, String type) { for (String pipe : targets) { File qemu_file = new File(pipe); if (qemu_file.exists()) { log("Check " + type + " is detected"); return true; } } return false; } private boolean checkQEmuProps() { int found_props = 0; for (Property property : PROPERTIES) { String property_value = getProp(mContext, property.name); if ((property.seek_value == null) && (property_value != null)) { found_props++; } if ((property.seek_value != null) && (property_value.contains(property.seek_value))) { found_props++; } } if (found_props >= MIN_PROPERTIES_THRESHOLD) { log("Check QEmuProps is detected"); return true; } return false; } private boolean checkIp() { boolean ipDetected = false; if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.INTERNET) == PackageManager.PERMISSION_GRANTED) { String[] args = {"/system/bin/netcfg"}; StringBuilder stringBuilder = new StringBuilder(); try { ProcessBuilder builder = new ProcessBuilder(args); builder.directory(new File("/system/bin/")); builder.redirectErrorStream(true); Process process = builder.start(); InputStream in = process.getInputStream(); byte[] re = new byte[1024]; while (in.read(re) != -1) { stringBuilder.append(new String(re)); } in.close(); } catch (Exception ex) { // empty catch } String netData = stringBuilder.toString(); log("netcfg data -> " + netData); if (!TextUtils.isEmpty(netData)) { String[] array = netData.split("\n"); for (String lan : array) { if ((lan.contains("wlan0") || lan.contains("tunl0") || lan.contains("eth0")) && lan.contains(IP)) { ipDetected = true; log("Check IP is detected"); break; } } } } return ipDetected; } private String getProp(Context context, String property) { try { ClassLoader classLoader = context.getClassLoader(); Class<?> systemProperties = classLoader.loadClass("android.os.SystemProperties"); Method get = systemProperties.getMethod("get", String.class); Object[] params = new Object[1]; params[0] = property; return (String) get.invoke(systemProperties, params); } catch (Exception exception) { // empty catch } return null; } private boolean isSupportTelePhony() { PackageManager packageManager = mContext.getPackageManager(); boolean isSupport = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY); log("Supported TelePhony: " + isSupport); return isSupport; } private void log(String str) { if (this.isDebug) { Log.d(getClass().getName(), str); } } } class Property { public String name; public String seek_value; public Property(String name, String seek_value) { this.name = name; this.seek_value = seek_value; } }
사용법
Kotlin에서 사용할때는 이렇게 하면된다.
EmulatorDetector.with(this).detect { isEmulator -> runOnUiThread { // TODO... } }
'프로그래밍 > Android' 카테고리의 다른 글
[안드로이드] Context 사용 범위 정리 (0) | 2021.03.12 |
---|---|
[안드로이드] Nested RecyclerView 구현시 스크롤 문제 해결하기 (0) | 2021.03.08 |
[안드로이드] RecyclerView를 잘 사용하기 위한 팁들. (0) | 2021.02.05 |
[안드로이드] 예제로 알아보는 바인드된 서비스 (Bound Service) (0) | 2021.01.18 |
[안드로이드] 패키지명(Package name)으로 앱 실행하기 (0) | 2021.01.18 |