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/lib/flutter_device_apps_platform_interface.dart b/lib/flutter_device_apps_platform_interface.dart index 951424d..916a775 100644 --- a/lib/flutter_device_apps_platform_interface.dart +++ b/lib/flutter_device_apps_platform_interface.dart @@ -16,6 +16,11 @@ class AppInfo { this.appName, this.versionName, this.versionCode, + this.uid, + this.apkPath, + this.apkSizeBytes, + this.dataPath, + this.isOnExternalStorage, this.firstInstallTime, this.lastUpdateTime, this.isSystem, @@ -57,6 +62,13 @@ 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, + 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()) + : null, firstInstallTime: firstInstallTimeDate, lastUpdateTime: lastUpdateTimeDate, isSystem: m['isSystem'] != null ? bool.tryParse(m['isSystem']!.toString()) : null, @@ -83,6 +95,25 @@ 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; + + /// 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; + + /// 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/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 diff --git a/test/app_info_test.dart b/test/app_info_test.dart index 793988e..1fd2192 100644 --- a/test/app_info_test.dart +++ b/test/app_info_test.dart @@ -13,6 +13,11 @@ void main() { expect(appInfo.appName, isNull); expect(appInfo.versionName, isNull); 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); expect(appInfo.lastUpdateTime, isNull); expect(appInfo.isSystem, isNull); @@ -35,6 +40,11 @@ void main() { appName: 'Example App', versionName: '1.0.0', 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, lastUpdateTime: lastUpdateTime, isSystem: false, @@ -51,6 +61,11 @@ void main() { expect(appInfo.appName, 'Example App'); 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.apkSizeBytes, 12345678); + 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); @@ -72,6 +87,11 @@ void main() { expect(appInfo.appName, isNull); expect(appInfo.versionName, isNull); 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); expect(appInfo.lastUpdateTime, isNull); expect(appInfo.isSystem, isNull); @@ -89,6 +109,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', }; @@ -97,12 +119,16 @@ 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'); }); test('parses integer fields from int values', () { final map = { 'versionCode': 42, + 'uid': 10123, + 'apkSizeBytes': 4096, 'category': 5, 'targetSdkVersion': 34, 'minSdkVersion': 23, @@ -112,6 +138,8 @@ void main() { final appInfo = AppInfo.fromMap(map); expect(appInfo.versionCode, 42); + expect(appInfo.uid, 10123); + expect(appInfo.apkSizeBytes, 4096); expect(appInfo.category, 5); expect(appInfo.targetSdkVersion, 34); expect(appInfo.minSdkVersion, 23); @@ -121,6 +149,8 @@ void main() { test('parses integer fields from string values', () { final map = { 'versionCode': '42', + 'uid': '10123', + 'apkSizeBytes': '4096', 'category': '5', 'targetSdkVersion': '34', 'minSdkVersion': '23', @@ -130,6 +160,8 @@ void main() { final appInfo = AppInfo.fromMap(map); expect(appInfo.versionCode, 42); + expect(appInfo.uid, 10123); + expect(appInfo.apkSizeBytes, 4096); expect(appInfo.category, 5); expect(appInfo.targetSdkVersion, 34); expect(appInfo.minSdkVersion, 23); @@ -139,6 +171,8 @@ void main() { test('handles invalid integer strings gracefully', () { final map = { 'versionCode': 'invalid', + 'uid': 'invalid_uid', + 'apkSizeBytes': 'invalid_size', 'category': 'not_a_number', 'targetSdkVersion': '', }; @@ -146,6 +180,8 @@ void main() { final appInfo = AppInfo.fromMap(map); expect(appInfo.versionCode, isNull); + expect(appInfo.uid, isNull); + expect(appInfo.apkSizeBytes, isNull); expect(appInfo.category, isNull); expect(appInfo.targetSdkVersion, isNull); }); @@ -154,36 +190,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', () { @@ -270,6 +312,11 @@ void main() { 'appName': 'Full Test App', 'versionName': '3.2.1', '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, 'lastUpdateTime': lastUpdateMs, 'isSystem': false, @@ -288,6 +335,11 @@ void main() { expect(appInfo.appName, 'Full Test App'); 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.apkSizeBytes, 555000); + 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);