From 8d32121725205816cd32776c7969a7361e4e1fdf Mon Sep 17 00:00:00 2001 From: Mansi Pandya Date: Fri, 29 Nov 2024 14:53:21 -0500 Subject: [PATCH] refactor: Migrate Internal DeviceAttributes class to kotlin --- android-core/build.gradle | 1 + .../mparticle/internal/DeviceAttributes.java | 350 ------------------ .../mparticle/internal/DeviceAttributes.kt | 343 +++++++++++++++++ .../internal/DeviceAttributesTest.kt | 112 ++++++ 4 files changed, 456 insertions(+), 350 deletions(-) delete mode 100644 android-core/src/main/java/com/mparticle/internal/DeviceAttributes.java create mode 100644 android-core/src/main/java/com/mparticle/internal/DeviceAttributes.kt diff --git a/android-core/build.gradle b/android-core/build.gradle index 288d463fb..a5323d20e 100644 --- a/android-core/build.gradle +++ b/android-core/build.gradle @@ -86,6 +86,7 @@ android { jvmArgs += ['--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED'] jvmArgs += ['--add-opens', 'java.base/java.util.concurrent.atomic=ALL-UNNAMED'] jvmArgs += ['--add-opens', 'java.base/java.lang.ref=ALL-UNNAMED'] + jvmArgs += ['--add-opens', 'java.base/java.lang.reflect=ALL-UNNAMED'] } if (useOrchestrator()) { execution 'ANDROIDX_TEST_ORCHESTRATOR' diff --git a/android-core/src/main/java/com/mparticle/internal/DeviceAttributes.java b/android-core/src/main/java/com/mparticle/internal/DeviceAttributes.java deleted file mode 100644 index 0153f9611..000000000 --- a/android-core/src/main/java/com/mparticle/internal/DeviceAttributes.java +++ /dev/null @@ -1,350 +0,0 @@ -package com.mparticle.internal; - -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.telephony.TelephonyManager; -import android.util.DisplayMetrics; -import android.view.WindowManager; - -import com.mparticle.MParticle; -import com.mparticle.internal.Constants.MessageKey; -import com.mparticle.internal.Constants.PrefKeys; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Locale; -import java.util.Set; -import java.util.TimeZone; - -public class DeviceAttributes { - - //re-use this whenever an attribute can't be determined - static final String UNKNOWN = "unknown"; - private static volatile String deviceImei; - private JSONObject deviceInfo; - private JSONObject appInfo; - private boolean firstCollection = true; - private MParticle.OperatingSystem operatingSystem; - - /** - * package-private - **/ - DeviceAttributes(MParticle.OperatingSystem operatingSystem) { - this.operatingSystem = operatingSystem; - } - - public static void setDeviceImei(String deviceImei) { - DeviceAttributes.deviceImei = deviceImei; - } - - public static String getDeviceImei() { - return deviceImei; - } - - private int getSideloadedKitsCount() { - try { - Set kits = MParticle.getInstance().Internal().getKitManager().getSupportedKits(); - int count = 0; - for (Integer kitId : kits) { - if (kitId >= 1000000) { - count++; - } - } - return count; - } catch (Exception e) { - Logger.debug("Exception while adding sideloadedKitsCount to Device Attribute"); - return 0; - } - } - - /** - * Generates a collection of application attributes that will not change during an app's process. - *

- * This contains logic that MUST only be called once per app run. - * - * @param appContext the application context - * @return a JSONObject of application-specific attributes - */ - public JSONObject getStaticApplicationInfo(Context appContext) { - JSONObject attributes = new JSONObject(); - SharedPreferences preferences = appContext.getSharedPreferences(Constants.PREFS_FILE, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - try { - long now = System.currentTimeMillis(); - PackageManager packageManager = appContext.getPackageManager(); - String packageName = appContext.getPackageName(); - attributes.put(MessageKey.APP_PACKAGE_NAME, packageName); - attributes.put(MessageKey.SIDELOADED_KITS_COUNT, getSideloadedKitsCount()); - String versionCode = UNKNOWN; - try { - PackageInfo pInfo = appContext.getPackageManager().getPackageInfo(packageName, 0); - versionCode = Integer.toString(pInfo.versionCode); - attributes.put(MessageKey.APP_VERSION, pInfo.versionName); - } catch (PackageManager.NameNotFoundException nnfe) { - } - - attributes.put(MessageKey.APP_VERSION_CODE, versionCode); - - String installerPackageName = packageManager.getInstallerPackageName(packageName); - if (installerPackageName != null) { - attributes.put(MessageKey.APP_INSTALLER_NAME, installerPackageName); - } - try { - ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0); - attributes.put(MessageKey.APP_NAME, packageManager.getApplicationLabel(appInfo)); - } catch (PackageManager.NameNotFoundException e) { - // ignore missing data - } - - attributes.put(MessageKey.BUILD_ID, MPUtility.getBuildUUID(versionCode)); - attributes.put(MessageKey.APP_DEBUG_SIGNING, MPUtility.isAppDebuggable(appContext)); - attributes.put(MessageKey.APP_PIRATED, preferences.getBoolean(PrefKeys.PIRATED, false)); - - attributes.put(MessageKey.MPARTICLE_INSTALL_TIME, preferences.getLong(PrefKeys.INSTALL_TIME, now)); - if (!preferences.contains(PrefKeys.INSTALL_TIME)) { - editor.putLong(PrefKeys.INSTALL_TIME, now); - } - UserStorage userStorage = ConfigManager.getUserStorage(appContext); - int totalRuns = userStorage.getTotalRuns(0) + 1; - userStorage.setTotalRuns(totalRuns); - attributes.put(MessageKey.LAUNCH_COUNT, totalRuns); - - long useDate = userStorage.getLastUseDate(0); - attributes.put(MessageKey.LAST_USE_DATE, useDate); - userStorage.setLastUseDate(now); - try { - PackageInfo pInfo = packageManager.getPackageInfo(packageName, 0); - int persistedVersion = preferences.getInt(PrefKeys.COUNTER_VERSION, -1); - int countSinceUpgrade = userStorage.getLaunchesSinceUpgrade(); - long upgradeDate = preferences.getLong(PrefKeys.UPGRADE_DATE, now); - - if (persistedVersion < 0 || persistedVersion != pInfo.versionCode) { - countSinceUpgrade = 0; - upgradeDate = now; - editor.putInt(PrefKeys.COUNTER_VERSION, pInfo.versionCode); - editor.putLong(PrefKeys.UPGRADE_DATE, upgradeDate); - } - countSinceUpgrade += 1; - userStorage.setLaunchesSinceUpgrade(countSinceUpgrade); - - attributes.put(MessageKey.LAUNCH_COUNT_SINCE_UPGRADE, countSinceUpgrade); - attributes.put(MessageKey.UPGRADE_DATE, upgradeDate); - } catch (PackageManager.NameNotFoundException e) { - // ignore missing data - } - MParticle instance = MParticle.getInstance(); - if (instance != null) { - attributes.put(MessageKey.ENVIRONMENT, instance.Internal().getConfigManager().getEnvironment().getValue()); - } - attributes.put(MessageKey.INSTALL_REFERRER, preferences.getString(Constants.PrefKeys.INSTALL_REFERRER, null)); - - boolean install = preferences.getBoolean(PrefKeys.FIRST_RUN_INSTALL, true); - attributes.put(MessageKey.FIRST_SEEN_INSTALL, install); - editor.putBoolean(PrefKeys.FIRST_RUN_INSTALL, false); - } catch (Exception e) { - // again different devices can do terrible things, make sure that we don't bail out completely - // and return at least what we've built so far. - } finally { - editor.apply(); - } - return attributes; - } - - void updateInstallReferrer(Context context, JSONObject attributes) { - SharedPreferences preferences = context.getSharedPreferences(Constants.PREFS_FILE, Context.MODE_PRIVATE); - try { - attributes.put(MessageKey.INSTALL_REFERRER, preferences.getString(Constants.PrefKeys.INSTALL_REFERRER, null)); - } catch (JSONException ignored) { - // this, hopefully, should never fail - } - } - - public static void addAndroidId(JSONObject attributes, Context context) throws JSONException { - String androidId = MPUtility.getAndroidID(context); - if (!MPUtility.isEmpty(androidId)) { - attributes.put(MessageKey.DEVICE_ID, androidId); - attributes.put(MessageKey.DEVICE_ANID, androidId); - attributes.put(MessageKey.DEVICE_OPEN_UDID, MPUtility.getOpenUDID(context)); - } - } - - /** - * Generates a collection of device attributes that will not change during an app's process. - *

- * This contains logic that MUST only be called once per app run. - * - * @param appContext the application context - * @return a JSONObject of device-specific attributes - */ - JSONObject getStaticDeviceInfo(Context appContext) { - final JSONObject attributes = new JSONObject(); - - try { - // device/OS attributes - attributes.put(MessageKey.BUILD_ID, android.os.Build.ID); - attributes.put(MessageKey.BRAND, Build.BRAND); - attributes.put(MessageKey.PRODUCT, android.os.Build.PRODUCT); - attributes.put(MessageKey.DEVICE, android.os.Build.DEVICE); - attributes.put(MessageKey.MANUFACTURER, android.os.Build.MANUFACTURER); - attributes.put(MessageKey.PLATFORM, getOperatingSystemString()); - attributes.put(MessageKey.OS_VERSION, Build.VERSION.SDK); - attributes.put(MessageKey.OS_VERSION_INT, Build.VERSION.SDK_INT); - attributes.put(MessageKey.MODEL, android.os.Build.MODEL); - attributes.put(MessageKey.RELEASE_VERSION, Build.VERSION.RELEASE); - - Application application = (Application) appContext; - // device ID - addAndroidId(attributes, application); - - attributes.put(MessageKey.DEVICE_BLUETOOTH_ENABLED, MPUtility.isBluetoothEnabled(appContext)); - attributes.put(MessageKey.DEVICE_BLUETOOTH_VERSION, MPUtility.getBluetoothVersion(appContext)); - attributes.put(MessageKey.DEVICE_SUPPORTS_NFC, MPUtility.hasNfc(appContext)); - attributes.put(MessageKey.DEVICE_SUPPORTS_TELEPHONY, MPUtility.hasTelephony(appContext)); - - JSONObject rootedObject = new JSONObject(); - rootedObject.put(MessageKey.DEVICE_ROOTED_CYDIA, MPUtility.isPhoneRooted()); - attributes.put(MessageKey.DEVICE_ROOTED, rootedObject); - - // screen height/width - DisplayMetrics displayMetrics = appContext.getResources().getDisplayMetrics(); - attributes.put(MessageKey.SCREEN_HEIGHT, displayMetrics.heightPixels); - attributes.put(MessageKey.SCREEN_WIDTH, displayMetrics.widthPixels); - attributes.put(MessageKey.SCREEN_DPI, displayMetrics.densityDpi); - - // locales - Locale locale = Locale.getDefault(); - attributes.put(MessageKey.DEVICE_COUNTRY, locale.getDisplayCountry()); - attributes.put(MessageKey.DEVICE_LOCALE_COUNTRY, locale.getCountry()); - attributes.put(MessageKey.DEVICE_LOCALE_LANGUAGE, locale.getLanguage()); - attributes.put(MessageKey.DEVICE_TIMEZONE_NAME, MPUtility.getTimeZone()); - attributes.put(MessageKey.TIMEZONE, TimeZone.getDefault().getRawOffset() / (1000 * 60 * 60)); - // network - TelephonyManager telephonyManager = (TelephonyManager) appContext - .getSystemService(Context.TELEPHONY_SERVICE); - int phoneType = telephonyManager.getPhoneType(); - if (phoneType != TelephonyManager.PHONE_TYPE_NONE) { - // NOTE: network attributes can be empty if phone is in airplane - // mode and will not be set - String networkCarrier = telephonyManager.getNetworkOperatorName(); - if (0 != networkCarrier.length()) { - attributes.put(MessageKey.NETWORK_CARRIER, networkCarrier); - } - String networkCountry = telephonyManager.getNetworkCountryIso(); - if (0 != networkCountry.length()) { - attributes.put(MessageKey.NETWORK_COUNTRY, networkCountry); - } - // android combines MNC+MCC into network operator - String networkOperator = telephonyManager.getNetworkOperator(); - if (6 == networkOperator.length()) { - attributes.put(MessageKey.MOBILE_COUNTRY_CODE, networkOperator.substring(0, 3)); - attributes.put(MessageKey.MOBILE_NETWORK_CODE, networkOperator.substring(3)); - } - - } - attributes.put(MessageKey.DEVICE_IS_TABLET, MPUtility.isTablet(appContext)); - attributes.put(MessageKey.DEVICE_IS_IN_DST, MPUtility.isInDaylightSavings()); - - if (!MPUtility.isEmpty(DeviceAttributes.deviceImei)) { - attributes.put(MessageKey.DEVICE_IMEI, DeviceAttributes.deviceImei); - } - - } catch (Exception e) { - //believe it or not, difference devices can be missing build.prop fields, or have otherwise - //strange version/builds of Android that cause unpredictable behavior - } - - return attributes; - } - - /** - * For the following fields we always want the latest values - */ - public void updateDeviceInfo(Context context, JSONObject deviceInfo) { - deviceInfo.remove(MessageKey.LIMIT_AD_TRACKING); - deviceInfo.remove(MessageKey.GOOGLE_ADV_ID); - MPUtility.AdIdInfo adIdInfo = MPUtility.getAdIdInfo(context); - String message = "Failed to collect Advertising ID, be sure to add Google Play services (com.google.android.gms:play-services-ads) or Amazon Ads (com.amazon.android:mobile-ads) to your app's dependencies."; - if (adIdInfo != null) { - try { - deviceInfo.put(MessageKey.LIMIT_AD_TRACKING, adIdInfo.isLimitAdTrackingEnabled); - MParticle instance = MParticle.getInstance(); - //check instance nullability here and decline to act if it is not available. Don't want to have the case where we are overriding isLimiAdTrackingEnabled - //just because there was a timing issue with the singleton - if (instance != null) { - if (adIdInfo.isLimitAdTrackingEnabled) { - message = adIdInfo.advertiser.descriptiveName + " Advertising ID tracking is disabled on this device."; - } else { - switch (adIdInfo.advertiser) { - case AMAZON: - deviceInfo.put(MessageKey.AMAZON_ADV_ID, adIdInfo.id); - break; - case GOOGLE: - deviceInfo.put(MessageKey.GOOGLE_ADV_ID, adIdInfo.id); - break; - } - message = "Successfully collected " + adIdInfo.advertiser.descriptiveName + " Advertising ID."; - } - } - } catch (JSONException jse) { - Logger.debug("Failed while building device-customAttributes object: ", jse.toString()); - } - } - if (firstCollection) { - Logger.debug(message); - firstCollection = false; - } - - try { - MParticle mParticle = MParticle.getInstance(); - if (mParticle != null) { - ConfigManager configManager = mParticle.Internal().getConfigManager(); - PushRegistrationHelper.PushRegistration registration = configManager.getPushRegistration(); - if (registration != null && !MPUtility.isEmpty(registration.instanceId)) { - deviceInfo.put(Constants.MessageKey.PUSH_TOKEN, registration.instanceId); - deviceInfo.put(Constants.MessageKey.PUSH_TOKEN_TYPE, Constants.GOOGLE_GCM); - } - } - } catch (JSONException jse) { - Logger.debug("Failed while building device-customAttributes object: ", jse.toString()); - } - } - - public JSONObject getDeviceInfo(Context context) { - if (deviceInfo == null) { - deviceInfo = getStaticDeviceInfo(context); - } - updateDeviceInfo(context, deviceInfo); - return deviceInfo; - } - - public JSONObject getAppInfo(Context context) { - return getAppInfo(context, false); - } - - JSONObject getAppInfo(Context context, boolean forceUpdateInstallReferrer) { - if (appInfo == null) { - appInfo = getStaticApplicationInfo(context); - updateInstallReferrer(context, appInfo); - } else if (forceUpdateInstallReferrer) { - updateInstallReferrer(context, appInfo); - } - return appInfo; - } - - String getOperatingSystemString() { - switch (operatingSystem) { - case ANDROID: - return Constants.Platform.ANDROID; - case FIRE_OS: - return Constants.Platform.FIRE_OS; - default: - return Constants.Platform.ANDROID; - } - } -} diff --git a/android-core/src/main/java/com/mparticle/internal/DeviceAttributes.kt b/android-core/src/main/java/com/mparticle/internal/DeviceAttributes.kt new file mode 100644 index 000000000..3ffafa810 --- /dev/null +++ b/android-core/src/main/java/com/mparticle/internal/DeviceAttributes.kt @@ -0,0 +1,343 @@ +package com.mparticle.internal + +import android.app.Application +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.telephony.TelephonyManager +import android.util.Log +import com.mparticle.MParticle +import com.mparticle.internal.Constants.MessageKey +import com.mparticle.internal.Constants.PrefKeys +import org.json.JSONException +import org.json.JSONObject +import java.util.Locale +import java.util.TimeZone + +class DeviceAttributes +/** + * package-private + */ internal constructor(private val operatingSystem: MParticle.OperatingSystem?) { + private var deviceInfo: JSONObject? = null + private var appInfo: JSONObject? = null + private var firstCollection = true + + private val sideloadedKitsCount: Int + get() { + try { + val kits = MParticle.getInstance()?.Internal()?.kitManager?.supportedKits + var count = 0 + if (kits != null) { + for (kitId in kits) { + if (kitId >= 1000000) { + count++ + } + } + } + return count + } catch (e: Exception) { + Logger.debug("Exception while adding sideloadedKitsCount to Device Attribute") + return 0 + } + } + + /** + * Generates a collection of application attributes that will not change during an app's process. + * + * + * This contains logic that MUST only be called once per app run. + * + * @param appContext the application context + * @return a JSONObject of application-specific attributes + */ + fun getStaticApplicationInfo(appContext: Context): JSONObject { + val attributes = JSONObject() + val preferences = appContext.getSharedPreferences(Constants.PREFS_FILE, Context.MODE_PRIVATE) + val editor = preferences.edit() + try { + val now = System.currentTimeMillis() + val packageManager = appContext.packageManager + val packageName = appContext.packageName + attributes.put(MessageKey.APP_PACKAGE_NAME, packageName) + attributes.put(MessageKey.SIDELOADED_KITS_COUNT, sideloadedKitsCount) + var versionCode = UNKNOWN + try { + val pInfo = appContext.packageManager.getPackageInfo(packageName, 0) + versionCode = pInfo.versionCode.toString() + attributes.put(MessageKey.APP_VERSION, pInfo.versionName) + } catch (nnfe: PackageManager.NameNotFoundException) { + } + + attributes.put(MessageKey.APP_VERSION_CODE, versionCode) + + val installerPackageName = packageManager.getInstallerPackageName(packageName) + if (installerPackageName != null) { + attributes.put(MessageKey.APP_INSTALLER_NAME, installerPackageName) + } + try { + val appInfo = packageManager.getApplicationInfo(packageName, 0) + attributes.put(MessageKey.APP_NAME, packageManager.getApplicationLabel(appInfo)) + } catch (e: PackageManager.NameNotFoundException) { + // ignore missing data + } + + attributes.put(MessageKey.BUILD_ID, MPUtility.getBuildUUID(versionCode)) + attributes.put(MessageKey.APP_DEBUG_SIGNING, MPUtility.isAppDebuggable(appContext)) + attributes.put(MessageKey.APP_PIRATED, preferences.getBoolean(PrefKeys.PIRATED, false)) + + attributes.put(MessageKey.MPARTICLE_INSTALL_TIME, preferences.getLong(PrefKeys.INSTALL_TIME, now)) + if (!preferences.contains(PrefKeys.INSTALL_TIME)) { + editor.putLong(PrefKeys.INSTALL_TIME, now) + } + val userStorage = ConfigManager.getUserStorage(appContext) + val totalRuns = userStorage.getTotalRuns(0) + 1 + userStorage.setTotalRuns(totalRuns) + attributes.put(MessageKey.LAUNCH_COUNT, totalRuns) + + val useDate = userStorage.getLastUseDate(0) + attributes.put(MessageKey.LAST_USE_DATE, useDate) + userStorage.lastUseDate = now + try { + val pInfo = packageManager.getPackageInfo(packageName, 0) + val persistedVersion = preferences.getInt(PrefKeys.COUNTER_VERSION, -1) + var countSinceUpgrade = userStorage.launchesSinceUpgrade + var upgradeDate = preferences.getLong(PrefKeys.UPGRADE_DATE, now) + + if (persistedVersion < 0 || persistedVersion != pInfo.versionCode) { + countSinceUpgrade = 0 + upgradeDate = now + editor.putInt(PrefKeys.COUNTER_VERSION, pInfo.versionCode) + editor.putLong(PrefKeys.UPGRADE_DATE, upgradeDate) + } + countSinceUpgrade += 1 + userStorage.launchesSinceUpgrade = countSinceUpgrade + + attributes.put(MessageKey.LAUNCH_COUNT_SINCE_UPGRADE, countSinceUpgrade) + attributes.put(MessageKey.UPGRADE_DATE, upgradeDate) + } catch (e: PackageManager.NameNotFoundException) { + // ignore missing data + } + val instance = MParticle.getInstance() + if (instance != null) { + attributes.put(MessageKey.ENVIRONMENT, ConfigManager.getEnvironment().value) + } + attributes.put(MessageKey.INSTALL_REFERRER, preferences.getString(PrefKeys.INSTALL_REFERRER, null)) + + val install = preferences.getBoolean(PrefKeys.FIRST_RUN_INSTALL, true) + attributes.put(MessageKey.FIRST_SEEN_INSTALL, install) + editor.putBoolean(PrefKeys.FIRST_RUN_INSTALL, false) + } catch (e: Exception) { + // again different devices can do terrible things, make sure that we don't bail out completely + // and return at least what we've built so far. + } finally { + editor.apply() + } + return attributes + } + + fun updateInstallReferrer(context: Context, attributes: JSONObject) { + val preferences = context.getSharedPreferences(Constants.PREFS_FILE, Context.MODE_PRIVATE) + try { + attributes.put(MessageKey.INSTALL_REFERRER, preferences.getString(PrefKeys.INSTALL_REFERRER, null)) + } catch (ignored: JSONException) { + // this, hopefully, should never fail + } + } + + /** + * Generates a collection of device attributes that will not change during an app's process. + * + * + * This contains logic that MUST only be called once per app run. + * + * @param appContext the application context + * @return a JSONObject of device-specific attributes + */ + fun getStaticDeviceInfo(appContext: Context): JSONObject { + val attributes = JSONObject() + + try { + // device/OS attributes + attributes.put(MessageKey.BUILD_ID, Build.ID) + attributes.put(MessageKey.BRAND, Build.BRAND) + attributes.put(MessageKey.PRODUCT, Build.PRODUCT) + attributes.put(MessageKey.DEVICE, Build.DEVICE) + attributes.put(MessageKey.MANUFACTURER, Build.MANUFACTURER) + attributes.put(MessageKey.PLATFORM, operatingSystemString) + attributes.put(MessageKey.OS_VERSION, Build.VERSION.SDK) + attributes.put(MessageKey.OS_VERSION_INT, Build.VERSION.SDK_INT) + attributes.put(MessageKey.MODEL, Build.MODEL) + attributes.put(MessageKey.RELEASE_VERSION, Build.VERSION.RELEASE) + + val application = appContext as Application + // device ID + addAndroidId(attributes, application) + + attributes.put(MessageKey.DEVICE_BLUETOOTH_ENABLED, MPUtility.isBluetoothEnabled(appContext)) + attributes.put(MessageKey.DEVICE_BLUETOOTH_VERSION, MPUtility.getBluetoothVersion(appContext)) + attributes.put(MessageKey.DEVICE_SUPPORTS_NFC, MPUtility.hasNfc(appContext)) + attributes.put(MessageKey.DEVICE_SUPPORTS_TELEPHONY, MPUtility.hasTelephony(appContext)) + + val rootedObject = JSONObject() + rootedObject.put(MessageKey.DEVICE_ROOTED_CYDIA, MPUtility.isPhoneRooted()) + attributes.put(MessageKey.DEVICE_ROOTED, rootedObject) + + // screen height/width + val displayMetrics = appContext.getResources().displayMetrics + attributes.put(MessageKey.SCREEN_HEIGHT, displayMetrics.heightPixels) + attributes.put(MessageKey.SCREEN_WIDTH, displayMetrics.widthPixels) + attributes.put(MessageKey.SCREEN_DPI, displayMetrics.densityDpi) + + // locales + val locale = Locale.getDefault() + attributes.put(MessageKey.DEVICE_COUNTRY, locale.displayCountry) + attributes.put(MessageKey.DEVICE_LOCALE_COUNTRY, locale.country) + attributes.put(MessageKey.DEVICE_LOCALE_LANGUAGE, locale.language) + attributes.put(MessageKey.DEVICE_TIMEZONE_NAME, MPUtility.getTimeZone()) + attributes.put(MessageKey.TIMEZONE, TimeZone.getDefault().rawOffset / (1000 * 60 * 60)) + // network + val telephonyManager = appContext + .getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + val phoneType = telephonyManager.phoneType + if (phoneType != TelephonyManager.PHONE_TYPE_NONE) { + // NOTE: network attributes can be empty if phone is in airplane + // mode and will not be set + val networkCarrier = telephonyManager.networkOperatorName + if (0 != networkCarrier.length) { + attributes.put(MessageKey.NETWORK_CARRIER, networkCarrier) + } + val networkCountry = telephonyManager.networkCountryIso + if (0 != networkCountry.length) { + attributes.put(MessageKey.NETWORK_COUNTRY, networkCountry) + } + // android combines MNC+MCC into network operator + val networkOperator = telephonyManager.networkOperator + if (6 == networkOperator.length) { + attributes.put(MessageKey.MOBILE_COUNTRY_CODE, networkOperator.substring(0, 3)) + attributes.put(MessageKey.MOBILE_NETWORK_CODE, networkOperator.substring(3)) + } + } + attributes.put(MessageKey.DEVICE_IS_TABLET, MPUtility.isTablet(appContext)) + attributes.put(MessageKey.DEVICE_IS_IN_DST, MPUtility.isInDaylightSavings()) + + if (!MPUtility.isEmpty(deviceImei)) { + attributes.put(MessageKey.DEVICE_IMEI, deviceImei) + } + } catch (e: Exception) { + // believe it or not, difference devices can be missing build.prop fields, or have otherwise + // strange version/builds of Android that cause unpredictable behavior + } + + return attributes + } + + /** + * For the following fields we always want the latest values + */ + fun updateDeviceInfo(context: Context, deviceInfo: JSONObject) { + deviceInfo.remove(MessageKey.LIMIT_AD_TRACKING) + deviceInfo.remove(MessageKey.GOOGLE_ADV_ID) + val adIdInfo = MPUtility.getAdIdInfo(context) + var message = + "Failed to collect Advertising ID, be sure to add Google Play services (com.google.android.gms:play-services-ads) or Amazon Ads (com.amazon.android:mobile-ads) to your app's dependencies." + if (adIdInfo != null) { + try { + deviceInfo.put(MessageKey.LIMIT_AD_TRACKING, adIdInfo.isLimitAdTrackingEnabled) + val instance = MParticle.getInstance() + // check instance nullability here and decline to act if it is not available. Don't want to have the case where we are overriding isLimiAdTrackingEnabled + // just because there was a timing issue with the singleton + if (instance != null) { + if (adIdInfo.isLimitAdTrackingEnabled) { + message = adIdInfo.advertiser.descriptiveName + " Advertising ID tracking is disabled on this device." + } else { + when (adIdInfo.advertiser) { + MPUtility.AdIdInfo.Advertiser.AMAZON -> deviceInfo.put(MessageKey.AMAZON_ADV_ID, adIdInfo.id) + MPUtility.AdIdInfo.Advertiser.GOOGLE -> deviceInfo.put(MessageKey.GOOGLE_ADV_ID, adIdInfo.id) + } + message = "Successfully collected " + adIdInfo.advertiser.descriptiveName + " Advertising ID." + } + } + } catch (jse: JSONException) { + Logger.debug("Failed while building device-customAttributes object: ", jse.toString()) + } + } + if (firstCollection) { + Logger.debug(message) + firstCollection = false + } + + try { + val mParticle = MParticle.getInstance() + if (mParticle != null) { + val configManager = mParticle.Internal().configManager + val registration = configManager.pushRegistration + if (registration != null && !MPUtility.isEmpty(registration.instanceId)) { + deviceInfo.put(MessageKey.PUSH_TOKEN, registration.instanceId) + deviceInfo.put(MessageKey.PUSH_TOKEN_TYPE, Constants.GOOGLE_GCM) + } + } + } catch (jse: JSONException) { + Logger.debug("Failed while building device-customAttributes object: ", jse.toString()) + } + } + + fun getDeviceInfo(context: Context): JSONObject { + if (deviceInfo == null) { + deviceInfo = getStaticDeviceInfo(context) + } + deviceInfo?.let { + updateDeviceInfo(context, it) + return it + } ?: run { + return JSONObject() + } + } + + fun getAppInfo(context: Context): JSONObject { + return getAppInfo(context, false) + } + + fun getAppInfo(context: Context, forceUpdateInstallReferrer: Boolean): JSONObject { + if (appInfo == null) { + appInfo = getStaticApplicationInfo(context) + appInfo?.let { updateInstallReferrer(context, it) } + } else if (forceUpdateInstallReferrer) { + appInfo?.let { updateInstallReferrer(context, it) } + } + return appInfo as JSONObject + } + + val operatingSystemString: String + get() = when (operatingSystem) { + MParticle.OperatingSystem.ANDROID -> Constants.Platform.ANDROID + MParticle.OperatingSystem.FIRE_OS -> Constants.Platform.FIRE_OS + else -> Constants.Platform.ANDROID + } + + companion object { + // re-use this whenever an attribute can't be determined + const val UNKNOWN: String = "unknown" + + @Volatile + private var _deviceImei: String? = null + + @get:JvmStatic + val deviceImei: String? + get() = _deviceImei ?: null + + @JvmStatic + fun setDeviceImei(deviceImei: String?) { + _deviceImei = deviceImei + } + + @Throws(JSONException::class) + fun addAndroidId(attributes: JSONObject, context: Context) { + val androidId = MPUtility.getAndroidID(context) + if (!MPUtility.isEmpty(androidId)) { + attributes.put(MessageKey.DEVICE_ID, androidId) + attributes.put(MessageKey.DEVICE_ANID, androidId) + attributes.put(MessageKey.DEVICE_OPEN_UDID, MPUtility.getOpenUDID(context)) + } + } + } +} diff --git a/android-core/src/test/kotlin/com/mparticle/internal/DeviceAttributesTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/DeviceAttributesTest.kt index 8737eb104..4ead582a4 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/DeviceAttributesTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/DeviceAttributesTest.kt @@ -1,18 +1,45 @@ package com.mparticle.internal +import android.app.Application +import android.bluetooth.BluetoothAdapter import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.content.res.Resources +import android.telephony.TelephonyManager +import android.util.DisplayMetrics import com.mparticle.MParticle import com.mparticle.MockMParticle +import com.mparticle.internal.PushRegistrationHelper.PushRegistration import com.mparticle.mock.MockContext import com.mparticle.mock.MockSharedPreferences import org.json.JSONObject import org.junit.Assert +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import org.powermock.api.mockito.PowerMockito +import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner +import java.lang.reflect.Field @RunWith(PowerMockRunner::class) +@PrepareForTest(Context::class, Application::class, BluetoothAdapter::class, TelephonyManager::class) class DeviceAttributesTest { + + private lateinit var context: Context + + @Before + fun setUp() { + PowerMockito.mockStatic(BluetoothAdapter::class.java) + PowerMockito.mockStatic(TelephonyManager::class.java) + } + @Test @Throws(Exception::class) fun testCollectAppInfo() { @@ -92,4 +119,89 @@ class DeviceAttributesTest { osStringValues.add(osString) } } + + @Test + fun testDeviceInfo() { + val application = PowerMockito.mock(Application::class.java) + val mockAdapter = mock(BluetoothAdapter::class.java) + val mockTelephonyManager = mock(TelephonyManager::class.java) + val mockResources = mock(Resources::class.java) + val mockDisplayMetrics = mock(DisplayMetrics::class.java) + val mockConfiguration = mock(Configuration::class.java) + `when`(application.getResources()).thenReturn(mockResources) + + PowerMockito.`when`(BluetoothAdapter.getDefaultAdapter()).thenReturn(mockAdapter) + `when`(mockResources.displayMetrics).thenReturn(mockDisplayMetrics) + `when`(mockResources.configuration).thenReturn(mockConfiguration) + Mockito.`when`(application.getSystemService(Context.TELEPHONY_SERVICE)) + .thenReturn(mockTelephonyManager) + Mockito.`when`(mockTelephonyManager.phoneType) + .thenReturn(TelephonyManager.PHONE_TYPE_GSM) + Mockito.`when`(mockTelephonyManager.networkOperatorName) + .thenReturn("TestingNetworkOperatorName") + Mockito.`when`(mockTelephonyManager.networkCountryIso) + .thenReturn("US") + Mockito.`when`(mockTelephonyManager.networkOperator) + .thenReturn("310260") + val packageManager = Mockito.mock(PackageManager::class.java) + doReturn(packageManager).`when`(application).packageManager + doReturn("TestPackage").`when`(application).packageName + doReturn("TestInstallerPackage").`when`(packageManager).getInstallerPackageName("TestPackage") + DeviceAttributes.setDeviceImei("123kalksdlkasd") + val attributes = + DeviceAttributes(MParticle.OperatingSystem.ANDROID).getDeviceInfo(application) + Assert.assertNotNull(attributes) + } + + @Test + fun testGetAppInfo() { + val mDeviceAttributes = DeviceAttributes(MParticle.OperatingSystem.ANDROID) + val application = PowerMockito.mock(Application::class.java) + val mockSharedPreferences = PowerMockito.mock(SharedPreferences::class.java) + val mockSharedPreferencesEditor = PowerMockito.mock(SharedPreferences.Editor::class.java) + Mockito.`when`(application.getSharedPreferences(Constants.PREFS_FILE, Context.MODE_PRIVATE)).thenReturn(mockSharedPreferences) + Mockito.`when`(application.getSharedPreferences(Constants.PREFS_FILE, Context.MODE_PRIVATE).edit()).thenReturn(mockSharedPreferencesEditor) + val field: Field = DeviceAttributes::class.java.getDeclaredField("appInfo") + field.isAccessible = true + val mockJson = JSONObject().apply { + put("key", "value") + } + // Set the private field value + field.set(mDeviceAttributes, mockJson) + val attributes = mDeviceAttributes.getAppInfo(application, true) as JSONObject + Assert.assertNotNull(attributes) + } + + @Test + fun testWhen_OperatingSystem_Is_NULL() { + val mDeviceAttributes = DeviceAttributes(null) + Assert.assertEquals("Android", mDeviceAttributes.operatingSystemString) + } + + @Test + @PrepareForTest(MPUtility::class, BluetoothAdapter::class, TelephonyManager::class) + fun testUpdateDeviceInfo() { + context = mock(Context::class.java) + val application = PowerMockito.mock(Application::class.java) + val packageManager = Mockito.mock(PackageManager::class.java) + PowerMockito.mockStatic(MPUtility::class.java) + val mockAdIdInfo = PowerMockito.mock(MPUtility.AdIdInfo::class.java) + + Mockito.`when`(MPUtility.getAdIdInfo(context)).thenReturn(mockAdIdInfo) + + doReturn(packageManager).`when`(application).packageManager + doReturn("TestPackage").`when`(application).packageName + doReturn("TestInstallerPackage").`when`(packageManager).getInstallerPackageName("TestPackage") + DeviceAttributes.setDeviceImei("123kalksdlkasd") + val mockMp = MockMParticle() + MParticle.setInstance(mockMp) + val registration = PushRegistration("instance id", "1234545") + Mockito.`when`( + MParticle.getInstance()!!.Internal().configManager.pushRegistration + ).thenReturn(registration) + val result = MPUtility.getAdIdInfo(context) + val attributes = + DeviceAttributes(MParticle.OperatingSystem.ANDROID).getDeviceInfo(application) + Assert.assertNotNull(attributes) + } }