From a0cbc58456f5e6a519b19f6561143eee8733a97c Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 30 Mar 2026 17:28:58 +0300 Subject: [PATCH 1/6] feat: changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1733acfb..7570fca25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 26.1.1 * Added Content feature method `previewContent(String contentId)` (Experimental!). * Improved content display and refresh mechanics. +* Added a new config option `setMetricProvider(MetricProvider)` to allow overriding default device metrics with custom values. * Mitigated an issue about health checks storage in explicit storage mode. From 93586d3220a99249a1402698ad739102c29f6d4e Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 30 Mar 2026 17:30:24 +0300 Subject: [PATCH 2/6] feat: feature impl --- .../ly/count/android/sdk/CountlyConfig.java | 13 + .../java/ly/count/android/sdk/DeviceInfo.java | 837 ++++++++---------- .../ly/count/android/sdk/MetricProvider.java | 110 ++- 3 files changed, 486 insertions(+), 474 deletions(-) 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 3b2131826..3cb26ac4c 100644 --- a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java +++ b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java @@ -376,6 +376,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..2a7e77f48 100644 --- a/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java +++ b/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java @@ -65,508 +65,457 @@ 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; - } + @NonNull + @Override + public String getResolution(@NonNull final Context context) { + 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"; + @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"; - } + @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"; + @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"; } - - // 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; + if (Utils.isDeviceTablet(context)) { + return "tablet"; } + return "mobile"; + } - /** - * 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)); - } + @Override + public String getTotalRAM() { + String ov = DeviceInfo.this.mpOverride.getTotalRAM(); + if (ov != null) return ov; + return Long.toString(getTotalRAMInternal()); + } - /** - * Returns the total device RAM amount. - */ - @NonNull - @Override - public String getRamTotal() { - return Long.toString(getTotalRAM()); - } + @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 current device cpu. - */ - @NonNull - @Override - public String getCpu() { - return Build.SUPPORTED_ABIS[0]; - } + @NonNull + @Override + public String getRamTotal() { + String ov = DeviceInfo.this.mpOverride.getRamTotal(); + if (ov != null) return ov; + return Long.toString(getTotalRAMInternal()); + } + + @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) { + if (featureInfo.name == null) { + if (featureInfo.reqGlEsVersion != FeatureInfo.GL_ES_VERSION_UNDEFINED) { + return Integer.toString((featureInfo.reqGlEsVersion & 0xffff0000) >> 16); + } else { + return "1"; } } } - return "1"; } + return "1"; + } + + @NonNull + public Map.Entry getDiskSpaces(Context context) { + Map.Entry 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 AbstractMap.SimpleEntry<>(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); + 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; } /** 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..f83c1ab78 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,111 @@ 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 Map.Entry getDiskSpaces(Context context) { + return null; + } } From 72e127fa18672ea62c503f16b255bf21c5a4f2ef Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 30 Mar 2026 17:34:28 +0300 Subject: [PATCH 3/6] feat: tests --- .../ly/count/android/sdk/DeviceInfoTests.java | 137 ++++++++++++++++++ .../android/sdk/MockedMetricProvider.java | 8 +- 2 files changed, 141 insertions(+), 4 deletions(-) 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..32f5553be 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/DeviceInfoTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/DeviceInfoTests.java @@ -31,6 +31,7 @@ of this software and associated documentation files (the "Software"), to deal import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.util.AbstractMap; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -42,6 +43,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 +298,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()); + + Map.Entry diskSpaces = deviceInfo.mp.getDiskSpaces(TestUtils.getContext()); + assertEquals("45", diskSpaces.getKey()); + assertEquals("23", diskSpaces.getValue()); + } + + @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 Map.Entry getDiskSpaces(Context context) { + return new AbstractMap.SimpleEntry<>("100", "50"); + } + }; + DeviceInfo deviceInfo = new DeviceInfo(diskOverride); + + Map.Entry diskSpaces = deviceInfo.mp.getDiskSpaces(TestUtils.getContext()); + assertEquals("100", diskSpaces.getKey()); + assertEquals("50", diskSpaces.getValue()); + + // 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..ba2a6d2ef 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/MockedMetricProvider.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/MockedMetricProvider.java @@ -41,8 +41,8 @@ public MockedMetricProvider() { return "G"; } - @Override public int getTimezoneOffset() { - return 66; + @Override public String getTimezoneOffset() { + return "66"; } @Override public String getLocale() { @@ -61,8 +61,8 @@ public MockedMetricProvider() { return "K"; } - @Override public long getTotalRAM() { - return 42; + @Override public String getTotalRAM() { + return "42"; } @Override public String getRamCurrent(Context context) { From 9880d38ddb850248cc22edf706b88564a59eddd6 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 2 Apr 2026 09:24:42 +0300 Subject: [PATCH 4/6] feat: disk metric impl --- .../ly/count/android/sdk/DeviceInfoTests.java | 17 +++++++-------- .../android/sdk/MockedMetricProvider.java | 6 ++---- .../java/ly/count/android/sdk/DeviceInfo.java | 13 ++++++------ .../java/ly/count/android/sdk/DiskMetric.java | 21 +++++++++++++++++++ .../ly/count/android/sdk/MetricProvider.java | 3 +-- 5 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 sdk/src/main/java/ly/count/android/sdk/DiskMetric.java 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 32f5553be..c3ee01099 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/DeviceInfoTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/DeviceInfoTests.java @@ -31,7 +31,6 @@ of this software and associated documentation files (the "Software"), to deal import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.util.AbstractMap; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -330,9 +329,9 @@ public void testMetricProviderOverride_fullOverride() { assertEquals("Z", deviceInfo.mp.hasHinge(TestUtils.getContext())); assertEquals("88", deviceInfo.mp.getRunningTime()); - Map.Entry diskSpaces = deviceInfo.mp.getDiskSpaces(TestUtils.getContext()); - assertEquals("45", diskSpaces.getKey()); - assertEquals("23", diskSpaces.getValue()); + DiskMetric diskMetric = deviceInfo.mp.getDiskSpaces(TestUtils.getContext()); + assertEquals("45", diskMetric.totalMb); + assertEquals("23", diskMetric.usedMb); } @Test @@ -384,15 +383,15 @@ public void testMetricProviderOverride_emptyOverride() { @Test public void testMetricProviderOverride_diskSpacesOverride() { MetricProvider diskOverride = new MetricProvider() { - @Override public Map.Entry getDiskSpaces(Context context) { - return new AbstractMap.SimpleEntry<>("100", "50"); + @Override public DiskMetric getDiskSpaces(Context context) { + return new DiskMetric("100", "50"); } }; DeviceInfo deviceInfo = new DeviceInfo(diskOverride); - Map.Entry diskSpaces = deviceInfo.mp.getDiskSpaces(TestUtils.getContext()); - assertEquals("100", diskSpaces.getKey()); - assertEquals("50", diskSpaces.getValue()); + 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()); 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 ba2a6d2ef..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 { @@ -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/DeviceInfo.java b/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java index 2a7e77f48..d01815841 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; @@ -310,8 +309,8 @@ public String getOpenGL(Context context) { } @NonNull - public Map.Entry getDiskSpaces(Context context) { - Map.Entry ov = DeviceInfo.this.mpOverride.getDiskSpaces(context); + public DiskMetric getDiskSpaces(Context context) { + DiskMetric ov = DeviceInfo.this.mpOverride.getDiskSpaces(context); if (ov != null) return ov; long totalBytes = 0; long usedBytes = 0; @@ -352,7 +351,7 @@ public Map.Entry getDiskSpaces(Context context) { 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)); + return new DiskMetric(Long.toString(totalMb), Long.toString(usedMb)); } @Nullable @@ -681,18 +680,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 f83c1ab78..137f8af07 100644 --- a/sdk/src/main/java/ly/count/android/sdk/MetricProvider.java +++ b/sdk/src/main/java/ly/count/android/sdk/MetricProvider.java @@ -3,7 +3,6 @@ import android.content.Context; import android.util.DisplayMetrics; import androidx.annotation.Nullable; -import java.util.Map; public interface MetricProvider { default String getOS() { @@ -106,7 +105,7 @@ default DisplayMetrics getDisplayMetrics(Context context) { return null; } - default Map.Entry getDiskSpaces(Context context) { + default DiskMetric getDiskSpaces(Context context) { return null; } } From b01c13a34e8c1c84611adcfc56d59368af14baf4 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:14:17 +0300 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0013476..f98f1fe76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ ## 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!). * Improved content display and refresh mechanics. -* Added a new config option `setMetricProvider(MetricProvider)` to allow overriding default device metrics with custom values. * Mitigated an issue about health checks storage in explicit storage mode. From 05b6124edd4440f091c1c5f569e6c8c7f28016b2 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:38:24 +0300 Subject: [PATCH 6/6] Update DeviceInfo.java --- .../java/ly/count/android/sdk/DeviceInfo.java | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) 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 d01815841..43800653b 100644 --- a/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java +++ b/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java @@ -101,9 +101,19 @@ public String getManufacturer() { return Build.MANUFACTURER; } + /** + * 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 = ""; @@ -204,6 +214,11 @@ public String getLocale() { return locale.getLanguage() + "_" + locale.getCountry(); } + /** + * 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) { @@ -221,6 +236,9 @@ public String getAppVersion(@NonNull final Context context) { return result; } + /** + * Returns the package name of the app that installed this app + */ @NonNull @Override public String getStore(@NonNull final Context context) { @@ -239,6 +257,11 @@ public String getStore(@NonNull final Context context) { return result; } + /** + * 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) { @@ -260,6 +283,10 @@ public String getTotalRAM() { return Long.toString(getTotalRAMInternal()); } + + /** + * Returns the current device RAM amount. + */ @NonNull @Override public String getRamCurrent(Context context) { @@ -271,6 +298,9 @@ public String getRamCurrent(Context context) { return Long.toString(getTotalRAMInternal() - (mi.availMem / 1_048_576L)); } + /** + * Returns the total device RAM amount. + */ @NonNull @Override public String getRamTotal() { @@ -296,11 +326,12 @@ public String getOpenGL(Context context) { 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"; + return "1"; // Lack of property means OpenGL ES version 1 } } } @@ -369,6 +400,7 @@ public String getBatteryLevel(Context context) { 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); }