[android_alarm_manager] Testing and documentation (#2283)

* [android_alarm_manager] Testing and documentation

Unit tests the plugin, and adds a lint for missing DartDocs.

* Review feedback

- Wrap InstanceWrappers into a single `setTestOverride` method.
- Replace potentially garbage collected lambdas with top level
functions.

* Review feedback
diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md
index 1506fd3..1494506 100644
--- a/packages/android_alarm_manager/CHANGELOG.md
+++ b/packages/android_alarm_manager/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.4.4+3
+
+* Add unit tests and DartDocs.
+
 ## 0.4.4+2
 
 * Remove AndroidX warning.
diff --git a/packages/android_alarm_manager/analysis_options.yaml b/packages/android_alarm_manager/analysis_options.yaml
deleted file mode 100644
index 8e4af76..0000000
--- a/packages/android_alarm_manager/analysis_options.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-# This is a temporary file to allow us to land a new set of linter rules in a
-# series of manageable patches instead of one gigantic PR. It disables some of
-# the new lints that are already failing on this plugin, for this plugin. It
-# should be deleted and the failing lints addressed as soon as possible.
-
-include: ../../analysis_options.yaml
-
-analyzer:
-  errors:
-    public_member_api_docs: ignore
diff --git a/packages/android_alarm_manager/example/lib/main.dart b/packages/android_alarm_manager/example/lib/main.dart
index e68735a..5e20364 100644
--- a/packages/android_alarm_manager/example/lib/main.dart
+++ b/packages/android_alarm_manager/example/lib/main.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// ignore_for_file: public_member_api_docs
+
 import 'dart:async';
 
 import 'package:android_alarm_manager/android_alarm_manager.dart';
diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart
index 76994b9..f31b4bc 100644
--- a/packages/android_alarm_manager/lib/android_alarm_manager.dart
+++ b/packages/android_alarm_manager/lib/android_alarm_manager.dart
@@ -54,14 +54,34 @@
   _channel.invokeMethod<void>('AlarmService.initialized');
 }
 
