[android_alarm_manager] Added Espresso test for background execution (#2482)

* Added test for android_alarm_manager background execution

Co-authored-by: Collin Jackson <jackson@google.com>
diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md
index cf935b5..d04ca96 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 d296caf..f066040 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 @@
 
 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 0000000..ce34b25
--- /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<DriverExtensionActivity> 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 0000000..c51a3c0
--- /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 6b69d39..86bb25c 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<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
+  @Rule
+  public ActivityTestRule<MainActivity> 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 0000000..e826cdd
--- /dev/null
+++ b/packages/android_alarm_manager/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="io.flutter.plugins.androidalarmmanagerexample">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <application android:usesCleartextTraffic="true">
+        <activity
+                android:name=".DriverExtensionActivity"
+                android:launchMode="singleTop"
+                android:theme="@style/LaunchTheme"
+                android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+                android:hardwareAccelerated="true"
+                android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
\ 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 2c80708..efe9064 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 @@
     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 12aad9b..4ba6977 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';
+
+/// A port used to communicate from a background isolate to the UI isolate.
+final ReceivePort port = ReceivePort();
+
+/// Global [SharedPreferences] object.
+SharedPreferences prefs;
 
 Future<void> main() async {
-  final int periodicID = 0;
-  final int oneShotID = 1;
-
+  // TODO(bkonyi): uncomment
   WidgetsFlutterBinding.ensureInitialized();
 
-  // Start the AlarmManager service.
-  await AndroidAlarmManager.initialize();
+  // 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());
+}
 
-  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);
+/// 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<void> _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<void> 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: <Widget>[
+            Text(
+              'Alarm fired $_counter times',
+              style: textStyle,
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                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 dbcf2c0..2fc1918 100644
--- a/packages/android_alarm_manager/example/pubspec.yaml
+++ b/packages/android_alarm_manager/example/pubspec.yaml
@@ -6,11 +6,12 @@
     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 8359bfd..a5bc1ac 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<void> 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 3efcbb4..7bdf68f 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: