From 77354ea32b34f200e0b56be1a7623e0ee2f2c0db Mon Sep 17 00:00:00 2001 From: okmsbun Date: Wed, 29 Apr 2026 16:57:02 +0300 Subject: [PATCH 1/4] Add UID field to AppInfo class and update tests --- lib/flutter_device_apps_platform_interface.dart | 7 +++++++ test/app_info_test.dart | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/flutter_device_apps_platform_interface.dart b/lib/flutter_device_apps_platform_interface.dart index 951424d..d28b58a 100644 --- a/lib/flutter_device_apps_platform_interface.dart +++ b/lib/flutter_device_apps_platform_interface.dart @@ -16,6 +16,7 @@ class AppInfo { this.appName, this.versionName, this.versionCode, + this.uid, this.firstInstallTime, this.lastUpdateTime, this.isSystem, @@ -57,6 +58,7 @@ class AppInfo { appName: m['appName']?.toString(), versionName: m['versionName']?.toString(), versionCode: m['versionCode'] != null ? int.tryParse(m['versionCode']!.toString()) : null, + uid: m['uid'] != null ? int.tryParse(m['uid']!.toString()) : null, firstInstallTime: firstInstallTimeDate, lastUpdateTime: lastUpdateTimeDate, isSystem: m['isSystem'] != null ? bool.tryParse(m['isSystem']!.toString()) : null, @@ -83,6 +85,11 @@ class AppInfo { /// The internal version code used for version comparison. final int? versionCode; + /// Linux/kernel-level UID assigned to the app on the device. + /// + /// This is not a globally unique or stable business identifier. + final int? uid; + /// The date and time when the app was first installed on the device. final DateTime? firstInstallTime; diff --git a/test/app_info_test.dart b/test/app_info_test.dart index 793988e..30a5ca3 100644 --- a/test/app_info_test.dart +++ b/test/app_info_test.dart @@ -13,6 +13,7 @@ void main() { expect(appInfo.appName, isNull); expect(appInfo.versionName, isNull); expect(appInfo.versionCode, isNull); + expect(appInfo.uid, isNull); expect(appInfo.firstInstallTime, isNull); expect(appInfo.lastUpdateTime, isNull); expect(appInfo.isSystem, isNull); @@ -35,6 +36,7 @@ void main() { appName: 'Example App', versionName: '1.0.0', versionCode: 10, + uid: 10123, firstInstallTime: firstInstallTime, lastUpdateTime: lastUpdateTime, isSystem: false, @@ -51,6 +53,7 @@ void main() { expect(appInfo.appName, 'Example App'); expect(appInfo.versionName, '1.0.0'); expect(appInfo.versionCode, 10); + expect(appInfo.uid, 10123); expect(appInfo.firstInstallTime, firstInstallTime); expect(appInfo.lastUpdateTime, lastUpdateTime); expect(appInfo.isSystem, false); @@ -72,6 +75,7 @@ void main() { expect(appInfo.appName, isNull); expect(appInfo.versionName, isNull); expect(appInfo.versionCode, isNull); + expect(appInfo.uid, isNull); expect(appInfo.firstInstallTime, isNull); expect(appInfo.lastUpdateTime, isNull); expect(appInfo.isSystem, isNull); @@ -103,6 +107,7 @@ void main() { test('parses integer fields from int values', () { final map = { 'versionCode': 42, + 'uid': 10123, 'category': 5, 'targetSdkVersion': 34, 'minSdkVersion': 23, @@ -112,6 +117,7 @@ void main() { final appInfo = AppInfo.fromMap(map); expect(appInfo.versionCode, 42); + expect(appInfo.uid, 10123); expect(appInfo.category, 5); expect(appInfo.targetSdkVersion, 34); expect(appInfo.minSdkVersion, 23); @@ -121,6 +127,7 @@ void main() { test('parses integer fields from string values', () { final map = { 'versionCode': '42', + 'uid': '10123', 'category': '5', 'targetSdkVersion': '34', 'minSdkVersion': '23', @@ -130,6 +137,7 @@ void main() { final appInfo = AppInfo.fromMap(map); expect(appInfo.versionCode, 42); + expect(appInfo.uid, 10123); expect(appInfo.category, 5); expect(appInfo.targetSdkVersion, 34); expect(appInfo.minSdkVersion, 23); @@ -139,6 +147,7 @@ void main() { test('handles invalid integer strings gracefully', () { final map = { 'versionCode': 'invalid', + 'uid': 'invalid_uid', 'category': 'not_a_number', 'targetSdkVersion': '', }; @@ -146,6 +155,7 @@ void main() { final appInfo = AppInfo.fromMap(map); expect(appInfo.versionCode, isNull); + expect(appInfo.uid, isNull); expect(appInfo.category, isNull); expect(appInfo.targetSdkVersion, isNull); }); @@ -270,6 +280,7 @@ void main() { 'appName': 'Full Test App', 'versionName': '3.2.1', 'versionCode': 321, + 'uid': 10199, 'firstInstallTime': firstInstallMs, 'lastUpdateTime': lastUpdateMs, 'isSystem': false, @@ -288,6 +299,7 @@ void main() { expect(appInfo.appName, 'Full Test App'); expect(appInfo.versionName, '3.2.1'); expect(appInfo.versionCode, 321); + expect(appInfo.uid, 10199); expect(appInfo.firstInstallTime, DateTime(2024)); expect(appInfo.lastUpdateTime, DateTime(2024, 12, 31)); expect(appInfo.isSystem, false); From b8f8d68a94f9d3b4cbc2e1d0dd5dcec405dbea97 Mon Sep 17 00:00:00 2001 From: okmsbun Date: Wed, 29 Apr 2026 18:04:57 +0300 Subject: [PATCH 2/4] Add additional fields to AppInfo class and update tests --- ...lutter_device_apps_platform_interface.dart | 17 +++++++++++ test/app_info_test.dart | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/lib/flutter_device_apps_platform_interface.dart b/lib/flutter_device_apps_platform_interface.dart index d28b58a..689cdd0 100644 --- a/lib/flutter_device_apps_platform_interface.dart +++ b/lib/flutter_device_apps_platform_interface.dart @@ -17,6 +17,9 @@ class AppInfo { this.versionName, this.versionCode, this.uid, + this.apkPath, + this.dataPath, + this.isOnExternalStorage, this.firstInstallTime, this.lastUpdateTime, this.isSystem, @@ -59,6 +62,11 @@ class AppInfo { versionName: m['versionName']?.toString(), versionCode: m['versionCode'] != null ? int.tryParse(m['versionCode']!.toString()) : null, uid: m['uid'] != null ? int.tryParse(m['uid']!.toString()) : null, + apkPath: m['apkPath']?.toString(), + dataPath: m['dataPath']?.toString(), + isOnExternalStorage: m['isOnExternalStorage'] != null + ? bool.tryParse(m['isOnExternalStorage']!.toString()) + : null, firstInstallTime: firstInstallTimeDate, lastUpdateTime: lastUpdateTimeDate, isSystem: m['isSystem'] != null ? bool.tryParse(m['isSystem']!.toString()) : null, @@ -90,6 +98,15 @@ class AppInfo { /// This is not a globally unique or stable business identifier. final int? uid; + /// Full path to the base APK file (Android ApplicationInfo.sourceDir). + final String? apkPath; + + /// Full path to the app's private data directory (Android ApplicationInfo.dataDir). + final String? dataPath; + + /// Raw Android flag from ApplicationInfo.FLAG_EXTERNAL_STORAGE. + final bool? isOnExternalStorage; + /// The date and time when the app was first installed on the device. final DateTime? firstInstallTime; diff --git a/test/app_info_test.dart b/test/app_info_test.dart index 30a5ca3..7420644 100644 --- a/test/app_info_test.dart +++ b/test/app_info_test.dart @@ -14,6 +14,9 @@ void main() { expect(appInfo.versionName, isNull); expect(appInfo.versionCode, isNull); expect(appInfo.uid, isNull); + expect(appInfo.apkPath, isNull); + expect(appInfo.dataPath, isNull); + expect(appInfo.isOnExternalStorage, isNull); expect(appInfo.firstInstallTime, isNull); expect(appInfo.lastUpdateTime, isNull); expect(appInfo.isSystem, isNull); @@ -37,6 +40,9 @@ void main() { versionName: '1.0.0', versionCode: 10, uid: 10123, + apkPath: '/data/app/com.example.app/base.apk', + dataPath: '/data/user/0/com.example.app', + isOnExternalStorage: false, firstInstallTime: firstInstallTime, lastUpdateTime: lastUpdateTime, isSystem: false, @@ -54,6 +60,9 @@ void main() { expect(appInfo.versionName, '1.0.0'); expect(appInfo.versionCode, 10); expect(appInfo.uid, 10123); + expect(appInfo.apkPath, '/data/app/com.example.app/base.apk'); + expect(appInfo.dataPath, '/data/user/0/com.example.app'); + expect(appInfo.isOnExternalStorage, false); expect(appInfo.firstInstallTime, firstInstallTime); expect(appInfo.lastUpdateTime, lastUpdateTime); expect(appInfo.isSystem, false); @@ -76,6 +85,9 @@ void main() { expect(appInfo.versionName, isNull); expect(appInfo.versionCode, isNull); expect(appInfo.uid, isNull); + expect(appInfo.apkPath, isNull); + expect(appInfo.dataPath, isNull); + expect(appInfo.isOnExternalStorage, isNull); expect(appInfo.firstInstallTime, isNull); expect(appInfo.lastUpdateTime, isNull); expect(appInfo.isSystem, isNull); @@ -93,6 +105,8 @@ void main() { 'packageName': 'com.example.app', 'appName': 'Example App', 'versionName': '2.1.0', + 'apkPath': '/data/app/com.example.app/base.apk', + 'dataPath': '/data/user/0/com.example.app', 'processName': 'com.example.process', }; @@ -101,6 +115,8 @@ void main() { expect(appInfo.packageName, 'com.example.app'); expect(appInfo.appName, 'Example App'); expect(appInfo.versionName, '2.1.0'); + expect(appInfo.apkPath, '/data/app/com.example.app/base.apk'); + expect(appInfo.dataPath, '/data/user/0/com.example.app'); expect(appInfo.processName, 'com.example.process'); }); @@ -164,36 +180,42 @@ void main() { final map = { 'isSystem': true, 'enabled': false, + 'isOnExternalStorage': true, }; final appInfo = AppInfo.fromMap(map); expect(appInfo.isSystem, true); expect(appInfo.enabled, false); + expect(appInfo.isOnExternalStorage, true); }); test('parses boolean fields from string values', () { final map = { 'isSystem': 'true', 'enabled': 'false', + 'isOnExternalStorage': 'true', }; final appInfo = AppInfo.fromMap(map); expect(appInfo.isSystem, true); expect(appInfo.enabled, false); + expect(appInfo.isOnExternalStorage, true); }); test('handles invalid boolean strings gracefully', () { final map = { 'isSystem': 'yes', 'enabled': 'no', + 'isOnExternalStorage': 'unknown', }; final appInfo = AppInfo.fromMap(map); expect(appInfo.isSystem, isNull); expect(appInfo.enabled, isNull); + expect(appInfo.isOnExternalStorage, isNull); }); test('parses DateTime fields from milliseconds', () { @@ -281,6 +303,9 @@ void main() { 'versionName': '3.2.1', 'versionCode': 321, 'uid': 10199, + 'apkPath': '/data/app/com.test.fullapp/base.apk', + 'dataPath': '/data/user/0/com.test.fullapp', + 'isOnExternalStorage': false, 'firstInstallTime': firstInstallMs, 'lastUpdateTime': lastUpdateMs, 'isSystem': false, @@ -300,6 +325,9 @@ void main() { expect(appInfo.versionName, '3.2.1'); expect(appInfo.versionCode, 321); expect(appInfo.uid, 10199); + expect(appInfo.apkPath, '/data/app/com.test.fullapp/base.apk'); + expect(appInfo.dataPath, '/data/user/0/com.test.fullapp'); + expect(appInfo.isOnExternalStorage, false); expect(appInfo.firstInstallTime, DateTime(2024)); expect(appInfo.lastUpdateTime, DateTime(2024, 12, 31)); expect(appInfo.isSystem, false); From fbc1e5507d789ee6bd28331bc0c64e305869c4ed Mon Sep 17 00:00:00 2001 From: okmsbun Date: Thu, 30 Apr 2026 11:08:10 +0300 Subject: [PATCH 3/4] Add apkSizeBytes field to AppInfo class and update tests --- lib/flutter_device_apps_platform_interface.dart | 7 +++++++ test/app_info_test.dart | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/flutter_device_apps_platform_interface.dart b/lib/flutter_device_apps_platform_interface.dart index 689cdd0..916a775 100644 --- a/lib/flutter_device_apps_platform_interface.dart +++ b/lib/flutter_device_apps_platform_interface.dart @@ -18,6 +18,7 @@ class AppInfo { this.versionCode, this.uid, this.apkPath, + this.apkSizeBytes, this.dataPath, this.isOnExternalStorage, this.firstInstallTime, @@ -63,6 +64,7 @@ class AppInfo { versionCode: m['versionCode'] != null ? int.tryParse(m['versionCode']!.toString()) : null, uid: m['uid'] != null ? int.tryParse(m['uid']!.toString()) : null, apkPath: m['apkPath']?.toString(), + apkSizeBytes: m['apkSizeBytes'] != null ? int.tryParse(m['apkSizeBytes']!.toString()) : null, dataPath: m['dataPath']?.toString(), isOnExternalStorage: m['isOnExternalStorage'] != null ? bool.tryParse(m['isOnExternalStorage']!.toString()) @@ -101,6 +103,11 @@ class AppInfo { /// Full path to the base APK file (Android ApplicationInfo.sourceDir). final String? apkPath; + /// APK size in bytes (base APK + split APK files when present). + /// + /// Null when not available. + final int? apkSizeBytes; + /// Full path to the app's private data directory (Android ApplicationInfo.dataDir). final String? dataPath; diff --git a/test/app_info_test.dart b/test/app_info_test.dart index 7420644..1fd2192 100644 --- a/test/app_info_test.dart +++ b/test/app_info_test.dart @@ -15,6 +15,7 @@ void main() { expect(appInfo.versionCode, isNull); expect(appInfo.uid, isNull); expect(appInfo.apkPath, isNull); + expect(appInfo.apkSizeBytes, isNull); expect(appInfo.dataPath, isNull); expect(appInfo.isOnExternalStorage, isNull); expect(appInfo.firstInstallTime, isNull); @@ -41,6 +42,7 @@ void main() { versionCode: 10, uid: 10123, apkPath: '/data/app/com.example.app/base.apk', + apkSizeBytes: 12345678, dataPath: '/data/user/0/com.example.app', isOnExternalStorage: false, firstInstallTime: firstInstallTime, @@ -61,6 +63,7 @@ void main() { expect(appInfo.versionCode, 10); expect(appInfo.uid, 10123); expect(appInfo.apkPath, '/data/app/com.example.app/base.apk'); + expect(appInfo.apkSizeBytes, 12345678); expect(appInfo.dataPath, '/data/user/0/com.example.app'); expect(appInfo.isOnExternalStorage, false); expect(appInfo.firstInstallTime, firstInstallTime); @@ -86,6 +89,7 @@ void main() { expect(appInfo.versionCode, isNull); expect(appInfo.uid, isNull); expect(appInfo.apkPath, isNull); + expect(appInfo.apkSizeBytes, isNull); expect(appInfo.dataPath, isNull); expect(appInfo.isOnExternalStorage, isNull); expect(appInfo.firstInstallTime, isNull); @@ -124,6 +128,7 @@ void main() { final map = { 'versionCode': 42, 'uid': 10123, + 'apkSizeBytes': 4096, 'category': 5, 'targetSdkVersion': 34, 'minSdkVersion': 23, @@ -134,6 +139,7 @@ void main() { expect(appInfo.versionCode, 42); expect(appInfo.uid, 10123); + expect(appInfo.apkSizeBytes, 4096); expect(appInfo.category, 5); expect(appInfo.targetSdkVersion, 34); expect(appInfo.minSdkVersion, 23); @@ -144,6 +150,7 @@ void main() { final map = { 'versionCode': '42', 'uid': '10123', + 'apkSizeBytes': '4096', 'category': '5', 'targetSdkVersion': '34', 'minSdkVersion': '23', @@ -154,6 +161,7 @@ void main() { expect(appInfo.versionCode, 42); expect(appInfo.uid, 10123); + expect(appInfo.apkSizeBytes, 4096); expect(appInfo.category, 5); expect(appInfo.targetSdkVersion, 34); expect(appInfo.minSdkVersion, 23); @@ -164,6 +172,7 @@ void main() { final map = { 'versionCode': 'invalid', 'uid': 'invalid_uid', + 'apkSizeBytes': 'invalid_size', 'category': 'not_a_number', 'targetSdkVersion': '', }; @@ -172,6 +181,7 @@ void main() { expect(appInfo.versionCode, isNull); expect(appInfo.uid, isNull); + expect(appInfo.apkSizeBytes, isNull); expect(appInfo.category, isNull); expect(appInfo.targetSdkVersion, isNull); }); @@ -304,6 +314,7 @@ void main() { 'versionCode': 321, 'uid': 10199, 'apkPath': '/data/app/com.test.fullapp/base.apk', + 'apkSizeBytes': 555000, 'dataPath': '/data/user/0/com.test.fullapp', 'isOnExternalStorage': false, 'firstInstallTime': firstInstallMs, @@ -326,6 +337,7 @@ void main() { expect(appInfo.versionCode, 321); expect(appInfo.uid, 10199); expect(appInfo.apkPath, '/data/app/com.test.fullapp/base.apk'); + expect(appInfo.apkSizeBytes, 555000); expect(appInfo.dataPath, '/data/user/0/com.test.fullapp'); expect(appInfo.isOnExternalStorage, false); expect(appInfo.firstInstallTime, DateTime(2024)); From 59a49148f8beb08b13e0110da52901c8596e61c0 Mon Sep 17 00:00:00 2001 From: okmsbun Date: Thu, 30 Apr 2026 11:37:16 +0300 Subject: [PATCH 4/4] Bump version to 0.7.0 and update CHANGELOG with new AppInfo fields and test coverage --- CHANGELOG.md | 6 +++++- pubspec.yaml | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f2c34d..cfea98a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.0 +- Expanded `AppInfo` with additional Android raw metadata fields: `uid`, `apkPath`, `apkSizeBytes`, `dataPath`, and `isOnExternalStorage`. +- Updated `AppInfo` unit tests to cover parsing and null-safety behavior for the new fields. + ## 0.6.0 - **BREAKING**: Removed `requestedPermissions` field from `AppInfo` class to improve performance and reduce memory usage - Added new API: `getRequestedPermissions(String packageName)` to fetch app permissions on demand @@ -31,4 +35,4 @@ App change events now forward the raw Android action string to Dart, which maps ## 0.1.0 - Initial release of platform interface for flutter_device_apps -- Defines AppInfo, AppChangeEvent, and FlutterDeviceAppsPlatform contract \ No newline at end of file +- Defines AppInfo, AppChangeEvent, and FlutterDeviceAppsPlatform contract diff --git a/pubspec.yaml b/pubspec.yaml index 32ae7d1..d5ded5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_device_apps_platform_interface description: Platform-agnostic API contract for flutter_device_apps (federated). -version: 0.6.0 +version: 0.7.0 repository: https://github.com/okmsbun/flutter_device_apps_platform_interface issue_tracker: https://github.com/okmsbun/flutter_device_apps_platform_interface/issues topics: @@ -18,4 +18,4 @@ dependencies: dev_dependencies: lints: ^6.1.0 - test: ^1.29.0 + test: ^1.31.1