diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index cf935b566e20..d04ca962d7ad 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.5+5 + +* Added an Espresso test. + ## 0.4.5+4 * Make the pedantic dev_dependency explicit. diff --git a/packages/android_alarm_manager/example/android/app/build.gradle b/packages/android_alarm_manager/example/android/app/build.gradle index d296cafa8e7c..f066040810c2 100644 --- a/packages/android_alarm_manager/example/android/app/build.gradle +++ b/packages/android_alarm_manager/example/android/app/build.gradle @@ -55,6 +55,8 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' + testImplementation "com.google.truth:truth:1.0" androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + api 'androidx.test:core:1.2.0' } diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java new file mode 100644 index 000000000000..ce34b25ec505 --- /dev/null +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java @@ -0,0 +1,64 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.androidalarmmanagerexample; + +import static androidx.test.espresso.Espresso.pressBackUnconditionally; +import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +import static androidx.test.espresso.flutter.action.FlutterActions.click; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.test.InstrumentationRegistry; +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class BackgroundExecutionTest { + private SharedPreferences prefs; + static final String COUNT_KEY = "flutter.count"; + + @Rule + public ActivityTestRule myActivityTestRule = + new ActivityTestRule<>(DriverExtensionActivity.class, true, false); + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE); + prefs.edit().putLong(COUNT_KEY, 0).apply(); + + ActivityScenario.launch(DriverExtensionActivity.class); + } + + @Test + public void startBackgroundIsolate() throws Exception { + + // Register a one shot alarm which will go off in ~5 seconds. + onFlutterWidget(withValueKey("RegisterOneShotAlarm")).perform(click()); + + // The alarm count should be 0 after installation. + assertEquals(prefs.getLong(COUNT_KEY, -1), 0); + + // Close the application to background it. + pressBackUnconditionally(); + + // The alarm should eventually fire, wake up the application, create a + // background isolate, and then increment the counter in the shared + // preferences. Timeout after 20s, just to be safe. + int tries = 0; + while ((prefs.getLong(COUNT_KEY, -1) == 0) && (tries < 200)) { + Thread.sleep(100); + ++tries; + } + assertEquals(prefs.getLong(COUNT_KEY, -1), 1); + } +} diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java new file mode 100644 index 000000000000..c51a3c0d8a55 --- /dev/null +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java @@ -0,0 +1,15 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.androidalarmmanagerexample; + +import androidx.annotation.NonNull; + +public class DriverExtensionActivity extends MainActivity { + @Override + @NonNull + public String getDartEntrypointFunctionName() { + return "appMain"; + } +} diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java index 6b69d39de003..86bb25cccff2 100644 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java @@ -5,11 +5,13 @@ package io.flutter.plugins.androidalarmmanagerexample; import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; +import dev.flutter.plugins.e2e.FlutterTestRunner; import org.junit.Rule; import org.junit.runner.RunWith; -@RunWith(FlutterRunner.class) +@RunWith(FlutterTestRunner.class) public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(MainActivity.class, true, false); } diff --git a/packages/android_alarm_manager/example/android/app/src/debug/AndroidManifest.xml b/packages/android_alarm_manager/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..e826cdd83ac7 --- /dev/null +++ b/packages/android_alarm_manager/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/MainActivity.java b/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/MainActivity.java index 2c80708c4e94..efe9064317cd 100644 --- a/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/MainActivity.java +++ b/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/MainActivity.java @@ -10,6 +10,7 @@ import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; import io.flutter.plugins.androidalarmmanager.AndroidAlarmManagerPlugin; import io.flutter.plugins.pathprovider.PathProviderPlugin; +import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; public class MainActivity extends FlutterActivity { // TODO(bkonyi): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. https://github.com/flutter/flutter/issues/42694 @@ -18,6 +19,7 @@ public void configureFlutterEngine(FlutterEngine flutterEngine) { ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine); flutterEngine.getPlugins().add(new AndroidAlarmManagerPlugin()); flutterEngine.getPlugins().add(new E2EPlugin()); + flutterEngine.getPlugins().add(new SharedPreferencesPlugin()); PathProviderPlugin.registerWith( shimPluginRegistry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin")); } diff --git a/packages/android_alarm_manager/example/lib/main.dart b/packages/android_alarm_manager/example/lib/main.dart index 12aad9b001a9..4ba697744dbf 100644 --- a/packages/android_alarm_manager/example/lib/main.dart +++ b/packages/android_alarm_manager/example/lib/main.dart @@ -4,32 +4,156 @@ // ignore_for_file: public_member_api_docs -import 'dart:async'; +import 'dart:isolate'; +import 'dart:math'; +import 'dart:ui'; import 'package:android_alarm_manager/android_alarm_manager.dart'; -import 'package:flutter/widgets.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/material.dart'; -void printMessage(String msg) => print('[${DateTime.now()}] $msg'); +/// The [SharedPreferences] key to access the alarm fire count. +const String countKey = 'count'; -void printPeriodic() => printMessage("Periodic!"); -void printOneShot() => printMessage("One shot!"); +/// The name associated with the UI isolate's [SendPort]. +const String isolateName = 'isolate'; -Future main() async { - final int periodicID = 0; - final int oneShotID = 1; +/// A port used to communicate from a background isolate to the UI isolate. +final ReceivePort port = ReceivePort(); + +/// Global [SharedPreferences] object. +SharedPreferences prefs; +Future main() async { + // TODO(bkonyi): uncomment WidgetsFlutterBinding.ensureInitialized(); - // Start the AlarmManager service. - await AndroidAlarmManager.initialize(); - - printMessage("main run"); - runApp(const Center( - child: - Text('See device log for output', textDirection: TextDirection.ltr))); - await AndroidAlarmManager.periodic( - const Duration(seconds: 5), periodicID, printPeriodic, - wakeup: true, exact: true); - await AndroidAlarmManager.oneShot( - const Duration(seconds: 5), oneShotID, printOneShot); + // Register the UI isolate's SendPort to allow for communication from the + // background isolate. + IsolateNameServer.registerPortWithName( + port.sendPort, + isolateName, + ); + prefs = await SharedPreferences.getInstance(); + if (!prefs.containsKey(countKey)) { + await prefs.setInt(countKey, 0); + } + runApp(AlarmManagerExampleApp()); +} + +/// Example app for Espresso plugin. +class AlarmManagerExampleApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + home: _AlarmHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class _AlarmHomePage extends StatefulWidget { + _AlarmHomePage({Key key, this.title}) : super(key: key); + final String title; + + @override + _AlarmHomePageState createState() => _AlarmHomePageState(); +} + +class _AlarmHomePageState extends State<_AlarmHomePage> { + int _counter = 0; + + @override + void initState() { + super.initState(); + AndroidAlarmManager.initialize(); + + // Register for events from the background isolate. These messages will + // always coincide with an alarm firing. + port.listen((_) async => await _incrementCounter()); + } + + Future _incrementCounter() async { + print('Increment counter!'); + + // Ensure we've loaded the updated count from the background isolate. + await prefs.reload(); + + setState(() { + _counter++; + }); + } + + // The background + static SendPort uiSendPort; + + // The callback for our alarm + static Future callback() async { + print('Alarm fired!'); + + // Get the previous cached count and increment it. + final prefs = await SharedPreferences.getInstance(); + int currentCount = prefs.getInt(countKey); + await prefs.setInt(countKey, currentCount + 1); + + // This will be null if we're running in the background. + uiSendPort ??= IsolateNameServer.lookupPortByName(isolateName); + uiSendPort?.send(null); + } + + @override + Widget build(BuildContext context) { + // TODO(jackson): This has been deprecated and should be replaced + // with `headline4` when it's available on all the versions of + // Flutter that we test. + // ignore: deprecated_member_use + final textStyle = Theme.of(context).textTheme.display1; + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Alarm fired $_counter times', + style: textStyle, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Total alarms fired: ', + style: textStyle, + ), + Text( + prefs.getInt(countKey).toString(), + key: ValueKey('BackgroundCountText'), + style: textStyle, + ), + ], + ), + RaisedButton( + child: Text( + 'Schedule OneShot Alarm', + ), + key: ValueKey('RegisterOneShotAlarm'), + onPressed: () async { + await AndroidAlarmManager.oneShot( + const Duration(seconds: 5), + // Ensure we have a unique alarm ID. + Random().nextInt(pow(2, 31)), + callback, + exact: true, + wakeup: true, + ); + }, + ), + ], + ), + ), + ); + } } diff --git a/packages/android_alarm_manager/example/pubspec.yaml b/packages/android_alarm_manager/example/pubspec.yaml index dbcf2c02b817..2fc191881c10 100644 --- a/packages/android_alarm_manager/example/pubspec.yaml +++ b/packages/android_alarm_manager/example/pubspec.yaml @@ -6,11 +6,12 @@ dependencies: sdk: flutter android_alarm_manager: path: ../ - e2e: ^0.2.1 + shared_preferences: ^0.5.6 + e2e: 0.3.0 path_provider: ^1.3.1 - dev_dependencies: + espresso: ^0.0.1+3 flutter_driver: sdk: flutter flutter_test: diff --git a/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart b/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart index 8359bfd59ef2..a5bc1ac0ba48 100644 --- a/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart +++ b/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart @@ -4,9 +4,11 @@ import 'dart:async'; import 'dart:io'; +import 'package:android_alarm_manager_example/main.dart' as app; import 'package:android_alarm_manager/android_alarm_manager.dart'; import 'package:e2e/e2e.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_driver/driver_extension.dart'; import 'package:path_provider/path_provider.dart'; // From https://flutter.dev/docs/cookbook/persistence/reading-writing-files @@ -44,14 +46,17 @@ Future readCounter() async { Future incrementCounter() async { final int value = await readCounter(); - print('incrementCounter to: ${value + 1}'); await writeCounter(value + 1); } +void appMain() { + enableFlutterDriverExtension(); + app.main(); +} + void main() { E2EWidgetsFlutterBinding.ensureInitialized(); - print('main'); setUp(() async { await AndroidAlarmManager.initialize(); }); diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index 3efcbb439816..7bdf68f393bd 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,7 +1,7 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -version: 0.4.5+4 +version: 0.4.5+5 homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager dependencies: