From 6f535d738fc11cc599aecdd50bd4303b47374a95 Mon Sep 17 00:00:00 2001 From: okmsbun Date: Wed, 4 Feb 2026 16:18:21 +0300 Subject: [PATCH 1/6] Refactor AppInfo class by removing requestedPermissions field and updating documentation for Android app details. Introduce getRequestedPermissions method in FlutterDeviceAppsPlatform for fetching app permissions. --- ...lutter_device_apps_platform_interface.dart | 51 ++++++------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/lib/flutter_device_apps_platform_interface.dart b/lib/flutter_device_apps_platform_interface.dart index 89e45b0..951424d 100644 --- a/lib/flutter_device_apps_platform_interface.dart +++ b/lib/flutter_device_apps_platform_interface.dart @@ -26,7 +26,6 @@ class AppInfo { this.enabled, this.processName, this.installLocation, - this.requestedPermissions, }); /// Creates an [AppInfo] from a map of key-value pairs. @@ -52,9 +51,6 @@ class AppInfo { final bool? enabled = m['enabled'] != null ? bool.tryParse(m['enabled']!.toString()) : null; final int? installLocation = m['installLocation'] != null ? int.tryParse(m['installLocation']!.toString()) : null; - final List? requestedPermissions = m['requestedPermissions'] is List - ? (m['requestedPermissions']! as List).map((e) => e.toString()).toList() - : null; return AppInfo( packageName: m['packageName']?.toString(), @@ -72,7 +68,6 @@ class AppInfo { enabled: enabled, processName: m['processName']?.toString(), installLocation: installLocation, - requestedPermissions: requestedPermissions, ); } @@ -100,47 +95,23 @@ class AppInfo { /// The app icon as raw bytes, if requested and available. final Uint8List? iconBytes; - /// App category (Android [ApplicationInfo.category], API 26+). Raw int from platform. Null when not set or API < 26. + /// App category (Android ApplicationInfo.category, API 26+). Raw int from platform. Null when not set or API < 26. final int? category; - /// Target SDK version (Android [ApplicationInfo.targetSdkVersion]). + /// Target SDK version (Android ApplicationInfo.targetSdkVersion). final int? targetSdkVersion; - /// Min SDK version (Android [ApplicationInfo.minSdkVersion]). + /// Min SDK version (Android ApplicationInfo.minSdkVersion). final int? minSdkVersion; - /// Whether the app is enabled (Android [ApplicationInfo.enabled]). + /// Whether the app is enabled (Android ApplicationInfo.enabled). final bool? enabled; - /// Process name (Android [ApplicationInfo.processName]). + /// Process name (Android ApplicationInfo.processName). final String? processName; - /// Install location (Android [PackageInfo.installLocation]). + /// Install location (Android PackageInfo.installLocation). final int? installLocation; - - /// Requested permissions (Android [PackageInfo.requestedPermissions]). - final List? requestedPermissions; - - /// Converts this [AppInfo] to a map representation. - /// - /// Useful for serialization to platform channels or other data formats. - Map toMap() => { - 'packageName': packageName, - 'appName': appName, - 'versionName': versionName, - 'versionCode': versionCode, - 'firstInstallTime': firstInstallTime?.millisecondsSinceEpoch, - 'lastUpdateTime': lastUpdateTime?.millisecondsSinceEpoch, - 'isSystem': isSystem, - 'iconBytes': iconBytes, - 'category': category, - 'targetSdkVersion': targetSdkVersion, - 'minSdkVersion': minSdkVersion, - 'enabled': enabled, - 'processName': processName, - 'installLocation': installLocation, - 'requestedPermissions': requestedPermissions, - }; } /// Base class every platform implementation must extend. @@ -175,6 +146,12 @@ abstract class FlutterDeviceAppsPlatform extends PlatformInterface { /// Gets details for a single app. Future getApp(String packageName, {bool includeIcon = false}); + /// Gets the requested permissions for a specific app. + /// + /// Implementations should return the Android PackageInfo.requestedPermissions + /// list for the given [packageName], or null if not available. + Future?> getRequestedPermissions(String packageName); + /// Best-effort: launches an app by package name. Returns false if not launchable. Future openApp(String packageName); @@ -222,6 +199,10 @@ class _UnimplementedPlatform extends FlutterDeviceAppsPlatform { Future getApp(String packageName, {bool includeIcon = false}) => Future.error(UnsupportedError('FlutterDeviceAppsPlatform not implemented')); + @override + Future?> getRequestedPermissions(String packageName) => + Future.error(UnsupportedError('FlutterDeviceAppsPlatform not implemented')); + @override Future openApp(String packageName) => Future.error(UnsupportedError('FlutterDeviceAppsPlatform not implemented')); From 1283e622736d5e5d7420232da4b9c636d6319c98 Mon Sep 17 00:00:00 2001 From: okmsbun Date: Thu, 5 Feb 2026 15:14:00 +0300 Subject: [PATCH 2/6] Add comprehensive tests for AppInfo class covering constructor and fromMap method --- test/app_info_test.dart | 323 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 test/app_info_test.dart diff --git a/test/app_info_test.dart b/test/app_info_test.dart new file mode 100644 index 0000000..575e470 --- /dev/null +++ b/test/app_info_test.dart @@ -0,0 +1,323 @@ +import 'dart:typed_data'; + +import 'package:flutter_device_apps_platform_interface/flutter_device_apps_platform_interface.dart'; +import 'package:test/test.dart'; + +void main() { + group('AppInfo', () { + group('constructor', () { + test('creates instance with all null values', () { + const appInfo = AppInfo(); + + expect(appInfo.packageName, isNull); + expect(appInfo.appName, isNull); + expect(appInfo.versionName, isNull); + expect(appInfo.versionCode, isNull); + expect(appInfo.firstInstallTime, isNull); + expect(appInfo.lastUpdateTime, isNull); + expect(appInfo.isSystem, isNull); + expect(appInfo.iconBytes, isNull); + expect(appInfo.category, isNull); + expect(appInfo.targetSdkVersion, isNull); + expect(appInfo.minSdkVersion, isNull); + expect(appInfo.enabled, isNull); + expect(appInfo.processName, isNull); + expect(appInfo.installLocation, isNull); + }); + + test('creates instance with all values', () { + final iconBytes = Uint8List.fromList([1, 2, 3]); + final firstInstallTime = DateTime(2024, 1, 15); + final lastUpdateTime = DateTime(2024, 6, 20); + + final appInfo = AppInfo( + packageName: 'com.example.app', + appName: 'Example App', + versionName: '1.0.0', + versionCode: 10, + firstInstallTime: firstInstallTime, + lastUpdateTime: lastUpdateTime, + isSystem: false, + iconBytes: iconBytes, + category: 3, + targetSdkVersion: 33, + minSdkVersion: 21, + enabled: true, + processName: 'com.example.app', + installLocation: 0, + ); + + expect(appInfo.packageName, 'com.example.app'); + expect(appInfo.appName, 'Example App'); + expect(appInfo.versionName, '1.0.0'); + expect(appInfo.versionCode, 10); + expect(appInfo.firstInstallTime, firstInstallTime); + expect(appInfo.lastUpdateTime, lastUpdateTime); + expect(appInfo.isSystem, false); + expect(appInfo.iconBytes, iconBytes); + expect(appInfo.category, 3); + expect(appInfo.targetSdkVersion, 33); + expect(appInfo.minSdkVersion, 21); + expect(appInfo.enabled, true); + expect(appInfo.processName, 'com.example.app'); + expect(appInfo.installLocation, 0); + }); + }); + + group('fromMap', () { + test('parses empty map', () { + final appInfo = AppInfo.fromMap({}); + + expect(appInfo.packageName, isNull); + expect(appInfo.appName, isNull); + expect(appInfo.versionName, isNull); + expect(appInfo.versionCode, isNull); + expect(appInfo.firstInstallTime, isNull); + expect(appInfo.lastUpdateTime, isNull); + expect(appInfo.isSystem, isNull); + expect(appInfo.iconBytes, isNull); + expect(appInfo.category, isNull); + expect(appInfo.targetSdkVersion, isNull); + expect(appInfo.minSdkVersion, isNull); + expect(appInfo.enabled, isNull); + expect(appInfo.processName, isNull); + expect(appInfo.installLocation, isNull); + }); + + test('parses all string fields correctly', () { + final map = { + 'packageName': 'com.example.app', + 'appName': 'Example App', + 'versionName': '2.1.0', + 'processName': 'com.example.process', + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.packageName, 'com.example.app'); + expect(appInfo.appName, 'Example App'); + expect(appInfo.versionName, '2.1.0'); + expect(appInfo.processName, 'com.example.process'); + }); + + test('parses integer fields from int values', () { + final map = { + 'versionCode': 42, + 'category': 5, + 'targetSdkVersion': 34, + 'minSdkVersion': 23, + 'installLocation': 1, + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.versionCode, 42); + expect(appInfo.category, 5); + expect(appInfo.targetSdkVersion, 34); + expect(appInfo.minSdkVersion, 23); + expect(appInfo.installLocation, 1); + }); + + test('parses integer fields from string values', () { + final map = { + 'versionCode': '42', + 'category': '5', + 'targetSdkVersion': '34', + 'minSdkVersion': '23', + 'installLocation': '1', + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.versionCode, 42); + expect(appInfo.category, 5); + expect(appInfo.targetSdkVersion, 34); + expect(appInfo.minSdkVersion, 23); + expect(appInfo.installLocation, 1); + }); + + test('handles invalid integer strings gracefully', () { + final map = { + 'versionCode': 'invalid', + 'category': 'not_a_number', + 'targetSdkVersion': '', + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.versionCode, isNull); + expect(appInfo.category, isNull); + expect(appInfo.targetSdkVersion, isNull); + }); + + test('parses boolean fields from bool values', () { + final map = { + 'isSystem': true, + 'enabled': false, + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.isSystem, true); + expect(appInfo.enabled, false); + }); + + test('parses boolean fields from string values', () { + final map = { + 'isSystem': 'true', + 'enabled': 'false', + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.isSystem, true); + expect(appInfo.enabled, false); + }); + + test('handles invalid boolean strings gracefully', () { + final map = { + 'isSystem': 'yes', + 'enabled': 'no', + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.isSystem, isNull); + expect(appInfo.enabled, isNull); + }); + + test('parses DateTime fields from milliseconds', () { + final int firstInstallMs = DateTime(2024, 1, 15).millisecondsSinceEpoch; + final int lastUpdateMs = DateTime(2024, 6, 20).millisecondsSinceEpoch; + + final map = { + 'firstInstallTime': firstInstallMs, + 'lastUpdateTime': lastUpdateMs, + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.firstInstallTime, DateTime(2024, 1, 15)); + expect(appInfo.lastUpdateTime, DateTime(2024, 6, 20)); + }); + + test('parses DateTime fields from string milliseconds', () { + final int firstInstallMs = DateTime(2024, 3, 10).millisecondsSinceEpoch; + final int lastUpdateMs = DateTime(2024, 8, 5).millisecondsSinceEpoch; + + final map = { + 'firstInstallTime': firstInstallMs.toString(), + 'lastUpdateTime': lastUpdateMs.toString(), + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.firstInstallTime, DateTime(2024, 3, 10)); + expect(appInfo.lastUpdateTime, DateTime(2024, 8, 5)); + }); + + test('handles invalid DateTime strings gracefully', () { + final map = { + 'firstInstallTime': 'not_a_timestamp', + 'lastUpdateTime': '', + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.firstInstallTime, isNull); + expect(appInfo.lastUpdateTime, isNull); + }); + + test('parses iconBytes from List', () { + final iconData = [255, 216, 255, 224, 0, 16]; + final map = { + 'iconBytes': iconData, + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.iconBytes, isA()); + expect(appInfo.iconBytes, [255, 216, 255, 224, 0, 16]); + }); + + test('handles non-List iconBytes gracefully', () { + final map = { + 'iconBytes': 'not_a_list', + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.iconBytes, isNull); + }); + + test('handles null iconBytes gracefully', () { + final map = { + 'iconBytes': null, + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.iconBytes, isNull); + }); + + test('parses complete map with all fields', () { + final int firstInstallMs = DateTime(2024).millisecondsSinceEpoch; + final int lastUpdateMs = DateTime(2024, 12, 31).millisecondsSinceEpoch; + final iconData = [1, 2, 3, 4, 5]; + + final map = { + 'packageName': 'com.test.fullapp', + 'appName': 'Full Test App', + 'versionName': '3.2.1', + 'versionCode': 321, + 'firstInstallTime': firstInstallMs, + 'lastUpdateTime': lastUpdateMs, + 'isSystem': false, + 'iconBytes': iconData, + 'category': 7, + 'targetSdkVersion': 35, + 'minSdkVersion': 24, + 'enabled': true, + 'processName': 'com.test.fullapp.process', + 'installLocation': 2, + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.packageName, 'com.test.fullapp'); + expect(appInfo.appName, 'Full Test App'); + expect(appInfo.versionName, '3.2.1'); + expect(appInfo.versionCode, 321); + expect(appInfo.firstInstallTime, DateTime(2024, 1, 1)); + expect(appInfo.lastUpdateTime, DateTime(2024, 12, 31)); + expect(appInfo.isSystem, false); + expect(appInfo.iconBytes, isA()); + expect(appInfo.iconBytes!.length, 5); + expect(appInfo.category, 7); + expect(appInfo.targetSdkVersion, 35); + expect(appInfo.minSdkVersion, 24); + expect(appInfo.enabled, true); + expect(appInfo.processName, 'com.test.fullapp.process'); + expect(appInfo.installLocation, 2); + }); + + test('handles mixed valid and invalid values', () { + final map = { + 'packageName': 'com.example.mixed', + 'versionCode': 'invalid', + 'isSystem': 'true', + 'enabled': 'maybe', + 'category': 10, + }; + + final appInfo = AppInfo.fromMap(map); + + expect(appInfo.packageName, 'com.example.mixed'); + expect(appInfo.versionCode, isNull); + expect(appInfo.isSystem, true); + expect(appInfo.enabled, isNull); + expect(appInfo.category, 10); + }); + }); + }); +} From 8e39c4bd139ee1c89f2536f41dd8b62767106e90 Mon Sep 17 00:00:00 2001 From: okmsbun Date: Thu, 5 Feb 2026 15:18:53 +0300 Subject: [PATCH 3/6] Add unit tests for AppChangeType and AppChangeEvent classes with comprehensive coverage --- test/app_change_event_test.dart | 263 ++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 test/app_change_event_test.dart diff --git a/test/app_change_event_test.dart b/test/app_change_event_test.dart new file mode 100644 index 0000000..b6ce084 --- /dev/null +++ b/test/app_change_event_test.dart @@ -0,0 +1,263 @@ +import 'package:flutter_device_apps_platform_interface/flutter_device_apps_app_change_event.dart'; +import 'package:test/test.dart'; + +void main() { + group('AppChangeType', () { + test('has all expected values', () { + expect(AppChangeType.values, hasLength(3)); + expect(AppChangeType.values, contains(AppChangeType.installed)); + expect(AppChangeType.values, contains(AppChangeType.removed)); + expect(AppChangeType.values, contains(AppChangeType.updated)); + }); + + test('enum names match expected strings', () { + expect(AppChangeType.installed.name, 'installed'); + expect(AppChangeType.removed.name, 'removed'); + expect(AppChangeType.updated.name, 'updated'); + }); + }); + + group('AppChangeEvent', () { + group('constructor', () { + test('creates instance with all null values', () { + const event = AppChangeEvent(); + + expect(event.packageName, isNull); + expect(event.type, isNull); + expect(event.isReplacing, isNull); + }); + + test('creates instance with all values', () { + const event = AppChangeEvent( + packageName: 'com.example.app', + type: AppChangeType.installed, + isReplacing: false, + ); + + expect(event.packageName, 'com.example.app'); + expect(event.type, AppChangeType.installed); + expect(event.isReplacing, false); + }); + + test('creates instance with partial values', () { + const event = AppChangeEvent( + packageName: 'com.example.app', + ); + + expect(event.packageName, 'com.example.app'); + expect(event.type, isNull); + expect(event.isReplacing, isNull); + }); + }); + + group('fromMap', () { + test('parses empty map', () { + final event = AppChangeEvent.fromMap({}); + + expect(event.packageName, isNull); + expect(event.type, isNull); + expect(event.isReplacing, isNull); + }); + + test('parses packageName correctly', () { + final event = AppChangeEvent.fromMap({ + 'packageName': 'com.test.app', + }); + + expect(event.packageName, 'com.test.app'); + }); + + test('parses type "installed" correctly', () { + final event = AppChangeEvent.fromMap({ + 'type': 'installed', + }); + + expect(event.type, AppChangeType.installed); + }); + + test('parses type "removed" correctly', () { + final event = AppChangeEvent.fromMap({ + 'type': 'removed', + }); + + expect(event.type, AppChangeType.removed); + }); + + test('parses type "updated" correctly', () { + final event = AppChangeEvent.fromMap({ + 'type': 'updated', + }); + + expect(event.type, AppChangeType.updated); + }); + + test('returns null type for invalid type string', () { + final event = AppChangeEvent.fromMap({ + 'type': 'unknown', + }); + + expect(event.type, isNull); + }); + + test('returns null type for empty type string', () { + final event = AppChangeEvent.fromMap({ + 'type': '', + }); + + expect(event.type, isNull); + }); + + test('parses isReplacing from bool value', () { + final eventTrue = AppChangeEvent.fromMap({ + 'isReplacing': true, + }); + final eventFalse = AppChangeEvent.fromMap({ + 'isReplacing': false, + }); + + expect(eventTrue.isReplacing, true); + expect(eventFalse.isReplacing, false); + }); + + test('parses isReplacing from string value', () { + final eventTrue = AppChangeEvent.fromMap({ + 'isReplacing': 'true', + }); + final eventFalse = AppChangeEvent.fromMap({ + 'isReplacing': 'false', + }); + + expect(eventTrue.isReplacing, true); + expect(eventFalse.isReplacing, false); + }); + + test('returns null isReplacing for invalid string', () { + final event = AppChangeEvent.fromMap({ + 'isReplacing': 'yes', + }); + + expect(event.isReplacing, isNull); + }); + + test('parses complete map with all fields', () { + final event = AppChangeEvent.fromMap({ + 'packageName': 'com.example.fullapp', + 'type': 'updated', + 'isReplacing': true, + }); + + expect(event.packageName, 'com.example.fullapp'); + expect(event.type, AppChangeType.updated); + expect(event.isReplacing, true); + }); + }); + + group('toMap', () { + test('converts empty event to map', () { + const event = AppChangeEvent(); + final Map map = event.toMap(); + + expect(map['packageName'], isNull); + expect(map['type'], isNull); + expect(map['isReplacing'], isNull); + }); + + test('converts full event to map', () { + const event = AppChangeEvent( + packageName: 'com.example.app', + type: AppChangeType.installed, + isReplacing: false, + ); + final Map map = event.toMap(); + + expect(map['packageName'], 'com.example.app'); + expect(map['type'], 'installed'); + expect(map['isReplacing'], false); + }); + + test('converts removed type correctly', () { + const event = AppChangeEvent(type: AppChangeType.removed); + final Map map = event.toMap(); + + expect(map['type'], 'removed'); + }); + + test('converts updated type correctly', () { + const event = AppChangeEvent(type: AppChangeType.updated); + final Map map = event.toMap(); + + expect(map['type'], 'updated'); + }); + + test('converts partial event to map', () { + const event = AppChangeEvent( + packageName: 'com.partial.app', + isReplacing: true, + ); + final Map map = event.toMap(); + + expect(map['packageName'], 'com.partial.app'); + expect(map['type'], isNull); + expect(map['isReplacing'], true); + }); + }); + + group('round-trip (fromMap -> toMap)', () { + test('installed event survives round-trip', () { + final originalMap = { + 'packageName': 'com.roundtrip.installed', + 'type': 'installed', + 'isReplacing': false, + }; + + final event = AppChangeEvent.fromMap(originalMap); + final Map resultMap = event.toMap(); + + expect(resultMap['packageName'], originalMap['packageName']); + expect(resultMap['type'], originalMap['type']); + expect(resultMap['isReplacing'], originalMap['isReplacing']); + }); + + test('removed event survives round-trip', () { + final originalMap = { + 'packageName': 'com.roundtrip.removed', + 'type': 'removed', + 'isReplacing': false, + }; + + final event = AppChangeEvent.fromMap(originalMap); + final Map resultMap = event.toMap(); + + expect(resultMap['packageName'], originalMap['packageName']); + expect(resultMap['type'], originalMap['type']); + expect(resultMap['isReplacing'], originalMap['isReplacing']); + }); + + test('updated event with replacing survives round-trip', () { + final originalMap = { + 'packageName': 'com.roundtrip.updated', + 'type': 'updated', + 'isReplacing': true, + }; + + final event = AppChangeEvent.fromMap(originalMap); + final Map resultMap = event.toMap(); + + expect(resultMap['packageName'], originalMap['packageName']); + expect(resultMap['type'], originalMap['type']); + expect(resultMap['isReplacing'], originalMap['isReplacing']); + }); + + test('empty event survives round-trip', () { + final originalMap = {}; + + final event = AppChangeEvent.fromMap(originalMap); + final Map resultMap = event.toMap(); + + expect(resultMap['packageName'], isNull); + expect(resultMap['type'], isNull); + expect(resultMap['isReplacing'], isNull); + }); + }); + }); +} From 0b8984a1ff653131fa9306d12e3a063bbd3d051c Mon Sep 17 00:00:00 2001 From: okmsbun Date: Thu, 5 Feb 2026 15:56:22 +0300 Subject: [PATCH 4/6] Update test for AppInfo to correct firstInstallTime to a specific year --- test/app_info_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/app_info_test.dart b/test/app_info_test.dart index 575e470..793988e 100644 --- a/test/app_info_test.dart +++ b/test/app_info_test.dart @@ -288,7 +288,7 @@ void main() { expect(appInfo.appName, 'Full Test App'); expect(appInfo.versionName, '3.2.1'); expect(appInfo.versionCode, 321); - expect(appInfo.firstInstallTime, DateTime(2024, 1, 1)); + expect(appInfo.firstInstallTime, DateTime(2024)); expect(appInfo.lastUpdateTime, DateTime(2024, 12, 31)); expect(appInfo.isSystem, false); expect(appInfo.iconBytes, isA()); From 3591557d2d6d9bb109f47fb9db5e746c8f30b87e Mon Sep 17 00:00:00 2001 From: okmsbun Date: Thu, 5 Feb 2026 16:13:32 +0300 Subject: [PATCH 5/6] Add CI workflows for quality checks and testing; update version to 0.6.0 --- .github/workflows/quality.yml | 39 +++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 33 +++++++++++++++++++++++++++++ CHANGELOG.md | 5 +++++ pubspec.yaml | 5 +++-- 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/quality.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..7c1adc2 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,39 @@ +name: Quality + +on: + pull_request: + branches: ["develop"] + +jobs: + quality: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Cache pub + uses: actions/cache@v4 + with: + path: | + ~/.pub-cache + .dart_tool + key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pub- + + - name: Install dependencies + run: dart pub get + + - name: Check formatting + run: dart format --set-exit-if-changed . + + - name: Static analysis + run: dart analyze + + - name: pub.dev dry-run + run: dart pub publish --dry-run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..761e66b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: + pull_request: + branches: ["develop"] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Cache pub + uses: actions/cache@v4 + with: + path: | + ~/.pub-cache + .dart_tool + key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pub- + + - name: Install dependencies + run: dart pub get + + - name: Run tests + run: dart test diff --git a/CHANGELOG.md b/CHANGELOG.md index e2dd7b7..6f2c34d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 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 +- Added comprehensive unit tests for `AppInfo`, `AppChangeType`, and `AppChangeEvent` classes + ## 0.5.1 - Expanded `AppInfo` with additional Android-facing fields: `category`, `targetSdkVersion`, `minSdkVersion`, `enabled`, `processName`, `installLocation`, `requestedPermissions`. diff --git a/pubspec.yaml b/pubspec.yaml index 9bf1176..32ae7d1 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.5.1 +version: 0.6.0 repository: https://github.com/okmsbun/flutter_device_apps_platform_interface issue_tracker: https://github.com/okmsbun/flutter_device_apps_platform_interface/issues topics: @@ -17,4 +17,5 @@ dependencies: plugin_platform_interface: ^2.1.8 dev_dependencies: - lints: ^6.0.0 + lints: ^6.1.0 + test: ^1.29.0 From a2977f00c6958354401fb82c3f5953dbda4c6177 Mon Sep 17 00:00:00 2001 From: okmsbun Date: Thu, 5 Feb 2026 16:19:00 +0300 Subject: [PATCH 6/6] Add GitHub workflow to enforce PRs from 'develop' to 'main' --- .github/workflows/only-develop-to-main.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/only-develop-to-main.yml diff --git a/.github/workflows/only-develop-to-main.yml b/.github/workflows/only-develop-to-main.yml new file mode 100644 index 0000000..f753bfb --- /dev/null +++ b/.github/workflows/only-develop-to-main.yml @@ -0,0 +1,18 @@ +name: only-develop-to-main + +on: + pull_request: + branches: ["main"] + +jobs: + enforce: + runs-on: ubuntu-latest + steps: + - name: Allow only develop -> main PRs + run: | + echo "head_ref: ${{ github.head_ref }}" + echo "base_ref: ${{ github.base_ref }}" + if [ "${{ github.base_ref }}" = "main" ] && [ "${{ github.head_ref }}" != "develop" ]; then + echo "Only PRs from 'develop' to 'main' are allowed." + exit 1 + fi