diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..5b928b1e --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,32 @@ +name: Publish plugin + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Install project dependencies + run: flutter pub get + - name: Dart Format Check + run: dart format lib/ test/ --set-exit-if-changed + - name: Import Sorter Check + run: flutter pub run import_sorter:main --no-comments --exit-if-changed + - name: Dart Analyze Check + run: flutter analyze + #- name: Check Publish Warnings + # run: dart pub publish --dry-run + - name: Publish + uses: k-paxian/dart-package-publisher@v1.5.1 + with: + credentialJson: ${{ secrets.CREDENTIAL_JSON }} + flutter: true + skipTests: true + force: true diff --git a/.github/workflows/pushMaster.yaml b/.github/workflows/pushMaster.yaml new file mode 100644 index 00000000..fa25204c --- /dev/null +++ b/.github/workflows/pushMaster.yaml @@ -0,0 +1,25 @@ +name: Push To Master + +on: + push: + branches: + - main + +jobs: + build: + name: Build Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Install project dependencies + run: flutter pub get + - name: Dart Format Check + run: dart format lib/ test/ --set-exit-if-changed + - name: Import Sorter Check + run: flutter pub run import_sorter:main --no-comments --exit-if-changed + - name: Dart Analyze Check + run: flutter analyze diff --git a/.gitignore b/.gitignore index dbef116d..3b2be347 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ doc/api/ *.js_ *.js.deps *.js.map +.DS_Store +.idea +.vscode +android/gradle* +example/ios/Podfile.lock \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dfa8877..8c311edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,64 @@ # Changelog ----------------------------------------------- + +[0.4.1] - 2025.02.03 + +* [Fix] Updates firebase messaging and android gradle + +[0.4.0] - 2023.08.24 + +* [fix] Extracting UI responsibility, named parameters, android dismissal fix (#189). +* [fix] make the Object nullable (#182) +* [chore] Updated to the latest flutter and firebase messaging (#185) +* [feat] Improve Android broadcasts and iOS delegate (#159) + +[0.3.3] - 2023.01.25 + +* [fix] Remove as `type` to allow null assignment. + +[0.3.2] - 2021.09.27 + +* [feat] Add backgroundMode for setup. +* [fix] Duplicated call onAnswer. New open method. (#111) +* [fix] Cannot receive answer call on Android 11 (#98) + +[0.3.1] - 2021.07.27 + +* Add foregroundService for Android 11. + +[0.3.0] - 2021.06.12 + +* null safety +* Add toolkit for testing. +* Fixed receiving Voip push in background mode for iOS 13+. +* Fix crash when iOS push uses the wrong push format (alert). + +[0.2.4] - 2021.01.08 + +* Fix crash when appName is not set. +* hasDefaultPhoneAccount give feedback about the user choice. + +[0.2.3] - 2021.01.08 + +* Fix backToForeground method. + +[0.2.2] - 2020.12.27 + +* Update json format for push payload. + +[0.2.1] - 2020.12.23 + +* Fix: Missing null check. +* Fix: change parameter handle to number. + +[0.2.0] - 2020.11.11 + +* Change FlutterCallKeep as a singleton. +* Add CallKeepPushKitToken event for iOS. +* Add firebase_messaging to example. +* Support waking CallKeep from PushKit when the app is closed. + [0.1.1] - 2020.09.17 * Fix compile error for iOS. diff --git a/README.md b/README.md index 7daafac2..09bccce3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,287 @@ # callkeep -iOS CallKit and Android ConnectionService for Flutter + +[![Financial Contributors on Open Collective](https://opencollective.com/flutter-webrtc/all/badge.svg?label=financial+contributors)](https://opencollective.com/flutter-webrtc) [![pub package](https://img.shields.io/pub/v/callkeep.svg)](https://pub.dartlang.org/packages/callkeep) [![slack](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://join.slack.com/t/flutterwebrtc/shared_invite/zt-q83o7y1s-FExGLWEvtkPKM8ku_F8cEQ) + +- iOS CallKit and Android ConnectionService for Flutter +- Support FCM and PushKit + +> Keep in mind Callkit is banned in China, so if you want your app in the chinese AppStore consider include a basic alternative for notifying calls (ex. FCM notifications with sound). + +`* P-C-M means -> presenter / controller / manager` + +## Introduction + +Callkeep acts as an intermediate between your call system (RTC, VOIP...) and the user, offering a native calling interface for handling your app calls. + +This allows you (for example) to answer calls when your device is locked even if your app is terminated. + +## Initial setup + +Basic configuration. In Android a popup is displayed before starting requesting some permissions to work properly. + +```dart +callKeep.setup( + showAlertDialog: () async { + final BuildContext context = navigatorKey.currentContext!; + + return await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Permissions Required'), + content: const Text( + 'This application needs to access your phone accounts'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(false), + ), + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ); + }, + ) ?? + false; + }, + options:{ + 'ios': { + 'appName': 'CallKeepDemo', + }, + 'android': { + 'additionalPermissions': [ + 'android.permission.CALL_PHONE', + 'android.permission.READ_PHONE_NUMBERS' + ], + // Required to get audio in background when using Android 11 + 'foregroundService': { + 'channelId': 'com.company.my', + 'channelName': 'Foreground service for my app', + 'notificationTitle': 'My app is running on background', + 'notificationIcon': 'mipmap/ic_notification_launcher', + }, + }, +}); +``` + +This configuration should be defined when your application wakes up, but keep in mind this alert will appear if you aren't granting the needed permissions yet. + +A clean alternative is to control by yourself the required permissions when your application wakes up, and only invoke the `setup()` method if those permissions are granted. + +## Events + +Callkeep offers some events to handle native actions during a call. + +These events are quite crucial because they act as an intermediate between the native calling UI and your call P-C-M. + +What does it mean? + +Assuming your application already implements some calling system (RTC, Voip, or whatever) with its own calling UI, you are using some basic controls: + +