+// A lambda that returns the current instant in the form of a [DateTime].
+typedef DateTime _Now();
+// A lambda that gets the handle for the given [callback].
+typedef CallbackHandle _GetCallbackHandle(Function callback);
+
 /// A Flutter plugin for registering Dart callbacks with the Android
 /// AlarmManager service.
 ///
 /// See the example/ directory in this package for sample usage.
 class AndroidAlarmManager {
   static const String _channelName = 'plugins.flutter.io/android_alarm_manager';
-  static const MethodChannel _channel =
-      MethodChannel(_channelName, JSONMethodCodec());
+  static MethodChannel _channel =
+      const MethodChannel(_channelName, JSONMethodCodec());
+  // Function used to get the current time. It's [DateTime.now] by default.
+  static _Now _now = () => DateTime.now();
+  // Callback used to get the handle for a callback. It's
+  // [PluginUtilities.getCallbackHandle] by default.
+  static _GetCallbackHandle _getCallbackHandle =
+      (Function callback) => PluginUtilities.getCallbackHandle(callback);
+
+  /// This is exposed for the unit tests. It should not be accessed by users of
+  /// the plugin.
+  @visibleForTesting
+  static void setTestOverides(
+      {_Now now, _GetCallbackHandle getCallbackHandle}) {
+    _now = (now ?? _now);
+    _getCallbackHandle = (getCallbackHandle ?? _getCallbackHandle);
+  }
 
   /// Starts the [AndroidAlarmManager] service. This must be called before
   /// setting any alarms.
@@ -70,7 +90,7 @@
   /// failure.
   static Future<bool> initialize() async {
     final CallbackHandle handle =
-        PluginUtilities.getCallbackHandle(_alarmManagerCallbackDispatcher);
+        _getCallbackHandle(_alarmManagerCallbackDispatcher);
     if (handle == null) {
       return false;
     }
@@ -127,7 +147,7 @@
     bool rescheduleOnReboot = false,
   }) =>
       oneShotAt(
-        DateTime.now().add(delay),
+        _now().add(delay),
         id,
         callback,
         alarmClock: alarmClock,
@@ -188,7 +208,7 @@
     assert(callback is Function() || callback is Function(int));
     assert(id.bitLength < 32);
     final int startMillis = time.millisecondsSinceEpoch;
-    final CallbackHandle handle = PluginUtilities.getCallbackHandle(callback);
+    final CallbackHandle handle = _getCallbackHandle(callback);
     if (handle == null) {
       return false;
     }
@@ -251,11 +271,11 @@
     // ignore: inference_failure_on_function_return_type
     assert(callback is Function() || callback is Function(int));
     assert(id.bitLength < 32);
-    final int now = DateTime.now().millisecondsSinceEpoch;
+    final int now = _now().millisecondsSinceEpoch;
     final int period = duration.inMilliseconds;
     final int first =
         startAt != null ? startAt.millisecondsSinceEpoch : now + period;
-    final CallbackHandle handle = PluginUtilities.getCallbackHandle(callback);
+    final CallbackHandle handle = _getCallbackHandle(callback);
     if (handle == null) {
       return false;
     }
diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml
index 6fe4ed9..29a9961 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.4+2
+version: 0.4.4+3
 author: Flutter Team <flutter-dev@googlegroups.com>
 homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager
 
@@ -9,6 +9,10 @@
   flutter:
     sdk: flutter
 
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
 flutter:
   plugin:
     androidPackage: io.flutter.plugins.androidalarmmanager
diff --git a/packages/android_alarm_manager/test/android_alarm_manager_test.dart b/packages/android_alarm_manager/test/android_alarm_manager_test.dart
new file mode 100644
index 0000000..1f9d285
--- /dev/null
+++ b/packages/android_alarm_manager/test/android_alarm_manager_test.dart
@@ -0,0 +1,201 @@
+// 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.
+
+import 'dart:ui';
+
+import 'package:android_alarm_manager/android_alarm_manager.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  String invalidCallback(String foo) => foo;
+  void validCallback(int id) => null;
+
+  const MethodChannel testChannel = MethodChannel(
+      'plugins.flutter.io/android_alarm_manager', JSONMethodCodec());
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  setUpAll(() {
+    testChannel.setMockMethodCallHandler((MethodCall call) => null);
+  });
+
+  test('${AndroidAlarmManager.initialize}', () async {
+    testChannel.setMockMethodCallHandler((MethodCall call) async {
+      assert(call.method == 'AlarmService.start');
+      return true;
+    });
+
+    final bool initialized = await AndroidAlarmManager.initialize();
+
+    expect(initialized, isTrue);
+  });
+
+  group('${AndroidAlarmManager.oneShotAt}', () {
+    test('validates input', () async {
+      final DateTime validTime = DateTime.utc(1993);
+      final int validId = 1;
+
+      // Callback should take a single int param.
+      await expectLater(
+          () => AndroidAlarmManager.oneShotAt(
+              validTime, validId, invalidCallback),
+          throwsAssertionError);
+
+      // ID should be less than 32 bits.
+      await expectLater(
+          () => AndroidAlarmManager.oneShotAt(
+              validTime, 2147483648, validCallback),
+          throwsAssertionError);
+    });
+
+    test('sends arguments to the platform', () async {
+      final DateTime alarm = DateTime(1993);
+      const int rawHandle = 4;
+      AndroidAlarmManager.setTestOverides(
+          getCallbackHandle: (Function _) =>
+              CallbackHandle.fromRawHandle(rawHandle));
+
+      final int id = 1;
+      final bool alarmClock = true;
+      final bool allowWhileIdle = true;
+      final bool exact = true;
+      final bool wakeup = true;
+      final bool rescheduleOnReboot = true;
+
+      testChannel.setMockMethodCallHandler((MethodCall call) async {
+        expect(call.method, 'Alarm.oneShotAt');
+        expect(call.arguments[0], id);
+        expect(call.arguments[1], alarmClock);
+        expect(call.arguments[2], allowWhileIdle);
+        expect(call.arguments[3], exact);
+        expect(call.arguments[4], wakeup);
+        expect(call.arguments[5], alarm.millisecondsSinceEpoch);
+        expect(call.arguments[6], rescheduleOnReboot);
+        expect(call.arguments[7], rawHandle);
+        return true;
+      });
+
+      final bool result = await AndroidAlarmManager.oneShotAt(
+          alarm, id, validCallback,
+          alarmClock: alarmClock,
+          allowWhileIdle: allowWhileIdle,
+          exact: exact,
+          wakeup: wakeup,
+          rescheduleOnReboot: rescheduleOnReboot);
+
+      expect(result, isTrue);
+    });
+  });
+
+  test('${AndroidAlarmManager.oneShot} calls through to oneShotAt', () async {
+    final DateTime now = DateTime(1993);
+    const int rawHandle = 4;
+    AndroidAlarmManager.setTestOverides(
+        now: () => now,
+        getCallbackHandle: (Function _) =>
+            CallbackHandle.fromRawHandle(rawHandle));
+
+    const Duration alarm = Duration(seconds: 1);
+    final int id = 1;
+    final bool alarmClock = true;
+    final bool allowWhileIdle = true;
+    final bool exact = true;
+    final bool wakeup = true;
+    final bool rescheduleOnReboot = true;
+
+    testChannel.setMockMethodCallHandler((MethodCall call) async {
+      expect(call.method, 'Alarm.oneShotAt');
+      expect(call.arguments[0], id);
+      expect(call.arguments[1], alarmClock);
+      expect(call.arguments[2], allowWhileIdle);
+      expect(call.arguments[3], exact);
+      expect(call.arguments[4], wakeup);
+      expect(
+          call.arguments[5], now.millisecondsSinceEpoch + alarm.inMilliseconds);
+      expect(call.arguments[6], rescheduleOnReboot);
+      expect(call.arguments[7], rawHandle);
+      return true;
+    });
+
+    final bool result = await AndroidAlarmManager.oneShot(
+        alarm, id, validCallback,
+        alarmClock: alarmClock,
+        allowWhileIdle: allowWhileIdle,
+        exact: exact,
+        wakeup: wakeup,
+        rescheduleOnReboot: rescheduleOnReboot);
+
+    expect(result, isTrue);
+  });
+
+  group('${AndroidAlarmManager.periodic}', () {
+    test('validates input', () async {
+      const Duration validDuration = Duration(seconds: 0);
+      final int validId = 1;
+
+      // Callback should take a single int param.
+      await expectLater(
+          () => AndroidAlarmManager.periodic(
+              validDuration, validId, invalidCallback),
+          throwsAssertionError);
+
+      // ID should be less than 32 bits.
+      await expectLater(
+          () => AndroidAlarmManager.periodic(
+              validDuration, 2147483648, validCallback),
+          throwsAssertionError);
+    });
+
+    test('sends arguments through to the platform', () async {
+      final DateTime now = DateTime(1993);
+      const int rawHandle = 4;
+      AndroidAlarmManager.setTestOverides(
+          now: () => now,
+          getCallbackHandle: (Function _) =>
+              CallbackHandle.fromRawHandle(rawHandle));
+
+      final int id = 1;
+      final bool exact = true;
+      final bool wakeup = true;
+      final bool rescheduleOnReboot = true;
+      const Duration period = Duration(seconds: 1);
+
+      testChannel.setMockMethodCallHandler((MethodCall call) async {
+        expect(call.method, 'Alarm.periodic');
+        expect(call.arguments[0], id);
+        expect(call.arguments[1], exact);
+        expect(call.arguments[2], wakeup);
+        expect(call.arguments[3],
+            (now.millisecondsSinceEpoch + period.inMilliseconds));
+        expect(call.arguments[4], period.inMilliseconds);
+        expect(call.arguments[5], rescheduleOnReboot);
+        expect(call.arguments[6], rawHandle);
+        return true;
+      });
+
+      final bool result = await AndroidAlarmManager.periodic(
+        period,
+        id,
+        (int id) => null,
+        exact: exact,
+        wakeup: wakeup,
+        rescheduleOnReboot: rescheduleOnReboot,
+      );
+
+      expect(result, isTrue);
+    });
+  });
+
+  test('${AndroidAlarmManager.cancel}', () async {
+    final int id = 1;
+    testChannel.setMockMethodCallHandler((MethodCall call) async {
+      assert(call.method == 'Alarm.cancel' && call.arguments[0] == id);
+      return true;
+    });
+
+    final bool canceled = await AndroidAlarmManager.cancel(id);
+
+    expect(canceled, isTrue);
+  });
+}