//@formatter:off /** * DatapointHelper.java Copyright (C) 2013 Char Software Inc., DBA Localytics. This code is provided under the Localytics Modified * BSD License. A copy of this license has been distributed in a file called LICENSE with this source code. Please visit * www.localytics.com for more information. */ //@formatter:on package com.localytics.android; import android.Manifest.permission; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; import android.telephony.TelephonyManager; import android.util.Log; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Provides a number of static functions to aid in the collection and formatting of datapoints. * <p> * Note: this is not a public API. */ /* package */final class DatapointHelper { /** * AndroidID known to be duplicated across many devices due to manufacturer bugs. */ private static final String INVALID_ANDROID_ID = "9774d56d682e549c"; //$NON-NLS-1$ /** * The path to the device_id file in previous versions of the Localytics library */ private static final String LEGACY_DEVICE_ID_FILE = "/localytics/device_id"; //$NON-NLS-1$ /** * Cached array of the String class, for reflection */ private static final Class<?>[] STRING_CLASS_ARRAY = new Class<?>[] { String.class }; /** * Cached array of the Android Wi-Fi hardware constant. */ private static final Object[] HARDWARE_WIFI = new Object[] { "android.hardware.wifi" }; //$NON-NLS-1$ /** * Cached array of the Android Wi-Fi hardware constant. */ private static final Object[] HARDWARE_TELEPHONY = new Object[] { "android.hardware.telephony" }; //$NON-NLS-1$ /** * Private constructor prevents instantiation * * @throws UnsupportedOperationException because this class cannot be instantiated. */ private DatapointHelper() { throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ } /** * @return current Android API level. */ /* package */static int getApiLevel() { try { // Although the Build.VERSION.SDK field has existed since API 1, it is deprecated and could be removed // in the future. Therefore use reflection to retrieve it for maximum forward compatibility. final Class<?> buildClass = Build.VERSION.class; final String sdkString = (String) buildClass.getField("SDK").get(null); //$NON-NLS-1$ return Integer.parseInt(sdkString); } catch (final Exception e) { Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$ // Although probably not necessary, protects from the aforementioned deprecation try { final Class<?> buildClass = Build.VERSION.class; return buildClass.getField("SDK_INT").getInt(null); //$NON-NLS-1$ } catch (final Exception ignore) { if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "Caught exception", ignore); //$NON-NLS-1$ } } } // Worse-case scenario, assume Cupcake return 3; } /** * Gets a 1-way hashed value of the device's Android ID. This value is encoded using a SHA-256 one way hash and therefore * cannot be used to determine what device this data came from. * * @param context The context used to access the settings resolver * @return An 1-way hashed version of the {@link android.provider.Settings.Secure#ANDROID_ID}. May return null if an Android * ID or the hashing algorithm is not available. */ public static String getAndroidIdHashOrNull(final Context context) { String androidId = getAndroidIdOrNull(context); return (androidId == null) ? null : getSha256_buggy(androidId); } /** * Gets the device's Android ID. * * @param context The context used to access the settings resolver * @return The Android ID. May return null if Android ID is not available. */ public static String getAndroidIdOrNull(final Context context) { // Make sure a legacy version of the SDK didn't leave behind a device ID. // If it did, this ID must be used to keep user counts accurate final File fp = new File(context.getFilesDir() + LEGACY_DEVICE_ID_FILE); if (fp.exists() && fp.length() > 0) { try { BufferedReader reader = null; try { final char[] buf = new char[100]; int numRead; reader = new BufferedReader(new FileReader(fp), 128); numRead = reader.read(buf); final String deviceId = String.copyValueOf(buf, 0, numRead); reader.close(); return deviceId; } catch (final FileNotFoundException e) { if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$ } } finally { if (null != reader) { reader.close(); } } } catch (final IOException e) { if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$ } } } final String androidId = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID); if (androidId == null || androidId.toLowerCase().equals(INVALID_ANDROID_ID)) { return null; } return androidId; } /** * Gets a 1-way hashed value of the device's unique serial number. This value is encoded using a SHA-256 one way hash and * therefore cannot be used to determine what device this data came from. * <p> * Note: {@link android.os.Build#SERIAL} was introduced in SDK 9. For older SDKs, this method will return null. * * @return An 1-way hashed version of the {@link android.os.Build#SERIAL}. May return null if a serial or the hashing * algorithm is not available. */ /* * Suppress JavaDoc warnings because the {@link android.os.Build#SERIAL} fails when built with SDK 4. */ @SuppressWarnings("javadoc") public static String getSerialNumberHashOrNull() { /* * Obtain the device serial number using reflection, since serial number was added in SDK 9 */ String serialNumber = null; if (Constants.CURRENT_API_LEVEL >= 9) { try { serialNumber = (String) Build.class.getField("SERIAL").get(null); //$NON-NLS-1$ } catch (final Exception e) { /* * This should never happen, as SERIAL is a public field added in SDK 9. */ throw new RuntimeException(e); } } if (serialNumber == null) { return null; } return getSha256_buggy(serialNumber); } /** * Gets the device's telephony ID (e.g. IMEI/MEID). * <p> * Note: this method will return null if {@link permission#READ_PHONE_STATE} is not available. This method will also return * null on devices that do not have telephony. * * @param context The context used to access the phone state. * @return An the {@link TelephonyManager#getDeviceId()}. Null if an ID is not available, or if * {@link permission#READ_PHONE_STATE} is not available. */ public static String getTelephonyDeviceIdOrNull(final Context context) { if (Constants.CURRENT_API_LEVEL >= 7) { final Boolean hasTelephony = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", STRING_CLASS_ARRAY, HARDWARE_TELEPHONY); //$NON-NLS-1$ if (!hasTelephony.booleanValue()) { if (Constants.IS_LOGGABLE) { Log.i(Constants.LOG_TAG, "Device does not have telephony; cannot read telephony id"); //$NON-NLS-1$ } return null; } } /* * Note: Sometimes Android will deny a package READ_PHONE_STATE permissions, even if the package has the permission. It * appears to be a race condition that occurs during installation. */ String id = null; if (context.getPackageManager().checkPermission(permission.READ_PHONE_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) { final TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); id = manager.getDeviceId(); } else { if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "Application does not have permission READ_PHONE_STATE; determining device id is not possible. Please consider requesting READ_PHONE_STATE in the AndroidManifest"); //$NON-NLS-1$ } } return id; } /** * Gets the device's Wi-Fi MAC address. * <p> * Note: this method will return null if {@link permission#ACCESS_WIFI_STATE} is not available. This method will also return * null on devices that do not have Wi-Fi. Even if the application has {@link permission#ACCESS_WIFI_STATE} and the device * does have Wi-Fi hardware, this method may still return null if Wi-Fi is disabled or not associated with an access point. * * @param context The context used to access the Wi-Fi state. * @return A SHA-256 of the Wi-Fi MAC address. Null if a MAC is not available, or if {@link permission#ACCESS_WIFI_STATE} is * not available. */ public static String getWifiMacHashOrNull(final Context context) { if (Constants.CURRENT_API_LEVEL >= 8) { final Boolean hasWifi = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", STRING_CLASS_ARRAY, HARDWARE_WIFI); //$NON-NLS-1$ if (!hasWifi.booleanValue()) { if (Constants.IS_LOGGABLE) { Log.i(Constants.LOG_TAG, "Device does not have Wi-Fi; cannot read Wi-Fi MAC"); //$NON-NLS-1$ } return null; } } /* * Most applications using the Localytics library probably shouldn't have Wi-Fi permissions. This is primarily for * customers who *really* need to identify devices. */ String id = null; if (context.getPackageManager().checkPermission(permission.ACCESS_WIFI_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) { final WifiManager manager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); final WifiInfo info = manager.getConnectionInfo(); if (null != info) { id = info.getMacAddress(); } } else { if (Constants.IS_LOGGABLE) { /* * Yes, this log message is different from the one that reads telephony ID. MAC address is less important than * telephony ID and most applications probably don't need the ACCESS_WIFI_STATE permission. */ Log.i(Constants.LOG_TAG, "Application does not have permission ACCESS_WIFI_STATE; determining MAC address is not possible."); //$NON-NLS-1$ } } if (null == id) { return null; } return getSha256_buggy(id); } /** * Determines the type of network this device is connected to. * * @param context the context used to access the device's WIFI * @param telephonyManager The manager used to access telephony info * @return The type of network, or unknown if the information is unavailable */ public static String getNetworkType(final Context context, final TelephonyManager telephonyManager) { try { if (context.getPackageManager().checkPermission(permission.ACCESS_WIFI_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) { final NetworkInfo wifiInfo = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getNetworkInfo(ConnectivityManager.TYPE_WIFI); if (null != wifiInfo && wifiInfo.isConnectedOrConnecting()) { return "wifi"; //$NON-NLS-1$ } } else { if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "Application does not have one more more of the following permissions: ACCESS_WIFI_STATE. Determining Wi-Fi connectivity is unavailable"); //$NON-NLS-1$ } } } catch (final SecurityException e) { /* * Although the documentation doesn't declare it, sometimes the ConnectivityService will throw an exception for * permission ACCESS_NETWORK_STATE */ if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "Application does not have the permission ACCESS_NETWORK_STATE. Determining Wi-Fi connectivity is unavailable", e); //$NON-NLS-1$ } } return "android_network_type_" + telephonyManager.getNetworkType(); //$NON-NLS-1$ } /** * Gets the device manufacturer's name. This is only available on SDK 4 or greater, so on SDK 3 this method returns the * constant string "unknown". * * @return A string naming the manufacturer */ public static String getManufacturer() { String mfg = "unknown"; //$NON-NLS-1$ if (Constants.CURRENT_API_LEVEL > 3) { try { final Class<?> buildClass = Build.class; mfg = (String) buildClass.getField("MANUFACTURER").get(null); //$NON-NLS-1$ } catch (final Exception ignore) { if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "Caught exception", ignore); //$NON-NLS-1$ } } } return mfg; } /** * Retrieve the Facebook attribution cookie at installation time * * @return Facebook attribution cookie or null if unavailable */ public static String getFBAttribution(final Context context) { String facebookAttribution = null; final ContentResolver contentResolver = context.getContentResolver(); final Uri uri = Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider"); //$NON-NLS-1$ final String columnName = "aid"; //$NON-NLS-1$ final String[] projection = { columnName }; Cursor cursor = null; try { cursor = contentResolver.query(uri, projection, null, null, null); if (null != cursor && cursor.moveToFirst()) { facebookAttribution = cursor.getString(cursor.getColumnIndex(columnName)); } } catch (final Exception e) { if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "Error reading FB attribution", e); //$NON-NLS-1$ } } finally { if (null != cursor) { cursor.close(); cursor = null; } } return facebookAttribution; } /** * Gets the versionName of the application. * * @param context {@link Context}. Cannot be null. * @return The application's version */ public static String getAppVersion(final Context context) { final PackageManager pm = context.getPackageManager(); try { final String versionName = pm.getPackageInfo(context.getPackageName(), 0).versionName; /* * If there is no versionName in the Android Manifest, the versionName will be null. */ if (null == versionName) { if (Constants.IS_LOGGABLE) { Log.w(Constants.LOG_TAG, "versionName was null--is a versionName attribute set in the Android Manifest?"); //$NON-NLS-1$ } return "unknown"; //$NON-NLS-1$ } return versionName; } catch (final PackageManager.NameNotFoundException e) { /* * This should never occur--our own package must exist for this code to be running */ throw new RuntimeException(e); } } public static String getLocalyticsAppKeyOrNull(final Context context) { String appKey = null; try { ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); Object metaData = applicationInfo.metaData.get(Constants.LOCALYTICS_METADATA_APP_KEY); if (metaData instanceof String) { appKey = (String)metaData; } } catch (final PackageManager.NameNotFoundException e) { /* * This should never occur--our own package must exist for this code to be running */ throw new RuntimeException(e); } return appKey; } public static String getLocalyticsRollupKeyOrNull(final Context context) { String rollupKey = null; try { ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); if (applicationInfo.metaData != null) { Object metaData = (String)applicationInfo.metaData.get(Constants.LOCALYTICS_METADATA_ROLLUP_KEY); if (metaData instanceof String) { rollupKey = (String)metaData; } } } catch (final PackageManager.NameNotFoundException e) { /* * This should never occur--our own package must exist for this code to be running */ throw new RuntimeException(e); } return rollupKey; } /** * Helper method to generate a SHA-256 hash of a given String. * <p> * Note: This implementation contains a minor bug. The returned value may be shorter than expected, because it will drop any * leading zeros on the front of the SHA-256 string. This bug cannot be fixed because the server may have already stored the * truncated SHA-256 and changing this will cause a mismatch. * * @param string String to hash. Cannot be null. * @return hashed version of the string using SHA-256. */ /* package */static String getSha256_buggy(final String string) { if (Constants.IS_PARAMETER_CHECKING_ENABLED) { if (null == string) { throw new IllegalArgumentException("string cannot be null"); //$NON-NLS-1$ } } try { final MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$ final byte[] digest = md.digest(string.getBytes("UTF-8")); //$NON-NLS-1$ final BigInteger hashedNumber = new BigInteger(1, digest); return hashedNumber.toString(16); } catch (final NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (final UnsupportedEncodingException e) { throw new RuntimeException(e); } } }