Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
- Defines AppInfo, AppChangeEvent, and FlutterDeviceAppsPlatform contract
31 changes: 31 additions & 0 deletions lib/flutter_device_apps_platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -18,4 +18,4 @@ dependencies:

dev_dependencies:
lints: ^6.1.0
test: ^1.29.0
test: ^1.31.1
52 changes: 52 additions & 0 deletions test/app_info_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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',
};

Expand All @@ -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 = <String, Object?>{
'versionCode': 42,
'uid': 10123,
'apkSizeBytes': 4096,
'category': 5,
'targetSdkVersion': 34,
'minSdkVersion': 23,
Expand All @@ -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);
Expand All @@ -121,6 +149,8 @@ void main() {
test('parses integer fields from string values', () {
final map = <String, Object?>{
'versionCode': '42',
'uid': '10123',
'apkSizeBytes': '4096',
'category': '5',
'targetSdkVersion': '34',
'minSdkVersion': '23',
Expand All @@ -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);
Expand All @@ -139,13 +171,17 @@ void main() {
test('handles invalid integer strings gracefully', () {
final map = <String, Object?>{
'versionCode': 'invalid',
'uid': 'invalid_uid',
'apkSizeBytes': 'invalid_size',
'category': 'not_a_number',
'targetSdkVersion': '',
};

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);
});
Expand All @@ -154,36 +190,42 @@ void main() {
final map = <String, Object?>{
'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 = <String, Object?>{
'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 = <String, Object?>{
'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', () {
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down
Loading