diff --git a/CHANGELOG.md b/CHANGELOG.md index 4309d5368..f98f1fe76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 26.1.2 * Added `CountlyInitProvider` ContentProvider to register activity lifecycle callbacks before `Application.onCreate()`. This ensures the SDK captures the current activity in single-activity frameworks (Flutter, React Native) and apps with deferred initialization. * Added `CountlyConfig.setInitialActivity(Activity)` as an explicit way for wrapper SDKs to provide the host activity during initialization. +* Added a new config option `setMetricProvider(MetricProvider)` to allow overriding default device metrics with custom values. ## 26.1.1 * Added Content feature method `previewContent(String contentId)` (Experimental!). diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/DeviceInfoTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/DeviceInfoTests.java index 15f3c8366..c3ee01099 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/DeviceInfoTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/DeviceInfoTests.java @@ -42,6 +42,7 @@ of this software and associated documentation files (the "Software"), to deal import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -296,6 +297,141 @@ public void testGetMetricsWithOverride_2() throws UnsupportedEncodingException, TestUtils.bothJSONObjEqual(json, calculatedJSON); } + // MetricProvider override tests + + @Test + public void testMetricProviderOverride_fullOverride() { + MockedMetricProvider override = new MockedMetricProvider(); + DeviceInfo deviceInfo = new DeviceInfo(override); + + assertEquals("A", deviceInfo.mp.getOS()); + assertEquals("B", deviceInfo.mp.getOSVersion()); + assertEquals("C", deviceInfo.mp.getDevice()); + assertEquals("D", deviceInfo.mp.getManufacturer()); + assertEquals("E", deviceInfo.mp.getResolution(TestUtils.getContext())); + assertEquals("F", deviceInfo.mp.getDensity(TestUtils.getContext())); + assertEquals("G", deviceInfo.mp.getCarrier(TestUtils.getContext())); + assertEquals("66", deviceInfo.mp.getTimezoneOffset()); + assertEquals("H", deviceInfo.mp.getLocale()); + assertEquals(Countly.DEFAULT_APP_VERSION, deviceInfo.mp.getAppVersion(TestUtils.getContext())); + assertEquals("J", deviceInfo.mp.getStore(TestUtils.getContext())); + assertEquals("K", deviceInfo.mp.getDeviceType(TestUtils.getContext())); + assertEquals("42", deviceInfo.mp.getTotalRAM()); + assertEquals("12", deviceInfo.mp.getRamCurrent(TestUtils.getContext())); + assertEquals("48", deviceInfo.mp.getRamTotal()); + assertEquals("N", deviceInfo.mp.getCpu()); + assertEquals("O", deviceInfo.mp.getOpenGL(TestUtils.getContext())); + assertEquals("6", deviceInfo.mp.getBatteryLevel(TestUtils.getContext())); + assertEquals("S", deviceInfo.mp.getOrientation(TestUtils.getContext())); + assertEquals("T", deviceInfo.mp.isRooted()); + assertEquals("U", deviceInfo.mp.isOnline(TestUtils.getContext())); + assertEquals("V", deviceInfo.mp.isMuted(TestUtils.getContext())); + assertEquals("Z", deviceInfo.mp.hasHinge(TestUtils.getContext())); + assertEquals("88", deviceInfo.mp.getRunningTime()); + + DiskMetric diskMetric = deviceInfo.mp.getDiskSpaces(TestUtils.getContext()); + assertEquals("45", diskMetric.totalMb); + assertEquals("23", diskMetric.usedMb); + } + + @Test + public void testMetricProviderOverride_partialOverride() { + MetricProvider partial = new MetricProvider() { + @Override public String getOS() { return "CustomOS"; } + @Override public String getDevice() { return "CustomDevice"; } + }; + DeviceInfo deviceInfo = new DeviceInfo(partial); + + // overridden values + assertEquals("CustomOS", deviceInfo.mp.getOS()); + assertEquals("CustomDevice", deviceInfo.mp.getDevice()); + + // non-overridden values should fall back to SDK defaults + assertEquals(android.os.Build.VERSION.RELEASE, deviceInfo.mp.getOSVersion()); + assertEquals(android.os.Build.MODEL, regularDeviceInfo.mp.getDevice()); + assertEquals(android.os.Build.MANUFACTURER, deviceInfo.mp.getManufacturer()); + assertNotNull(deviceInfo.mp.getLocale()); + assertNotNull(deviceInfo.mp.getTimezoneOffset()); + assertNotNull(deviceInfo.mp.getAppVersion(TestUtils.getContext())); + } + + @Test + public void testMetricProviderOverride_nullOverride() { + DeviceInfo deviceInfo = new DeviceInfo(null); + + // all values should be SDK defaults, no crash + assertEquals("Android", deviceInfo.mp.getOS()); + assertEquals(android.os.Build.VERSION.RELEASE, deviceInfo.mp.getOSVersion()); + assertEquals(android.os.Build.MODEL, deviceInfo.mp.getDevice()); + assertEquals(android.os.Build.MANUFACTURER, deviceInfo.mp.getManufacturer()); + assertNotNull(deviceInfo.mp.getLocale()); + assertNotNull(deviceInfo.mp.getTimezoneOffset()); + } + + @Test + public void testMetricProviderOverride_emptyOverride() { + MetricProvider emptyOverride = new MetricProvider() {}; + DeviceInfo deviceInfo = new DeviceInfo(emptyOverride); + + // empty override should behave same as null override + assertEquals("Android", deviceInfo.mp.getOS()); + assertEquals(android.os.Build.VERSION.RELEASE, deviceInfo.mp.getOSVersion()); + assertEquals(android.os.Build.MODEL, deviceInfo.mp.getDevice()); + assertNotNull(deviceInfo.mp.getLocale()); + } + + @Test + public void testMetricProviderOverride_diskSpacesOverride() { + MetricProvider diskOverride = new MetricProvider() { + @Override public DiskMetric getDiskSpaces(Context context) { + return new DiskMetric("100", "50"); + } + }; + DeviceInfo deviceInfo = new DeviceInfo(diskOverride); + + DiskMetric diskMetric = deviceInfo.mp.getDiskSpaces(TestUtils.getContext()); + assertEquals("100", diskMetric.totalMb); + assertEquals("50", diskMetric.usedMb); + + // other metrics should be defaults + assertEquals("Android", deviceInfo.mp.getOS()); + } + + @Test + public void testMetricProviderOverride_timezoneAndRamOverride() { + MetricProvider override = new MetricProvider() { + @Override public String getTimezoneOffset() { return "120"; } + @Override public String getTotalRAM() { return "8192"; } + @Override public String getRamTotal() { return "8192"; } + @Override public String getRamCurrent(Context context) { return "4096"; } + }; + DeviceInfo deviceInfo = new DeviceInfo(override); + + assertEquals("120", deviceInfo.mp.getTimezoneOffset()); + assertEquals("8192", deviceInfo.mp.getTotalRAM()); + assertEquals("8192", deviceInfo.mp.getRamTotal()); + assertEquals("4096", deviceInfo.mp.getRamCurrent(TestUtils.getContext())); + } + + @Test + public void testMetricProviderOverride_metricsJson() throws UnsupportedEncodingException, JSONException { + MetricProvider partial = new MetricProvider() { + @Override public String getOS() { return "CustomOS"; } + @Override public String getDevice() { return "CustomDevice"; } + @Override public String getManufacturer() { return "CustomMfg"; } + }; + DeviceInfo deviceInfo = new DeviceInfo(partial); + + String calculatedMetrics = URLDecoder.decode(deviceInfo.getMetrics(TestUtils.getContext(), null, new ModuleLog()), "UTF-8"); + JSONObject json = new JSONObject(calculatedMetrics); + + assertEquals("CustomOS", json.getString("_os")); + assertEquals("CustomDevice", json.getString("_device")); + assertEquals("CustomMfg", json.getString("_manufacturer")); + // non-overridden should still be present with defaults + assertEquals(android.os.Build.VERSION.RELEASE, json.getString("_os_version")); + } + @Test public void getAppVersionWithOverride() { Map metricOverride = new HashMap<>(); diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/MockedMetricProvider.java b/sdk/src/androidTest/java/ly/count/android/sdk/MockedMetricProvider.java index b16cb561f..3fcda7ee8 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/MockedMetricProvider.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/MockedMetricProvider.java @@ -4,8 +4,6 @@ import android.util.DisplayMetrics; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.AbstractMap; -import java.util.Map; public class MockedMetricProvider implements MetricProvider { @@ -41,8 +39,8 @@ public MockedMetricProvider() { return "G"; } - @Override public int getTimezoneOffset() { - return 66; + @Override public String getTimezoneOffset() { + return "66"; } @Override public String getLocale() { @@ -61,8 +59,8 @@ public MockedMetricProvider() { return "K"; } - @Override public long getTotalRAM() { - return 42; + @Override public String getTotalRAM() { + return "42"; } @Override public String getRamCurrent(Context context) { @@ -113,7 +111,7 @@ public MockedMetricProvider() { return new DisplayMetrics(); } - @Override public Map.Entry getDiskSpaces(Context context) { - return new AbstractMap.SimpleEntry<>("45", "23"); + @Override public DiskMetric getDiskSpaces(Context context) { + return new DiskMetric("45", "23"); } } diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java index b19598235..9a8ab46d0 100644 --- a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java +++ b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java @@ -379,6 +379,19 @@ public synchronized CountlyConfig setLoggingEnabled(boolean enabled) { return this; } + /** + * Set a custom metric provider to override default device metrics. + * Only the methods you override will replace the SDK defaults. + * Methods that return null will fall back to the SDK's built-in values. + * + * @param metricProvider Your custom MetricProvider implementation + * @return Returns the same config object for convenient linking + */ + public synchronized CountlyConfig setMetricProvider(MetricProvider metricProvider) { + this.metricProviderOverride = metricProvider; + return this; + } + /** * Call to enable uncaught crash reporting * diff --git a/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java b/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java index 10a9e30f3..43800653b 100644 --- a/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java +++ b/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java @@ -45,7 +45,6 @@ of this software and associated documentation files (the "Software"), to deal import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; -import java.util.AbstractMap; import java.util.Date; import java.util.Locale; import java.util.Map; @@ -65,508 +64,489 @@ class DeviceInfo { private static long totalMemory = 0; MetricProvider mp; + private final MetricProvider mpOverride; public DeviceInfo(MetricProvider mpOverride) { - mp = mpOverride; - - if (mp == null) { - mp = new MetricProvider() { - /** - * Returns the display name of the current operating system. - */ - @NonNull - @Override public String getOS() { - return "Android"; - } - - /** - * Returns the current operating system version as a displayable string. - */ - @SuppressWarnings("SameReturnValue") - @NonNull - @Override - public String getOSVersion() { - return android.os.Build.VERSION.RELEASE; - } - - /** - * Returns the current device model. - */ - @SuppressWarnings("SameReturnValue") - @NonNull - @Override - public String getDevice() { - return android.os.Build.MODEL; - } + this.mpOverride = mpOverride != null ? mpOverride : new MetricProvider() {}; + + mp = new MetricProvider() { + @NonNull + @Override public String getOS() { + String ov = DeviceInfo.this.mpOverride.getOS(); + if (ov != null) return ov; + return "Android"; + } - @SuppressWarnings("SameReturnValue") - @NonNull - @Override - public String getManufacturer() { - return Build.MANUFACTURER; - } + @NonNull + @Override + public String getOSVersion() { + String ov = DeviceInfo.this.mpOverride.getOSVersion(); + if (ov != null) return ov; + return android.os.Build.VERSION.RELEASE; + } - /** - * Returns the non-scaled pixel resolution of the current default display being used by the - * WindowManager in the specified context. - * - * @param context context to use to retrieve the current WindowManager - * @return a string in the format "WxH", or the empty string "" if resolution cannot be determined - */ - @NonNull - @Override - public String getResolution(@NonNull final Context context) { - // user reported NPE in this method; that means either getSystemService or getDefaultDisplay - // were returning null, even though the documentation doesn't say they should do so; so now - // we catch Throwable and return empty string if that happens - String resolution = ""; - try { - final DisplayMetrics metrics = getDisplayMetrics(context); - resolution = metrics.widthPixels + "x" + metrics.heightPixels; - } catch (Throwable t) { - Countly.sharedInstance().L.i("[DeviceInfo] Device resolution cannot be determined"); - } - return resolution; - } + @NonNull + @Override + public String getDevice() { + String ov = DeviceInfo.this.mpOverride.getDevice(); + if (ov != null) return ov; + return android.os.Build.MODEL; + } - @NonNull - @Override - public DisplayMetrics getDisplayMetrics(@NonNull final Context context) { - return UtilsDevice.getDisplayMetrics(context); - } + @NonNull + @Override + public String getManufacturer() { + String ov = DeviceInfo.this.mpOverride.getManufacturer(); + if (ov != null) return ov; + return Build.MANUFACTURER; + } - /** - * Maps the current display density to a string constant. - * - * @param context context to use to retrieve the current display metrics - * @return a string constant representing the current display density, or the - * empty string if the density is unknown - */ - @NonNull - @Override - public String getDensity(@NonNull final Context context) { - String densityStr; - final int density = context.getResources().getDisplayMetrics().densityDpi; - switch (density) { - case DisplayMetrics.DENSITY_LOW: - densityStr = "LDPI"; - break; - case DisplayMetrics.DENSITY_MEDIUM: - densityStr = "MDPI"; - break; - case DisplayMetrics.DENSITY_TV: - densityStr = "TVDPI"; - break; - case DisplayMetrics.DENSITY_HIGH: - densityStr = "HDPI"; - break; - case DisplayMetrics.DENSITY_260: - case DisplayMetrics.DENSITY_280: - case DisplayMetrics.DENSITY_300: - case DisplayMetrics.DENSITY_XHIGH: - densityStr = "XHDPI"; - break; - case DisplayMetrics.DENSITY_340: - case DisplayMetrics.DENSITY_360: - case DisplayMetrics.DENSITY_400: - case DisplayMetrics.DENSITY_420: - case DisplayMetrics.DENSITY_XXHIGH: - densityStr = "XXHDPI"; - break; - case DisplayMetrics.DENSITY_560: - case DisplayMetrics.DENSITY_XXXHIGH: - densityStr = "XXXHDPI"; - break; - default: - densityStr = "other"; - break; - } - return densityStr; - } + /** + * Returns the non-scaled pixel resolution of the current default display being used by the + * WindowManager in the specified context. + * + * @param context context to use to retrieve the current WindowManager + * @return a string in the format "WxH", or the empty string "" if resolution cannot be determined + */ + @NonNull + @Override + public String getResolution(@NonNull final Context context) { + // user reported NPE in this method; that means either getSystemService or getDefaultDisplay + // were returning null, even though the documentation doesn't say they should do so; so now + // we catch Throwable and return empty string if that happens + String ov = DeviceInfo.this.mpOverride.getResolution(context); + if (ov != null) return ov; + String resolution = ""; + try { + final DisplayMetrics metrics = getDisplayMetrics(context); + resolution = metrics.widthPixels + "x" + metrics.heightPixels; + } catch (Throwable t) { + Countly.sharedInstance().L.i("[DeviceInfo] Device resolution cannot be determined"); + } + return resolution; + } - /** - * Returns the display name of the current network operator from the - * TelephonyManager from the specified context. - * - * @param context context to use to retrieve the TelephonyManager from - * @return the display name of the current network operator, or the empty - * string if it cannot be accessed or determined - */ - @NonNull - @Override - public String getCarrier(@NonNull final Context context) { - String carrier = ""; - final TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (manager != null) { - carrier = manager.getNetworkOperatorName(); - } - if (carrier == null || carrier.length() == 0) { - carrier = ""; - Countly.sharedInstance().L.i("[DeviceInfo] No carrier found"); - } + @NonNull + @Override + public DisplayMetrics getDisplayMetrics(@NonNull final Context context) { + DisplayMetrics ov = DeviceInfo.this.mpOverride.getDisplayMetrics(context); + if (ov != null) return ov; + return UtilsDevice.getDisplayMetrics(context); + } - if (carrier.equals("--")) { - //if for some reason the carrier is returned as "--", just clear it and set to empty string - carrier = ""; - } + @NonNull + @Override + public String getDensity(@NonNull final Context context) { + String ov = DeviceInfo.this.mpOverride.getDensity(context); + if (ov != null) return ov; + String densityStr; + final int density = context.getResources().getDisplayMetrics().densityDpi; + switch (density) { + case DisplayMetrics.DENSITY_LOW: + densityStr = "LDPI"; + break; + case DisplayMetrics.DENSITY_MEDIUM: + densityStr = "MDPI"; + break; + case DisplayMetrics.DENSITY_TV: + densityStr = "TVDPI"; + break; + case DisplayMetrics.DENSITY_HIGH: + densityStr = "HDPI"; + break; + case DisplayMetrics.DENSITY_260: + case DisplayMetrics.DENSITY_280: + case DisplayMetrics.DENSITY_300: + case DisplayMetrics.DENSITY_XHIGH: + densityStr = "XHDPI"; + break; + case DisplayMetrics.DENSITY_340: + case DisplayMetrics.DENSITY_360: + case DisplayMetrics.DENSITY_400: + case DisplayMetrics.DENSITY_420: + case DisplayMetrics.DENSITY_XXHIGH: + densityStr = "XXHDPI"; + break; + case DisplayMetrics.DENSITY_560: + case DisplayMetrics.DENSITY_XXXHIGH: + densityStr = "XXXHDPI"; + break; + default: + densityStr = "other"; + break; + } + return densityStr; + } - return carrier; + @NonNull + @Override + public String getCarrier(@NonNull final Context context) { + String ov = DeviceInfo.this.mpOverride.getCarrier(context); + if (ov != null) return ov; + String carrier = ""; + final TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (manager != null) { + carrier = manager.getNetworkOperatorName(); } - - @Override - public int getTimezoneOffset() { - return TimeZone.getDefault().getOffset(new Date().getTime()) / 60_000; + if (carrier == null || carrier.length() == 0) { + carrier = ""; + Countly.sharedInstance().L.i("[DeviceInfo] No carrier found"); } - - /** - * Returns the current locale (ex. "en_US"). - */ - @NonNull - @Override - public String getLocale() { - final Locale locale = Locale.getDefault(); - return locale.getLanguage() + "_" + locale.getCountry(); + if (carrier.equals("--")) { + carrier = ""; } + return carrier; + } - /** - * Returns the application version string stored in the specified - * context's package info versionName field, or "1.0" if versionName - * is not present. - */ - @NonNull - @Override - public String getAppVersion(@NonNull final Context context) { - String result = Countly.DEFAULT_APP_VERSION; - try { - String tmpVersion = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; - if (tmpVersion != null) { - result = tmpVersion; - } - } catch (PackageManager.NameNotFoundException e) { - Countly.sharedInstance().L.i("[DeviceInfo] No app version found"); - } - return result; - } + @Override + public String getTimezoneOffset() { + String ov = DeviceInfo.this.mpOverride.getTimezoneOffset(); + if (ov != null) return ov; + return Integer.toString(TimeZone.getDefault().getOffset(new Date().getTime()) / 60_000); + } - /** - * Returns the package name of the app that installed this app - */ - @NonNull - @Override - public String getStore(@NonNull final Context context) { - String result = ""; - try { - result = context.getPackageManager().getInstallerPackageName(context.getPackageName()); - } catch (Exception e) { - Countly.sharedInstance().L.d("[DeviceInfo, getStore] Can't get Installer package "); - } - if (result == null || result.length() == 0) { - result = ""; - Countly.sharedInstance().L.d("[DeviceInfo, getStore] No store found"); - } - return result; - } + @NonNull + @Override + public String getLocale() { + String ov = DeviceInfo.this.mpOverride.getLocale(); + if (ov != null) return ov; + final Locale locale = Locale.getDefault(); + return locale.getLanguage() + "_" + locale.getCountry(); + } - /** - * Returns what kind of device this is. The potential values are: - * ["console", "mobile", "tablet", "smarttv", "wearable", "embedded", "desktop"] - * Currently the Android SDK differentiates between ["mobile", "tablet", "smarttv"] - */ - @NonNull - @Override - public String getDeviceType(@NonNull final Context context) { - if (Utils.isDeviceTv(context)) { - return "smarttv"; + /** + * Returns the application version string stored in the specified + * context's package info versionName field, or "1.0" if versionName + * is not present. + */ + @NonNull + @Override + public String getAppVersion(@NonNull final Context context) { + String ov = DeviceInfo.this.mpOverride.getAppVersion(context); + if (ov != null) return ov; + String result = Countly.DEFAULT_APP_VERSION; + try { + String tmpVersion = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + if (tmpVersion != null) { + result = tmpVersion; } + } catch (PackageManager.NameNotFoundException e) { + Countly.sharedInstance().L.i("[DeviceInfo] No app version found"); + } + return result; + } - if (Utils.isDeviceTablet(context)) { - return "tablet"; - } + /** + * Returns the package name of the app that installed this app + */ + @NonNull + @Override + public String getStore(@NonNull final Context context) { + String ov = DeviceInfo.this.mpOverride.getStore(context); + if (ov != null) return ov; + String result = ""; + try { + result = context.getPackageManager().getInstallerPackageName(context.getPackageName()); + } catch (Exception e) { + Countly.sharedInstance().L.d("[DeviceInfo, getStore] Can't get Installer package "); + } + if (result == null || result.length() == 0) { + result = ""; + Countly.sharedInstance().L.d("[DeviceInfo, getStore] No store found"); + } + return result; + } - return "mobile"; - } + /** + * Returns what kind of device this is. The potential values are: + * ["console", "mobile", "tablet", "smarttv", "wearable", "embedded", "desktop"] + * Currently the Android SDK differentiates between ["mobile", "tablet", "smarttv"] + */ + @NonNull + @Override + public String getDeviceType(@NonNull final Context context) { + String ov = DeviceInfo.this.mpOverride.getDeviceType(context); + if (ov != null) return ov; + if (Utils.isDeviceTv(context)) { + return "smarttv"; + } + if (Utils.isDeviceTablet(context)) { + return "tablet"; + } + return "mobile"; + } - // Crash related calls - @Override - public long getTotalRAM() { - if (totalMemory == 0) { - RandomAccessFile reader = null; - String load; - try { - reader = new RandomAccessFile("/proc/meminfo", "r"); - load = reader.readLine(); - - // Get the Number value from the string - Pattern p = Pattern.compile("(\\d+)"); - Matcher m = p.matcher(load); - String value = ""; - while (m.find()) { - value = m.group(1); - } - try { - if (value != null) { - totalMemory = Long.parseLong(value) / 1024; - } else { - totalMemory = 0; - } - } catch (NumberFormatException ex) { - totalMemory = 0; - } - } catch (IOException ex) { - try { - if (reader != null) { - reader.close(); - } - } catch (IOException exc) { - exc.printStackTrace(); - } - ex.printStackTrace(); - } finally { - try { - if (reader != null) { - reader.close(); - } - } catch (IOException exc) { - exc.printStackTrace(); - } - } - } - return totalMemory; - } + @Override + public String getTotalRAM() { + String ov = DeviceInfo.this.mpOverride.getTotalRAM(); + if (ov != null) return ov; + return Long.toString(getTotalRAMInternal()); + } - /** - * Returns the current device RAM amount. - */ - @NonNull - @Override - public String getRamCurrent(Context context) { - ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); - ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - activityManager.getMemoryInfo(mi); - return Long.toString(getTotalRAM() - (mi.availMem / 1_048_576L)); - } + + /** + * Returns the current device RAM amount. + */ + @NonNull + @Override + public String getRamCurrent(Context context) { + String ov = DeviceInfo.this.mpOverride.getRamCurrent(context); + if (ov != null) return ov; + ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + activityManager.getMemoryInfo(mi); + return Long.toString(getTotalRAMInternal() - (mi.availMem / 1_048_576L)); + } - /** - * Returns the total device RAM amount. - */ - @NonNull - @Override - public String getRamTotal() { - return Long.toString(getTotalRAM()); - } + /** + * Returns the total device RAM amount. + */ + @NonNull + @Override + public String getRamTotal() { + String ov = DeviceInfo.this.mpOverride.getRamTotal(); + if (ov != null) return ov; + return Long.toString(getTotalRAMInternal()); + } - /** - * Returns the current device cpu. - */ - @NonNull - @Override - public String getCpu() { - return Build.SUPPORTED_ABIS[0]; - } + @NonNull + @Override + public String getCpu() { + String ov = DeviceInfo.this.mpOverride.getCpu(); + if (ov != null) return ov; + return Build.SUPPORTED_ABIS[0]; + } - /** - * Returns the current device openGL version. - */ - @NonNull - @Override - public String getOpenGL(Context context) { - PackageManager packageManager = context.getPackageManager(); - FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures(); - if (featureInfos != null && featureInfos.length > 0) { - for (FeatureInfo featureInfo : featureInfos) { - // Null feature name means this feature is the open gl es version feature. - if (featureInfo.name == null) { - if (featureInfo.reqGlEsVersion != FeatureInfo.GL_ES_VERSION_UNDEFINED) { - return Integer.toString((featureInfo.reqGlEsVersion & 0xffff0000) >> 16); - } else { - return "1"; // Lack of property means OpenGL ES version 1 - } + @NonNull + @Override + public String getOpenGL(Context context) { + String ov = DeviceInfo.this.mpOverride.getOpenGL(context); + if (ov != null) return ov; + PackageManager packageManager = context.getPackageManager(); + FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures(); + if (featureInfos != null && featureInfos.length > 0) { + for (FeatureInfo featureInfo : featureInfos) { + // Null feature name means this feature is the open gl es version feature. + if (featureInfo.name == null) { + if (featureInfo.reqGlEsVersion != FeatureInfo.GL_ES_VERSION_UNDEFINED) { + return Integer.toString((featureInfo.reqGlEsVersion & 0xffff0000) >> 16); + } else { + return "1"; // Lack of property means OpenGL ES version 1 } } } - return "1"; } + return "1"; + } + + @NonNull + public DiskMetric getDiskSpaces(Context context) { + DiskMetric ov = DeviceInfo.this.mpOverride.getDiskSpaces(context); + if (ov != null) return ov; + long totalBytes = 0; + long usedBytes = 0; - /** - * Returns total and used storage in MB for internal storage + root partition. - * SD card is excluded. - */ - @NonNull - public Map.Entry getDiskSpaces(Context context) { - long totalBytes = 0; - long usedBytes = 0; - - StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); - if (sm != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - for (android.os.storage.StorageVolume sv : sm.getStorageVolumes()) { - if (sv.isPrimary()) { - StorageStatsManager stm = (StorageStatsManager) context.getSystemService(Context.STORAGE_STATS_SERVICE); - totalBytes += stm.getTotalBytes(StorageManager.UUID_DEFAULT); - usedBytes += stm.getTotalBytes(StorageManager.UUID_DEFAULT) - stm.getFreeBytes(StorageManager.UUID_DEFAULT); - } + StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + if (sm != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + for (android.os.storage.StorageVolume sv : sm.getStorageVolumes()) { + if (sv.isPrimary()) { + StorageStatsManager stm = (StorageStatsManager) context.getSystemService(Context.STORAGE_STATS_SERVICE); + totalBytes += stm.getTotalBytes(StorageManager.UUID_DEFAULT); + usedBytes += stm.getTotalBytes(StorageManager.UUID_DEFAULT) - stm.getFreeBytes(StorageManager.UUID_DEFAULT); } - } catch (Exception e) { - Countly.sharedInstance().L.w("[DeviceInfo] getDiskSpaces, Got exception while trying to get all volumes storage", e); } - } else { - try { - File path = Environment.getDataDirectory(); // /data - StatFs statFs = new StatFs(path.getAbsolutePath()); + } catch (Exception e) { + Countly.sharedInstance().L.w("[DeviceInfo] getDiskSpaces, Got exception while trying to get all volumes storage", e); + } + } else { + try { + File path = Environment.getDataDirectory(); // /data + StatFs statFs = new StatFs(path.getAbsolutePath()); - long blockSize, totalBlocks, availableBlocks; + long blockSize, totalBlocks, availableBlocks; - blockSize = statFs.getBlockSizeLong(); - totalBlocks = statFs.getBlockCountLong(); - availableBlocks = statFs.getAvailableBlocksLong(); + blockSize = statFs.getBlockSizeLong(); + totalBlocks = statFs.getBlockCountLong(); + availableBlocks = statFs.getAvailableBlocksLong(); - totalBytes = totalBlocks * blockSize; - long freeBytes = availableBlocks * blockSize; - usedBytes = totalBytes - freeBytes; - } catch (Exception e) { - Countly.sharedInstance().L.w("[DeviceInfo] getDiskSpaces, Got exception while trying to get all volumes storage", e); - } + totalBytes = totalBlocks * blockSize; + long freeBytes = availableBlocks * blockSize; + usedBytes = totalBytes - freeBytes; + } catch (Exception e) { + Countly.sharedInstance().L.w("[DeviceInfo] getDiskSpaces, Got exception while trying to get all volumes storage", e); } - - long totalMb = totalBytes / 1024 / 1024; - long usedMb = usedBytes / 1024 / 1024; - - Countly.sharedInstance().L.d("[DeviceInfo] getDiskSpaces, totalSpaceInMB:[" + totalMb + "], usedSpaceInMB:[" + usedMb + "]"); - return new AbstractMap.SimpleEntry<>(Long.toString(totalMb), Long.toString(usedMb)); } - /** - * Returns the current device battery level. - */ - @Nullable - @Override - public String getBatteryLevel(Context context) { - try { - Intent batteryIntent; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - batteryIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED), null, null, Context.RECEIVER_NOT_EXPORTED); - } else { - batteryIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - } - if (batteryIntent != null) { - int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); - int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + long totalMb = totalBytes / 1024 / 1024; + long usedMb = usedBytes / 1024 / 1024; - // Error checking that probably isn't needed but I added just in case. - if (level > -1 && scale > 0) { - return Float.toString(((float) level / (float) scale) * 100.0f); - } + Countly.sharedInstance().L.d("[DeviceInfo] getDiskSpaces, totalSpaceInMB:[" + totalMb + "], usedSpaceInMB:[" + usedMb + "]"); + return new DiskMetric(Long.toString(totalMb), Long.toString(usedMb)); + } + + @Nullable + @Override + public String getBatteryLevel(Context context) { + String ov = DeviceInfo.this.mpOverride.getBatteryLevel(context); + if (ov != null) return ov; + try { + Intent batteryIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + batteryIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED), null, null, Context.RECEIVER_NOT_EXPORTED); + } else { + batteryIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + } + if (batteryIntent != null) { + int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + // Error checking that probably isn't needed but I added just in case. + if (level > -1 && scale > 0) { + return Float.toString(((float) level / (float) scale) * 100.0f); } - } catch (Exception e) { - Countly.sharedInstance().L.i("Can't get battery level"); } - - return null; + } catch (Exception e) { + Countly.sharedInstance().L.i("Can't get battery level"); } + return null; + } - /** - * Returns the current device orientation. - */ - @Nullable - @Override - public String getOrientation(Context context) { - int orientation = context.getResources().getConfiguration().orientation; - switch (orientation) { - case Configuration.ORIENTATION_LANDSCAPE: - return "Landscape"; - case Configuration.ORIENTATION_PORTRAIT: - return "Portrait"; - case Configuration.ORIENTATION_SQUARE: - return "Square"; - case Configuration.ORIENTATION_UNDEFINED: - return "Unknown"; - default: - return null; - } + @Nullable + @Override + public String getOrientation(Context context) { + String ov = DeviceInfo.this.mpOverride.getOrientation(context); + if (ov != null) return ov; + int orientation = context.getResources().getConfiguration().orientation; + switch (orientation) { + case Configuration.ORIENTATION_LANDSCAPE: + return "Landscape"; + case Configuration.ORIENTATION_PORTRAIT: + return "Portrait"; + case Configuration.ORIENTATION_SQUARE: + return "Square"; + case Configuration.ORIENTATION_UNDEFINED: + return "Unknown"; + default: + return null; } + } + + @NonNull + @Override + public String isRooted() { + String ov = DeviceInfo.this.mpOverride.isRooted(); + if (ov != null) return ov; + String[] paths = { + "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", + "/system/bin/failsafe/su", "/data/local/su" + }; + for (String path : paths) { + if (new File(path).exists()) return "true"; + } + return "false"; + } - /** - * Checks if device is rooted. - */ - @NonNull - @Override - public String isRooted() { - String[] paths = { - "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", - "/system/bin/failsafe/su", "/data/local/su" - }; - for (String path : paths) { - if (new File(path).exists()) return "true"; + @SuppressLint("MissingPermission") + @Nullable + @Override + public String isOnline(Context context) { + String ov = DeviceInfo.this.mpOverride.isOnline(context); + if (ov != null) return ov; + try { + ConnectivityManager conMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (conMgr != null && conMgr.getActiveNetworkInfo() != null + && conMgr.getActiveNetworkInfo().isAvailable() + && conMgr.getActiveNetworkInfo().isConnected()) { + + return "true"; } return "false"; + } catch (Exception e) { + Countly.sharedInstance().L.w("isOnline, Got exception determining netwprl connectivity", e); } + return null; + } - /** - * Checks if device is online. - */ - @SuppressLint("MissingPermission") - @Nullable - @Override - public String isOnline(Context context) { - try { - ConnectivityManager conMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (conMgr != null && conMgr.getActiveNetworkInfo() != null - && conMgr.getActiveNetworkInfo().isAvailable() - && conMgr.getActiveNetworkInfo().isConnected()) { - + @NonNull + @Override + public String isMuted(Context context) { + String ov = DeviceInfo.this.mpOverride.isMuted(context); + if (ov != null) return ov; + try { + AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + switch (audio.getRingerMode()) { + case AudioManager.RINGER_MODE_SILENT: + case AudioManager.RINGER_MODE_VIBRATE: return "true"; - } - return "false"; - } catch (Exception e) { - Countly.sharedInstance().L.w("isOnline, Got exception determining netwprl connectivity", e); + default: + return "false"; } - return null; + } catch (Throwable thr) { + return "false"; } + } - /** - * Checks if device is muted. - */ - @NonNull - @Override - public String isMuted(Context context) { - try { - AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - switch (audio.getRingerMode()) { - case AudioManager.RINGER_MODE_SILENT: - // Fall-through - case AudioManager.RINGER_MODE_VIBRATE: - return "true"; - default: - return "false"; - } - } catch (Throwable thr) { - return "false"; - } + @Override + public String hasHinge(Context context) { + String ov = DeviceInfo.this.mpOverride.hasHinge(context); + if (ov != null) return ov; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE) + ""; } + return "false"; + } - /** - * Check if device is foldable - * requires API level 30 - * - * @param context to use - * @return true if device is foldable - */ - @Override - public String hasHinge(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE) + ""; + @Override public String getRunningTime() { + String ov = DeviceInfo.this.mpOverride.getRunningTime(); + if (ov != null) return ov; + return Integer.toString(UtilsTime.currentTimestampSeconds() - startTime); + } + }; + } + + private long getTotalRAMInternal() { + if (totalMemory == 0) { + RandomAccessFile reader = null; + String load; + try { + reader = new RandomAccessFile("/proc/meminfo", "r"); + load = reader.readLine(); + + Pattern p = Pattern.compile("(\\d+)"); + Matcher m = p.matcher(load); + String value = ""; + while (m.find()) { + value = m.group(1); + } + try { + if (value != null) { + totalMemory = Long.parseLong(value) / 1024; + } else { + totalMemory = 0; } - return "false"; + } catch (NumberFormatException ex) { + totalMemory = 0; } - - /** - * Get app's running time before crashing. - */ - @Override public String getRunningTime() { - return Integer.toString(UtilsTime.currentTimestampSeconds() - startTime); + } catch (IOException ex) { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException exc) { + exc.printStackTrace(); + } + ex.printStackTrace(); + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException exc) { + exc.printStackTrace(); } - }; + } } + return totalMemory; } /** @@ -732,18 +712,18 @@ JSONObject getCrashDataJSON(@NonNull CrashData crashData, final boolean isNative Map getCrashMetrics(@NonNull final Context context, boolean isNativeCrash, @Nullable final Map metricOverride, @NonNull ModuleLog L) { Map metrics = getCommonMetrics(context, metricOverride, L); - Map.Entry storageMb = mp.getDiskSpaces(context); + DiskMetric diskMetric = mp.getDiskSpaces(context); putIfNotNullAndNotEmpty(metrics, "_cpu", mp.getCpu()); putIfNotNullAndNotEmpty(metrics, "_opengl", mp.getOpenGL(context)); putIfNotNullAndNotEmpty(metrics, "_root", mp.isRooted()); putIfNotNullAndNotEmpty(metrics, "_ram_total", mp.getRamTotal()); - putIfNotNullAndNotEmpty(metrics, "_disk_total", storageMb.getKey()); + putIfNotNullAndNotEmpty(metrics, "_disk_total", diskMetric.totalMb); if (!isNativeCrash) { //if is not a native crash putIfNotNullAndNotEmpty(metrics, "_ram_current", mp.getRamCurrent(context)); - putIfNotNullAndNotEmpty(metrics, "_disk_current", storageMb.getValue()); + putIfNotNullAndNotEmpty(metrics, "_disk_current", diskMetric.usedMb); putIfNotNullAndNotEmpty(metrics, "_bat", mp.getBatteryLevel(context)); putIfNotNullAndNotEmpty(metrics, "_run", mp.getRunningTime()); putIfNotNullAndNotEmpty(metrics, "_orientation", mp.getOrientation(context)); diff --git a/sdk/src/main/java/ly/count/android/sdk/DiskMetric.java b/sdk/src/main/java/ly/count/android/sdk/DiskMetric.java new file mode 100644 index 000000000..d16a723f1 --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/DiskMetric.java @@ -0,0 +1,21 @@ +package ly.count.android.sdk; + +/** + * Holds disk usage metrics in megabytes. + */ +public class DiskMetric { + /** + * Total disk space in megabytes + */ + public final String totalMb; + + /** + * Used (current) disk space in megabytes + */ + public final String usedMb; + + public DiskMetric(String totalMb, String usedMb) { + this.totalMb = totalMb; + this.usedMb = usedMb; + } +} diff --git a/sdk/src/main/java/ly/count/android/sdk/MetricProvider.java b/sdk/src/main/java/ly/count/android/sdk/MetricProvider.java index cae565a86..137f8af07 100644 --- a/sdk/src/main/java/ly/count/android/sdk/MetricProvider.java +++ b/sdk/src/main/java/ly/count/android/sdk/MetricProvider.java @@ -2,61 +2,110 @@ import android.content.Context; import android.util.DisplayMetrics; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.Map; -interface MetricProvider { - String getOS(); +public interface MetricProvider { + default String getOS() { + return null; + } - String getOSVersion(); + default String getOSVersion() { + return null; + } - String getDevice(); + default String getDevice() { + return null; + } - String getManufacturer(); + default String getManufacturer() { + return null; + } - String getResolution(final Context context); + default String getResolution(final Context context) { + return null; + } - String getDensity(final Context context); + default String getDensity(final Context context) { + return null; + } - String getCarrier(final Context context); + default String getCarrier(final Context context) { + return null; + } - int getTimezoneOffset(); + default String getTimezoneOffset() { + return null; + } - String getLocale(); + default String getLocale() { + return null; + } - @NonNull - String getAppVersion(final Context context); + default String getAppVersion(final Context context) { + return null; + } - String getStore(final Context context); + default String getStore(final Context context) { + return null; + } - String getDeviceType(final Context context); + default String getDeviceType(final Context context) { + return null; + } - long getTotalRAM(); + default String getTotalRAM() { + return null; + } - String getRamCurrent(Context context); + default String getRamCurrent(Context context) { + return null; + } - String getRamTotal(); + default String getRamTotal() { + return null; + } - String getCpu(); + default String getCpu() { + return null; + } - String getOpenGL(Context context); + default String getOpenGL(Context context) { + return null; + } - @Nullable String getBatteryLevel(Context context); + @Nullable default String getBatteryLevel(Context context) { + return null; + } - @Nullable String getOrientation(Context context); + @Nullable default String getOrientation(Context context) { + return null; + } - String isRooted(); + default String isRooted() { + return null; + } - @Nullable String isOnline(Context context); + @Nullable default String isOnline(Context context) { + return null; + } - String isMuted(Context context); + default String isMuted(Context context) { + return null; + } - String hasHinge(Context context); + default String hasHinge(Context context) { + return null; + } - String getRunningTime(); + default String getRunningTime() { + return null; + } - DisplayMetrics getDisplayMetrics(Context context); - - Map.Entry getDiskSpaces(Context context); + default DisplayMetrics getDisplayMetrics(Context context) { + return null; + } + + default DiskMetric getDiskSpaces(Context context) { + return null; + } }