Skip to content

Commit 200f1e2

Browse files
committed
Update gradle, kotlin and Add tests
1 parent ea5da04 commit 200f1e2

12 files changed

Lines changed: 204 additions & 42 deletions

File tree

packages/app_usage/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 4.1.0
2+
3+
* Upgrade gradle version 8.12.3
4+
* Upgrade APG
5+
* Upgrade Kotlin to 2.2.0
6+
* Add tests
7+
* Update Flutter SDK version
8+
19
## 4.0.1
210

311
* Upgrading gradle version
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Additional information about this file can be found at
2+
# https://dart.dev/guides/language/analysis-options
3+
4+
include: package:lints/recommended.yaml
5+
6+
analyzer:
7+
exclude: [build/**]
8+
language:
9+
strict-casts: true
10+
strict-inference: true
11+
strict-raw-types: true
12+
13+
linter:
14+
rules:
15+
cancel_subscriptions: true
16+
constant_identifier_names: false
17+
depend_on_referenced_packages: false
18+
use_string_in_part_of_directives: false

packages/app_usage/android/build.gradle

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ group = "dk.cachet.app_usage"
22
version = "1.0-SNAPSHOT"
33

44
buildscript {
5-
ext.kotlin_version = "1.8.22"
5+
ext.kotlin_version = "2.2.0"
66
repositories {
77
google()
88
mavenCentral()
99
}
1010

1111
dependencies {
12-
classpath("com.android.tools.build:gradle:8.1.0")
12+
classpath("com.android.tools.build:gradle:8.12.3")
1313
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
1414
}
1515
}
@@ -35,7 +35,7 @@ android {
3535
}
3636

3737
kotlinOptions {
38-
jvmTarget = JavaVersion.VERSION_11
38+
jvmTarget = "11"
3939
}
4040

4141
sourceSets {
@@ -64,5 +64,3 @@ android {
6464
}
6565
}
6666
}
67-
68-

packages/app_usage/example/android/app/build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ android {
1111
ndkVersion = flutter.ndkVersion
1212

1313
compileOptions {
14-
sourceCompatibility = JavaVersion.VERSION_1_8
15-
targetCompatibility = JavaVersion.VERSION_1_8
14+
sourceCompatibility = JavaVersion.VERSION_11
15+
targetCompatibility = JavaVersion.VERSION_11
1616
}
1717

1818
kotlinOptions {
19-
jvmTarget = JavaVersion.VERSION_1_8
19+
jvmTarget = "11"
2020
}
2121

2222
defaultConfig {
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
org.gradle.jvmargs=-Xmx1536M
2-
android.enableR8=true
32
android.useAndroidX=true
43
android.enableJetifier=true

packages/app_usage/example/android/settings.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ pluginManagement {
1818

1919
plugins {
2020
id "dev.flutter.flutter-plugin-loader" version "1.0.2"
21-
id "com.android.application" version "8.1.0" apply false
22-
id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
21+
id "com.android.application" version "8.12.3" apply false
22+
id 'org.jetbrains.kotlin.android' version '2.2.0' apply false
2323
}
2424

2525
include ":app"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:app_usage_example/main.dart' as app;
2+
import 'package:app_usage/app_usage.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter/services.dart';
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:integration_test/integration_test.dart';
7+
8+
void main() {
9+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
10+
11+
const MethodChannel channel = MethodChannel('app_usage.methodChannel');
12+
13+
tearDown(() async {
14+
AppUsage.debugIsAndroidOverride = null;
15+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
16+
.setMockMethodCallHandler(channel, null);
17+
});
18+
19+
testWidgets('loads usage information into the example app', (tester) async {
20+
AppUsage.debugIsAndroidOverride = true;
21+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
22+
.setMockMethodCallHandler(channel, (call) async {
23+
expect(call.method, 'getUsage');
24+
25+
final arguments = Map<String, dynamic>.from(call.arguments as Map);
26+
final start = arguments['start'] as int;
27+
final end = arguments['end'] as int;
28+
29+
expect(start, lessThan(end));
30+
31+
return <String, List<double>>{
32+
'com.example.mail': <double>[
33+
120,
34+
start / 1000,
35+
end / 1000,
36+
(end - const Duration(minutes: 5).inMilliseconds) / 1000,
37+
],
38+
};
39+
});
40+
41+
app.main();
42+
await tester.pumpAndSettle();
43+
44+
expect(find.text('No usage data loaded yet.'), findsOneWidget);
45+
46+
await tester.tap(find.byKey(const Key('load_usage_button')));
47+
await tester.pumpAndSettle();
48+
49+
expect(find.text('mail'), findsOneWidget);
50+
expect(find.text('com.example.mail'), findsOneWidget);
51+
expect(find.text('0:02:00.000000'), findsOneWidget);
52+
});
53+
54+
testWidgets('shows channel errors in the example app', (tester) async {
55+
AppUsage.debugIsAndroidOverride = true;
56+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
57+
.setMockMethodCallHandler(channel, (call) async {
58+
throw PlatformException(
59+
code: 'permission_denied',
60+
message: 'Usage access is not granted.',
61+
);
62+
});
63+
64+
app.main();
65+
await tester.pumpAndSettle();
66+
67+
await tester.tap(find.byKey(const Key('load_usage_button')));
68+
await tester.pumpAndSettle();
69+
70+
expect(find.byKey(const Key('usage_error_message')), findsOneWidget);
71+
expect(find.textContaining('permission_denied'), findsOneWidget);
72+
});
73+
}
Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import 'package:flutter/material.dart';
22
import 'package:app_usage/app_usage.dart';
33

4-
void main() => runApp(AppUsageApp());
4+
void main() => runApp(const AppUsageApp());
55

66
class AppUsageApp extends StatefulWidget {
7+
const AppUsageApp({super.key});
8+
79
@override
810
AppUsageAppState createState() => AppUsageAppState();
911
}
1012

1113
class AppUsageAppState extends State<AppUsageApp> {
1214
List<AppUsageInfo> _infos = [];
15+
String? _errorMessage;
1316

1417
@override
1518
void initState() {
@@ -18,13 +21,19 @@ class AppUsageAppState extends State<AppUsageApp> {
1821

1922
void getUsageStats() async {
2023
try {
21-
DateTime endDate = DateTime.now();
22-
DateTime startDate = endDate.subtract(Duration(hours: 1));
23-
List<AppUsageInfo> infoList =
24+
final DateTime endDate = DateTime.now();
25+
final DateTime startDate = endDate.subtract(const Duration(hours: 1));
26+
final List<AppUsageInfo> infoList =
2427
await AppUsage().getAppUsage(startDate, endDate);
25-
setState(() => _infos = infoList);
28+
setState(() {
29+
_infos = infoList;
30+
_errorMessage = null;
31+
});
2632
} catch (exception) {
27-
print(exception);
33+
setState(() {
34+
_infos = [];
35+
_errorMessage = exception.toString();
36+
});
2837
}
2938
}
3039

@@ -36,16 +45,45 @@ class AppUsageAppState extends State<AppUsageApp> {
3645
title: const Text('App Usage Example'),
3746
backgroundColor: Colors.green,
3847
),
39-
body: ListView.builder(
40-
itemCount: _infos.length,
41-
itemBuilder: (context, index) {
42-
return ListTile(
43-
title: Text(_infos[index].appName),
44-
trailing: Text(_infos[index].usage.toString()));
45-
}),
48+
body: _buildBody(),
4649
floatingActionButton: FloatingActionButton(
47-
onPressed: getUsageStats, child: Icon(Icons.file_download)),
50+
key: const Key('load_usage_button'),
51+
onPressed: getUsageStats,
52+
child: const Icon(Icons.file_download),
53+
),
4854
),
4955
);
5056
}
57+
58+
Widget _buildBody() {
59+
if (_errorMessage != null) {
60+
return Center(
61+
child: Text(
62+
_errorMessage!,
63+
key: const Key('usage_error_message'),
64+
textAlign: TextAlign.center,
65+
),
66+
);
67+
}
68+
69+
if (_infos.isEmpty) {
70+
return const Center(
71+
child: Text(
72+
'No usage data loaded yet.',
73+
key: Key('usage_empty_state'),
74+
),
75+
);
76+
}
77+
78+
return ListView.builder(
79+
itemCount: _infos.length,
80+
itemBuilder: (context, index) {
81+
return ListTile(
82+
title: Text(_infos[index].appName),
83+
subtitle: Text(_infos[index].packageName),
84+
trailing: Text(_infos[index].usage.toString()),
85+
);
86+
},
87+
);
88+
}
5189
}

packages/app_usage/example/pubspec.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description: Demonstrates how to use the app_usage plugin.
66
publish_to: "none" # Remove this line if you wish to publish to pub.dev
77

88
environment:
9-
sdk: ">=3.2.0 <4.0.0"
9+
sdk: ">=3.8.0 <4.0.0"
1010

1111
dependencies:
1212
flutter:
@@ -25,6 +25,8 @@ dependencies:
2525
cupertino_icons: ^1.0.2
2626

2727
dev_dependencies:
28+
integration_test:
29+
sdk: flutter
2830
flutter_test:
2931
sdk: flutter
3032

packages/app_usage/lib/app_usage.dart

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import 'dart:async';
22

3+
import 'package:flutter/foundation.dart';
34
import 'package:flutter/services.dart';
45
import 'dart:io' show Platform;
56

67
/// Information on app usage.
78
class AppUsageInfo {
89
late String _packageName, _appName;
910
late Duration _usage;
10-
DateTime _startDate, _endDate, _lastForeground;
11+
final DateTime _startDate, _endDate, _lastForeground;
1112

1213
AppUsageInfo(
1314
String name,
@@ -48,20 +49,28 @@ class AppUsageInfo {
4849

4950
/// Singleton class to get app usage statistics.
5051
class AppUsage {
51-
static const MethodChannel _methodChannel =
52-
const MethodChannel("app_usage.methodChannel");
52+
static const MethodChannel _methodChannel = MethodChannel(
53+
"app_usage.methodChannel",
54+
);
55+
static bool? _debugIsAndroidOverride;
5356

5457
static final AppUsage _instance = AppUsage._();
5558
AppUsage._();
5659
factory AppUsage() => _instance;
5760

61+
@visibleForTesting
62+
static set debugIsAndroidOverride(bool? value) {
63+
_debugIsAndroidOverride = value;
64+
}
65+
5866
/// Get app usage statistics for the specified interval. Only works on Android.
5967
/// Returns an empty list if called on iOS.
6068
Future<List<AppUsageInfo>> getAppUsage(
6169
DateTime startDate,
6270
DateTime endDate,
6371
) async {
64-
if (!Platform.isAndroid) return [];
72+
final bool isAndroid = _debugIsAndroidOverride ?? Platform.isAndroid;
73+
if (!isAndroid) return [];
6574

6675
if (endDate.isBefore(startDate)) {
6776
throw ArgumentError('End date must be after start date');
@@ -72,22 +81,33 @@ class AppUsage {
7281
int start = startDate.millisecondsSinceEpoch;
7382

7483
// Set parameters
75-
Map<String, int> interval = {'start': start, 'end': end};
84+
final Map<String, int> interval = <String, int>{'start': start, 'end': end};
7685

7786
// Get result and parse it as a Map of <String, List<double>>
78-
Map usage = await _methodChannel.invokeMethod('getUsage', interval);
87+
final Map<Object?, Object?> usage =
88+
await _methodChannel.invokeMethod<Map<Object?, Object?>>(
89+
'getUsage',
90+
interval,
91+
) ??
92+
<Object?, Object?>{};
7993

8094
// Convert to list of AppUsageInfo
81-
List<AppUsageInfo> result = [];
82-
for (String key in usage.keys) {
83-
List<double> temp = List<double>.from(usage[key]);
95+
final List<AppUsageInfo> result = <AppUsageInfo>[];
96+
for (final MapEntry<Object?, Object?> entry in usage.entries) {
97+
final String key = entry.key as String;
98+
final List<double> temp = List<double>.from(
99+
entry.value as Iterable<Object?>,
100+
);
84101
if (temp[0] > 0) {
85-
result.add(AppUsageInfo(
102+
result.add(
103+
AppUsageInfo(
86104
key,
87105
temp[0],
88106
DateTime.fromMillisecondsSinceEpoch(temp[1].round() * 1000),
89107
DateTime.fromMillisecondsSinceEpoch(temp[2].round() * 1000),
90-
DateTime.fromMillisecondsSinceEpoch(temp[3].round() * 1000)));
108+
DateTime.fromMillisecondsSinceEpoch(temp[3].round() * 1000),
109+
),
110+
);
91111
}
92112
}
93113

0 commit comments

Comments
 (0)