/* * Copyright (c) 2015. Thomas Haertel * * Licensed under Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.thomashaertel.device.identification.internal; import android.Manifest.permission; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.content.pm.PackageManager; import android.net.wifi.WifiManager; import android.os.Build; import android.provider.Settings.Secure; import android.telephony.TelephonyManager; import android.util.Log; import com.thomashaertel.device.identification.internal.util.HashUtil; import java.security.NoSuchAlgorithmException; import java.util.UUID; public final class DeviceIdentifier { public static final String UNKNOWN = "unknown"; private static final String PROPERTY_SERIAL_NO = "ro.serialno"; /** * see http://code.google.com/p/android/issues/detail?id=10603 */ private static final String ANDROID_ID_BUG_MSG = "The device suffers from " + "the Android ID bug - its ID is the emulator ID : " + IDs.BUGGY_ANDROID_ID; private static volatile String deviceId; // volatile needed - see EJ item 71 private static volatile String uuid; // volatile needed - see EJ item 71 private static volatile String md5hash; // volatile needed - see EJ item 71 private DeviceIdentifier() { } // need lazy initialization to get a context /** * Returns a unique identifier for this device. The first (in the order the * enums constants as defined in the IDs enum) non null identifier is * returned or a DeviceIDException is thrown. A DeviceIDException is also * thrown if ignoreBuggyAndroidID is false and the device has the Android ID * bug * * @param ctx an Android constant (to retrieve system services) * @param ignoreBuggyAndroidID if false, on a device with the android ID bug, the buggy * android ID is not returned instead a DeviceIDException is * thrown * @return a *device* ID - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID */ public static String getDeviceIdentifier(Context ctx, boolean ignoreBuggyAndroidID) throws DeviceIDException { String result = deviceId; if (result == null) { synchronized (DeviceIdentifier.class) { result = deviceId; if (result == null) { for (IDs id : IDs.values()) { try { result = deviceId = id.getId(ctx); } catch (DeviceIDNotUniqueException e) { if (!ignoreBuggyAndroidID) throw new DeviceIDException(e); } if (result != null) return result; } throw new DeviceIDException(); } } } return result; } /** * Returns a UUID specific to the device. There are possibly some instances where this does * not work e.g. in the emulator or if there is no SIM in the phone. Then a DeviceIDException * is thrown. A DeviceIDException is also thrown if ignoreBuggyAndroidID is false and the device * has the Android ID bug. If ignoreBuggyAndroidID is true and the device has the Android ID bug * a pseudo ID is taken instead. * * @param ctx an Android constant (to retrieve system services) * @param ignoreBuggyAndroidID if false, on a device with the android ID bug, the buggy * android ID is not returned instead a DeviceIDException is * thrown * @return a *device* ID - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID * see http://stackoverflow.com/questions/4402262/device-identifier-of-android-emulator */ public static String getDeviceIdentifierUuid(Context ctx, boolean ignoreBuggyAndroidID) throws DeviceIDException { String result = uuid; if (result == null) { synchronized (DeviceIdentifier.class) { result = uuid; if (result == null) { final String deviceId = IDs.IMSI.getId(ctx); final String serialNo = IDs.SIM_SERIAL_NO.getId(ctx); UUID deviceUuid = null; // compute least significant bits for uuid final long lsb = ((long) deviceId.hashCode() << 32) | serialNo.hashCode(); try { final String androidId = IDs.ANDROID_ID.getId(ctx); deviceUuid = new UUID(androidId.hashCode(), lsb); } catch (DeviceIDNotUniqueException e) { if (!ignoreBuggyAndroidID) throw new DeviceIDException(e); final String androidId = getPseudoDeviceId(); deviceUuid = new UUID(androidId.hashCode(), lsb); } result = uuid = deviceUuid.toString(); } } } return result; } /** * Returns a UUID specific to the device. There are possibly some instances where this does * not work e.g. in the emulator or if there is no SIM in the phone. Then a DeviceIDException * is thrown. A DeviceIDException is also thrown if ignoreBuggyAndroidID is false and the device * has the Android ID bug. If ignoreBuggyAndroidID is true and the device has the Android ID bug * a pseudo ID is taken instead. * * @param ctx an Android constant (to retrieve system services) * @param ignoreBuggyAndroidID if false, on a device with the android ID bug, the buggy * android ID is not returned instead a DeviceIDException is * thrown * @return a *device* ID - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID * see http://www.pocketmagic.net/android-unique-device-id/ */ public static String getDeviceIdentifierMd5(Context ctx, boolean ignoreBuggyAndroidID) throws DeviceIDException { String result = md5hash; if (result == null) { synchronized (DeviceIdentifier.class) { result = md5hash; if (result == null) { StringBuilder sb = new StringBuilder(); for (IDs id : IDs.values()) { try { sb.append(id.getId(ctx)); } catch (DeviceIDNotUniqueException e) { if (!ignoreBuggyAndroidID) throw new DeviceIDException(e); } try { result = md5hash = HashUtil.computeHashMD5(sb.toString()); } catch (NoSuchAlgorithmException e) { throw new DeviceIDException(e); } if (result != null) return result; } throw new DeviceIDException(); } } } return result; } /** * Returns the IMEI for GSM and the MEID or ESN for CDMA phones. * * @param ctx an Android constant (to retrieve system services) * @return the IMEI or MEID - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID */ public static synchronized String getImei(Context ctx) throws DeviceIDException { return IDs.IMEI.getId(ctx); } /** * Returns the IMSI of a phone. * * @param ctx an Android constant (to retrieve system services) * @return the IMSI - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID */ public static synchronized String getImsi(Context ctx) throws DeviceIDException { return IDs.IMSI.getId(ctx); } /** * Returns the sim card serial number of a phone. * * @param ctx an Android constant (to retrieve system services) * @return the sim serial number - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID */ public static synchronized String getSimSerialNo(Context ctx) throws DeviceIDException { return IDs.SIM_SERIAL_NO.getId(ctx); } /** * Returns the serial number of a device. * * @param ctx an Android constant (to retrieve system services) * @return the device serial number - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID */ public static synchronized String getSerialNo(Context ctx) throws DeviceIDException { return IDs.SERIAL_NO.getId(ctx); } /** * Returns the unique DeviceID * Works for Android 2.2 and above, but buggy on some devices * * @param ctx an Android constant (to retrieve system services) * @return the android id - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID* */ public static synchronized String getAndroidId(Context ctx) throws DeviceIDException { return IDs.ANDROID_ID.getId(ctx); } /** * Returns the unique wifi hardware address * Works only if wifi module is present * * @param ctx an Android constant (to retrieve system services) * @return the wifi MAC address - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID* */ public static synchronized String getWifiId(Context ctx) throws DeviceIDException { return IDs.WIFI_MAC.getId(ctx); } /** * Returns the unique bluetooth hardware address * Works only if bluetooth module is present * * @param ctx an Android constant (to retrieve system services) * @return the wifi MAC address - null is never returned, instead a DeviceIDException is thrown * @throws DeviceIDException if none of the enum methods manages to return a device ID* */ public static synchronized String getBluetoothId(Context ctx) throws DeviceIDException { return IDs.BLUETOOTH_MAC.getId(ctx); } /** * Returns a generated unique pseudo ID from android.os.Build Constants * Works for all devices and Android versions * * @return a pseudo id - null is never returned * see http://www.pocketmagic.net/2011/02/android-unique-device-id/ */ public static String getPseudoDeviceId() { return "35" + //we make this look like a valid IMEI Build.BOARD.length() % 10 + Build.BRAND.length() % 10 + Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 + Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 + Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 + Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 + Build.TAGS.length() % 10 + Build.TYPE.length() % 10 + Build.USER.length() % 10; //13 digits } private static void assertPermission(Context ctx, String perm) { final int checkPermission = ctx.getPackageManager().checkPermission( perm, ctx.getPackageName()); if (checkPermission != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Permission " + perm + " is required"); } } private static enum IDs { IMEI { /** * getDeviceId() function Returns the unique device ID. * for example,the IMEI for GSM and the MEID or ESN for CDMA phones. */ @Override String getId(Context ctx) { final TelephonyManager tm = (TelephonyManager) ctx .getSystemService(Context.TELEPHONY_SERVICE); if (tm == null) { w("Telephony Manager not available"); return null; } assertPermission(ctx, permission.READ_PHONE_STATE); return tm.getDeviceId(); } }, IMSI { /** * getSubscriberId() function Returns the unique subscriber ID, * for example, the IMSI for a GSM phone. */ @Override String getId(Context ctx) { final TelephonyManager tm = (TelephonyManager) ctx .getSystemService(Context.TELEPHONY_SERVICE); if (tm == null) { w("Telephony Manager not available"); return null; } assertPermission(ctx, permission.READ_PHONE_STATE); return tm.getSubscriberId(); } }, SIM_SERIAL_NO { /** * getSimSerialNumber() function Returns the unique sim card ID, * for example, the SIM Card ID for a GSM phone. */ @Override String getId(Context ctx) { final TelephonyManager tm = (TelephonyManager) ctx .getSystemService(Context.TELEPHONY_SERVICE); if (tm == null) { w("Telephony Manager not available"); return null; } assertPermission(ctx, permission.READ_PHONE_STATE); return tm.getSimSerialNumber(); } }, SERIAL_NO { /** * System Property ro.serialno returns the serial number as unique number * Works for Android 2.3 and above */ @Override String getId(Context ctx) { // no permission needed ! return SystemPropertiesProxy.get(ctx, PROPERTY_SERIAL_NO, UNKNOWN); } }, ANDROID_ID { /** * Settings.Secure.ANDROID_ID returns the unique DeviceID * Works for Android 2.2 and above */ @Override String getId(Context ctx) throws DeviceIDException { // no permission needed ! final String androidId = Secure.getString( ctx.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID); if (BUGGY_ANDROID_ID.equals(androidId)) { e(ANDROID_ID_BUG_MSG); throw new DeviceIDNotUniqueException(); } return androidId; } }, WIFI_MAC { /** * getMacAddress() returns the unique wifi hardware address * Works only if wifi module is present */ @Override String getId(Context ctx) { WifiManager wm = (WifiManager) ctx .getSystemService(Context.WIFI_SERVICE); if (wm == null) { w("Wifi Manager not available"); return null; } assertPermission(ctx, permission.ACCESS_WIFI_STATE); // I guess getMacAddress() has no java doc !!! return wm.getConnectionInfo().getMacAddress(); } }, BLUETOOTH_MAC { /** * getMacAddress() returns the unique bluetooth hardware address * Works only if bluetooth module is present */ @Override String getId(Context ctx) { BluetoothAdapter ba = BluetoothAdapter.getDefaultAdapter(); if (ba == null) { w("Bluetooth Adapter not available"); return null; } assertPermission(ctx, permission.BLUETOOTH); return ba.getAddress(); } }, PSEUDO_ID { /** * returns a generated pseudo unique ID * Works for all devices and Android versions */ @Override String getId(Context ctx) { return getPseudoDeviceId(); } }; static final String BUGGY_ANDROID_ID = "9774d56d682e549c"; private final static String TAG = IDs.class.getSimpleName(); private static void w(String msg) { Log.w(TAG, msg); } private static void e(String msg) { Log.e(TAG, msg); } abstract String getId(Context ctx) throws DeviceIDException; } // ========================================================================= // Exceptions // ========================================================================= public static class DeviceIDException extends Exception { private static final long serialVersionUID = -8083699995384519417L; private static final String NO_ANDROID_ID = "Could not retrieve a device ID"; public DeviceIDException(Throwable throwable) { super(NO_ANDROID_ID, throwable); } public DeviceIDException(String detailMessage) { super(detailMessage); } public DeviceIDException() { super(NO_ANDROID_ID); } } public static final class DeviceIDNotUniqueException extends DeviceIDException { private static final long serialVersionUID = -8940090896069484955L; public DeviceIDNotUniqueException() { super(ANDROID_ID_BUG_MSG); } } }