+ +> before implementing `callkeep` + +- Hang up -> `presenter.hangUp()` +- Microphone switcher -> `presenter.microSwitch()` + +> after implementing `callkeep` + +- Hang up -> `callkeep.endCall(call_uuid)` +- Microphone switcher -> `callKeep.setMutedCall(uuid, true / false)` + +Then you handle the action: + +```dart +Future answerCall(CallKeepPerformAnswerCallAction event) async { + print('CallKeepPerformAnswerCallAction ${event.callUUID}'); + // notify to your call P-C-M the answer action +}; + + Future endCall(CallKeepPerformEndCallAction event) async { + print('CallKeepPerformEndCallAction ${event.callUUID}'); + // notify to your call P-C-M the end action +}; + +Future didPerformSetMutedCallAction(CallKeepDidPerformSetMutedCallAction event) async { + print('CallKeepDidPerformSetMutedCallAction ${event.callUUID}'); + // notify to your call P-C-M the muted switch action +}; + + Future didToggleHoldCallAction(CallKeepDidToggleHoldAction event) async { + print('CallKeepDidToggleHoldAction ${event.callUUID}'); + // notify to your call P-C-M the hold switch action +}; +``` + +```dart + + @override + void initState() { + super.initState(); + callKeep.on(didDisplayIncomingCall); + callKeep.on(answerCall); + callKeep.on(endCall); + callKeep.on(didToggleHoldCallAction); + } +``` + +## Display incoming calls in foreground, background or terminate state + +The incoming call concept we are looking for is firing an incoming call action when "something" is received in our app. + +I've tested this concept with FCM and it works pretty fine. + +```dart +final FlutterCallkeep _callKeep = FlutterCallkeep(); +bool _callKeepStarted = false; + +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + await Firebase.initializeApp(); + if (!_callKeepStarted) { + try { + await _callKeep.setup(callSetup); + _callKeepStarted = true; + } catch (e) { + print(e); + } + } + + // then process your remote message looking for some call uuid + // and display any incoming call +} +``` + +Displaying incoming calls is really simple if you are receiving FCM messages (or whatever). This example shows how to show and close any incoming call: + +> Notice that getting data from the payload can be done as you want, this is an example. + +A payload data example: + +```json +{ + "uuid": "xxxxx-xxxxx-xxxxx-xxxxx", + "caller_id": "+0123456789", + "caller_name": "Draco", + "caller_id_type": "number", + "has_video": "false" +} +``` + +A `RemoteMessage` extension for getting data: + +```dart +import 'dart:convert'; + +import 'package:firebase_messaging/firebase_messaging.dart'; + +extension RemoteMessageExt on RemoteMessage { + Map getContent() { + return jsonDecode(this.data["content"]); + } + + Map payload() { + return getContent()["payload"]; + } +} +``` + +Methods to show and close incoming calls: + +```dart +Future showIncomingCall( + BuildContext context, + RemoteMessage remoteMessage, + FlutterCallkeep callKeep, +) async { + var callerIdFrom = remoteMessage.payload()["caller_id"] as String; + var callerName = remoteMessage.payload()["caller_name"] as String; + var uuid = remoteMessage.payload()["uuid"] as String; + var hasVideo = remoteMessage.payload()["has_video"] == "true"; + + callKeep.on(onHold); + callKeep.on(answerAction); + callKeep.on(endAction); + callKeep.on(setMuted); + + print('backgroundMessage: displayIncomingCall ($uuid)'); + + bool hasPhoneAccount = await callKeep.hasPhoneAccount(); + if (!hasPhoneAccount) { + hasPhoneAccount = await callKeep.hasDefaultPhoneAccount(context, callSetup["android"]); + } + + if (!hasPhoneAccount) { + return; + } + + await callKeep.displayIncomingCall(uuid, callerIdFrom, localizedCallerName: callerName, hasVideo: hasVideo); + callKeep.backToForeground(); +} + +Future closeIncomingCall( + RemoteMessage remoteMessage, + FlutterCallkeep callKeep, +) async { + var uuid = remoteMessage.payload()[MessageManager.CALLER_UUID] as String; + print('backgroundMessage: closeIncomingCall ($uuid)'); + bool hasPhoneAccount = await callKeep.hasPhoneAccount(); + if (!hasPhoneAccount) { + return; + } + await callKeep.endAllCalls(); +} +``` + +Pass in your own dialog UI for permissions alerts + +````dart +showAlertDialog: () async { + final BuildContext context = navigatorKey.currentContext!; + + return await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Permissions Required'), + content: const Text( + 'This application needs to access your phone accounts'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(false), + ), + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ); + }, + ) ?? + false; + }, +``` + + + + +### FAQ + +> I don't receive the incoming call + +Receiving incoming calls depends on FCM push messages (or the system you use) for handling the call information and displaying it. +Remember FCM push messages not always works due to data-only messages are classified as "low priority". Devices can throttle and ignore these messages if your application is in the background, terminated, or a variety of other conditions such as low battery or currently high CPU usage. To help improve delivery, you can bump the priority of messages. Note; this does still not guarantee delivery. More info [here](https://firebase.flutter.dev/docs/messaging/usage/#low-priority-messages) + +> How can I manage the call if the app is terminated and the device is locked? + +Even in this scenario, the `backToForeground()` method will open the app and your call P-C-M will be able to work. + +## push test tool + +Please refer to the [Push Toolkit](/tools/) to test callkeep offline push. +```` diff --git a/analysis_options.yaml b/analysis_options.yaml index b65afa88..6cb62a1d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,25 @@ -include: package:pedantic/analysis_options.yaml + +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. rules: - always_declare_return_types - avoid_empty_else @@ -30,7 +49,12 @@ linter: - use_rethrow_when_possible - valid_regexps - void_checks - + + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options + analyzer: errors: # treat missing required parameters as a warning (not a hint) @@ -45,4 +69,3 @@ analyzer: # Ignore analyzer hints for updating pubspecs when using Future or # Stream and not importing dart:async # Please see https://github.com/flutter/flutter/pull/24528 for details. - sdk_version_async_exported_from_core: ignore diff --git a/android/build.gradle b/android/build.gradle index da4fba2a..6e991c4a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -22,10 +22,11 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 35 + namespace 'com.github.cloudwebrtc.flutter_callkeep' defaultConfig { - minSdkVersion 23 + minSdkVersion 21 } lintOptions { disable 'InvalidPackage' @@ -36,10 +37,9 @@ android { } } - dependencies { implementation 'com.android.support:support-v4:28.0.0' implementation 'com.android.support:appcompat-v7:28.0.0' - implementation "com.android.support:support-core-utils:28.0.0" + implementation 'com.android.support:support-core-utils:28.0.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 01a286e9..00000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a2c1457a..8680616d 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ - + - \ No newline at end of file + diff --git a/android/src/main/java/com/github/cloudwebrtc/flutter_callkeep/FlutterCallkeepPlugin.java b/android/src/main/java/com/github/cloudwebrtc/flutter_callkeep/FlutterCallkeepPlugin.java index d9d5c2d3..8337f46d 100644 --- a/android/src/main/java/com/github/cloudwebrtc/flutter_callkeep/FlutterCallkeepPlugin.java +++ b/android/src/main/java/com/github/cloudwebrtc/flutter_callkeep/FlutterCallkeepPlugin.java @@ -12,39 +12,17 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.Registrar; import io.wazo.callkeep.CallKeepModule; /** FlutterCallkeepPlugin */ +/// The MethodChannel that will the communication between Flutter and native Android +/// +/// This local reference serves to register the plugin with the Flutter Engine and unregister it +/// when the Flutter Engine is detached from the Activity public class FlutterCallkeepPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity private MethodChannel channel; private CallKeepModule callKeep; - - /** - * Plugin registration. - */ - public static void registerWith(Registrar registrar) { - final FlutterCallkeepPlugin plugin = new FlutterCallkeepPlugin(); - - plugin.startListening(registrar.context(), registrar.messenger()); - - if (registrar.activeContext() instanceof Activity) { - plugin.setActivity((Activity) registrar.activeContext()); - } - - registrar.addViewDestroyListener(view -> { - plugin.stopListening(); - return false; - }); - } - - private void setActivity(@NonNull Activity activity) { - callKeep.setActivity(activity); - } + private Activity activity; private void startListening(final Context context, BinaryMessenger messenger) { channel = new MethodChannel(messenger, "FlutterCallKeep.Method"); @@ -53,10 +31,14 @@ private void startListening(final Context context, BinaryMessenger messenger) { } private void stopListening() { - channel.setMethodCallHandler(null); - channel = null; - callKeep.dispose(); - callKeep = null; + if (channel != null) { + channel.setMethodCallHandler(null); + channel = null; + } + if (callKeep != null) { + callKeep.dispose(); + callKeep = null; + } } @Override @@ -66,7 +48,7 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin @Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { - if (!callKeep.HandleMethodCall(call, result)) { + if (!callKeep.handleMethodCall(call, result)) { result.notImplemented(); } } @@ -78,21 +60,31 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - callKeep.setActivity(binding.getActivity()); + activity = binding.getActivity(); + if (callKeep != null) { + callKeep.setActivity(activity); + } } @Override public void onDetachedFromActivityForConfigChanges() { - callKeep.setActivity(null); + if (callKeep != null) { + callKeep.setActivity(null); + } } @Override public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - callKeep.setActivity(binding.getActivity()); + activity = binding.getActivity(); + if (callKeep != null) { + callKeep.setActivity(activity); + } } @Override public void onDetachedFromActivity() { - callKeep.setActivity(null); + if (callKeep != null) { + callKeep.setActivity(null); + } } } diff --git a/android/src/main/java/io/wazo/callkeep/CallKeepBackgroundMessagingService.java b/android/src/main/java/io/wazo/callkeep/CallKeepBackgroundMessagingService.java index 126c5e72..90b9e087 100644 --- a/android/src/main/java/io/wazo/callkeep/CallKeepBackgroundMessagingService.java +++ b/android/src/main/java/io/wazo/callkeep/CallKeepBackgroundMessagingService.java @@ -48,7 +48,9 @@ public static void acquireWakeLockNow(Context context) { @Nullable @Override public IBinder onBind(Intent intent) { - Log.d(TAG, "wakeUpApplication: " + intent.getStringExtra("callUUID") + ", number : " + intent.getStringExtra("handle") + ", displayName:" + intent.getStringExtra("name")); + Log.d(TAG, "wakeUpApplication: " + intent.getStringExtra(CallKeepConstants.EXTRA_CALL_UUID) + + ", number : " + intent.getStringExtra(CallKeepConstants.EXTRA_CALL_NUMBER) + + ", displayName:" + intent.getStringExtra(CallKeepConstants.EXTRA_CALLER_NAME)); //TODO: not implemented return null; } diff --git a/android/src/main/java/io/wazo/callkeep/Constants.java b/android/src/main/java/io/wazo/callkeep/CallKeepConstants.java similarity index 56% rename from android/src/main/java/io/wazo/callkeep/Constants.java rename to android/src/main/java/io/wazo/callkeep/CallKeepConstants.java index 4310de56..8f90c4f3 100644 --- a/android/src/main/java/io/wazo/callkeep/Constants.java +++ b/android/src/main/java/io/wazo/callkeep/CallKeepConstants.java @@ -1,19 +1,34 @@ package io.wazo.callkeep; -public class Constants { +public class CallKeepConstants { + public static final String ACTION_WAKEUP_CALL = "ACTION_WAKEUP_CALL"; + public static final String ACTION_ANSWER_CALL = "ACTION_ANSWER_CALL"; + public static final String ACTION_REJECT_CALL = "ACTION_REJECT_CALL"; + public static final String ACTION_INCOMING_CALL = "ACTION_INCOMING_CALL"; + public static final String ACTION_FAILED_CALL = "ACTION_FAILED_CALL"; + public static final String ACTION_ONGOING_CALL = "ACTION_ONGOING_CALL"; + public static final String ACTION_AUDIO_SESSION = "ACTION_AUDIO_SESSION"; public static final String ACTION_CHECK_REACHABILITY = "ACTION_CHECK_REACHABILITY"; public static final String ACTION_DTMF_TONE = "ACTION_DTMF_TONE"; public static final String ACTION_END_CALL = "ACTION_END_CALL"; public static final String ACTION_HOLD_CALL = "ACTION_HOLD_CALL"; public static final String ACTION_MUTE_CALL = "ACTION_MUTE_CALL"; - public static final String ACTION_ONGOING_CALL = "ACTION_ONGOING_CALL"; + public static final String ACTION_AUDIO_CALL = "ACTION_AUDIO_CALL"; public static final String ACTION_UNHOLD_CALL = "ACTION_UNHOLD_CALL"; public static final String ACTION_UNMUTE_CALL = "ACTION_UNMUTE_CALL"; public static final String ACTION_WAKE_APP = "ACTION_WAKE_APP"; public static final String EXTRA_CALL_NUMBER = "EXTRA_CALL_NUMBER"; public static final String EXTRA_CALL_UUID = "EXTRA_CALL_UUID"; + public static final String EXTRA_CALL_DATA = "EXTRA_CALL_EXTRAS"; public static final String EXTRA_CALLER_NAME = "EXTRA_CALLER_NAME"; + public static final String EXTRA_CALL_ATTRIB = "EXTRA_CALL_ATTRIB"; + + + public static final String HOLD_SUPPORT_DATA_KEY = "io.wazo.callkeep.HoldSupported"; + public static final String BROADCAST_RECEIVER_META_DATA_KEY = "io.wazo.callkeep.BroadcastReceiver"; + + public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128; } diff --git a/android/src/main/java/io/wazo/callkeep/CallKeepModule.java b/android/src/main/java/io/wazo/callkeep/CallKeepModule.java index 6dc2ed92..3bb3ab03 100644 --- a/android/src/main/java/io/wazo/callkeep/CallKeepModule.java +++ b/android/src/main/java/io/wazo/callkeep/CallKeepModule.java @@ -25,13 +25,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.telecom.CallAudioState; import android.telecom.Connection; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; @@ -40,92 +40,106 @@ import android.util.Log; import android.view.WindowManager; +import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; -import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel.Result; import io.wazo.callkeep.utils.Callback; import io.wazo.callkeep.utils.ConstraintsMap; import io.wazo.callkeep.utils.ConstraintsArray; +import io.wazo.callkeep.utils.MapUtils; import io.wazo.callkeep.utils.PermissionUtils; -import static io.wazo.callkeep.Constants.*; +import static io.wazo.callkeep.CallKeepConstants.*; + +import org.json.JSONException; +import org.json.JSONObject; // @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionServiceActivity.java public class CallKeepModule { - public static final int REQUEST_READ_PHONE_STATE = 1337; - public static final int REQUEST_REGISTER_CALL_PROVIDER = 394859; - private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST"; - private static final String REACT_NATIVE_MODULE_NAME = "CallKeep"; - private static final String[] permissions = { Manifest.permission.READ_PHONE_STATE, - Manifest.permission.CALL_PHONE, Manifest.permission.RECORD_AUDIO }; - + private static final String E_CONNECTION_SERVICE_NOT_AVAILABLE = "E_CONNECTION_SERVICE_NOT_AVAILABLE"; private static final String TAG = "FLT:CallKeepModule"; + private static TelecomManager telecomManager; private static TelephonyManager telephonyManager; - private static MethodChannel.Result hasPhoneAccountPromise; - private Context _context; - public static PhoneAccountHandle handle; + private static PhoneAccountHandle accountHandle; + private static ConstraintsMap settings; + private static boolean hasSetup = false; + private final Context context; private boolean isReceiverRegistered = false; private VoiceBroadcastReceiver voiceBroadcastReceiver; - private ConstraintsMap _settings; - Activity _currentActivity = null; - MethodChannel _eventChannel; + private final List requiredPermissions = new LinkedList<>(); + private Activity currentActivity = null; + private final MethodChannel eventChannel; public CallKeepModule(Context context, BinaryMessenger messenger) { - this._context = context; - this._eventChannel = new MethodChannel(messenger, "FlutterCallKeep.Event"); + this.context = context; + this.eventChannel = new MethodChannel(messenger, "FlutterCallKeep.Event"); + } + + public static PhoneAccountHandle getAccountHandle() { + return accountHandle; } public void setActivity(Activity activity) { - this._currentActivity = activity; + this.currentActivity = activity; } - public void dispose(){ - LocalBroadcastManager.getInstance(this._context).unregisterReceiver(voiceBroadcastReceiver); + public void dispose() { + if (voiceBroadcastReceiver == null || this.context == null) return; + LocalBroadcastManager.getInstance(this.context).unregisterReceiver(voiceBroadcastReceiver); VoiceConnectionService.setPhoneAccountHandle(null); + isReceiverRegistered = false; } - public boolean HandleMethodCall(@NonNull MethodCall call, @NonNull Result result) { - switch(call.method) { + public boolean handleMethodCall(@NonNull MethodCall call, @NonNull Result result) { + switch (call.method) { case "setup": { - setup(new ConstraintsMap((Map)call.argument("options"))); + setup(new ConstraintsMap(call.argument("options"))); result.success(null); } break; case "displayIncomingCall": { - displayIncomingCall((String)call.argument("uuid"), (String)call.argument("handle"), (String)call.argument("localizedCallerName")); + displayIncomingCallImpl( + call.argument("uuid"), + call.argument("handle"), + call.argument("callerName"), + call.argument("additionalData") + ); result.success(null); } break; case "answerIncomingCall": { - answerIncomingCall((String)call.argument("uuid")); + answerIncomingCall(call.argument("uuid")); result.success(null); } break; case "startCall": { - startCall((String)call.argument("uuid"), (String)call.argument("number"), (String)call.argument("callerName")); + startCall( + call.argument("uuid"), + call.argument("handle"), + call.argument("callerName"), + call.argument("additionalData") + ); result.success(null); } break; case "endCall": { - endCall((String)call.argument("uuid")); + endCall(call.argument("uuid")); result.success(null); } break; @@ -134,8 +148,8 @@ public boolean HandleMethodCall(@NonNull MethodCall call, @NonNull Result result result.success(null); } break; - case "checkPhoneAccountPermission": { - checkPhoneAccountPermission(new ConstraintsArray((ArrayList)call.argument("optionalPermissions")), result); + case "requestPermissions": { + requestPermissions(new ConstraintsArray(call.argument("additionalPermissions")), result); } break; case "checkDefaultPhoneAccount": { @@ -143,32 +157,42 @@ public boolean HandleMethodCall(@NonNull MethodCall call, @NonNull Result result } break; case "setOnHold": { - setOnHold((String)call.argument("uuid"), (Boolean) call.argument("hold")); + setOnHold(call.argument("uuid"), call.argument("hold")); result.success(null); } break; case "reportEndCallWithUUID": { - reportEndCallWithUUID((String)call.argument("uuid"), (int)call.argument("reason")); + reportEndCallWithUUID(call.argument("uuid"), call.argument("reason"), call.argument("notify")); + result.success(null); + } + break; + case "reportStartedCallWithUUID": { + reportStartedCallWithUUID(call.argument("uuid")); result.success(null); } break; case "rejectCall": { - rejectCall((String)call.argument("uuid")); + rejectCall(call.argument("uuid")); result.success(null); } break; case "setMutedCall": { - setMutedCall((String)call.argument("uuid"), (Boolean)call.argument("muted")); + setMutedCall(call.argument("uuid"), call.argument("muted")); + result.success(null); + } + break; + case "setCallAudio": { + setCallAudio(call.argument("uuid"), call.argument("audioRoute")); result.success(null); } break; case "sendDTMF": { - sendDTMF((String)call.argument("uuid"), (String)call.argument("key")); + sendDTMF(call.argument("uuid"), call.argument("key")); result.success(null); } break; case "updateDisplay": { - updateDisplay((String)call.argument("uuid"), (String)call.argument("displayName"), (String)call.argument("handle")); + updateDisplay(call.argument("uuid"), call.argument("callerName"), call.argument("handle")); result.success(null); } break; @@ -185,17 +209,17 @@ public boolean HandleMethodCall(@NonNull MethodCall call, @NonNull Result result } break; case "setAvailable": { - setAvailable((Boolean) call.argument("available")); + setAvailable(call.argument("available")); result.success(null); } break; case "setReachable": { - setReachable(); + setReachable(call.argument("reachable")); result.success(null); } break; case "setCurrentCallActive": { - setCurrentCallActive((String)call.argument("uuid")); + setCurrentCallActive(call.argument("uuid")); result.success(null); } break; @@ -207,64 +231,121 @@ public boolean HandleMethodCall(@NonNull MethodCall call, @NonNull Result result backToForeground(result); } break; + case "foregroundService": { + updateSettings(new ConstraintsMap(call.argument("settings"))); + result.success(null); + } + break; + case "isCallActive": { + isCallActive(call.argument("uuid"), result); + } + break; + case "activeCalls": { + activeCalls(result); + } + break; default: return false; } return true; } - - public void setup(ConstraintsMap options) { - VoiceConnectionService.setAvailable(false); - this._settings = options; - if (isConnectionServiceAvailable()) { - this.registerPhoneAccount(); - this.registerEvents(); + private void setup(ConstraintsMap options) { + if (isReceiverRegistered) { + return; + } + updateSettings(options); + if (setupImpl(context, options)) { + registerEvents(); + } + setupRequiredPermissions(options); + } + + private static boolean setupImpl(Context context, ConstraintsMap options) { + boolean isServiceAvailable = isConnectionServiceAvailable(); + if (hasSetup) return isServiceAvailable; + VoiceConnectionService.setAvailable(false); + if (isServiceAvailable) { + registerPhoneAccount(context, options); + VoiceConnectionService.setPhoneAccountHandle(accountHandle); VoiceConnectionService.setAvailable(true); } + hasSetup = true; + return isServiceAvailable; } - - public void registerPhoneAccount() { - if (!isConnectionServiceAvailable()) { - return; + public static ConstraintsMap getSettings(@Nullable Context context) { + if (settings == null) { + fetchStoredSettings(context); + } + return settings; + } + + private void updateSettings(ConstraintsMap options) { + if (settings == null) { + settings = options; + } else { + settings.merge(options.toMap()); } + storeSettings(settings); + } - this.registerPhoneAccount(this.getAppContext()); + private void setupRequiredPermissions(ConstraintsMap options) { + requiredPermissions.add(Manifest.permission.READ_PHONE_STATE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + requiredPermissions.add(Manifest.permission.READ_PHONE_NUMBERS); + } + if (isSelfManaged(options)) { + requiredPermissions.add(Manifest.permission.MANAGE_OWN_CALLS); + } else { + requiredPermissions.add(Manifest.permission.CALL_PHONE); + } } - - public void registerEvents() { + private void registerEvents() { if (!isConnectionServiceAvailable()) { return; } - voiceBroadcastReceiver = new VoiceBroadcastReceiver(); registerReceiver(); - VoiceConnectionService.setPhoneAccountHandle(handle); } - - public void displayIncomingCall(String uuid, String number, String callerName) { + public static void displayIncomingCall(Context context, + String uuid, + String handle, + String callerName, + Map additionalData) { + if (setupImpl(context, getSettings(context))) { + displayIncomingCallImpl(uuid, handle, callerName, additionalData); + } + } + + private static void displayIncomingCallImpl(String uuid, + String handle, + String callerName, + Map additionalData) { + Log.d(TAG, "Called displayIncomingCall"); if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { return; } - Log.d(TAG, "displayIncomingCall number: " + number + ", callerName: " + callerName); + Log.d(TAG, "displayIncomingCall number: " + handle + ", callerName: " + callerName); Bundle extras = new Bundle(); - Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); + Uri uri = Uri.fromParts(getHandleSchema(), handle, null); + + Bundle callExtras = createCallBundle(uuid, handle, callerName, additionalData); extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri); - extras.putString(EXTRA_CALLER_NAME, callerName); - extras.putString(EXTRA_CALL_UUID, uuid); + extras.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, callExtras); - telecomManager.addNewIncomingCall(handle, extras); + telecomManager.addNewIncomingCall(accountHandle, extras); + Log.d(TAG, "Finished displayIncomingCall"); } - - public void answerIncomingCall(String uuid) { + + private void answerIncomingCall(String uuid) { if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { return; } @@ -277,30 +358,49 @@ public void answerIncomingCall(String uuid) { conn.onAnswer(); } - - public void startCall(String uuid, String number, String callerName) { - if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || number == null) { + + @SuppressLint("MissingPermission") + private void startCall(String uuid, + String handle, + String callerName, + Map additionalData) { + if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || handle == null) { return; } - Log.d(TAG, "startCall number: " + number + ", callerName: " + callerName); + Log.d(TAG, "startCall number: " + handle + ", callerName: " + callerName); Bundle extras = new Bundle(); - Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); - Bundle callExtras = new Bundle(); - callExtras.putString(EXTRA_CALLER_NAME, callerName); - callExtras.putString(EXTRA_CALL_UUID, uuid); - callExtras.putString(EXTRA_CALL_NUMBER, number); + Uri uri = Uri.fromParts(getHandleSchema(), handle, null); - extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle); - extras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras); + Bundle callExtras = createCallBundle(uuid, handle, callerName, additionalData); + extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle); + extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras); telecomManager.placeCall(uri, extras); } - - public void endCall(String uuid) { + private static String getHandleSchema() { + if (settings == null || settings.isNull("handleSchema")) { + return PhoneAccount.SCHEME_TEL; + } else { + return Objects.requireNonNull(settings).getString("handleSchema"); + } + } + + private static Bundle createCallBundle(String uuid, String handle, String callerName, Map additionalData) { + Bundle extras = new Bundle(); + extras.putString(EXTRA_CALL_UUID, uuid); + extras.putString(EXTRA_CALLER_NAME, callerName); + extras.putString(EXTRA_CALL_NUMBER, handle); + if (additionalData != null) { + extras.putSerializable(EXTRA_CALL_DATA, new HashMap<>(additionalData)); + } + return extras; + } + + private void endCall(String uuid) { Log.d(TAG, "endCall called"); if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { return; @@ -315,67 +415,48 @@ public void endCall(String uuid) { Log.d(TAG, "endCall executed"); } - - public void endAllCalls() { + + private void endAllCalls() { Log.d(TAG, "endAllCalls called"); if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { return; } - Map currentConnections = VoiceConnectionService.currentConnections; - for (Map.Entry connectionEntry : currentConnections.entrySet()) { - Connection connectionToEnd = connectionEntry.getValue(); - connectionToEnd.onDisconnect(); - } + VoiceConnectionService.endAllCalls(); Log.d(TAG, "endAllCalls executed"); } - - public void checkPhoneAccountPermission(ConstraintsArray optionalPermissions, @NonNull MethodChannel.Result result) { + + private void requestPermissions(ConstraintsArray additionalPermissions, @NonNull MethodChannel.Result result) { if (!isConnectionServiceAvailable()) { - result.error(E_ACTIVITY_DOES_NOT_EXIST, "ConnectionService not available for this version of Android.", null); + result.error(E_CONNECTION_SERVICE_NOT_AVAILABLE, "ConnectionService not available for this version of Android.", null); return; } - if (_currentActivity == null) { + + if (currentActivity == null) { result.error(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist", null); return; } - String[] optionalPermsArr = new String[optionalPermissions.size()]; - for (int i = 0; i < optionalPermissions.size(); i++) { - optionalPermsArr[i] = optionalPermissions.getString(i); - } - - String[] allPermissions = Arrays.copyOf(permissions, permissions.length + optionalPermsArr.length); - System.arraycopy(optionalPermsArr, 0, allPermissions, permissions.length, optionalPermsArr.length); if (!this.hasPermissions()) { - //requestPermissions(_currentActivity, allPermissions, REQUEST_READ_PHONE_STATE); - ArrayList list = new ArrayList(); - Collections.addAll(list, allPermissions); + List allPermissions = new LinkedList<>(requiredPermissions); + for (int i = 0; i < additionalPermissions.size(); i++) { + allPermissions.add(additionalPermissions.getString(i)); + } requestPermissions( - list, - /* successCallback */ new Callback() { - @Override - public void invoke(Object... args) { - List grantedPermissions = (List) args[0]; - result.success(grantedPermissions.size() == list.size()); - } - }, - /* errorCallback */ new Callback() { - @Override - public void invoke(Object... args) { - result.success(false); - } - }); - return; - } - - result.success(!hasPhoneAccount()); - } - - - public void checkDefaultPhoneAccount(@NonNull MethodChannel.Result result) { + currentActivity, + allPermissions.toArray(new String[0]), + grantedPermissions -> result.success(grantedPermissions.size() == allPermissions.size()), + failedPermissions -> result.success(false) + ); + } else { + result.success(true); + } + } + + @SuppressLint("MissingPermission") + private void checkDefaultPhoneAccount(@NonNull MethodChannel.Result result) { if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { result.success(true); return; @@ -387,40 +468,53 @@ public void checkDefaultPhoneAccount(@NonNull MethodChannel.Result result) { } boolean hasSim = telephonyManager.getSimState() != TelephonyManager.SIM_STATE_ABSENT; - boolean hasDefaultAccount = telecomManager.getDefaultOutgoingPhoneAccount("tel") != null; - result.success(!hasSim || hasDefaultAccount); + boolean hasDefaultAccount = telecomManager.getDefaultOutgoingPhoneAccount(getHandleSchema()) != null; + + result.success(!hasSim || !hasDefaultAccount); } - - public void setOnHold(String uuid, boolean shouldHold) { + + private void setOnHold(String uuid, Boolean shouldHold) { Connection conn = VoiceConnectionService.getConnection(uuid); if (conn == null) { return; } - if (shouldHold == true) { + if (Boolean.TRUE.equals(shouldHold)) { conn.onHold(); } else { conn.onUnhold(); } } - - public void reportEndCallWithUUID(String uuid, int reason) { + + private void reportEndCallWithUUID(String uuid, Integer reason, Boolean notify) { + if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { + return; + } + + VoiceConnection conn = VoiceConnectionService.getConnection(uuid); + if (conn == null) { + return; + } + conn.reportDisconnect(reason, Boolean.TRUE.equals(notify)); + } + + private void reportStartedCallWithUUID(String uuid) { if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { return; } - VoiceConnection conn = (VoiceConnection) VoiceConnectionService.getConnection(uuid); + VoiceConnection conn = VoiceConnectionService.getConnection(uuid); if (conn == null) { return; } - conn.reportDisconnect(reason); + conn.onStarted(); } - - public void rejectCall(String uuid) { + + private void rejectCall(String uuid) { if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { return; } @@ -433,27 +527,28 @@ public void rejectCall(String uuid) { conn.onReject(); } - - public void setMutedCall(String uuid, boolean shouldMute) { - Connection conn = VoiceConnectionService.getConnection(uuid); + + private void setMutedCall(String uuid, Boolean shouldMute) { + VoiceConnection conn = VoiceConnectionService.getConnection(uuid); if (conn == null) { return; } - - CallAudioState newAudioState = null; //if the requester wants to mute, do that. otherwise unmute - if (shouldMute) { - newAudioState = new CallAudioState(true, conn.getCallAudioState().getRoute(), - conn.getCallAudioState().getSupportedRouteMask()); - } else { - newAudioState = new CallAudioState(false, conn.getCallAudioState().getRoute(), - conn.getCallAudioState().getSupportedRouteMask()); + conn.setMuted(Boolean.TRUE.equals(shouldMute)); + } + + + private void setCallAudio(String uuid, Integer audioRoute) { + VoiceConnection conn = VoiceConnectionService.getConnection(uuid); + if (conn == null) { + return; } - conn.onCallAudioStateChanged(newAudioState); + //if the requester wants to mute, do that. otherwise unmute + conn.setAudio(audioRoute); } - - public void sendDTMF(String uuid, String key) { + + private void sendDTMF(String uuid, String key) { Connection conn = VoiceConnectionService.getConnection(uuid); if (conn == null) { return; @@ -462,59 +557,69 @@ public void sendDTMF(String uuid, String key) { conn.onPlayDtmfTone(dtmf); } - - public void updateDisplay(String uuid, String displayName, String uri) { - Connection conn = VoiceConnectionService.getConnection(uuid); + private void updateDisplay(String uuid, String callerName, String handle) { + VoiceConnection conn = VoiceConnectionService.getConnection(uuid); if (conn == null) { return; } - - conn.setAddress(Uri.parse(uri), TelecomManager.PRESENTATION_ALLOWED); - conn.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED); + conn.updateDisplay(callerName, handle); } - - public void hasPhoneAccount(@NonNull MethodChannel.Result result) { - if (telecomManager == null) { - this.initializeTelecomManager(); - } + private void hasPhoneAccount(@NonNull MethodChannel.Result result) { + ensureTelecomManagerInitialize(getAppContext()); result.success(hasPhoneAccount()); } - - public void hasOutgoingCall(@NonNull MethodChannel.Result result) { + + private void hasOutgoingCall(@NonNull MethodChannel.Result result) { result.success(VoiceConnectionService.hasOutgoingCall); } - - public void hasPermissions(@NonNull MethodChannel.Result result) { + + private void hasPermissions(@NonNull MethodChannel.Result result) { result.success(this.hasPermissions()); } - - public void setAvailable(Boolean active) { + private void isCallActive(String uuid, @NonNull MethodChannel.Result result) { + if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { + result.success(false); + return; + } + + Connection conn = VoiceConnectionService.getConnection(uuid); + result.success(conn != null); + } + + private void activeCalls(@NonNull MethodChannel.Result result) { + if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { + result.success(new ArrayList<>()); + return; + } + + result.success(VoiceConnectionService.getActiveConnections()); + } + + + private void setAvailable(Boolean active) { VoiceConnectionService.setAvailable(active); } - - public void setReachable() { - VoiceConnectionService.setReachable(); + + private void setReachable(Boolean active) { + VoiceConnectionService.setReachable(active); } - - public void setCurrentCallActive(String uuid) { - Connection conn = VoiceConnectionService.getConnection(uuid); + + private void setCurrentCallActive(String uuid) { + VoiceConnection conn = VoiceConnectionService.getConnection(uuid); if (conn == null) { return; } - - conn.setConnectionCapabilities(conn.getConnectionCapabilities() | Connection.CAPABILITY_HOLD); - conn.setActive(); + conn.setCurrent(); } - - public void openPhoneAccounts(@NonNull MethodChannel.Result result) { + private void openPhoneAccounts(@NonNull MethodChannel.Result result) { if (!isConnectionServiceAvailable()) { result.error("ConnectionServiceNotAvailable", null, null); return; @@ -524,30 +629,31 @@ public void openPhoneAccounts(@NonNull MethodChannel.Result result) { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); intent.setComponent(new ComponentName("com.android.server.telecom", - "com.android.server.telecom.settings.EnableAccountPreferenceActivity")); - this._currentActivity.startActivity(intent); + "com.android.server.telecom.settings.EnableAccountPreferenceActivity")); + this.currentActivity.startActivity(intent); result.success(null); return; } Intent intent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - this._currentActivity.startActivity(intent); + this.currentActivity.startActivity(intent); result.success(null); } - - public static Boolean isConnectionServiceAvailable() { + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M) + private static Boolean isConnectionServiceAvailable() { // PhoneAccount is available since api level 23 return Build.VERSION.SDK_INT >= 23; } - + @SuppressLint("WrongConstant") - public void backToForeground(@NonNull MethodChannel.Result result) { + private void backToForeground(@NonNull MethodChannel.Result result) { Context context = getAppContext(); - String packageName = context.getApplicationContext().getPackageName(); - Intent focusIntent = context.getPackageManager().getLaunchIntentForPackage(packageName).cloneFilter(); - Activity activity = this._currentActivity; + String packageName = context.getPackageName(); + Intent focusIntent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName)).cloneFilter(); + Activity activity = this.currentActivity; boolean isOpened = activity != null; Log.d(TAG, "backToForeground, app isOpened ?" + (isOpened ? "true" : "false")); if (isOpened) { @@ -558,50 +664,57 @@ public void backToForeground(@NonNull MethodChannel.Result result) { WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - - this._currentActivity.startActivity(focusIntent); + context.startActivity(focusIntent); } - result.success(null); + result.success(isOpened); } - private void initializeTelecomManager() { - Context context = this.getAppContext(); - ComponentName cName = new ComponentName(context, VoiceConnectionService.class); - String appName = this.getApplicationName(context); - - handle = new PhoneAccountHandle(cName, appName); - telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); - } - - private void registerPhoneAccount(Context appContext) { - if (!isConnectionServiceAvailable()) { - return; + private static void registerPhoneAccount(Context appContext, ConstraintsMap options) { + ensureTelecomManagerInitialize(appContext); + String appName = getApplicationName(appContext); + PhoneAccount.Builder builder = new PhoneAccount.Builder(accountHandle, appName); + int capabilities = 0; + if (isSelfManaged(options)) { + capabilities |= PhoneAccount.CAPABILITY_SELF_MANAGED; + } else { + capabilities |= PhoneAccount.CAPABILITY_CALL_PROVIDER; } - this.initializeTelecomManager(); - String appName = this.getApplicationName(this.getAppContext()); - - PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, appName) - .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER); + builder.setCapabilities(capabilities); - if (_settings != null && _settings.hasKey("imageName")) { - int identifier = appContext.getResources().getIdentifier(_settings.getString("imageName"), "drawable", appContext.getPackageName()); + if (!options.isNull("imageName")) { + int identifier = appContext.getResources().getIdentifier(settings.getString("imageName"), "drawable", appContext.getPackageName()); Icon icon = Icon.createWithResource(appContext, identifier); builder.setIcon(icon); } PhoneAccount account = builder.build(); + telecomManager.registerPhoneAccount(account); + Log.d(TAG, "Registered phone account " + account); + } - telephonyManager = (TelephonyManager) this.getAppContext().getSystemService(Context.TELEPHONY_SERVICE); + private static void ensureTelecomManagerInitialize(Context context) { + if (telecomManager == null) { + ComponentName cName = new ComponentName(context, VoiceConnectionService.class); + String appName = getApplicationName(context); + accountHandle = new PhoneAccountHandle(cName, appName); + telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + } + } - telecomManager.registerPhoneAccount(account); + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) + private static boolean isSelfManaged(ConstraintsMap options) { + return !options.isNull("isSelfManaged") && + options.getBoolean("isSelfManaged") && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } - private void sendEventToFlutter(String eventName, @Nullable ConstraintsMap params) { - _eventChannel.invokeMethod(eventName, params != null? params.toMap() : null); + private void sendEventToFlutter(String eventName, @NonNull ConstraintsMap params) { + eventChannel.invokeMethod(eventName, params.toMap()); } - private String getApplicationName(Context appContext) { + private static String getApplicationName(Context appContext) { ApplicationInfo applicationInfo = appContext.getApplicationInfo(); int stringId = applicationInfo.labelRes; @@ -610,8 +723,8 @@ private String getApplicationName(Context appContext) { private Boolean hasPermissions() { boolean hasPermissions = true; - for (String permission : permissions) { - int permissionCheck = ContextCompat.checkSelfPermission(_currentActivity, permission); + for (String permission : requiredPermissions) { + int permissionCheck = ContextCompat.checkSelfPermission(currentActivity, permission); if (permissionCheck != PackageManager.PERMISSION_GRANTED) { hasPermissions = false; } @@ -621,8 +734,10 @@ private Boolean hasPermissions() { } private static boolean hasPhoneAccount() { - return isConnectionServiceAvailable() && telecomManager != null - && telecomManager.getPhoneAccount(handle) != null && telecomManager.getPhoneAccount(handle).isEnabled(); + if (telecomManager == null) return false; + PhoneAccount phoneAccount = telecomManager.getPhoneAccount(accountHandle); + if (phoneAccount == null) return false; + return phoneAccount.isEnabled(); } private void registerReceiver() { @@ -630,61 +745,96 @@ private void registerReceiver() { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_END_CALL); intentFilter.addAction(ACTION_ANSWER_CALL); + intentFilter.addAction(ACTION_INCOMING_CALL); + intentFilter.addAction(ACTION_ONGOING_CALL); + intentFilter.addAction(ACTION_FAILED_CALL); + intentFilter.addAction(ACTION_REJECT_CALL); intentFilter.addAction(ACTION_MUTE_CALL); intentFilter.addAction(ACTION_UNMUTE_CALL); intentFilter.addAction(ACTION_DTMF_TONE); intentFilter.addAction(ACTION_UNHOLD_CALL); intentFilter.addAction(ACTION_HOLD_CALL); - intentFilter.addAction(ACTION_ONGOING_CALL); intentFilter.addAction(ACTION_AUDIO_SESSION); intentFilter.addAction(ACTION_CHECK_REACHABILITY); - LocalBroadcastManager.getInstance(this._context).registerReceiver(voiceBroadcastReceiver, intentFilter); + LocalBroadcastManager.getInstance(this.context).registerReceiver(voiceBroadcastReceiver, intentFilter); isReceiverRegistered = true; } } private Context getAppContext() { - return this._context.getApplicationContext(); + return this.context.getApplicationContext(); } - @RequiresApi(api = Build.VERSION_CODES.M) private void requestPermissions( - final ArrayList permissions, - final Callback successCallback, - final Callback errorCallback) { + Activity activity, + final String[] permissions, + final Callback> successCallback, + final Callback> failureCallback) { PermissionUtils.Callback callback = (permissions_, grantResults) -> { - List grantedPermissions = new ArrayList<>(); - List deniedPermissions = new ArrayList<>(); - - for (int i = 0; i < permissions_.length; ++i) { - String permission = permissions_[i]; - int grantResult = grantResults[i]; - - if (grantResult == PackageManager.PERMISSION_GRANTED) { - grantedPermissions.add(permission); - } else { - deniedPermissions.add(permission); - } - } + List grantedPermissions = new ArrayList<>(); + List deniedPermissions = new ArrayList<>(); + + for (int i = 0; i < permissions_.length; ++i) { + String permission = permissions_[i]; + int grantResult = grantResults[i]; + + if (grantResult == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.add(permission); + } else { + deniedPermissions.add(permission); + } + } - // Success means that all requested permissions were granted. - for (String p : permissions) { - if (!grantedPermissions.contains(p)) { - // According to step 6 of the getUserMedia() algorithm - // "if the result is denied, jump to the step Permission - // Failure." - errorCallback.invoke(deniedPermissions); - return; - } - } - successCallback.invoke(grantedPermissions); - }; + // Success means that all requested permissions were granted. + for (String p : permissions) { + if (!grantedPermissions.contains(p)) { + // According to step 6 of the getUserMedia() algorithm + // "if the result is denied, jump to the step Permission + // Failure." + failureCallback.invoke(deniedPermissions); + return; + } + } + successCallback.invoke(grantedPermissions); + }; - final Activity activity = _currentActivity; - if (activity != null) { - PermissionUtils.requestPermissions( - activity, permissions.toArray(new String[permissions.size()]), callback); + PermissionUtils.requestPermissions(activity, permissions, callback); + } + + // Store all callkeep settings in JSON + private void storeSettings(ConstraintsMap options) { + Context context = getAppContext(); + if (context == null) { + Log.w(TAG, "[CallKeepModule][storeSettings] no context found."); + return; + } + SharedPreferences sharedPref = context.getSharedPreferences("settings-callkeep", Context.MODE_PRIVATE); + try { + JSONObject jsonObject = MapUtils.convertMapToJson(options); + String jsonString = jsonObject.toString(); + sharedPref.edit().putString("settings", jsonString).apply(); + } catch (JSONException e) { + Log.w(TAG, "[CallKeepModule][storeSettings] exception: " + e); + } + } + + private static void fetchStoredSettings(Context context) { + if (context == null) { + Log.w(TAG, "[CallKeepModule][fetchStoredSettings] no context found."); + return; + } + settings = new ConstraintsMap(); + + SharedPreferences sharedPref = context.getSharedPreferences("settings-callkeep", Context.MODE_PRIVATE); + try { + String jsonString = sharedPref.getString("settings", (new JSONObject()).toString()); + if (jsonString != null) { + JSONObject jsonObject = new JSONObject(jsonString); + settings = MapUtils.convertJsonToMap(jsonObject); + } + } catch(JSONException e) { + Log.w(TAG, "[CallKeepModule][fetchStoredSettings] exception: " + e); } } @@ -692,64 +842,92 @@ private class VoiceBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { ConstraintsMap args = new ConstraintsMap(); - HashMap attributeMap = (HashMap)intent.getSerializableExtra("attributeMap"); + Map attributeMap = (Map) intent.getSerializableExtra(EXTRA_CALL_ATTRIB); - switch (intent.getAction()) { + switch (Objects.requireNonNull(intent.getAction())) { case ACTION_END_CALL: - args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); sendEventToFlutter("CallKeepPerformEndCallAction", args); break; + case ACTION_REJECT_CALL: + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); + sendEventToFlutter("CallKeepPerformRejectCallAction", args); + break; case ACTION_ANSWER_CALL: - args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); + args.putString("handle", (String) attributeMap.get(EXTRA_CALL_NUMBER)); + args.putString("name", (String) attributeMap.get(EXTRA_CALLER_NAME)); + args.putMap("additionalData", (Map) attributeMap.get(EXTRA_CALL_DATA)); sendEventToFlutter("CallKeepPerformAnswerCallAction", args); break; + case ACTION_INCOMING_CALL: + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); + args.putString("handle", (String) attributeMap.get(EXTRA_CALL_NUMBER)); + args.putString("name", (String) attributeMap.get(EXTRA_CALLER_NAME)); + args.putMap("additionalData", (Map) attributeMap.get(EXTRA_CALL_DATA)); + sendEventToFlutter("CallKeepShowIncomingCallAction", args); + break; + case ACTION_ONGOING_CALL: + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); + args.putString("handle", (String) attributeMap.get(EXTRA_CALL_NUMBER)); + args.putString("name", (String) attributeMap.get(EXTRA_CALLER_NAME)); + args.putMap("additionalData", (Map) attributeMap.get(EXTRA_CALL_DATA)); + sendEventToFlutter("CallKeepDidReceiveStartCallAction", args); + break; + case ACTION_FAILED_CALL: + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); + args.putString("handle", (String) attributeMap.get(EXTRA_CALL_NUMBER)); + args.putString("name", (String) attributeMap.get(EXTRA_CALLER_NAME)); + args.putMap("additionalData", (Map) attributeMap.get(EXTRA_CALL_DATA)); + sendEventToFlutter("CallKeepDidReceiveFailedCallAction", args); + break; case ACTION_HOLD_CALL: args.putBoolean("hold", true); - args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); sendEventToFlutter("CallKeepDidToggleHoldAction", args); break; case ACTION_UNHOLD_CALL: args.putBoolean("hold", false); - args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); sendEventToFlutter("CallKeepDidToggleHoldAction", args); break; case ACTION_MUTE_CALL: args.putBoolean("muted", true); - args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); sendEventToFlutter("CallKeepDidPerformSetMutedCallAction", args); break; case ACTION_UNMUTE_CALL: args.putBoolean("muted", false); - args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); sendEventToFlutter("CallKeepDidPerformSetMutedCallAction", args); break; case ACTION_DTMF_TONE: - args.putString("digits", attributeMap.get("DTMF")); - args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); + args.putString("digits", (String) attributeMap.get("DTMF")); + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); sendEventToFlutter("CallKeepDidPerformDTMFAction", args); break; - case ACTION_ONGOING_CALL: - args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); - args.putString("handle", attributeMap.get(EXTRA_CALL_NUMBER)); - args.putString("name", attributeMap.get(EXTRA_CALLER_NAME)); - sendEventToFlutter("CallKeepDidReceiveStartCallAction", args); + case ACTION_AUDIO_CALL: + args.putString("route", (String) attributeMap.get("audioRoute")); + args.putString("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); + sendEventToFlutter("CallKeepDidChangeAudioAction", args); break; case ACTION_AUDIO_SESSION: - sendEventToFlutter("CallKeepDidActivateAudioSession", null); + sendEventToFlutter("CallKeepDidActivateAudioSession", args); break; case ACTION_CHECK_REACHABILITY: - sendEventToFlutter("CallKeepCheckReachability", null); + sendEventToFlutter("CallKeepCheckReachability", args); break; case ACTION_WAKE_APP: - Intent headlessIntent = new Intent(_context, CallKeepBackgroundMessagingService.class); - headlessIntent.putExtra("callUUID", attributeMap.get(EXTRA_CALL_UUID)); - headlessIntent.putExtra("name", attributeMap.get(EXTRA_CALLER_NAME)); - headlessIntent.putExtra("handle", attributeMap.get(EXTRA_CALL_NUMBER)); + Intent headlessIntent = new Intent(CallKeepModule.this.context, CallKeepBackgroundMessagingService.class); + headlessIntent.putExtra("callUUID", (String) attributeMap.get(EXTRA_CALL_UUID)); + headlessIntent.putExtra("name", (String) attributeMap.get(EXTRA_CALLER_NAME)); + headlessIntent.putExtra("handle", (String) attributeMap.get(EXTRA_CALL_NUMBER)); + headlessIntent.putExtra("additionalData", (HashMap) attributeMap.get(EXTRA_CALL_DATA)); Log.d(TAG, "wakeUpApplication: " + attributeMap.get(EXTRA_CALL_UUID) + ", number : " + attributeMap.get(EXTRA_CALL_NUMBER) + ", displayName:" + attributeMap.get(EXTRA_CALLER_NAME)); - ComponentName name = _context.startService(headlessIntent); + ComponentName name = CallKeepModule.this.context.startService(headlessIntent); if (name != null) { - CallKeepBackgroundMessagingService.acquireWakeLockNow(_context); + CallKeepBackgroundMessagingService.acquireWakeLockNow(CallKeepModule.this.context); } break; } diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnection.java b/android/src/main/java/io/wazo/callkeep/VoiceConnection.java index 080fc0e5..a7e766fa 100644 --- a/android/src/main/java/io/wazo/callkeep/VoiceConnection.java +++ b/android/src/main/java/io/wazo/callkeep/VoiceConnection.java @@ -17,11 +17,25 @@ package io.wazo.callkeep; -import android.annotation.TargetApi; +import static io.wazo.callkeep.CallKeepConstants.ACTION_ANSWER_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_AUDIO_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_AUDIO_SESSION; +import static io.wazo.callkeep.CallKeepConstants.ACTION_DTMF_TONE; +import static io.wazo.callkeep.CallKeepConstants.ACTION_END_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_HOLD_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_INCOMING_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_MUTE_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_REJECT_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_UNHOLD_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_UNMUTE_CALL; +import static io.wazo.callkeep.CallKeepConstants.EXTRA_CALLER_NAME; +import static io.wazo.callkeep.CallKeepConstants.EXTRA_CALL_ATTRIB; +import static io.wazo.callkeep.CallKeepConstants.EXTRA_CALL_NUMBER; +import static io.wazo.callkeep.CallKeepConstants.EXTRA_CALL_UUID; + import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.telecom.CallAudioState; @@ -30,178 +44,245 @@ import android.telecom.TelecomManager; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import java.util.HashMap; +import java.util.Map; +import java.util.Objects; -import static io.wazo.callkeep.Constants.*; - -@TargetApi(Build.VERSION_CODES.M) public class VoiceConnection extends Connection { - private boolean isMuted = false; - private HashMap handle; - private Context context; private static final String TAG = "RNCK:VoiceConnection"; + private final HashMap connectionData; + private final Context context; - VoiceConnection(Context context, HashMap handle) { + VoiceConnection(@NonNull Context context, @NonNull HashMap connectionData) { super(); - this.handle = handle; + this.connectionData = connectionData; this.context = context; + updateDisplay(); + } - String number = handle.get(EXTRA_CALL_NUMBER); - String name = handle.get(EXTRA_CALLER_NAME); + public void updateDisplay(String callerName, String handle) { + if (handle != null) { + connectionData.put(EXTRA_CALL_NUMBER, handle); + } + if (callerName != null) { + connectionData.put(EXTRA_CALLER_NAME, callerName); + } + updateDisplay(); + } - if (number != null) { - setAddress(Uri.parse(number), TelecomManager.PRESENTATION_ALLOWED); + private void updateDisplay() { + Object address = connectionData.get(EXTRA_CALL_NUMBER); + Object name = connectionData.get(EXTRA_CALLER_NAME); + if (address instanceof String) { + setAddress(Uri.parse((String) address), TelecomManager.PRESENTATION_ALLOWED); } - if (name != null && !name.equals("")) { - setCallerDisplayName(name, TelecomManager.PRESENTATION_ALLOWED); + if (name instanceof String) { + setCallerDisplayName((String) name, TelecomManager.PRESENTATION_ALLOWED); } } @Override public void onExtrasChanged(Bundle extras) { super.onExtrasChanged(extras); - HashMap attributeMap = (HashMap)extras.getSerializable("attributeMap"); + Map attributeMap = (Map) extras.getSerializable(EXTRA_CALL_ATTRIB); if (attributeMap != null) { - handle = attributeMap; + connectionData.putAll(attributeMap); } } + public void setMuted(boolean muted) { + CallAudioState currentAudioState = getCurrentAudioState(); + CallAudioState newAudioState = new CallAudioState(muted, + currentAudioState.getRoute(), + currentAudioState.getSupportedRouteMask() + ); + onCallAudioStateChanged(newAudioState); + } + + public void setAudio(Integer audioRoute) { + CallAudioState currentAudioState = getCurrentAudioState(); + CallAudioState newAudioState = new CallAudioState( + currentAudioState.isMuted(), + audioRoute, + currentAudioState.getSupportedRouteMask() + ); + onCallAudioStateChanged(newAudioState); + } + @Override public void onCallAudioStateChanged(CallAudioState state) { - if (state.isMuted() == this.isMuted) { - return; + super.onCallAudioStateChanged(state); + if (state != null) { + if (!Objects.equals(connectionData.get("isMuted"), state.isMuted())) { + connectionData.put("isMuted", state.isMuted()); + sendCallRequestToActivity(state.isMuted() ? ACTION_MUTE_CALL : ACTION_UNMUTE_CALL, connectionData); + } + if (!Objects.equals(connectionData.get("audioRoute"), state.getRoute())) { + connectionData.put("audioRoute", state.getRoute()); + HashMap data = new HashMap<>(connectionData); + data.put("audioRoute", state.getRoute()); + data.put("audioRouteName", CallAudioState.audioRouteToString(state.getRoute())); + sendCallRequestToActivity(ACTION_AUDIO_CALL, data); + } } + } - this.isMuted = state.isMuted(); - sendCallRequestToActivity(isMuted ? ACTION_MUTE_CALL : ACTION_UNMUTE_CALL, handle); + private CallAudioState getCurrentAudioState() { + CallAudioState current = getCallAudioState(); + if (current != null) return current; + throw new UnsupportedOperationException(); } @Override public void onAnswer() { super.onAnswer(); Log.d(TAG, "onAnswer called"); + Log.d(TAG, "onAnswer ignored"); + } + + @Override + public void onAnswer(int videoState) { + super.onAnswer(videoState); + Log.d(TAG, "onAnswer videoState called: " + videoState); + onAnswered(); + Log.d(TAG, "onAnswer videoState executed"); + } + + private void onAnswered() { + initCall(); + setCurrent(); + sendCallRequestToActivity(ACTION_ANSWER_CALL, connectionData); + } - setConnectionCapabilities(getConnectionCapabilities() | Connection.CAPABILITY_HOLD); + public void initCall() { + setHoldableIfSupported(); setAudioModeIsVoip(true); + sendCallRequestToActivity(ACTION_AUDIO_SESSION, connectionData); + } - sendCallRequestToActivity(ACTION_ANSWER_CALL, handle); - sendCallRequestToActivity(ACTION_AUDIO_SESSION, handle); - Log.d(TAG, "onAnswer executed"); + @Override + public void onShowIncomingCallUi() { + sendCallRequestToActivity(ACTION_INCOMING_CALL, connectionData); + super.onShowIncomingCallUi(); } @Override public void onPlayDtmfTone(char dtmf) { - try { - handle.put("DTMF", Character.toString(dtmf)); - } catch (Throwable exception) { - Log.e(TAG, "Handle map error", exception); - } - sendCallRequestToActivity(ACTION_DTMF_TONE, handle); + HashMap data = new HashMap<>(connectionData); + data.put("DTMF", Character.toString(dtmf)); + sendCallRequestToActivity(ACTION_DTMF_TONE, data); } @Override public void onDisconnect() { super.onDisconnect(); - setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); - sendCallRequestToActivity(ACTION_END_CALL, handle); + close(DisconnectCause.LOCAL); + sendCallRequestToActivity(ACTION_END_CALL, connectionData); Log.d(TAG, "onDisconnect executed"); - try { - ((VoiceConnectionService) context).deinitConnection(handle.get(EXTRA_CALL_UUID)); - } catch(Throwable exception) { - Log.e(TAG, "Handle map error", exception); - } - destroy(); } - public void reportDisconnect(int reason) { + public void reportDisconnect(int reason, boolean notify) { super.onDisconnect(); + Integer causeCode = null; switch (reason) { case 1: - setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); + causeCode = DisconnectCause.ERROR; break; case 2: case 5: - setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); + causeCode = DisconnectCause.REMOTE; break; case 3: - setDisconnected(new DisconnectCause(DisconnectCause.BUSY)); + causeCode = DisconnectCause.BUSY; break; case 4: - setDisconnected(new DisconnectCause(DisconnectCause.ANSWERED_ELSEWHERE)); + causeCode = DisconnectCause.ANSWERED_ELSEWHERE; break; case 6: - setDisconnected(new DisconnectCause(DisconnectCause.MISSED)); + causeCode = DisconnectCause.MISSED; break; default: break; } - ((VoiceConnectionService)context).deinitConnection(handle.get(EXTRA_CALL_UUID)); - destroy(); + if (causeCode != null) { + close(causeCode); + if (notify) { + sendCallRequestToActivity(ACTION_END_CALL, connectionData); + } + } } @Override public void onAbort() { super.onAbort(); - setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); - sendCallRequestToActivity(ACTION_END_CALL, handle); + close(DisconnectCause.REJECTED); + sendCallRequestToActivity(ACTION_END_CALL, connectionData); Log.d(TAG, "onAbort executed"); - try { - ((VoiceConnectionService) context).deinitConnection(handle.get(EXTRA_CALL_UUID)); - } catch(Throwable exception) { - Log.e(TAG, "Handle map error", exception); - } - destroy(); } @Override public void onHold() { super.onHold(); this.setOnHold(); - sendCallRequestToActivity(ACTION_HOLD_CALL, handle); + sendCallRequestToActivity(ACTION_HOLD_CALL, connectionData); } @Override public void onUnhold() { super.onUnhold(); - sendCallRequestToActivity(ACTION_UNHOLD_CALL, handle); - setActive(); + sendCallRequestToActivity(ACTION_UNHOLD_CALL, connectionData); + setCurrent(); } @Override public void onReject() { super.onReject(); - setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); - sendCallRequestToActivity(ACTION_END_CALL, handle); + close(DisconnectCause.REJECTED); + sendCallRequestToActivity(ACTION_REJECT_CALL, connectionData); Log.d(TAG, "onReject executed"); - try { - ((VoiceConnectionService) context).deinitConnection(handle.get(EXTRA_CALL_UUID)); - } catch(Throwable exception) { - Log.e(TAG, "Handle map error", exception); + } + + public void onStarted() { + setCurrent(); + Log.d(TAG, "onStarted executed"); + } + + public void setCurrent() { + setHoldableIfSupported(); + setActive(); + } + + private void setHoldableIfSupported() { + if ((getConnectionCapabilities() & CAPABILITY_SUPPORT_HOLD) == CAPABILITY_SUPPORT_HOLD) { + setConnectionCapabilities(getConnectionCapabilities() | CAPABILITY_HOLD); } + } + + private void close(int causeCode) { + setDisconnected(new DisconnectCause(causeCode)); + VoiceConnectionService.deinitConnection((String) connectionData.get(EXTRA_CALL_UUID)); destroy(); } /* * Send call request to the RNCallKeepModule */ - private void sendCallRequestToActivity(final String action, @Nullable final HashMap attributeMap) { - final VoiceConnection instance = this; + private void sendCallRequestToActivity(String action, @Nullable HashMap attributeMap) { final Handler handler = new Handler(); - - handler.post(new Runnable() { - @Override - public void run() { - Intent intent = new Intent(action); - if (attributeMap != null) { - Bundle extras = new Bundle(); - extras.putSerializable("attributeMap", attributeMap); - intent.putExtras(extras); - } - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + handler.post(() -> { + Intent intent = new Intent(action); + if (attributeMap != null) { + Bundle extras = new Bundle(); + extras.putSerializable(EXTRA_CALL_ATTRIB, attributeMap); + intent.putExtras(extras); } + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); }); } + + } diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java index c1dc7078..0b477725 100644 --- a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java +++ b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java @@ -17,12 +17,30 @@ package io.wazo.callkeep; -import android.annotation.TargetApi; +import static io.wazo.callkeep.CallKeepConstants.ACTION_CHECK_REACHABILITY; +import static io.wazo.callkeep.CallKeepConstants.ACTION_FAILED_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_ONGOING_CALL; +import static io.wazo.callkeep.CallKeepConstants.ACTION_WAKEUP_CALL; +import static io.wazo.callkeep.CallKeepConstants.BROADCAST_RECEIVER_META_DATA_KEY; +import static io.wazo.callkeep.CallKeepConstants.EXTRA_CALLER_NAME; +import static io.wazo.callkeep.CallKeepConstants.EXTRA_CALL_ATTRIB; +import static io.wazo.callkeep.CallKeepConstants.EXTRA_CALL_NUMBER; +import static io.wazo.callkeep.CallKeepConstants.EXTRA_CALL_UUID; +import static io.wazo.callkeep.CallKeepConstants.FOREGROUND_SERVICE_TYPE_MICROPHONE; +import static io.wazo.callkeep.CallKeepConstants.HOLD_SUPPORT_DATA_KEY; + import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -31,54 +49,83 @@ import android.telecom.ConnectionRequest; import android.telecom.ConnectionService; import android.telecom.DisconnectCause; +import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; -import static io.wazo.callkeep.Constants.*; +import io.wazo.callkeep.utils.ConstraintsMap; // @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionService.java -@TargetApi(Build.VERSION_CODES.M) public class VoiceConnectionService extends ConnectionService { private static Boolean isAvailable; private static Boolean isInitialized; private static Boolean isReachable; - private static String notReachableCallUuid; - private static ConnectionRequest currentConnectionRequest; - private static PhoneAccountHandle phoneAccountHandle; - private static String TAG = "RNCK:VoiceConnectionService"; - public static Map currentConnections = new HashMap<>(); + private static PhoneAccountHandle phoneAccountHandle = null; + private static final String TAG = "RNCK:VoiceConnectionService"; + private static final Map currentConnections = new HashMap<>(); public static Boolean hasOutgoingCall = false; public static VoiceConnectionService currentConnectionService = null; - public static Connection getConnection(String connectionId) { + public static VoiceConnection getConnection(String connectionId) { if (currentConnections.containsKey(connectionId)) { return currentConnections.get(connectionId); } return null; } + public static ConstraintsMap getSettings(@Nullable Context context) { + return CallKeepModule.getSettings(context); + } + + public static ConstraintsMap getForegroundSettings(@Nullable Context context) { + ConstraintsMap settings = VoiceConnectionService.getSettings(context); + if (settings == null) { + return null; + } + return settings.getMap("foregroundService"); + } + + public static List getActiveConnections() { + return new ArrayList<>(currentConnections.keySet()); + } + + public static void endAllCalls() { + Map connectionMap = new HashMap<>(currentConnections); + for (Map.Entry connectionEntry : connectionMap.entrySet()) { + Connection connectionToEnd = connectionEntry.getValue(); + connectionToEnd.onDisconnect(); + } + } + public VoiceConnectionService() { super(); Log.e(TAG, "Constructor"); isReachable = false; isInitialized = false; isAvailable = false; - currentConnectionRequest = null; currentConnectionService = this; } + @Override + public void onCreate() { + super.onCreate(); + checkReachability(); + } + public static void setPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) { VoiceConnectionService.phoneAccountHandle = phoneAccountHandle; } @@ -92,162 +139,285 @@ public static void setAvailable(Boolean value) { isAvailable = value; } - public static void setReachable() { + + public static void setReachable(Boolean value) { Log.d(TAG, "setReachable"); - isReachable = true; - VoiceConnectionService.currentConnectionRequest = null; + isReachable = value; } public static void deinitConnection(String connectionId) { Log.d(TAG, "deinitConnection:" + connectionId); VoiceConnectionService.hasOutgoingCall = false; - if (currentConnections.containsKey(connectionId)) { - currentConnections.remove(connectionId); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + currentConnectionService.stopForegroundService(); + } + + currentConnections.remove(connectionId); + } + + private ConstraintsMap getMetadataSettings() { + try { + Bundle metaData = getMetaData(); + if (metaData != null) { + return new ConstraintsMap(bundleToMap(metaData)); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, null, e); } + return new ConstraintsMap(); + } + + @Override + public void onCreateIncomingConnectionFailed(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { + super.onCreateIncomingConnectionFailed(connectionManagerPhoneAccount, request); + Bundle extras = request.getExtras(); + assert extras != null; + extras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS); + onConnectionFailed(request, Objects.requireNonNull(extras)); } @Override - public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { - Bundle extra = request.getExtras(); - Uri number = request.getAddress(); - String name = extra.getString(EXTRA_CALLER_NAME); - Connection incomingCallConnection = createConnection(request); - incomingCallConnection.setRinging(); - incomingCallConnection.setInitialized(); + public void onCreateOutgoingConnectionFailed(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { + super.onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount, request); + onConnectionFailed(request, request.getExtras()); + } - return incomingCallConnection; + private void onConnectionFailed(ConnectionRequest request, Bundle extras) { + String extrasUuid = extras.getString(EXTRA_CALL_UUID); + String extrasNumber = extras.getString(EXTRA_CALL_NUMBER); + String displayName = extras.getString(EXTRA_CALLER_NAME); + Log.d(TAG, "onConnectionFailed: " + extrasUuid + ", number: " + extrasNumber + ", displayName:" + displayName); + HashMap connectionData = this.bundleToMap(extras); + sendCallRequestToActivity(ACTION_FAILED_CALL, connectionData); + Log.d(TAG, "onConnectionFailed: calling"); } @Override - public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { + public Connection onCreateIncomingConnection(PhoneAccountHandle phoneAccount, ConnectionRequest request) { + return makeIncomingCall(request); + } + + private Connection makeIncomingCall(ConnectionRequest request) { + Bundle extras = request.getExtras(); + assert extras != null; + extras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS); + Connection connection = makeOngoingCall(request, Objects.requireNonNull(extras)); + connection.setRinging(); + return connection; + } + + @Override + public Connection onCreateOutgoingConnection(PhoneAccountHandle phoneAccount, ConnectionRequest request) { VoiceConnectionService.hasOutgoingCall = true; - String uuid = UUID.randomUUID().toString(); if (!isInitialized && !isReachable) { - this.notReachableCallUuid = uuid; - this.currentConnectionRequest = request; - this.checkReachability(); + this.checkReachability(request); } - return this.makeOutgoingCall(request, uuid, false); + return makeOutgoingCall(request); } - private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Boolean forceWakeUp) { - Bundle extras = request.getExtras(); - Connection outgoingCallConnection = null; - String number = request.getAddress().getSchemeSpecificPart(); - String extrasNumber = extras.getString(EXTRA_CALL_NUMBER); - String displayName = extras.getString(EXTRA_CALLER_NAME); - Boolean isForeground = VoiceConnectionService.isRunning(this.getApplicationContext()); - - Log.d(TAG, "makeOutgoingCall:" + uuid + ", number: " + number + ", displayName:" + displayName); - - // Wakeup application if needed - if (!isForeground || forceWakeUp) { - Log.d(TAG, "onCreateOutgoingConnection: Waking up application"); - this.wakeUpApplication(uuid, number, displayName); - } else if (!this.canMakeOutgoingCall() && isReachable) { - Log.d(TAG, "onCreateOutgoingConnection: not available"); + private Connection makeOutgoingCall(ConnectionRequest request) { + fixMissingNumber(request.getAddress(), request.getExtras()); + fixMissingCallId(request.getExtras()); + if (!wakeAndCheckAvailability(request.getExtras(), false)) { return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.LOCAL)); + } else { + VoiceConnection connection = makeOngoingCall(request, request.getExtras()); + connection.setDialing(); + connection.initCall(); + return connection; } + } + private VoiceConnection makeOngoingCall(ConnectionRequest request, Bundle extras) { + String extrasUuid = extras.getString(EXTRA_CALL_UUID); + String extrasNumber = extras.getString(EXTRA_CALL_NUMBER); + String displayName = extras.getString(EXTRA_CALLER_NAME); + Log.d(TAG, "makeOngoingCall: " + extrasUuid + ", number: " + extrasNumber + ", displayName:" + displayName); // TODO: Hold all other calls + HashMap connectionData = this.bundleToMap(extras); + VoiceConnection connection = new VoiceConnection(this, connectionData); + initConnection(extrasUuid, connection, extras, request.getAccountHandle()); + startForegroundService(); + sendCallRequestToActivity(ACTION_ONGOING_CALL, connectionData); + Log.d(TAG, "makeOngoingCall: calling"); + return connection; + } + + private void fixMissingNumber(Uri address, Bundle callExtras) { + String number = address.getSchemeSpecificPart(); + String extrasNumber = callExtras.getString(EXTRA_CALL_NUMBER); if (extrasNumber == null || !extrasNumber.equals(number)) { - extras.putString(EXTRA_CALL_UUID, uuid); - extras.putString(EXTRA_CALLER_NAME, displayName); - extras.putString(EXTRA_CALL_NUMBER, number); + callExtras.putString(EXTRA_CALL_NUMBER, number); } + } - outgoingCallConnection = createConnection(request); - outgoingCallConnection.setDialing(); - outgoingCallConnection.setAudioModeIsVoip(true); - outgoingCallConnection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED); + private void fixMissingCallId(Bundle callExtras) { + String extrasUUID = callExtras.getString(EXTRA_CALL_UUID); + if (extrasUUID == null) { + callExtras.putString(EXTRA_CALL_UUID, UUID.randomUUID().toString()); + } + } - // ‍️Weirdly on some Samsung phones (A50, S9...) using `setInitialized` will not display the native UI ... - // when making a call from the native Phone application. The call will still be displayed correctly without it. - if (!Build.MANUFACTURER.equalsIgnoreCase("Samsung")) { - outgoingCallConnection.setInitialized(); + private boolean wakeAndCheckAvailability(Bundle callExtras, Boolean forceWakeUp) { + boolean isRunning = VoiceConnectionService.isRunning(this.getApplicationContext()); + // Wakeup application if needed + if (!isRunning || forceWakeUp) { + Log.d(TAG, "makeOngoingCall: Waking up application"); + this.wakeUpApplication(callExtras); } + if (this.canMakeOutgoingCall() && isReachable) { + return true; + } + Log.d(TAG, "makeOngoingCall: not available"); + return false; + } - HashMap extrasMap = this.bundleToMap(extras); + private void startForegroundService() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // Foreground services not required before SDK 28 + return; + } + Log.d(TAG, "[VoiceConnectionService] startForegroundService"); + ConstraintsMap foregroundSettings = getForegroundSettings(getApplicationContext()); + if (foregroundSettings == null) { + Log.w(TAG, "[VoiceConnectionService] Not creating foregroundService because not configured"); + return; + } + String NOTIFICATION_CHANNEL_ID = foregroundSettings.getString("channelId"); + String channelName = foregroundSettings.getString("channelName"); + NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE); + chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + assert manager != null; + manager.createNotificationChannel(chan); + + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); + notificationBuilder.setOngoing(true) + .setContentTitle(foregroundSettings.getString("notificationTitle")) + .setPriority(NotificationManager.IMPORTANCE_MIN) + .setCategory(Notification.CATEGORY_SERVICE); + + if (foregroundSettings.hasKey("notificationIcon")) { + Context context = this.getApplicationContext(); + Resources res = context.getResources(); + String smallIcon = foregroundSettings.getString("notificationIcon"); + String mipmap = "mipmap/"; + String drawable = "drawable/"; + if (smallIcon.contains(mipmap)) { + notificationBuilder.setSmallIcon( + res.getIdentifier(smallIcon.replace(mipmap, ""), + "mipmap", context.getPackageName())); + } else if (smallIcon.contains(drawable)) { + notificationBuilder.setSmallIcon( + res.getIdentifier(smallIcon.replace(drawable, ""), + "drawable", context.getPackageName())); + } + } - sendCallRequestToActivity(ACTION_ONGOING_CALL, extrasMap); - sendCallRequestToActivity(ACTION_AUDIO_SESSION, extrasMap); + Log.d(TAG, "[VoiceConnectionService] Starting foreground service"); - Log.d(TAG, "onCreateOutgoingConnection: calling"); + Notification notification = notificationBuilder.build(); + int notificationId = FOREGROUND_SERVICE_TYPE_MICROPHONE; + if (!foregroundSettings.isNull("notificationId")) { + notificationId = foregroundSettings.getInt("notificationId"); + } + startForeground(notificationId, notification); + } - return outgoingCallConnection; + @RequiresApi(api = Build.VERSION_CODES.N) + private void stopForegroundService() { + Log.d(TAG, "[VoiceConnectionService] stopForegroundService"); + ConstraintsMap foregroundSettings = getForegroundSettings(getApplicationContext()); + if (foregroundSettings == null) { + Log.d(TAG, "[VoiceConnectionService] Discarding stop foreground service, no service configured"); + return; + } + stopForeground(Service.STOP_FOREGROUND_REMOVE); } - private void wakeUpApplication(String uuid, String number, String displayName) { + private void wakeUpApplication(Bundle extras) { Intent headlessIntent = new Intent( - this.getApplicationContext(), - CallKeepBackgroundMessagingService.class + this.getApplicationContext(), + CallKeepBackgroundMessagingService.class ); - headlessIntent.putExtra("callUUID", uuid); - headlessIntent.putExtra("name", displayName); - headlessIntent.putExtra("handle", number); - Log.d(TAG, "wakeUpApplication: " + uuid + ", number : " + number + ", displayName:" + displayName); - + headlessIntent.putExtras(new Bundle(extras)); + Log.d(TAG, "wakeUpApplication: " + + extras.get(EXTRA_CALL_UUID) + + ", number : " + extras.get(EXTRA_CALL_NUMBER) + + ", displayName:" + extras.get(EXTRA_CALLER_NAME)); ComponentName name = this.getApplicationContext().startService(headlessIntent); if (name != null) { CallKeepBackgroundMessagingService.acquireWakeLockNow(this.getApplicationContext()); } + broadcastAction(ACTION_WAKEUP_CALL, bundleToMap(extras)); } - private void wakeUpAfterReachabilityTimeout(ConnectionRequest request) { - if (this.currentConnectionRequest == null) { - return; - } - Log.d(TAG, "checkReachability timeout, force wakeup"); - Bundle extras = request.getExtras(); - String number = request.getAddress().getSchemeSpecificPart(); - String displayName = extras.getString(EXTRA_CALLER_NAME); - wakeUpApplication(this.notReachableCallUuid, number, displayName); - - VoiceConnectionService.currentConnectionRequest = null; + private void checkReachability(ConnectionRequest request) { + Log.d(TAG, "checkReachability"); + checkReachability(); + new Handler().postDelayed( + () -> { + Log.d(TAG, "checkReachability timeout, force wakeup"); + wakeUpApplication(request.getExtras()); + }, + 2000); } private void checkReachability() { - Log.d(TAG, "checkReachability"); - - final VoiceConnectionService instance = this; sendCallRequestToActivity(ACTION_CHECK_REACHABILITY, null); - - new android.os.Handler().postDelayed( - new Runnable() { - public void run() { - instance.wakeUpAfterReachabilityTimeout(instance.currentConnectionRequest); - } - }, 2000); + broadcastAction(ACTION_CHECK_REACHABILITY, null); } private Boolean canMakeOutgoingCall() { return isAvailable; } - private Connection createConnection(ConnectionRequest request) { - Bundle extras = request.getExtras(); - HashMap extrasMap = this.bundleToMap(extras); - extrasMap.put(EXTRA_CALL_NUMBER, request.getAddress().toString()); - VoiceConnection connection = new VoiceConnection(this, extrasMap); - connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD); + private void initConnection(String uuid, VoiceConnection connection, Bundle extras, PhoneAccountHandle accountHandle) { connection.setInitializing(); connection.setExtras(extras); - currentConnections.put(extras.getString(EXTRA_CALL_UUID), connection); - // Get other connections for conferencing - Map otherConnections = new HashMap<>(); - for (Map.Entry entry : currentConnections.entrySet()) { - if(!(extras.getString(EXTRA_CALL_UUID).equals(entry.getKey()))) { - otherConnections.put(entry.getKey(), entry.getValue()); + int capabilities = connection.getConnectionCapabilities() | Connection.CAPABILITY_MUTE; + ConstraintsMap settings = getSettings(getApplicationContext()); + if (settings != null) { + if (!settings.isNull("supportsHolding") && settings.getBoolean("supportsHolding")) { + capabilities |= Connection.CAPABILITY_SUPPORT_HOLD; + } + } else { + ConstraintsMap metDataSettings = getMetadataSettings(); + if (Boolean.TRUE.equals(metDataSettings.getBoolean(HOLD_SUPPORT_DATA_KEY))) { + capabilities |= Connection.CAPABILITY_SUPPORT_HOLD; } } - List conferenceConnections = new ArrayList(otherConnections.values()); + connection.setConnectionCapabilities(capabilities); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Context context = getApplicationContext(); + TelecomManager telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + PhoneAccount phoneAccount = telecomManager.getPhoneAccount(accountHandle); + + //If the phone account is self managed, then this connection must also be self managed. + if ((phoneAccount.getCapabilities() & PhoneAccount.CAPABILITY_SELF_MANAGED) == PhoneAccount.CAPABILITY_SELF_MANAGED) { + Log.d(TAG, "[VoiceConnectionService] PhoneAccount is SELF_MANAGED, so connection will be too"); + connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED); + } else { + Log.d(TAG, "[VoiceConnectionService] PhoneAccount is not SELF_MANAGED, so connection won't be either"); + } + } + + // Get other connections for conferencing + List conferenceConnections = new ArrayList<>(currentConnections.values()); connection.setConferenceableConnections(conferenceConnections); - return connection; + currentConnections.put(uuid, connection); + + // ‍️Weirdly on some Samsung phones (A50, S9...) using `setInitialized` will not display the native UI ... + // when making a call from the native Phone application. The call will still be displayed correctly without it. + if (!Build.MANUFACTURER.equalsIgnoreCase("Samsung")) { + connection.setInitialized(); + } } @Override @@ -269,33 +439,50 @@ public void onConference(Connection connection1, Connection connection2) { /* * Send call request to the RNCallKeepModule */ + private void broadcastAction(final String action, @Nullable final HashMap attributeMap) { + try { + Bundle appMetaData = getMetaData(); + if (appMetaData == null) return; + String receiverName = appMetaData.getString(BROADCAST_RECEIVER_META_DATA_KEY); + if (receiverName == null) return; + Class clazz = Class.forName(receiverName); + Intent intent = new Intent(getApplicationContext(), clazz); + intent.setAction(getPackageName() + "." + action); + if (attributeMap != null) { + intent.putExtra(EXTRA_CALL_ATTRIB, attributeMap); + } + sendBroadcast(intent); + } catch (ClassNotFoundException | PackageManager.NameNotFoundException e) { + Log.e(TAG, null, e); + } + } + private void sendCallRequestToActivity(final String action, @Nullable final HashMap attributeMap) { - final VoiceConnectionService instance = this; - final Handler handler = new Handler(); - - handler.post(new Runnable() { - @Override - public void run() { - Intent intent = new Intent(action); - if (attributeMap != null) { - Bundle extras = new Bundle(); - extras.putSerializable("attributeMap", attributeMap); - intent.putExtras(extras); - } - LocalBroadcastManager.getInstance(instance).sendBroadcast(intent); + new Handler().post(() -> { + Intent intent = new Intent(action); + if (attributeMap != null) { + intent.putExtra(EXTRA_CALL_ATTRIB, attributeMap); } + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); }); } - private HashMap bundleToMap(Bundle extras) { - HashMap extrasMap = new HashMap<>(); + @Nullable + protected Bundle getMetaData() throws PackageManager.NameNotFoundException { + ServiceInfo serviceInfo = getPackageManager().getServiceInfo(new ComponentName(this, getClass()), PackageManager.GET_META_DATA); + return serviceInfo.metaData; + } + + private HashMap bundleToMap(Bundle extras) { + HashMap extrasMap = new HashMap<>(); Set keySet = extras.keySet(); - Iterator iterator = keySet.iterator(); - while(iterator.hasNext()) { - String key = iterator.next(); + for (String key : keySet) { if (extras.get(key) != null) { - extrasMap.put(key, extras.get(key).toString()); + Object value = extras.get(key); + if (value != null) { + extrasMap.put(key, value); + } } } return extrasMap; diff --git a/android/src/main/java/io/wazo/callkeep/utils/Callback.java b/android/src/main/java/io/wazo/callkeep/utils/Callback.java index b7830f91..d397f04f 100644 --- a/android/src/main/java/io/wazo/callkeep/utils/Callback.java +++ b/android/src/main/java/io/wazo/callkeep/utils/Callback.java @@ -1,6 +1,6 @@ package io.wazo.callkeep.utils; -public interface Callback { +public interface Callback { - void invoke(Object... args); + void invoke(Arg arg); } diff --git a/android/src/main/java/io/wazo/callkeep/utils/ConstraintsArray.java b/android/src/main/java/io/wazo/callkeep/utils/ConstraintsArray.java index 37fdfe55..53e100cf 100644 --- a/android/src/main/java/io/wazo/callkeep/utils/ConstraintsArray.java +++ b/android/src/main/java/io/wazo/callkeep/utils/ConstraintsArray.java @@ -12,7 +12,7 @@ public ConstraintsArray(){ } public ConstraintsArray(ArrayList array){ - this.mArray = array; + this.mArray = array; } public int size(){ @@ -59,9 +59,11 @@ public ObjectType getType(int index) { } else if (object instanceof Boolean) { return ObjectType.Boolean; } else if (object instanceof Double || - object instanceof Float || - object instanceof Integer) { - return ObjectType.Number; + object instanceof Float) { + return ObjectType.Double; + } else if (object instanceof Integer || + object instanceof Long) { + return ObjectType.Integer; } else if (object instanceof String) { return ObjectType.String; } else if (object instanceof ArrayList) { diff --git a/android/src/main/java/io/wazo/callkeep/utils/ConstraintsMap.java b/android/src/main/java/io/wazo/callkeep/utils/ConstraintsMap.java index 546b3510..a93a998e 100644 --- a/android/src/main/java/io/wazo/callkeep/utils/ConstraintsMap.java +++ b/android/src/main/java/io/wazo/callkeep/utils/ConstraintsMap.java @@ -59,8 +59,10 @@ public ObjectType getType(String name) { Object value = mMap.get(name); if (value == null) { return ObjectType.Null; - } else if (value instanceof Number) { - return ObjectType.Number; + } else if (value instanceof Integer) { + return ObjectType.Integer; + } else if (value instanceof Double) { + return ObjectType.Double; } else if (value instanceof String) { return ObjectType.String; } else if (value instanceof Boolean) { diff --git a/android/src/main/java/io/wazo/callkeep/utils/MapUtils.java b/android/src/main/java/io/wazo/callkeep/utils/MapUtils.java new file mode 100644 index 00000000..5c289283 --- /dev/null +++ b/android/src/main/java/io/wazo/callkeep/utils/MapUtils.java @@ -0,0 +1,60 @@ +package io.wazo.callkeep.utils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; + +public class MapUtils { + + public static ConstraintsMap convertJsonToMap(JSONObject jsonObject) throws JSONException { + ConstraintsMap map = new ConstraintsMap(); + + Iterator iterator = jsonObject.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = jsonObject.get(key); + if (value instanceof JSONObject) { + map.putMap(key, convertJsonToMap((JSONObject) value).toMap()); + } else if (value instanceof Boolean) { + map.putBoolean(key, (Boolean) value); + } else if (value instanceof Integer) { + map.putInt(key, (Integer) value); + } else if (value instanceof Double) { + map.putDouble(key, (Double) value); + } else if (value instanceof String) { + map.putString(key, (String) value); + } else { + map.putString(key, value.toString()); + } + } + return map; + } + + public static JSONObject convertMapToJson(ConstraintsMap readableMap) throws JSONException { + JSONObject object = new JSONObject(); + for (String key : readableMap.toMap().keySet()) { + switch (readableMap.getType(key)) { + case Null: + object.put(key, JSONObject.NULL); + break; + case Boolean: + object.put(key, readableMap.getBoolean(key)); + break; + case Integer: + object.put(key, readableMap.getInt(key)); + break; + case Double: + object.put(key, readableMap.getDouble(key)); + break; + case String: + object.put(key, readableMap.getString(key)); + break; + case Map: + object.put(key, convertMapToJson(readableMap.getMap(key))); + break; + } + } + return object; + } +} \ No newline at end of file diff --git a/android/src/main/java/io/wazo/callkeep/utils/ObjectType.java b/android/src/main/java/io/wazo/callkeep/utils/ObjectType.java index a9e3b815..ecfeaae0 100644 --- a/android/src/main/java/io/wazo/callkeep/utils/ObjectType.java +++ b/android/src/main/java/io/wazo/callkeep/utils/ObjectType.java @@ -3,7 +3,8 @@ public enum ObjectType { Null, Boolean, - Number, + Integer, + Double, String, Map, Array, diff --git a/example/.metadata b/example/.metadata deleted file mode 100644 index e7f82014..00000000 --- a/example/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 49fac9a885df6f1029799fd208fdd79df019c387 - channel: master - -project_type: app diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..8d81c200 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 1664094e..b50b490a 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -22,10 +22,18 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +dependencies { + // Add the dependencies for any other desired Firebase products + // https://firebase.google.com/docs/android/setup#available-libraries + implementation 'com.google.firebase:firebase-messaging:20.3.0' +} + android { - compileSdkVersion 28 + compileSdkVersion 30 + namespace "com.github.cloudwebrtc.flutter_callkeep_example" lintOptions { disable 'InvalidPackage' @@ -35,7 +43,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.github.cloudwebrtc.flutter_callkeep_example" minSdkVersion 23 - targetSdkVersion 28 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index ab16d963..ebcda7a3 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ diff --git a/example/android/app/src/main/java/com/github/cloudwebrtc/flutter_callkeep_example/Application.java b/example/android/app/src/main/java/com/github/cloudwebrtc/flutter_callkeep_example/Application.java new file mode 100644 index 00000000..441173dd --- /dev/null +++ b/example/android/app/src/main/java/com/github/cloudwebrtc/flutter_callkeep_example/Application.java @@ -0,0 +1,28 @@ +package com.github.cloudwebrtc.flutter_callkeep_example; + +import android.os.Bundle; + +import androidx.annotation.CallSuper; + +import io.flutter.app.FlutterApplication; +import io.flutter.app.FlutterActivity; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback; +import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService; +import io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin; +import com.github.cloudwebrtc.flutter_callkeep.FlutterCallkeepPlugin; + +public class Application extends FlutterApplication implements PluginRegistrantCallback { + @Override + public void onCreate() { + super.onCreate(); + FlutterFirebaseMessagingService.setPluginRegistrant(this); + } + + @Override + public void registerWith(PluginRegistry pluginRegistry) { + FirebaseMessagingPlugin.registerWith(pluginRegistry.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin")); + FlutterCallkeepPlugin.registerWith(pluginRegistry.registrarFor("com.github.cloudwebrtc.flutter_callkeep.FlutterCallkeepPlugin")); + } +} \ No newline at end of file diff --git a/example/android/build.gradle b/example/android/build.gradle index e0d7ae2c..92e0e4a3 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,6 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.google.gms:google-services:4.3.4' } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b..dfbc8f4f 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip diff --git a/example/android/settings_aar.gradle b/example/android/settings_aar.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/example/ios/Podfile b/example/ios/Podfile index 41d619ae..5981b62b 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '10.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index f0934e16..1494cf9c 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -151,7 +151,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 55E51B0683F0D8A00CACA3AC /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -245,23 +244,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 55E51B0683F0D8A00CACA3AC /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -375,6 +357,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -508,6 +491,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -536,6 +520,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16..919434a6 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h index 36e21bbf..ef872bf6 100644 --- a/example/ios/Runner/AppDelegate.h +++ b/example/ios/Runner/AppDelegate.h @@ -1,6 +1,7 @@ #import #import +#import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m index 24da2a75..89ba88ac 100644 --- a/example/ios/Runner/AppDelegate.m +++ b/example/ios/Runner/AppDelegate.m @@ -6,17 +6,19 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + [CallKeep setDelegate:self]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } -- (BOOL)application:(UIApplication *)application - continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler { - return [CallKeep application:application - continueUserActivity:userActivity - restorationHandler:restorationHandler]; - +- (nullable NSDictionary *)mapPushPayload:(NSDictionary * _Nonnull)payload { + NSLog(@"Mapper called with %@", [payload description]); + return payload; +} + +- (void)onCallEvent:(NSString *)event withCallData:(NSDictionary *)callData { + NSLog(@"Delegate called on %@ with %@", event, [callData description]); } + @end diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 99ce784c..99939835 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -24,7 +24,7 @@ UIBackgroundModes - fetch + processing remote-notification voip diff --git a/example/lib/main.dart b/example/lib/main.dart index 41532905..0e7f6d1b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,18 +1,121 @@ import 'dart:async'; +import 'dart:io'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:callkeep/callkeep.dart'; +import 'package:logger/logger.dart'; import 'package:uuid/uuid.dart'; +/// For fcm background message handler. +final FlutterCallkeep _callKeep = FlutterCallkeep(); +bool _callKeepInited = false; + +/* +{ + "uuid": "xxxxx-xxxxx-xxxxx-xxxxx", + "caller_id": "+8618612345678", + "caller_name": "hello", + "caller_id_type": "number", + "has_video": false, + + "extra": { + "foo": "bar", + "key": "value", + } +} +*/ + +Future myBackgroundMessageHandler(RemoteMessage message) { + Logger logger = Logger(); + logger.d('backgroundMessage: message => ${message.toString()}'); + + // Handle data message + var data = message.data; + var callerId = data['caller_id'] ?? message.senderId ?? "No Sender Id"; + var callerName = data['caller_name'] as String; + var callUUID = data['uuid'] ?? const Uuid().v4(); + var hasVideo = data['has_video'] == "true"; + + _callKeep.on( + (CallKeepPerformAnswerCallAction event) { + logger.d( + 'backgroundMessage: CallKeepPerformAnswerCallAction ${event.callData.callUUID}'); + Timer(const Duration(seconds: 1), () { + logger.d( + '[setCurrentCallActive] $callUUID, callerId: $callerId, callerName: $callerName'); + _callKeep.setCurrentCallActive(callUUID); + }); + //_callKeep.endCall(event.callUUID); + }); + + _callKeep + .on((CallKeepPerformEndCallAction event) { + logger + .d('backgroundMessage: CallKeepPerformEndCallAction ${event.callUUID}'); + }); + if (!_callKeepInited) { + _callKeep.setup( + showAlertDialog: null, + options: { + 'ios': { + 'appName': 'CallKeepDemo', + }, + 'android': { + 'additionalPermissions': [ + 'android.permission.CALL_PHONE', + 'android.permission.READ_PHONE_NUMBERS' + ], + 'foregroundService': { + 'channelId': 'com.example.call-kit-test', + 'channelName': 'callKitTest', + 'notificationTitle': 'My app is running on background', + 'notificationIcon': 'Path to the resource icon of the notification', + }, + }, + }, + ); + _callKeepInited = true; + } + + logger.d('backgroundMessage: displayIncomingCall ($callerId)'); + _callKeep.displayIncomingCall( + uuid: callUUID, + handle: callerId, + callerName: callerName, + hasVideo: hasVideo, + ); + _callKeep.backToForeground(); + /* + + if (message.containsKey('data')) { + // Handle data message + final dynamic data = message['data']; + } + + if (message.containsKey('notification')) { + // Handle notification message + final dynamic notification = message['notification']; + logger.d('notification => ${notification.toString()}'); + } + + // Or do other work. + */ + return Future.value(null); +} + void main() { - runApp(MyApp()); + Logger.level = Level.all; + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'Welcome to Flutter', debugShowCheckedModeBanner: false, home: HomePage(), @@ -21,8 +124,10 @@ class MyApp extends StatelessWidget { } class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + MyAppState createState() => MyAppState(); } class Call { @@ -32,11 +137,22 @@ class Call { bool muted = false; } -class _MyAppState extends State { +class MyAppState extends State { final FlutterCallkeep _callKeep = FlutterCallkeep(); Map calls = {}; - - String newUUID() => Uuid().v4(); + String newUUID() => const Uuid().v4(); + final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; + Logger logger = Logger(); + + void iOSPermission() async { + NotificationSettings settings = await _firebaseMessaging.requestPermission( + alert: true, + badge: true, + sound: true, + provisional: false, + ); + logger.d('Settings registered: $settings'); + } void removeCall(String callUUID) { setState(() { @@ -46,73 +162,98 @@ class _MyAppState extends State { void setCallHeld(String callUUID, bool held) { setState(() { - calls[callUUID].held = held; + calls[callUUID]?.held = held; }); } void setCallMuted(String callUUID, bool muted) { setState(() { - calls[callUUID].muted = muted; + calls[callUUID]?.muted = muted; }); } Future answerCall(CallKeepPerformAnswerCallAction event) async { - final String callUUID = event.callUUID; - final String number = calls[callUUID].number; - print('[answerCall] $callUUID, number: $number'); + final callUUID = event.callData.callUUID; + final number = calls[callUUID]?.number; + if (callUUID == null) { + logger.e("Tried to answer call but callUUID is null"); + return; + } + logger.d('[answerCall] $callUUID, number: $number'); - _callKeep.startCall(event.callUUID, number, number); Timer(const Duration(seconds: 1), () { - print('[setCurrentCallActive] $callUUID, number: $number'); + logger.d('[setCurrentCallActive] $callUUID, number: $number'); _callKeep.setCurrentCallActive(callUUID); }); } Future endCall(CallKeepPerformEndCallAction event) async { - print('endCall: ${event.callUUID}'); - removeCall(event.callUUID); + final callUUID = event.callUUID; + if (callUUID == null) { + logger.e("Tried to endcall but callUUID is null"); + return; + } + logger.d('[endCall] $callUUID'); + removeCall(callUUID); } Future didPerformDTMFAction(CallKeepDidPerformDTMFAction event) async { - print('[didPerformDTMFAction] ${event.callUUID}, digits: ${event.digits}'); + logger + .d('[didPerformDTMFAction] ${event.callUUID}, digits: ${event.digits}'); } Future didReceiveStartCallAction( - CallKeepDidReceiveStartCallAction event) async { - if (event.handle == null) { + CallKeepDidReceiveStartCallAction event, + ) async { + final callData = event.callData; + if (callData.handle == null) { // @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined` return; } - final String callUUID = event.callUUID ?? newUUID(); + final String callUUID = callData.callUUID ?? newUUID(); + final Call call = Call(callData.handle ?? "No Handle"); setState(() { - calls[callUUID] = Call(event.handle); + calls[callUUID] = call; }); - print('[didReceiveStartCallAction] $callUUID, number: ${event.handle}'); + logger + .d('[didReceiveStartCallAction] $callUUID, number: ${callData.handle}'); - _callKeep.startCall(callUUID, event.handle, event.handle); + _callKeep.startCall( + uuid: callUUID, handle: call.number, callerName: call.number); Timer(const Duration(seconds: 1), () { - print('[setCurrentCallActive] $callUUID, number: ${event.handle}'); + logger.d('[setCurrentCallActive] $callUUID, number: ${callData.handle}'); _callKeep.setCurrentCallActive(callUUID); }); } Future didPerformSetMutedCallAction( CallKeepDidPerformSetMutedCallAction event) async { - final String number = calls[event.callUUID].number; - print( - '[didPerformSetMutedCallAction] ${event.callUUID}, number: $number (${event.muted})'); + final callUUID = event.callUUID; + if (callUUID == null) { + logger.e("Tried to mute call but callUUID is null"); + return; + } + final number = calls[callUUID]?.number ?? "No Number"; + final muted = event.muted ?? false; + logger.d( + '[didPerformSetMutedCallAction] $callUUID, number: $number ($muted)'); - setCallMuted(event.callUUID, event.muted); + setCallMuted(callUUID, muted); } Future didToggleHoldCallAction( CallKeepDidToggleHoldAction event) async { - final String number = calls[event.callUUID].number; - print( - '[didToggleHoldCallAction] ${event.callUUID}, number: $number (${event.hold})'); + final callUUID = event.callUUID; + if (callUUID == null) { + logger.e("Tried to hold call but callUUID is null"); + return; + } + final number = calls[callUUID]?.number ?? "No Number"; + final hold = event.hold ?? false; + logger.d('[didToggleHoldCallAction] $callUUID, number: $number ($hold)'); - setCallHeld(event.callUUID, event.hold); + setCallHeld(callUUID, hold); } Future hangup(String callUUID) async { @@ -121,31 +262,31 @@ class _MyAppState extends State { } Future setOnHold(String callUUID, bool held) async { - _callKeep.setOnHold(callUUID, held); - final String handle = calls[callUUID].number; - print('[setOnHold: $held] $callUUID, number: $handle'); + _callKeep.setOnHold(uuid: callUUID, shouldHold: held); + final String handle = calls[callUUID]?.number ?? "No Number"; + logger.d('[setOnHold: $held] $callUUID, number: $handle'); setCallHeld(callUUID, held); } Future setMutedCall(String callUUID, bool muted) async { - _callKeep.setMutedCall(callUUID, muted); - final String handle = calls[callUUID].number; - print('[setMutedCall: $muted] $callUUID, number: $handle'); + _callKeep.setMutedCall(uuid: callUUID, shouldMute: muted); + final String handle = calls[callUUID]?.number ?? "No Number"; + logger.d('[setMutedCall: $muted] $callUUID, number: $handle'); setCallMuted(callUUID, muted); } Future updateDisplay(String callUUID) async { - final String number = calls[callUUID].number; + final String number = calls[callUUID]?.number ?? "No Number"; // Workaround because Android doesn't display well displayName, se we have to switch ... if (isIOS) { - _callKeep.updateDisplay(callUUID, - displayName: 'New Name', handle: number); + _callKeep.updateDisplay( + uuid: callUUID, callerName: 'New Name', handle: number); } else { - _callKeep.updateDisplay(callUUID, - displayName: number, handle: 'New Name'); + _callKeep.updateDisplay( + uuid: callUUID, callerName: number, handle: 'New Name'); } - print('[updateDisplay: $number] $callUUID'); + logger.d('[updateDisplay: $number] $callUUID'); } Future displayIncomingCallDelayed(String number) async { @@ -159,47 +300,136 @@ class _MyAppState extends State { setState(() { calls[callUUID] = Call(number); }); - print('Display incoming call now'); + logger.d('Display incoming call now'); final bool hasPhoneAccount = await _callKeep.hasPhoneAccount(); if (!hasPhoneAccount) { - await _callKeep.hasDefaultPhoneAccount(context, { + await _callKeep.hasDefaultPhoneAccount({ 'alertTitle': 'Permissions required', 'alertDescription': 'This application needs to access your phone accounts', 'cancelButton': 'Cancel', 'okButton': 'ok', + 'foregroundService': { + 'channelId': 'com.company.my', + 'channelName': 'Foreground service for my app', + 'notificationTitle': 'My app is running on background', + 'notificationIcon': 'Path to the resource icon of the notification', + }, }); } - print('[displayIncomingCall] $callUUID number: $number'); - _callKeep.displayIncomingCall(callUUID, number, - handleType: 'number', hasVideo: false); + logger.d('[displayIncomingCall] $callUUID number: $number'); + _callKeep.displayIncomingCall( + uuid: callUUID, handle: number, handleType: 'number', hasVideo: false); + } + + void didDisplayIncomingCall(CallKeepDidDisplayIncomingCall event) { + final callUUID = event.callData.callUUID; + final number = event.callData.handle ?? "No Number"; + if (callUUID == null) { + logger.e("Tried to diplay incoming call but callUUID is null"); + return; + } + logger.d('[displayIncomingCall] $callUUID number: $number'); + setState(() { + calls[callUUID] = Call(number); + }); + } + + void onPushKitToken(CallKeepPushKitToken event) { + logger.d('[onPushKitToken] token => ${event.token}'); } @override void initState() { super.initState(); - _callKeep.on(CallKeepPerformAnswerCallAction(), answerCall); - _callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction); - _callKeep.on( - CallKeepDidReceiveStartCallAction(), didReceiveStartCallAction); - _callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction); - _callKeep.on( - CallKeepDidPerformSetMutedCallAction(), didPerformSetMutedCallAction); - _callKeep.on(CallKeepPerformEndCallAction(), endCall); - - _callKeep.setup({ - 'ios': { - 'appName': 'CallKeepDemo', - }, - 'android': { - 'alertTitle': 'Permissions required', - 'alertDescription': - 'This application needs to access your phone accounts', - 'cancelButton': 'Cancel', - 'okButton': 'ok', + _callKeep.on(didDisplayIncomingCall); + _callKeep.on(answerCall); + _callKeep.on(didPerformDTMFAction); + _callKeep.on(didReceiveStartCallAction); + _callKeep.on(didToggleHoldCallAction); + _callKeep + .on(didPerformSetMutedCallAction); + _callKeep.on(endCall); + _callKeep.on(onPushKitToken); + + _callKeep.setup( + showAlertDialog: () => showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Permissions Required'), + content: const Text( + 'This application needs to access your phone accounts'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(false), + ), + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ); + }, + ).then((value) => value ?? false), + options: { + 'ios': { + 'appName': 'CallKeepDemo', + }, + 'android': { + 'additionalPermissions': [ + 'android.permission.CALL_PHONE', + 'android.permission.READ_PHONE_NUMBERS' + ], + 'foregroundService': { + 'channelId': 'com.example.call-kit-test', + 'channelName': 'callKitTest', + 'notificationTitle': 'My app is running on background', + 'notificationIcon': 'Path to the resource icon of the notification', + }, + }, }, - }); + ); + + if (Platform.isIOS) iOSPermission(); + + if (Platform.isAndroid) { + _firebaseMessaging.getToken().then((token) { + logger.d('[FCM] token => $token'); + }); + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + print('Got a message whilst in the foreground!'); + print('Message data: ${message.data}'); + logger.d('onMessage: $message'); + + // Handle data message + var data = message.data; + var callerId = data['caller_id'] ?? message.senderId ?? "No Sender Id"; + var callerName = data['caller_name'] as String; + var callUUID = data['uuid'] ?? const Uuid().v4(); + var hasVideo = data['has_video'] == "true"; + + setState(() { + calls[callUUID] = Call(callerId); + }); + _callKeep.displayIncomingCall( + uuid: callUUID, + handle: callerId, + callerName: callerName, + hasVideo: hasVideo, + ); + + if (message.notification != null) { + print( + 'Message also contained a notification: ${message.notification}'); + } + }); + FirebaseMessaging.onBackgroundMessage(myBackgroundMessageHandler); + } } Widget buildCallingWidgets() { @@ -213,25 +443,25 @@ class _MyAppState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - RaisedButton( + ElevatedButton( onPressed: () async { setOnHold(item.key, !item.value.held); }, child: Text(item.value.held ? 'Unhold' : 'Hold'), ), - RaisedButton( + ElevatedButton( onPressed: () async { updateDisplay(item.key); }, child: const Text('Display'), ), - RaisedButton( + ElevatedButton( onPressed: () async { setMutedCall(item.key, !item.value.muted); }, child: Text(item.value.muted ? 'Unmute' : 'Mute'), ), - RaisedButton( + ElevatedButton( onPressed: () async { hangup(item.key); }, @@ -254,13 +484,13 @@ class _MyAppState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - RaisedButton( + ElevatedButton( onPressed: () async { displayIncomingCall('10086'); }, child: const Text('Display incoming call now'), ), - RaisedButton( + ElevatedButton( onPressed: () async { displayIncomingCallDelayed('10086'); }, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index de0cdafa..53188d1a 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,17 +1,15 @@ name: flutter_callkeep_example description: Demonstrates how to use the flutter_callkeep plugin. +version: 0.1.1 # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.2.2 <3.0.0" + sdk: ">=2.12.2 <4.0.0" dependencies: - flutter: - sdk: flutter - callkeep: # When depending on this package from a real application you should use: # callkeep: ^x.y.z @@ -20,11 +18,14 @@ dependencies: # the parent directory to use the current plugin's version. path: ../ - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.0 - uuid: ^2.0.2 + firebase_messaging: ^15.2.0 + flutter: + sdk: flutter + logger: ^2.0.2+1 + uuid: ^4.4.0 + dev_dependencies: + flutter_lints: ^5.0.0 flutter_test: sdk: flutter @@ -33,7 +34,6 @@ dev_dependencies: # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index c097d3ef..747fa191 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -6,20 +6,19 @@ // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - import 'package:flutter_callkeep_example/main.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(const MyApp()); // Verify that platform version is retrieved. expect( find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data.startsWith('Running on:'), + (Widget widget) => + widget is Text && widget.data!.startsWith('Running on:'), ), findsOneWidget, ); diff --git a/images/sample.png b/images/sample.png new file mode 100644 index 00000000..de3f8b41 Binary files /dev/null and b/images/sample.png differ diff --git a/ios/Classes/CallKeep.h b/ios/Classes/CallKeep.h index 3a8daa11..b5e4749a 100644 --- a/ios/Classes/CallKeep.h +++ b/ios/Classes/CallKeep.h @@ -10,45 +10,64 @@ #import #import #import -//#import +#import +#import "CallKeepPushDelegate.h" -@interface CallKeep: NSObject +static NSString *_Nonnull const CallKeepHandleStartCallNotification = @"CallKeepHandleStartCallNotification"; +static NSString *_Nonnull const CallKeepDidReceiveStartCallAction = @"CallKeepDidReceiveStartCallAction"; +static NSString *_Nonnull const CallKeepPerformAnswerCallAction = @"CallKeepPerformAnswerCallAction"; +static NSString *_Nonnull const CallKeepPerformEndCallAction = @"CallKeepPerformEndCallAction"; +static NSString *_Nonnull const CallKeepDidActivateAudioSession = @"CallKeepDidActivateAudioSession"; +static NSString *_Nonnull const CallKeepDidDeactivateAudioSession = @"CallKeepDidDeactivateAudioSession"; +static NSString *_Nonnull const CallKeepDidDisplayIncomingCall = @"CallKeepDidDisplayIncomingCall"; +static NSString *_Nonnull const CallKeepDidFailCallAction = @"CallKeepDidFailCallAction"; +static NSString *_Nonnull const CallKeepDidPerformSetMutedCallAction = @"CallKeepDidPerformSetMutedCallAction"; +static NSString *_Nonnull const CallKeepPerformPlayDTMFCallAction = @"CallKeepDidPerformDTMFAction"; +static NSString *_Nonnull const CallKeepDidToggleHoldAction = @"CallKeepDidToggleHoldAction"; +static NSString *_Nonnull const CallKeepProviderReset = @"CallKeepProviderReset"; +static NSString *_Nonnull const CallKeepCheckReachability = @"CallKeepCheckReachability"; +static NSString *_Nonnull const CallKeepDidLoadWithEvents = @"CallKeepDidLoadWithEvents"; +static NSString *_Nonnull const CallKeepPushKitToken = @"CallKeepPushKitToken"; +static NSString *_Nonnull const CallKeepActionAnswer = @"CallKeepActionAnswer"; +static NSString *_Nonnull const CallKeepActionEnd = @"CallKeepActionEnd"; -@property (nonatomic, strong) FlutterMethodChannel* eventChannel; -@property (nonatomic, strong) CXCallController *callKeepCallController; -@property (nonatomic, strong) CXProvider *callKeepProvider; +@interface CallKeep: NSObject +@property (nonatomic, strong, nullable) CXCallController *callKeepCallController; +@property (nonatomic, strong, nullable) CXProvider *callKeepProvider; +@property (nonatomic, strong, nullable) FlutterMethodChannel* eventChannel; +- (BOOL)handleMethodCall:(FlutterMethodCall* _Nonnull)call result:(FlutterResult _Nonnull )result; -- (BOOL)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; ++ (BOOL)application:(UIApplication * _Nonnull)application + openURL:(NSURL * _Nonnull)url + options:(NSDictionary * _Nonnull)options NS_AVAILABLE_IOS(9_0); -+ (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - options:(NSDictionary *)options NS_AVAILABLE_IOS(9_0); ++ (BOOL)application:(UIApplication * _Nonnull)application +continueUserActivity:(NSUserActivity * _Nonnull)userActivity + restorationHandler:(void(^ _Nonnull)(NSArray> * _Nonnull restorableObjects))restorationHandler; -+ (BOOL)application:(UIApplication *)application -continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void(^)(NSArray> * __nullable restorableObjects))restorationHandler; ++ (void)setDelegate:(NSObject* _Nullable)delegate; -+ (void)reportNewIncomingCall:(NSString *)uuidString - handle:(NSString *)handle - handleType:(NSString *)handleType ++ (void)reportNewIncomingCall:(NSString * _Nonnull)uuidString + handle:(NSString * _Nonnull)handle + handleType:(NSString * _Nonnull)handleType hasVideo:(BOOL)hasVideo - localizedCallerName:(NSString * _Nullable)localizedCallerName + callerName:(NSString * _Nullable)callerName fromPushKit:(BOOL)fromPushKit payload:(NSDictionary * _Nullable)payload; -+ (void)reportNewIncomingCall:(NSString *)uuidString - handle:(NSString *)handle - handleType:(NSString *)handleType ++ (void)reportNewIncomingCall:(NSString * _Nonnull)uuidString + handle:(NSString * _Nonnull)handle + handleType:(NSString * _Nonnull)handleType hasVideo:(BOOL)hasVideo - localizedCallerName:(NSString * _Nullable)localizedCallerName + callerName:(NSString * _Nullable)callerName fromPushKit:(BOOL)fromPushKit payload:(NSDictionary * _Nullable)payload withCompletionHandler:(void (^_Nullable)(void))completion; -+ (void)endCallWithUUID:(NSString *)uuidString ++ (void)endCallWithUUID:(NSString * _Nonnull)uuidString reason:(int)reason; -+ (BOOL)isCallActive:(NSString *)uuidString; ++ (BOOL)isCallActive:(NSString * _Nonnull)uuidString; @end diff --git a/ios/Classes/CallKeep.m b/ios/Classes/CallKeep.m index 74d478f6..721c12d3 100644 --- a/ios/Classes/CallKeep.m +++ b/ios/Classes/CallKeep.m @@ -6,28 +6,9 @@ // SPDX-License-Identifier: ISC, MIT // #import -#import "CallKeep.h" -#import +#import -#ifdef DEBUG -static int const OUTGOING_CALL_WAKEUP_DELAY = 10; -#else -static int const OUTGOING_CALL_WAKEUP_DELAY = 5; -#endif - -static NSString *const CallKeepHandleStartCallNotification = @"CallKeepHandleStartCallNotification"; -static NSString *const CallKeepDidReceiveStartCallAction = @"CallKeepDidReceiveStartCallAction"; -static NSString *const CallKeepPerformAnswerCallAction = @"CallKeepPerformAnswerCallAction"; -static NSString *const CallKeepPerformEndCallAction = @"CallKeepPerformEndCallAction"; -static NSString *const CallKeepDidActivateAudioSession = @"CallKeepDidActivateAudioSession"; -static NSString *const CallKeepDidDeactivateAudioSession = @"CallKeepDidDeactivateAudioSession"; -static NSString *const CallKeepDidDisplayIncomingCall = @"CallKeepDidDisplayIncomingCall"; -static NSString *const CallKeepDidPerformSetMutedCallAction = @"CallKeepDidPerformSetMutedCallAction"; -static NSString *const CallKeepPerformPlayDTMFCallAction = @"CallKeepDidPerformDTMFAction"; -static NSString *const CallKeepDidToggleHoldAction = @"CallKeepDidToggleHoldAction"; -static NSString *const CallKeepProviderReset = @"CallKeepProviderReset"; -static NSString *const CallKeepCheckReachability = @"CallKeepCheckReachability"; -static NSString *const CallKeepDidLoadWithEvents = @"CallKeepDidLoadWithEvents"; +#import "CallKeep.h" @implementation CallKeep { @@ -47,6 +28,8 @@ - (void)setEventChannel:(FlutterMethodChannel *)eventChannel } static CXProvider* sharedProvider; +static NSDictionary *settings; +static NSObject* _delegate; - (instancetype)init { @@ -88,61 +71,51 @@ - (BOOL)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self setup:argsMap[@"options"]]; result(nil); } else if ([@"displayIncomingCall" isEqualToString:method]) { - [self displayIncomingCall:argsMap[@"uuid"] handle:argsMap[@"handle"] handleType:argsMap[@"handleType"] hasVideo:[argsMap[@"hasVideo"] boolValue] localizedCallerName:argsMap[@"localizedCallerName"]]; + [self displayIncomingCall:argsMap[@"uuid"] handle:argsMap[@"handle"] handleType:argsMap[@"handleType"] hasVideo:[argsMap[@"hasVideo"] boolValue] callerName:argsMap[@"callerName"] payload:argsMap[@"additionalData"]]; result(nil); - } - else if ([@ "startCall" isEqualToString:method]) { - [self startCall:argsMap[@"uuid"] handle:argsMap[@"handle"] contactIdentifier:argsMap[@"callerName"] handleType:argsMap[@"handleType"] video:[argsMap[@"hasVideo"] boolValue]]; + } else if ([@ "startCall" isEqualToString:method]) { + [self startCall:argsMap[@"uuid"] handle:argsMap[@"handle"] callerName:argsMap[@"callerName"] handleType:argsMap[@"handleType"] video:[argsMap[@"hasVideo"] boolValue]]; result(nil); - } - else if ([@"isCallActive" isEqualToString:method]) { + } else if ([@"isCallActive" isEqualToString:method]) { result(@([self isCallActive:argsMap[@"uuid"]])); - } - else if ([@"endCall" isEqualToString:method]) { + } else if ([@"activeCalls" isEqualToString:method]) { + result([self activeCalls]); + } else if ([@"answerIncomingCall" isEqualToString:method]) { + [self answerIncomingCall:argsMap[@"uuid"]]; + result(nil); + } else if ([@"endCall" isEqualToString:method]) { [self endCall:argsMap[@"uuid"]]; result(nil); - } - else if ([@"endAllCalls" isEqualToString:method]) { + } else if ([@"endAllCalls" isEqualToString:method]) { [self endAllCalls]; result(nil); - } - else if ([@ "setOnHold" isEqualToString:method]) { + } else if ([@ "setOnHold" isEqualToString:method]) { [self setOnHold:argsMap[@"uuid"] shouldHold:[argsMap[@"hold"] boolValue]]; result(nil); - } - else if ([@ "reportEndCallWithUUID" isEqualToString:method]) { + } else if ([@ "reportEndCallWithUUID" isEqualToString:method]) { [self reportEndCallWithUUID:argsMap[@"uuid"] reason:[argsMap[@"reason"] intValue]]; result(nil); - } - else if ([@"setMutedCall" isEqualToString:method]) { + } else if ([@"setMutedCall" isEqualToString:method]) { [self setMutedCall:argsMap[@"uuid"] muted:[argsMap[@"muted"] boolValue]]; result(nil); - } - else if ([@ "sendDTMF" isEqualToString:method]) { + } else if ([@ "sendDTMF" isEqualToString:method]) { [self sendDTMF:argsMap[@"uuid"] dtmf:argsMap[@"key"]]; result(nil); - } - else if ([@ "updateDisplay" isEqualToString:method]) { - [self updateDisplay:argsMap[@"uuid"] displayName:argsMap[@"displayName"] uri:argsMap[@"handle"]]; + } else if ([@ "updateDisplay" isEqualToString:method]) { + [self updateDisplay:argsMap[@"uuid"] callerName:argsMap[@"callerName"] uri:argsMap[@"handle"]]; result(nil); - } - else if([@ "checkIfBusy" isEqualToString:method]){ + } else if([@ "checkIfBusy" isEqualToString:method]){ [self checkIfBusyWithResult:result]; - } - else if([@ "checkSpeaker" isEqualToString:method]){ + } else if([@ "checkSpeaker" isEqualToString:method]){ [self checkSpeakerResult:result]; - } - else if ([@"reportConnectingOutgoingCallWithUUID" isEqualToString:method]) { + } else if ([@"reportConnectingOutgoingCallWithUUID" isEqualToString:method]) { [self reportConnectingOutgoingCallWithUUID:argsMap[@"uuid"]]; - } - else if ([@"reportConnectedOutgoingCallWithUUID" isEqualToString:method]) { + } else if ([@"reportConnectedOutgoingCallWithUUID" isEqualToString:method]) { [self reportConnectedOutgoingCallWithUUID:argsMap[@"uuid"]]; - } - else if([@"reportUpdatedCall" isEqualToString:method]){ - [self reportUpdatedCall:argsMap[@"uuid"] contactIdentifier:argsMap[@"localizedCallerName"]]; + } else if([@"reportUpdatedCall" isEqualToString:method]){ + [self reportUpdatedCall:argsMap[@"uuid"] contactIdentifier:argsMap[@"callerName"]]; result(nil); - } - else { + } else { return NO; } return YES; @@ -165,17 +138,18 @@ - (void)sendEventWithName:(NSString *)name body:(id)body { [self.eventChannel invokeMethod:name arguments:body]; } -- (void)sendEventWithNameWrapper:(NSString *)name body:(id)body { - if (_hasListeners) { - [self sendEventWithName:name body:body]; - } else { - [self.eventChannel invokeMethod:name arguments:body]; +- (void)sendEventWithNameWrapper:(NSString *)name body:(NSDictionary*)body { + [self sendEventWithName:name body:body]; + if (_delegate && [_delegate respondsToSelector:@selector(onCallEvent:withCallData:)]) { + [_delegate onCallEvent:name withCallData:body]; } } + (void)initCallKitProvider { + if (settings == nil) { + settings = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"CallKeepSettings"]; + } if (sharedProvider == nil) { - NSDictionary *settings = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"CallKeepSettings"]; sharedProvider = [[CXProvider alloc] initWithConfiguration:[CallKeep getProviderConfiguration:settings]]; } } @@ -187,7 +161,18 @@ -(void)setup:(NSDictionary *)options #endif _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; self.callKeepCallController = [[CXCallController alloc] init]; - NSDictionary *settings = [[NSMutableDictionary alloc] initWithDictionary:options]; + NSMutableDictionary* _settings = [[NSMutableDictionary alloc] initWithDictionary:options]; + NSEnumerator *enumerator = [options keyEnumerator]; + id key; + while ((key = [enumerator nextObject])) { + id tmp = [options objectForKey:key]; + if ([tmp isKindOfClass:[NSString class]] || [tmp isKindOfClass:[NSNumber class]]) { + _settings[key] = tmp; + } else { + _settings[key] = [tmp description]; + } + } + settings = _settings; // Store settings in NSUserDefault [[NSUserDefaults standardUserDefaults] setObject:settings forKey:@"CallKeepSettings"]; [[NSUserDefaults standardUserDefaults] synchronize]; @@ -196,8 +181,99 @@ -(void)setup:(NSDictionary *)options self.callKeepProvider = sharedProvider; [self.callKeepProvider setDelegate:self queue:nil]; + [self voipRegistration]; +} + +#pragma mark - PushKit + ++ (void)setDelegate:(NSObject* _Nullable)delegate { + _delegate = delegate; +} + +-(void)voipRegistration +{ + PKPushRegistry* voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; + voipRegistry.delegate = self; + voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; +} + +- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type { + const unsigned *tokenBytes = [pushCredentials.token bytes]; + NSString *hexToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x", + ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]), + ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]), + ntohl(tokenBytes[6]), ntohl(tokenBytes[7])]; + + NSLog(@"\n[VoIP Token]: %@\n\n",hexToken); + + [self sendEventWithNameWrapper:CallKeepPushKitToken body:@{ @"token": hexToken }]; } +- (NSString *)createUUID { + CFUUIDRef uuidObject = CFUUIDCreate(kCFAllocatorDefault); + NSString *uuidStr = (NSString *)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuidObject)); + CFRelease(uuidObject); + return [uuidStr lowercaseString]; +} + + +- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(nonnull void (^)(void))completion { + // Process the received push + NSLog(@"didReceiveIncomingPushWithPayload payload = %@", payload.type); + /* payload example. + { + "uuid": "xxxxx-xxxxx-xxxxx-xxxxx", + "caller_id": "+8618612345678", + "caller_name": "hello", + "caller_id_type": "number", + "has_video": false, + } + */ + + NSDictionary *dic = payload.dictionaryPayload; + + if (_delegate) { + dic = [_delegate mapPushPayload:dic]; + } + + if (!dic || dic[@"aps"] != nil) { + NSLog(@"Do not use the 'alert' format for push type %@.", payload.type); + if(completion != nil) { + completion(); + } + return; + } + + NSString *uuid = dic[@"uuid"]; + NSString *callerId = dic[@"caller_id"]; + NSString *callerName = dic[@"caller_name"]; + BOOL hasVideo = [dic[@"has_video"] boolValue]; + NSString *callerIdType = dic[@"caller_id_type"]; + + + if( uuid == nil) { + uuid = [self createUUID]; + } + + NSLog(@"Got here %@.", [dic description]); + + [CallKeep reportNewIncomingCall:uuid + handle:callerId + handleType:callerIdType + hasVideo:hasVideo + callerName:callerName + fromPushKit:YES + payload:dic + withCompletionHandler:completion]; +} + +- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type { + [self pushRegistry:registry didReceiveIncomingPushWithPayload:payload forType:type withCompletionHandler:^(){ + NSLog(@"[CallKeep] received"); + }]; +} + + -(void) checkIfBusyWithResult:(FlutterResult)result { #ifdef DEBUG @@ -222,14 +298,15 @@ -(void) displayIncomingCall:(NSString *)uuidString handle:(NSString *)handle handleType:(NSString *)handleType hasVideo:(BOOL)hasVideo - localizedCallerName:(NSString * _Nullable)localizedCallerName + callerName:(NSString * _Nullable)callerName + payload:(NSDictionary * _Nullable)payload { - [CallKeep reportNewIncomingCall: uuidString handle:handle handleType:handleType hasVideo:hasVideo localizedCallerName:localizedCallerName fromPushKit: NO payload:nil withCompletionHandler:nil]; + [CallKeep reportNewIncomingCall: uuidString handle:handle handleType:handleType hasVideo:hasVideo callerName:callerName fromPushKit: NO payload:payload withCompletionHandler:nil]; } -(void) startCall:(NSString *)uuidString handle:(NSString *)handle -contactIdentifier:(NSString * _Nullable)contactIdentifier + callerName:(NSString * _Nullable)callerName handleType:(NSString *)handleType video:(BOOL)video { @@ -241,9 +318,30 @@ -(void) startCall:(NSString *)uuidString CXHandle *callHandle = [[CXHandle alloc] initWithType:_handleType value:handle]; CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle]; [startCallAction setVideo:video]; - [startCallAction setContactIdentifier:contactIdentifier]; + [startCallAction setContactIdentifier:callerName]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction]; + [self requestTransaction:transaction withSuccessListener:^(CXAction* action) { + // CXStartCallAction + if ([action isKindOfClass:[CXStartCallAction class]]) { + CXStartCallAction *startCallAction = (CXStartCallAction *)action; + CXCallUpdate* callUpdate = [CallKeep createCallUpdate]; + callUpdate.remoteHandle = startCallAction.handle; + callUpdate.hasVideo = startCallAction.video; + callUpdate.localizedCallerName = startCallAction.contactIdentifier; + [sharedProvider reportCallWithUUID:startCallAction.callUUID updated:callUpdate]; + } + }]; +} + +-(void) answerIncomingCall:(NSString *)uuidString +{ +#ifdef DEBUG + NSLog(@"[CallKeep][answerIncomingCall] uuidString = %@", uuidString); +#endif + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; + CXAnswerCallAction *answerCallAction = [[CXAnswerCallAction alloc] initWithCallUUID:uuid]; + CXTransaction *transaction = [[CXTransaction alloc] initWithAction:answerCallAction]; [self requestTransaction:transaction]; } @@ -272,6 +370,24 @@ -(void) endAllCalls } } +-(NSArray*)activeCalls +{ +#ifdef DEBUG + NSLog(@"[CallKeep][activeCalls]"); +#endif + CXCallObserver *callObserver = [[CXCallObserver alloc] init]; + + NSMutableString *uuids = [NSMutableString string]; + + for(CXCall *call in callObserver.calls){ + NSLog(@"[CallKeep] activeCall %@ ", call.UUID); + NSString *uuid = [call.UUID UUIDString]; + [uuids appendString: uuid]; + } + + return [NSArray arrayWithObject:uuids]; +} + -(void) setOnHold:(NSString *)uuidString shouldHold:(BOOL)shouldHold { #ifdef DEBUG @@ -302,15 +418,15 @@ -(void) reportEndCallWithUUID:(NSString *)uuidString reason:(int)reason [CallKeep endCallWithUUID: uuidString reason:reason]; } --(void) updateDisplay:(NSString *)uuidString displayName:(NSString *)displayName uri:(NSString *)uri +-(void) updateDisplay:(NSString *)uuidString callerName:(NSString *)callerName uri:(NSString *)uri { #ifdef DEBUG - NSLog(@"[CallKeep][updateDisplay] uuidString = %@ displayName = %@ uri = %@", uuidString, displayName, uri); + NSLog(@"[CallKeep][updateDisplay] uuidString = %@ displayName = %@ uri = %@", uuidString, callerName, uri); #endif NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:uri]; CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - callUpdate.localizedCallerName = displayName; + callUpdate.localizedCallerName = callerName; callUpdate.remoteHandle = callHandle; [self.callKeepProvider reportCallWithUUID:uuid updated:callUpdate]; } @@ -349,6 +465,12 @@ -(BOOL) isCallActive:(NSString *)uuidString } - (void)requestTransaction:(CXTransaction *)transaction +{ + [self requestTransaction:transaction withSuccessListener:nil]; +} + +- (void)requestTransaction:(CXTransaction *)transaction + withSuccessListener:(void(^)(CXAction*))onSuccess { #ifdef DEBUG NSLog(@"[CallKeep][requestTransaction] transaction = %@", transaction); @@ -361,18 +483,8 @@ - (void)requestTransaction:(CXTransaction *)transaction NSLog(@"[CallKeep][requestTransaction] Error requesting transaction (%@): (%@)", transaction.actions, error); } else { NSLog(@"[CallKeep][requestTransaction] Requested transaction successfully"); - // CXStartCallAction - if ([[transaction.actions firstObject] isKindOfClass:[CXStartCallAction class]]) { - CXStartCallAction *startCallAction = [transaction.actions firstObject]; - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - callUpdate.remoteHandle = startCallAction.handle; - callUpdate.hasVideo = startCallAction.video; - callUpdate.localizedCallerName = startCallAction.contactIdentifier; - callUpdate.supportsDTMF = YES; - callUpdate.supportsHolding = YES; - callUpdate.supportsGrouping = NO; - callUpdate.supportsUngrouping = NO; - [self.callKeepProvider reportCallWithUUID:startCallAction.callUUID updated:callUpdate]; + if (onSuccess) { + onSuccess([transaction.actions firstObject]); } } }]; @@ -399,44 +511,65 @@ + (void)endCallWithUUID:(NSString *)uuidString NSLog(@"[CallKeep][reportEndCallWithUUID] uuidString = %@ reason = %d", uuidString, reason); #endif NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; + CXCallEndedReason reasonO; + CXCallEndedReason* reasonE = nil; switch (reason) { case 1: - [sharedProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonFailed]; + reasonO = CXCallEndedReasonFailed; + reasonE = &reasonO; break; case 2: case 6: - [sharedProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonRemoteEnded]; + reasonO = CXCallEndedReasonRemoteEnded; + reasonE = &reasonO; break; case 3: - [sharedProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonUnanswered]; + reasonO = CXCallEndedReasonUnanswered; + reasonE = &reasonO; break; case 4: - [sharedProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonAnsweredElsewhere]; + reasonO = CXCallEndedReasonAnsweredElsewhere; + reasonE = &reasonO; break; case 5: - [sharedProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonDeclinedElsewhere]; + reasonO = CXCallEndedReasonDeclinedElsewhere; + reasonE = &reasonO; break; default: break; } + if (reasonE) { + [sharedProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:*reasonE]; + } } + (void)reportNewIncomingCall:(NSString *)uuidString handle:(NSString *)handle handleType:(NSString *)handleType hasVideo:(BOOL)hasVideo - localizedCallerName:(NSString * _Nullable)localizedCallerName + callerName:(NSString * _Nullable)callerName fromPushKit:(BOOL)fromPushKit payload:(NSDictionary * _Nullable)payload { - [CallKeep reportNewIncomingCall:uuidString handle:handle handleType:handleType hasVideo:hasVideo localizedCallerName:localizedCallerName fromPushKit:fromPushKit payload:payload withCompletionHandler:nil]; + [CallKeep reportNewIncomingCall:uuidString handle:handle handleType:handleType hasVideo:hasVideo callerName:callerName fromPushKit:fromPushKit payload:payload withCompletionHandler:nil]; +} + + ++ (void)reportNewIncomingCall:(NSString *)uuidString + handle:(NSString *)handle + handleType:(NSString *)handleType + hasVideo:(BOOL)hasVideo + callerName:(NSString * _Nullable)callerName + fromPushKit:(BOOL)fromPushKit +{ + [CallKeep reportNewIncomingCall: uuidString handle:handle handleType:handleType hasVideo:hasVideo callerName:callerName fromPushKit: fromPushKit payload:nil withCompletionHandler:nil]; } + (void)reportNewIncomingCall:(NSString *)uuidString handle:(NSString *)handle handleType:(NSString *)handleType hasVideo:(BOOL)hasVideo - localizedCallerName:(NSString * _Nullable)localizedCallerName + callerName:(NSString * _Nullable)callerName fromPushKit:(BOOL)fromPushKit payload:(NSDictionary * _Nullable)payload withCompletionHandler:(void (^_Nullable)(void))completion @@ -444,28 +577,26 @@ + (void)reportNewIncomingCall:(NSString *)uuidString #ifdef DEBUG NSLog(@"[CallKeep][reportNewIncomingCall] uuidString = %@", uuidString); #endif + [CallKeep initCallKitProvider]; + int _handleType = [CallKeep getHandleType:handleType]; NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + + CXCallUpdate *callUpdate = [CallKeep createCallUpdate]; callUpdate.remoteHandle = [[CXHandle alloc] initWithType:_handleType value:handle]; - callUpdate.supportsDTMF = YES; - callUpdate.supportsHolding = YES; - callUpdate.supportsGrouping = NO; - callUpdate.supportsUngrouping = NO; callUpdate.hasVideo = hasVideo; - callUpdate.localizedCallerName = localizedCallerName; + callUpdate.localizedCallerName = callerName; - [CallKeep initCallKitProvider]; [sharedProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError * _Nullable error) { CallKeep *callKeep = [CallKeep allocWithZone: nil]; [callKeep sendEventWithNameWrapper:CallKeepDidDisplayIncomingCall body:@{ @"error": error && error.localizedDescription ? error.localizedDescription : @"", @"callUUID": uuidString, @"handle": handle, - @"localizedCallerName": localizedCallerName ? localizedCallerName : @"", - @"hasVideo": hasVideo ? @"1" : @"0", - @"fromPushKit": fromPushKit ? @"1" : @"0", - @"payload": payload ? payload : @"", + @"name": callerName ? callerName : @"", + @"hasVideo": @(hasVideo), + @"fromPushKit": @(fromPushKit), + @"additionalData": payload ? payload : @"", }]; if (error == nil) { // Workaround per https://forums.developer.apple.com/message/169511 @@ -479,14 +610,29 @@ + (void)reportNewIncomingCall:(NSString *)uuidString }]; } -+ (void)reportNewIncomingCall:(NSString *)uuidString - handle:(NSString *)handle - handleType:(NSString *)handleType - hasVideo:(BOOL)hasVideo - localizedCallerName:(NSString * _Nullable)localizedCallerName - fromPushKit:(BOOL)fromPushKit ++(CXCallUpdate*)createCallUpdate { - [CallKeep reportNewIncomingCall: uuidString handle:handle handleType:handleType hasVideo:hasVideo localizedCallerName:localizedCallerName fromPushKit: fromPushKit payload:nil withCompletionHandler:nil]; + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + callUpdate.supportsDTMF = settings[@"supportsDTMF"] ? [settings[@"supportsDTMF"] boolValue] : NO; + callUpdate.supportsHolding = settings[@"supportsHolding"] ? [settings[@"supportsHolding"] boolValue] : NO; + callUpdate.supportsGrouping = settings[@"supportsGrouping"] ? [settings[@"supportsGrouping"] boolValue] : NO; + callUpdate.supportsUngrouping = settings[@"supportsUngrouping"] ? [settings[@"supportsUngrouping"] boolValue] : NO; + + return callUpdate; +} + +// Update call contact info +// @deprecated +-(void) reportUpdatedCall:(NSString *)uuidString contactIdentifier:(NSString *)contactIdentifier +{ +#ifdef DEBUG + NSLog(@"[CallKeep][reportUpdatedCall] contactIdentifier = %@", contactIdentifier); +#endif + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + callUpdate.localizedCallerName = contactIdentifier; + + [self.callKeepProvider reportCallWithUUID:uuid updated:callUpdate]; } - (BOOL)lessThanIos10_2 @@ -520,7 +666,11 @@ + (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary*)settings #ifdef DEBUG NSLog(@"[CallKeep][getProviderConfiguration]"); #endif - CXProviderConfiguration *providerConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:settings[@"appName"]]; + NSString *appName = @"Unknown App"; + if (settings != nil) { + appName = settings[@"appName"]; + } + CXProviderConfiguration *providerConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:appName]; providerConfiguration.supportsVideo = YES; providerConfiguration.maximumCallGroups = 3; providerConfiguration.maximumCallsPerCallGroup = 1; @@ -598,7 +748,7 @@ + (BOOL)application:(UIApplication *)application + (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void(^)(NSArray> * __nullable restorableObjects))restorationHandler + restorationHandler:(void(^)(NSArray> * _Nonnull restorableObjects))restorationHandler { #ifdef DEBUG NSLog(@"[CallKeep][application:continueUserActivity]"); @@ -606,6 +756,7 @@ + (BOOL)application:(UIApplication *)application INInteraction *interaction = userActivity.interaction; INPerson *contact; NSString *handle; + NSString *displayName; BOOL isAudioCall; BOOL isVideoCall; @@ -645,11 +796,13 @@ + (BOOL)application:(UIApplication *)application if (contact != nil) { handle = contact.personHandle.value; + displayName = contact.displayName; } if (handle != nil && handle.length > 0 ){ NSDictionary *userInfo = @{ @"handle": handle, + @"name": displayName, @"video": @(isVideoCall) }; @@ -673,7 +826,7 @@ - (void)providerDidReset:(CXProvider *)provider{ #endif //this means something big changed, so tell the JS. The JS should //probably respond by hanging up all calls. - [self sendEventWithNameWrapper:CallKeepProviderReset body:nil]; + [self sendEventWithNameWrapper:CallKeepProviderReset body:@{}]; } // Starting outgoing call @@ -689,20 +842,6 @@ - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallActio [action fulfill]; } -// Update call contact info -// @deprecated --(void) reportUpdatedCall:(NSString *)uuidString contactIdentifier:(NSString *)contactIdentifier -{ -#ifdef DEBUG - NSLog(@"[CallKeep][reportUpdatedCall] contactIdentifier = %i", contactIdentifier); -#endif - NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - callUpdate.localizedCallerName = contactIdentifier; - - [self.callKeepProvider reportCallWithUUID:uuid updated:callUpdate]; -} - // Answering incoming call - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { @@ -757,6 +896,18 @@ - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)act #ifdef DEBUG NSLog(@"[CallKeep][CXProviderDelegate][provider:timedOutPerformingAction]"); #endif + NSDictionary* body; + if ([action isKindOfClass:[CXAnswerCallAction class]]) { + CXAnswerCallAction* answerAction = ((CXAnswerCallAction*)action); + body = @{ @"callUUID": [answerAction.callUUID.UUIDString lowercaseString], @"action": CallKeepActionAnswer }; + } else if ([action isKindOfClass:[CXEndCallAction class]]) { + CXEndCallAction* answerAction = ((CXEndCallAction*)action); + body = @{ @"callUUID": [answerAction.callUUID.UUIDString lowercaseString], @"action": CallKeepActionEnd }; + } + + if (body) { + [self sendEventWithNameWrapper:CallKeepDidFailCallAction body:body]; + } } - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession @@ -764,15 +915,9 @@ - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession #ifdef DEBUG NSLog(@"[CallKeep][CXProviderDelegate][provider:didActivateAudioSession]"); #endif - NSDictionary *userInfo - = @{ - AVAudioSessionInterruptionTypeKey: [NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded], - AVAudioSessionInterruptionOptionKey: [NSNumber numberWithInt:AVAudioSessionInterruptionOptionShouldResume] - }; - [[NSNotificationCenter defaultCenter] postNotificationName:AVAudioSessionInterruptionNotification object:nil userInfo:userInfo]; - + [self sendDefaultAudioInterruptionNotificationToStartAudioResource]; [self configureAudioSession]; - [self sendEventWithNameWrapper:CallKeepDidActivateAudioSession body:nil]; + [self sendEventWithNameWrapper:CallKeepDidActivateAudioSession body:@{}]; } - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession @@ -780,7 +925,16 @@ - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSessio #ifdef DEBUG NSLog(@"[CallKeep][CXProviderDelegate][provider:didDeactivateAudioSession]"); #endif - [self sendEventWithNameWrapper:CallKeepDidDeactivateAudioSession body:nil]; + [self sendEventWithNameWrapper:CallKeepDidDeactivateAudioSession body:@{}]; +} + +-(void)sendDefaultAudioInterruptionNotificationToStartAudioResource +{ + NSDictionary *userInfo = @{ + AVAudioSessionInterruptionTypeKey: [NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded], + AVAudioSessionInterruptionOptionKey: [NSNumber numberWithInt:AVAudioSessionInterruptionOptionShouldResume] + }; + [[NSNotificationCenter defaultCenter] postNotificationName:AVAudioSessionInterruptionNotification object:nil userInfo:userInfo]; } @end diff --git a/ios/Classes/CallKeepPushDelegate.h b/ios/Classes/CallKeepPushDelegate.h new file mode 100644 index 00000000..1d6d33b0 --- /dev/null +++ b/ios/Classes/CallKeepPushDelegate.h @@ -0,0 +1,14 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol CallKeepPushDelegate + +- (nullable NSDictionary*)mapPushPayload:(NSDictionary* _Nonnull)payload; + +@optional +- (void)onCallEvent:(NSString* _Nonnull)event withCallData:(NSDictionary* _Nonnull)callData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Classes/FlutterCallkeepPlugin.h b/ios/Classes/FlutterCallkeepPlugin.h index 097fb9ad..3fd1329b 100644 --- a/ios/Classes/FlutterCallkeepPlugin.h +++ b/ios/Classes/FlutterCallkeepPlugin.h @@ -1,11 +1,11 @@ #import @interface FlutterCallkeepPlugin : NSObject -+ (instancetype)sharedInstance; -+ (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - options:(NSDictionary *)options NS_AVAILABLE_IOS(9_0); -- (BOOL)application:(UIApplication *)application - continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void (^)(NSArray *_Nullable))restorationHandler; ++ (instancetype _Nullable)sharedInstance; ++ (BOOL)application:(UIApplication * _Nullable)application + openURL:(NSURL * _Nullable)url + options:(NSDictionary * _Nullable)options NS_AVAILABLE_IOS(9_0); +- (BOOL)application:(UIApplication * _Nullable)application + continueUserActivity:(NSUserActivity * _Nullable)userActivity + restorationHandler:(void (^ __nullable)(NSArray *_Nullable))restorationHandler; @end diff --git a/ios/Classes/FlutterCallkeepPlugin.m b/ios/Classes/FlutterCallkeepPlugin.m index 2bd577da..6ee072e6 100644 --- a/ios/Classes/FlutterCallkeepPlugin.m +++ b/ios/Classes/FlutterCallkeepPlugin.m @@ -64,7 +64,9 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *_Nullable))restorationHandler { - return NO; + return [CallKeep application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; } + (BOOL)application:(UIApplication *)application diff --git a/lib/callkeep.dart b/lib/callkeep.dart index 9ffa5076..c40468b0 100644 --- a/lib/callkeep.dart +++ b/lib/callkeep.dart @@ -1,3 +1,4 @@ export 'src/actions.dart'; export 'src/api.dart'; +export 'src/call.dart'; export 'src/event.dart'; diff --git a/lib/src/actions.dart b/lib/src/actions.dart index ea8386eb..18331589 100644 --- a/lib/src/actions.dart +++ b/lib/src/actions.dart @@ -1,78 +1,94 @@ +import 'package:callkeep/src/call.dart'; import 'event.dart'; class CallKeepDidReceiveStartCallAction extends EventType { - CallKeepDidReceiveStartCallAction(); CallKeepDidReceiveStartCallAction.fromMap(Map arguments) - : callUUID = arguments['callUUID'] as String, - handle = arguments['handle'] as String, - name = arguments['name'] as String; - String callUUID; - String handle; - String name; + : callData = CallData.fromMap(arguments); + final CallData callData; } class CallKeepPerformAnswerCallAction extends EventType { - CallKeepPerformAnswerCallAction(); CallKeepPerformAnswerCallAction.fromMap(Map arguments) - : callUUID = arguments['callUUID'] as String; - String callUUID; + : callData = CallData.fromMap(arguments); + final CallData callData; +} + +class CallKeepShowIncomingCallAction extends EventType { + CallKeepShowIncomingCallAction.fromMap(Map arguments) + : callData = CallData.fromMap(arguments); + final CallData callData; } class CallKeepPerformEndCallAction extends EventType { - CallKeepPerformEndCallAction(); CallKeepPerformEndCallAction.fromMap(Map arguments) - : callUUID = arguments['callUUID'] as String; - String callUUID; + : callUUID = arguments['callUUID']; + final String? callUUID; +} + +class CallKeepPerformRejectCallAction extends EventType { + CallKeepPerformRejectCallAction.fromMap(Map arguments) + : callUUID = arguments['callUUID']; + final String? callUUID; +} + +class CallKeepDidChangeAudioAction extends EventType { + CallKeepDidChangeAudioAction.fromMap(Map arguments) + : callUUID = arguments['callUUID'], + audioRoute = arguments['audioRoute']; + final String? callUUID; + final int? audioRoute; +} + +class CallKeepDidReceiveFailedCallAction extends EventType { + CallKeepDidReceiveFailedCallAction.fromMap(Map arguments) + : callData = CallData.fromMap(arguments); + final CallData callData; +} + +class CallKeepDidFailCallAction extends EventType { + CallKeepDidFailCallAction.fromMap(Map arguments) + : callUUID = arguments['callUUID'], + action = arguments['action']; + final String? callUUID; + final String? action; } class CallKeepDidActivateAudioSession extends EventType { - CallKeepDidActivateAudioSession(); + const CallKeepDidActivateAudioSession(); } class CallKeepDidDeactivateAudioSession extends EventType { - CallKeepDidDeactivateAudioSession(); + const CallKeepDidDeactivateAudioSession(); } class CallKeepDidDisplayIncomingCall extends EventType { - CallKeepDidDisplayIncomingCall(); CallKeepDidDisplayIncomingCall.fromMap(Map arguments) - : callUUID = arguments['callUUID'] as String, - handle = arguments['handle'] as String, - localizedCallerName = arguments['localizedCallerName'] as String, - hasVideo = arguments['hasVideo'] as bool, - fromPushKit = arguments['fromPushKit'] as bool; - String callUUID; - String handle; - String localizedCallerName; - bool hasVideo; - bool fromPushKit; + : callData = CallData.fromMap(arguments); + final CallData callData; } class CallKeepDidPerformSetMutedCallAction extends EventType { - CallKeepDidPerformSetMutedCallAction(); CallKeepDidPerformSetMutedCallAction.fromMap(Map arguments) - : callUUID = arguments['callUUID'] as String, - muted = arguments['muted'] as bool; - String callUUID; - bool muted; + : callUUID = arguments['callUUID'], + muted = arguments['muted']; + final String? callUUID; + final bool? muted; } class CallKeepDidToggleHoldAction extends EventType { - CallKeepDidToggleHoldAction(); CallKeepDidToggleHoldAction.fromMap(Map arguments) - : callUUID = arguments['callUUID'] as String, - hold = arguments['hold'] as bool; - String callUUID; - bool hold; + : callUUID = arguments['callUUID'], + hold = arguments['hold']; + final String? callUUID; + final bool? hold; } class CallKeepDidPerformDTMFAction extends EventType { - CallKeepDidPerformDTMFAction(); CallKeepDidPerformDTMFAction.fromMap(Map arguments) - : callUUID = arguments['callUUID'] as String, - digits = arguments['digits'] as String; - String callUUID; - String digits; + : callUUID = arguments['callUUID'], + digits = arguments['digits']; + final String? callUUID; + final String? digits; } class CallKeepProviderReset extends EventType { @@ -86,3 +102,9 @@ class CallKeepCheckReachability extends EventType { class CallKeepDidLoadWithEvents extends EventType { CallKeepDidLoadWithEvents(); } + +class CallKeepPushKitToken extends EventType { + CallKeepPushKitToken.fromMap(Map arguments) + : token = arguments['token']; + final String? token; +} diff --git a/lib/src/api.dart b/lib/src/api.dart index 5791462b..45a8ea99 100644 --- a/lib/src/api.dart +++ b/lib/src/api.dart @@ -1,16 +1,9 @@ import 'dart:async'; import 'dart:io'; + import 'package:flutter/services.dart'; -import 'package:flutter/material.dart' - show - showDialog, - AlertDialog, - BuildContext, - FlatButton, - Navigator, - Text, - Widget; -import 'package:flutter/services.dart' show MethodChannel; + +import 'package:logger/logger.dart'; import 'actions.dart'; import 'event.dart'; @@ -20,18 +13,34 @@ bool get supportConnectionService => !isIOS && int.parse(Platform.version) >= 23; class FlutterCallkeep extends EventManager { - FlutterCallkeep() { + factory FlutterCallkeep() { + return _instance; + } + FlutterCallkeep._internal() { _event.setMethodCallHandler(eventListener); } - BuildContext _context; + static final FlutterCallkeep _instance = FlutterCallkeep._internal(); static const MethodChannel _channel = MethodChannel('FlutterCallKeep.Method'); static const MethodChannel _event = MethodChannel('FlutterCallKeep.Event'); + Future Function()? _showAlertDialog; - Future setup(Map options) async { + @override + Logger logger = Logger(); + + Future setup({ + Future Function()? showAlertDialog, + required Map options, + bool backgroundMode = false, + }) async { + _showAlertDialog = showAlertDialog; if (!isIOS) { - return _setupAndroid(options['android'] as Map); + await _setupAndroid( + options: options['android'], + backgroundMode: backgroundMode, + ); + return; } - return _setupIOS(options['ios'] as Map); + await _setupIOS(options: options['ios']); } Future registerPhoneAccount() async { @@ -49,73 +58,85 @@ class FlutterCallkeep extends EventManager { return _channel.invokeMethod('registerEvents', {}); } - Future hasDefaultPhoneAccount( - BuildContext context, Map options) async { - _context = context; + Future hasDefaultPhoneAccount( + Map options, + ) async { if (!isIOS) { - return _hasDefaultPhoneAccount(options); + return await _hasDefaultPhoneAccount(options); } - return; + + // return true on iOS because we don't want to block the endUser + return true; } - Future _checkDefaultPhoneAccount() async { - return await _channel - .invokeMethod('checkDefaultPhoneAccount', {}); + Future _checkDefaultPhoneAccount() async { + return await _channel.invokeMethod( + 'checkDefaultPhoneAccount', + {}, + ); } - Future _hasDefaultPhoneAccount(Map options) async { + Future _hasDefaultPhoneAccount(Map options) async { final hasDefault = await _checkDefaultPhoneAccount(); - final shouldOpenAccounts = await _alert(options, hasDefault); - if (shouldOpenAccounts) { - await _openPhoneAccounts(); + if (hasDefault == true) { + final shouldOpenAccounts = await _alert(); + if (shouldOpenAccounts) { + await _openPhoneAccounts(); + return true; + } + return false; } + return true; } - Future displayIncomingCall(String uuid, String handle, - {String localizedCallerName = '', - String handleType = 'number', - bool hasVideo = false}) async { - if (!isIOS) { - await _channel.invokeMethod( - 'displayIncomingCall', { - 'uuid': uuid, - 'handle': handle, - 'localizedCallerName': localizedCallerName - }); - return; - } + Future _hasPhoneAccount() async { + final result = await _channel.invokeMethod( + 'hasPhoneAccount', + {}, + ); + return result ?? false; + } + + Future displayIncomingCall({ + required String uuid, + required String handle, + String callerName = '', + String handleType = 'number', + bool hasVideo = false, + Map additionalData = const {}, + }) async { await _channel.invokeMethod('displayIncomingCall', { 'uuid': uuid, 'handle': handle, 'handleType': handleType, 'hasVideo': hasVideo, - 'localizedCallerName': localizedCallerName + 'callerName': callerName, + 'additionalData': additionalData }); } Future answerIncomingCall(String uuid) async { - if (!isIOS) { - await _channel.invokeMethod( - 'answerIncomingCall', {'uuid': uuid}); - } + await _channel.invokeMethod( + 'answerIncomingCall', + {'uuid': uuid}, + ); } - Future startCall(String uuid, String handle, String callerName, - {String handleType = 'number', bool hasVideo = false}) async { - if (!isIOS) { - await _channel.invokeMethod('startCall', { - 'uuid': uuid, - 'handle': handle, - 'callerName': callerName - }); - return; - } + Future startCall({ + required String uuid, + required String handle, + required String callerName, + String handleType = 'number', + bool hasVideo = false, + Map additionalData = const {}, + }) async { await _channel.invokeMethod('startCall', { 'uuid': uuid, 'handle': handle, 'callerName': callerName, 'handleType': handleType, - 'hasVideo': hasVideo + 'hasVideo': hasVideo, + 'additionalData': additionalData }); } @@ -135,9 +156,27 @@ class FlutterCallkeep extends EventManager { } } - Future reportEndCallWithUUID(String uuid, int reason) async => - await _channel.invokeMethod('reportEndCallWithUUID', - {'uuid': uuid, 'reason': reason}); + Future reportStartedCallWithUUID(String uuid) async { + if (!isIOS) { + await _channel.invokeMethod( + 'reportStartedCallWithUUID', {'uuid': uuid}); + } + } + + Future reportEndCallWithUUID({ + required String uuid, + required int reason, + bool notify = true, + }) async { + return await _channel.invokeMethod( + 'reportEndCallWithUUID', + { + 'uuid': uuid, + 'reason': reason, + 'notify': notify, + }, + ); + } /* * Android explicitly states we reject a call @@ -153,8 +192,29 @@ class FlutterCallkeep extends EventManager { } } - Future isCallActive(String uuid) async => await _channel - .invokeMethod('isCallActive', {'uuid': uuid}); + Future isCallActive(String uuid) async { + var resp = await _channel + .invokeMethod('isCallActive', {'uuid': uuid}); + if (resp != null) { + return resp; + } + return false; + } + + Future> activeCalls() async { + var resp = await _channel + .invokeMethod?>('activeCalls', {}); + if (resp != null) { + var uuids = []; + resp.forEach((element) { + if (element is String) { + uuids.add(element); + } + }); + return uuids; + } + return []; + } Future endCall(String uuid) async => await _channel .invokeMethod('endCall', {'uuid': uuid}); @@ -166,23 +226,32 @@ class FlutterCallkeep extends EventManager { if (isIOS) { return true; } - return await _channel + var resp = await _channel .invokeMethod('hasPhoneAccount', {}); + if (resp != null) { + return resp; + } + return false; } Future hasOutgoingCall() async { if (isIOS) { return true; } - return await _channel + var resp = await _channel .invokeMethod('hasOutgoingCall', {}); + if (resp != null) { + return resp; + } + return false; } - Future setMutedCall(String uuid, bool shouldMute) async => + Future setMutedCall( + {required String uuid, required bool shouldMute}) async => await _channel.invokeMethod( 'setMutedCall', {'uuid': uuid, 'muted': shouldMute}); - Future sendDTMF(String uuid, String key) async => + Future sendDTMF({required String uuid, required String key}) async => await _channel.invokeMethod( 'sendDTMF', {'uuid': uuid, 'key': key}); @@ -194,13 +263,13 @@ class FlutterCallkeep extends EventManager { ? await _channel.invokeMethod('checkSpeaker', {}) : throw Exception('CallKeep.checkSpeaker was called from unsupported OS'); - Future setAvailable(String state) async { + Future setAvailable({bool available = true}) async { if (isIOS) { return; } // Tell android that we are able to make outgoing calls - await _channel - .invokeMethod('setAvailable', {'state': state}); + await _channel.invokeMethod( + 'setAvailable', {'available': available}); } Future setCurrentCallActive(String callUUID) async { @@ -212,50 +281,62 @@ class FlutterCallkeep extends EventManager { 'setCurrentCallActive', {'uuid': callUUID}); } - Future updateDisplay(String uuid, - {String displayName, String handle}) async => + Future updateDisplay({ + required String uuid, + required String callerName, + required String handle, + }) async => await _channel.invokeMethod('updateDisplay', { 'uuid': uuid, - 'displayName': displayName, + 'callerName': callerName, 'handle': handle }); - Future setOnHold(String uuid, bool shouldHold) async => + Future setOnHold( + {required String uuid, required bool shouldHold}) async => await _channel.invokeMethod( 'setOnHold', {'uuid': uuid, 'hold': shouldHold}); - Future setReachable() async { + Future setReachable({bool reachable = true}) async { if (isIOS) { return; } - await _channel.invokeMethod('setReachable', {}); + await _channel.invokeMethod('setReachable', { + 'reachable': reachable, + }); } // @deprecated - Future reportUpdatedCall( - String uuid, String localizedCallerName) async { - print( + Future reportUpdatedCall({ + required String uuid, + required String callerName, + }) async { + logger.d( 'CallKeep.reportUpdatedCall is deprecated, use CallKeep.updateDisplay instead'); return isIOS - ? await _channel.invokeMethod( - 'reportUpdatedCall', { + ? await _channel + .invokeMethod('reportUpdatedCall', { 'uuid': uuid, - 'localizedCallerName': localizedCallerName + 'callerName': callerName, }) : throw Exception( 'CallKeep.reportUpdatedCall was called from unsupported OS'); } - Future backToForeground() async { + Future backToForeground() async { if (isIOS) { - return; + return false; } - - await _channel.invokeMethod('backToForeground', {}); + var resp = await _channel + .invokeMethod('backToForeground', {}); + if (resp != null) { + return resp; + } + return false; } - Future _setupIOS(Map options) async { + Future _setupIOS({required Map options}) async { if (options['appName'] == null) { throw Exception('CallKeep.setup: option "appName" is required'); } @@ -267,12 +348,26 @@ class FlutterCallkeep extends EventManager { .invokeMethod('setup', {'options': options}); } - Future _setupAndroid(Map options) async { + Future _setupAndroid({ + required Map options, + required bool backgroundMode, + }) async { await _channel.invokeMethod('setup', {'options': options}); - final showAccountAlert = await _checkPhoneAccountPermission( - options['additionalPermissions'] as List ?? []); - final shouldOpenAccounts = await _alert(options, showAccountAlert); + if (backgroundMode) { + return true; + } + + final additionalPermissions = options['additionalPermissions'] as List?; + final hasPermissions = await requestPermissions( + additionalPermissions?.cast(), + ); + if (!hasPermissions) return false; + + final hasPhoneAccount = await _hasPhoneAccount(); + if (hasPhoneAccount != false) return true; + + final shouldOpenAccounts = await _alert(); if (shouldOpenAccounts) { await _openPhoneAccounts(); return true; @@ -280,96 +375,87 @@ class FlutterCallkeep extends EventManager { return false; } + Future openPhoneAccounts() => _openPhoneAccounts(); + Future _openPhoneAccounts() async { - if (!Platform.isAndroid) { + if (isIOS) { return; } await _channel.invokeMethod('openPhoneAccounts', {}); } - Future _checkPhoneAccountPermission( - [List optionalPermissions]) async { - if (!Platform.isAndroid) { + Future requestPermissions([List? optionalPermissions]) async { + if (isIOS) { return true; } - return await _channel - .invokeMethod('checkPhoneAccountPermission', { - 'optionalPermissions': optionalPermissions ?? [], + var resp = await _channel + .invokeMethod('requestPermissions', { + 'additionalPermissions': optionalPermissions ?? [], }); + return resp ?? false; + } + + Future hasPermissions() async { + if (isIOS) { + return true; + } + var resp = await _channel.invokeMethod('hasPermissions'); + return resp ?? false; } - Future _alert(Map options, bool condition) async { - if (_context == null) { + Future _alert() async { + if (_showAlertDialog == null) { + logger.w('No alert dialog function provided. Defaulting to false.'); return false; } - return await _showAlertDialog( - _context, - options['alertTitle'] as String, - options['alertDescription'] as String, - options['cancelButton'] as String, - options['okButton'] as String); - } - - Future _showAlertDialog(BuildContext context, String alertTitle, - String alertDescription, String cancelButton, String okButton) { - return showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text(alertTitle ?? 'Permissions required'), - content: Text(alertDescription ?? - 'This application needs to access your phone accounts'), - actions: [ - FlatButton( - child: Text(cancelButton ?? 'Cancel'), - onPressed: () => - Navigator.of(context, rootNavigator: true).pop(false), - ), - FlatButton( - child: Text(okButton ?? 'ok'), - onPressed: () => - Navigator.of(context, rootNavigator: true).pop(true), - ), - ], - ), - ); + return await _showAlertDialog!(); + } + + Future setForegroundServiceSettings({ + required Map settings, + }) async { + if (isIOS) { + return; + } + await _channel.invokeMethod('foregroundService', { + 'settings': {'foregroundService': settings} + }); } Future eventListener(MethodCall call) async { - print('[CallKeep] INFO: received event "${call.method}" ${call.arguments}'); + logger.d( + '[CallKeep] INFO: received event "${call.method}" ${call.arguments}'); + final data = call.arguments as Map; switch (call.method) { case 'CallKeepDidReceiveStartCallAction': - emit(CallKeepDidReceiveStartCallAction.fromMap( - call.arguments as Map)); + emit(CallKeepDidReceiveStartCallAction.fromMap(data)); break; case 'CallKeepPerformAnswerCallAction': - emit(CallKeepPerformAnswerCallAction.fromMap( - call.arguments as Map)); + emit(CallKeepPerformAnswerCallAction.fromMap(data)); + break; + case 'CallKeepPerformRejectCallAction': + emit(CallKeepPerformRejectCallAction.fromMap(data)); break; case 'CallKeepPerformEndCallAction': - emit(CallKeepPerformEndCallAction.fromMap( - call.arguments as Map)); + emit(CallKeepPerformEndCallAction.fromMap(data)); break; case 'CallKeepDidActivateAudioSession': emit(CallKeepDidActivateAudioSession()); break; case 'CallKeepDidDeactivateAudioSession': - emit(CallKeepDidActivateAudioSession()); + emit(CallKeepDidDeactivateAudioSession()); break; case 'CallKeepDidDisplayIncomingCall': - emit(CallKeepDidDisplayIncomingCall.fromMap( - call.arguments as Map)); + emit(CallKeepDidDisplayIncomingCall.fromMap(data)); break; case 'CallKeepDidPerformSetMutedCallAction': - emit(CallKeepDidPerformSetMutedCallAction.fromMap( - call.arguments as Map)); + emit(CallKeepDidPerformSetMutedCallAction.fromMap(data)); break; case 'CallKeepDidToggleHoldAction': - emit(CallKeepDidToggleHoldAction.fromMap( - call.arguments as Map)); + emit(CallKeepDidToggleHoldAction.fromMap(data)); break; case 'CallKeepDidPerformDTMFAction': - emit(CallKeepDidPerformDTMFAction.fromMap( - call.arguments as Map)); + emit(CallKeepDidPerformDTMFAction.fromMap(data)); break; case 'CallKeepProviderReset': emit(CallKeepProviderReset()); @@ -380,6 +466,9 @@ class FlutterCallkeep extends EventManager { case 'CallKeepDidLoadWithEvents': emit(CallKeepDidLoadWithEvents()); break; + case 'CallKeepPushKitToken': + emit(CallKeepPushKitToken.fromMap(data)); + break; } } } diff --git a/lib/src/call.dart b/lib/src/call.dart new file mode 100644 index 00000000..c2c07dfa --- /dev/null +++ b/lib/src/call.dart @@ -0,0 +1,34 @@ +class CallData { + const CallData( + this.callUUID, + this.handle, + this.name, + this.hasVideo, + this.fromPushKit, + this.additionalData, + ); + + factory CallData.fromMap(Map arguments) { + final callUUID = arguments['callUUID']; + final handle = arguments['handle']; + final name = arguments['name']; + final hasVideo = arguments['hasVideo']; + final fromPushKit = arguments['fromPushKit']; + final additionalData = arguments['additionalData']; + return CallData( + callUUID, + handle, + name, + hasVideo, + fromPushKit, + additionalData == null ? null : Map.from(additionalData), + ); + } + + final String? callUUID; + final String? handle; + final String? name; + final bool? hasVideo; + final bool? fromPushKit; + final Map? additionalData; +} diff --git a/lib/src/event.dart b/lib/src/event.dart index ef59dbbd..74462a0a 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,5 +1,9 @@ +import 'package:flutter/cupertino.dart'; + +import 'package:logger/web.dart'; + abstract class EventType { - EventType(); + const EventType(); void sanityCheck() {} } @@ -21,7 +25,8 @@ abstract class EventType { /// -- do something here /// }); class EventManager { - Map> listeners = >{}; + Logger logger = Logger(); + Map> listeners = >{}; /// returns true if there are any listeners associated with the EventType for this instance of EventManager bool hasListeners(EventType event) { @@ -39,25 +44,21 @@ class EventManager { /// /// Thus: /// - /// on(EventCallState(),(EventCallState event){ + /// on((EventCallState event){ /// -- do something here /// }); - void on(T eventType, void Function(T event) listener) { - assert(listener != null, 'Null listener'); - assert(eventType != null, 'Null eventType'); - _addListener(eventType.runtimeType, listener); + void on(ValueChanged listener) { + _addListener(T, listener); } /// It isn't possible to have type constraints here on the listener, /// BUT very importantly this method is private and /// all the methods that call it enforce the types!!!! - void _addListener(Type runtimeType, dynamic listener) { - assert(listener != null, 'Null listener'); - assert(runtimeType != null, 'Null runtimeType'); + void _addListener(Type runtimeType, Function listener) { try { var targets = listeners[runtimeType]; if (targets == null) { - targets = []; + targets = []; listeners[runtimeType] = targets; } targets.remove(listener); @@ -69,7 +70,7 @@ class EventManager { /// add all event handlers from an other instance of EventManager to this one. void addAllEventHandlers(EventManager other) { - other.listeners.forEach((Type runtimeType, List otherListeners) { + other.listeners.forEach((Type runtimeType, List otherListeners) { // ignore: avoid_function_literals_in_foreach_calls otherListeners.forEach((dynamic otherListener) { _addListener(runtimeType, otherListener); @@ -77,15 +78,11 @@ class EventManager { }); } - void remove( - T eventType, void Function(T event) listener) { - final targets = listeners[eventType.runtimeType]; - if (targets == null) { - return; - } - // logger.warn("removing $eventType on $listener"); + void remove(ValueChanged listener) { + final targets = listeners[T]; + if (targets == null) return; if (!targets.remove(listener)) { - print('Failed to remove any listeners for EventType $eventType'); + logger.d('Failed to remove any listeners for EventType $T'); } } @@ -97,9 +94,8 @@ class EventManager { if (targets != null) { // avoid concurrent modification - final copy = List.from(targets); // ignore: avoid_function_literals_in_foreach_calls - copy.forEach((dynamic target) { + targets.toList(growable: false).forEach((dynamic target) { try { // logger.warn("invoking $event on $target"); target(event); diff --git a/pubspec.yaml b/pubspec.yaml index 8db27edc..7124e68f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,22 +1,23 @@ name: callkeep description: iOS CallKit framework and Android ConnectionService for Flutter. -version: 0.1.1 -author: duanweiwei1982@gmail.com +version: 0.4.1 homepage: https://github.com/flutter-webrtc/callkeep environment: - sdk: ">=2.2.2 <3.0.0" - flutter: ^1.10.0 + sdk: ">=2.12.0 <4.0.0" + flutter: ">=1.22.0" dependencies: + firebase_messaging: ^15.2.1 flutter: sdk: flutter + logger: ^2.5.0 + uuid: ^4.5.1 dev_dependencies: flutter_test: sdk: flutter - - pedantic: ^1.9.0 + import_sorter: ^4.6.0 flutter: plugin: diff --git a/tool/README.md b/tool/README.md new file mode 100644 index 00000000..6c554df6 --- /dev/null +++ b/tool/README.md @@ -0,0 +1,23 @@ +# CallKeep test tool + +This tool is used to demonstrate the basic push process for verifying callkeep. written in golang. + +prepare to run: + +`cd tools && go mod tidy` + +## For iOS APNS + +please refer to `https://developer.apple.com/account/resources/certificates/add`. + +Choose `VoIP Services Certificate` to create a push certificate, download `voip_services.cer` and install it to the keychain tool, export its private key rename it to `callkeep-apns.p12` + +`go run cmd/main.go -i +8618612345678 -p apns -d $ios_device_token` + +## For Android FCM + +please refer to `https://console.firebase.google.com/project/[your project]/settings/serviceaccounts/adminsdk` + +Select the `go` sdk format under your fcm project to download `serviceAccountKey.json` and rename it to `callkeep-fcm.json` + +`go run cmd/main.go -i +8618612345678 -p fcm -d $android_fcm_token` diff --git a/tool/cmd/main.go b/tool/cmd/main.go new file mode 100644 index 00000000..ed5db82b --- /dev/null +++ b/tool/cmd/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + + "github.com/flutter-webrtc/callkeep/tools/pkg/fcm" + "github.com/flutter-webrtc/callkeep/tools/pkg/pushkit" + log "github.com/pion/ion-log" +) + +func pushCallKeep(provider string, token string, payload map[string]string) error { + log.Infof("CallKeep Push Request") + + log.Infof("provider=%v", provider) + log.Infof("token=%v", token) + + data, _ := json.MarshalIndent(payload, "", " ") + log.Infof("payload=\n%v", string(data)) + + switch provider { + case "apns": + pushkit.APNSPush("./callkeep-apns.p12", token, payload) + return nil + case "fcm": + fcm.FCMPush("./callkeep-fcm.json", token, payload) + return nil + } + return fmt.Errorf("%v provider not found", provider) +} + +func main() { + log.Init("info") + h := false + flag.BoolVar(&h, "h", false, "help") + provider := "" + flag.StringVar(&provider, "p", "fcm", "push provider type fcm | apns.") + token := "" + flag.StringVar(&token, "d", "", "device token.") + cid := "" + flag.StringVar(&cid, "i", "", "caller id.") + flag.Parse() + + if h || len(token) == 0 || len(cid) == 0 { + flag.Usage() + return + } + + log.Infof("try push") + + payload := map[string]string{ + "caller_id": cid, + "caller_name": "push test", + "caller_id_type": "number", + "has_video": "false", + } + + err := pushCallKeep(provider, token, payload) + if err != nil { + log.Errorf("push failed %v", err) + } +} diff --git a/tool/go.mod b/tool/go.mod new file mode 100644 index 00000000..b059a42d --- /dev/null +++ b/tool/go.mod @@ -0,0 +1,13 @@ +module github.com/flutter-webrtc/callkeep/tools + +go 1.16 + +require ( + cloud.google.com/go/firestore v1.5.0 // indirect + firebase.google.com/go v3.13.0+incompatible + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/pion/ion-log v1.2.0 + github.com/sideshow/apns2 v0.20.0 + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect + google.golang.org/api v0.48.0 +) diff --git a/tool/go.sum b/tool/go.sum new file mode 100644 index 00000000..99fcc1e5 --- /dev/null +++ b/tool/go.sum @@ -0,0 +1,537 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0 h1:bAMqZidYkmIsUqe6PtkEPT7Q+vfizScn+jfNA6jwK9c= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.5.0 h1:4qNItsmc4GP6UOZPGemmHY4ZfPofVhcaKXsYw9wm9oA= +cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4= +firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/pion/ion-log v1.2.0 h1:FYJd+BWmg9FQYP729sgj9ctrr3KaLysnK2BILVpYL+Y= +github.com/pion/ion-log v1.2.0/go.mod h1:oUlvCy7LZNPzOxmCZVraaMhcS/hB9XFog4m1A8QpVgM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sideshow/apns2 v0.20.0 h1:5Lzk4DUq+waVc6/BkKzpDTpQjtk/BZOP0YsayBpY1NE= +github.com/sideshow/apns2 v0.20.0/go.mod h1:f7dArLPLbiZ3qPdzzrZXdCSlMp8FD0p6z7tHssDOLvk= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0 h1:RDAPWfNFY06dffEXfn7hZF5Fr1ZbnChzfQZAPyBd1+I= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08 h1:pc16UedxnxXXtGxHCSUhafAoVHQZ0yXl8ZelMH4EETc= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/tool/pkg/fcm/fcm.go b/tool/pkg/fcm/fcm.go new file mode 100644 index 00000000..f9aa06f0 --- /dev/null +++ b/tool/pkg/fcm/fcm.go @@ -0,0 +1,36 @@ +package fcm + +import ( + "context" + + firebase "firebase.google.com/go" + "firebase.google.com/go/messaging" + log "github.com/pion/ion-log" + "google.golang.org/api/option" +) + +func FCMPush(fcmCert string, token string, payload map[string]string) (string, error) { + opt := option.WithCredentialsFile(fcmCert) + app, err := firebase.NewApp(context.Background(), nil, opt) + + // Obtain a messaging.Client from the App. + ctx := context.Background() + client, err := app.Messaging(ctx) + + // See documentation on defining a message payload. + message := &messaging.Message{ + Data: payload, + Token: token, + } + + // Send a message to the device corresponding to the provided + // registration token. + response, err := client.Send(ctx, message) + if err != nil { + log.Errorf("FCMPush: err %v", err) + return "", err + } + // Response is a message ID string. + log.Infof("Successfully sent message: %v", response) + return response, err +} diff --git a/tool/pkg/pushkit/pushkit.go b/tool/pkg/pushkit/pushkit.go new file mode 100644 index 00000000..82e9211a --- /dev/null +++ b/tool/pkg/pushkit/pushkit.go @@ -0,0 +1,35 @@ +package pushkit + +import ( + "encoding/json" + + log "github.com/pion/ion-log" + "github.com/sideshow/apns2" + "github.com/sideshow/apns2/certificate" +) + +func APNSPush(p12 string, token string, payload map[string]string) { + + cert, err := certificate.FromP12File(p12, "") + if err != nil { + log.Errorf("Cert Error: %v", err) + } + + data, _ := json.Marshal(payload) + notification := &apns2.Notification{} + notification.DeviceToken = token + notification.PushType = "voip" // voip | alert + notification.Payload = []byte(data) // See Payload section below + + // If you want to test push notifications for builds running directly from XCode (Development), use + client := apns2.NewClient(cert).Development() + // For apps published to the app store or installed as an ad-hoc distribution use Production() + //client := apns2.NewClient(cert).Production() + res, err := client.Push(notification) + + if err != nil { + log.Errorf("Error: %v", err) + } + + log.Infof("Push response: %v %v %v\n", res.StatusCode, res.ApnsID, res.Reason) +}