diff --git a/android/build.gradle b/android/build.gradle index e25d58e..5c77876 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion 29 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/android/src/main/kotlin/co/sunnyapp/flutter_phone_state/FlutterPhoneStatePlugin.kt b/android/src/main/kotlin/co/sunnyapp/flutter_phone_state/FlutterPhoneStatePlugin.kt index 7fc1ea4..6107817 100644 --- a/android/src/main/kotlin/co/sunnyapp/flutter_phone_state/FlutterPhoneStatePlugin.kt +++ b/android/src/main/kotlin/co/sunnyapp/flutter_phone_state/FlutterPhoneStatePlugin.kt @@ -4,30 +4,40 @@ import android.content.Context import android.telephony.PhoneStateListener import android.telephony.TelephonyManager import io.flutter.plugin.common.EventChannel +import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall 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 androidx.annotation.NonNull -class FlutterPhoneStatePlugin(context: Context) : MethodCallHandler, EventChannel.StreamHandler { - companion object { - @JvmStatic - fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), "flutter_phone_state") - val plugin = FlutterPhoneStatePlugin(registrar.context()) - channel.setMethodCallHandler(plugin) +class FlutterPhoneStatePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { + private lateinit var channel: MethodChannel + private lateinit var eventSink: EventChannel + private lateinit var binding: FlutterPlugin.FlutterPluginBinding + private lateinit var context: Context - val eventSink = EventChannel(registrar.messenger(), "co.sunnyapp/phone_events") - eventSink.setStreamHandler(plugin) - } - } - - private val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + private lateinit var telephonyManager: TelephonyManager /// So it doesn't get collected private lateinit var listener:PhoneStateListener + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + binding = flutterPluginBinding + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_phone_state") + channel.setMethodCallHandler(this) + eventSink = EventChannel(flutterPluginBinding.binaryMessenger, "co.sunnyapp/phone_events") + eventSink.setStreamHandler(this) + context = flutterPluginBinding.applicationContext + telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + } + + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + eventSink.setStreamHandler(null) + } + override fun onMethodCall(call: MethodCall, result: Result) { if (call.method == "getPlatformVersion") { result.success("Android ${android.os.Build.VERSION.RELEASE}") diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 5b9135f..7c69198 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> + diff --git a/example/android/app/src/main/kotlin/co/sunnyapp/flutter_phone_state_example/MainActivity.kt b/example/android/app/src/main/kotlin/co/sunnyapp/flutter_phone_state_example/MainActivity.kt index 300c6d5..08004c0 100644 --- a/example/android/app/src/main/kotlin/co/sunnyapp/flutter_phone_state_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/co/sunnyapp/flutter_phone_state_example/MainActivity.kt @@ -1,12 +1,9 @@ package co.sunnyapp.flutter_phone_state_example -import android.os.Bundle -import io.flutter.app.FlutterActivity -import io.flutter.plugins.GeneratedPluginRegistrant +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.firebase.core.FirebaseCorePlugin; class MainActivity: FlutterActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - GeneratedPluginRegistrant.registerWith(this) - } + } diff --git a/example/android/build.gradle b/example/android/build.gradle index 3100ad2..c505a86 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/ios/Podfile b/example/ios/Podfile index b30a428..1e8c3c9 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -10,81 +10,32 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end - generated_key_values + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + target 'Runner' do use_frameworks! use_modular_headers! - - # Flutter Pod - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end - end - - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' - - # Plugin Pods - - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') - end + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end -# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. -install! 'cocoapods', :disable_input_output_paths => true - post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 7fa8557..9b683b7 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -19,10 +19,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher/ios" SPEC CHECKSUMS: - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c flutter_phone_state: ebb27962d725cbe0bdf498654c2a9083acf91b75 - url_launcher: a1c0cc845906122c4784c542523d8cacbded5626 + url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef -PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83 +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c -COCOAPODS: 1.8.4 +COCOAPODS: 1.10.1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index df5b730..52fc0f8 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,12 +9,8 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5510357B74253A2A913E4BDA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B14E8BC5A505DE4661E9079 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -27,8 +23,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -39,7 +33,6 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 5B2F318F1C214126BF760093 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -47,7 +40,6 @@ 955826ACA8BDC5D39FFBC095 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -62,8 +54,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 5510357B74253A2A913E4BDA /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -92,9 +82,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -230,7 +218,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -274,9 +262,14 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/flutter_phone_state/flutter_phone_state.framework", + "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_phone_state.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -319,7 +312,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -397,7 +389,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -453,7 +444,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 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/lib/main.dart b/example/lib/main.dart index 5a8a099..96f8bbd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -18,11 +18,11 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - List _rawEvents; - List _phoneEvents; + late List _rawEvents; + late List _phoneEvents; /// The result of the user typing - String _phoneNumber; + String? _phoneNumber; @override void initState() { @@ -31,7 +31,7 @@ class _MyAppState extends State { _rawEvents = _accumulate(FlutterPhoneState.rawPhoneEvents); } - List _accumulate(Stream input) { + List _accumulate(Stream input) { final items = []; input.forEach((item) { if (item != null) { @@ -44,8 +44,7 @@ class _MyAppState extends State { } /// Extracts a list of phone calls from the accumulated events - Iterable get _completedCalls => - Map.fromEntries(_phoneEvents.reversed.map((PhoneCallEvent event) { + Iterable get _completedCalls => Map.fromEntries(_phoneEvents.reversed.map((PhoneCallEvent event) { return MapEntry(event.call.id, event.call); })).values.where((c) => c.isComplete).toList(); @@ -64,29 +63,26 @@ class _MyAppState extends State { flex: 1, child: TextField( onChanged: (v) => _phoneNumber = v, - decoration: InputDecoration(labelText: "Phone number"), + decoration: InputDecoration(labelText: 'Phone number'), )), MaterialButton( onPressed: () => _initiateCall(), - child: Text("Make Call", style: TextStyle(color: Colors.white)), color: Colors.blue, + child: Text('Make Call', style: TextStyle(color: Colors.white)), ), ]), verticalSpace, - _title("Current Calls"), - for (final call in FlutterPhoneState.activeCalls) - _CallCard(phoneCall: call), - if (FlutterPhoneState.activeCalls.isEmpty) - Center(child: Text("No Active Calls")), - _title("Call History"), + _title('Current Calls'), + for (final call in FlutterPhoneState.activeCalls) _CallCard(phoneCall: call), + if (FlutterPhoneState.activeCalls.isEmpty) Center(child: Text('No Active Calls')), + _title('Call History'), for (final call in _completedCalls) _CallCard( phoneCall: call, ), - if (_completedCalls.isEmpty) - Center(child: Text("No Completed Calls")), + if (_completedCalls.isEmpty) Center(child: Text('No Completed Calls')), verticalSpace, - _title("Raw Event History"), + _title('Raw Event History'), if (_rawEvents.isNotEmpty) Padding( padding: EdgeInsets.all(10), @@ -94,12 +90,12 @@ class _MyAppState extends State { children: [ TableRow(children: [ Text( - "id", + 'id', style: listHeaderStyle, maxLines: 1, ), - Text("number", style: listHeaderStyle), - Text("event", style: listHeaderStyle), + Text('number', style: listHeaderStyle), + Text('event', style: listHeaderStyle), ]), for (final event in _rawEvents) TableRow(children: [ @@ -110,7 +106,7 @@ class _MyAppState extends State { ], ), ), - if (_rawEvents.isEmpty) Center(child: Text("No Raw Events")), + if (_rawEvents.isEmpty) Center(child: Text('No Raw Events')), ], ), ), @@ -132,10 +128,11 @@ class _MyAppState extends State { child: Text(text?.toString() ?? '-', maxLines: 1, style: headerStyle)); } - _initiateCall() { - if (_phoneNumber?.isNotEmpty == true) { + Future _initiateCall() async { + final phoneNumber = _phoneNumber; + if (phoneNumber != null && phoneNumber.isNotEmpty) { setState(() { - FlutterPhoneState.startPhoneCall(_phoneNumber); + FlutterPhoneState.startPhoneCall(phoneNumber); }); } } @@ -144,15 +141,17 @@ class _MyAppState extends State { class _CallCard extends StatelessWidget { final PhoneCall phoneCall; - const _CallCard({Key key, this.phoneCall}) : super(key: key); + const _CallCard({ + Key? key, + required this.phoneCall, + }) : super(key: key); @override Widget build(BuildContext context) { return Card( child: ListTile( dense: true, - leading: Icon( - phoneCall.isOutbound ? Icons.arrow_upward : Icons.arrow_downward), + leading: Icon(phoneCall.isOutbound ? Icons.arrow_upward : Icons.arrow_downward), title: Text( "+${phoneCall.phoneNumber ?? "Unknown number"}: ${value(phoneCall.status)}", overflow: TextOverflow.visible, @@ -160,11 +159,10 @@ class _CallCard extends StatelessWidget { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (phoneCall.id?.isNotEmpty == true) - Text("id: ${truncate(phoneCall.id, 12)}"), + if (phoneCall.id.isNotEmpty) Text('id: ${truncate(phoneCall.id, 12)}'), for (final event in phoneCall.events) Text( - "- ${value(event.status) ?? "-"}", + '- ${value(event.status)}', maxLines: 1, ), ], @@ -172,7 +170,7 @@ class _CallCard extends StatelessWidget { trailing: FutureBuilder( builder: (context, snap) { if (snap.hasData && snap.data?.isComplete == true) { - return Text("${phoneCall.duration?.inSeconds ?? '?'}s"); + return Text('${phoneCall.duration.inSeconds}s'); } else { return CircularProgressIndicator(); } diff --git a/example/pubspec.lock b/example/pubspec.lock index 6851511..c9125f2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,56 +7,49 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.5.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" + version: "1.15.0" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "3.0.1" cupertino_icons: dependency: "direct main" description: @@ -70,7 +63,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" flutter: dependency: "direct main" description: flutter @@ -82,7 +75,7 @@ packages: path: ".." relative: true source: path - version: "0.5.9" + version: "2.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -100,55 +93,55 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.16.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.1" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" - platform_detect: - dependency: transitive + version: "1.8.0" + pedantic: + dependency: "direct dev" description: - name: platform_detect + name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.11.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -160,105 +153,112 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.0" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" + version: "0.2.19" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" url_launcher: dependency: transitive description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.5.0" + version: "6.0.6" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+1" + version: "2.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+7" + version: "2.0.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "2.0.3" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "2.0.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" uuid: dependency: transitive description: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.4" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0" sdks: - dart: ">=2.10.0-110 <2.11.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + dart: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a7d1b6d..4729abd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,25 +1,26 @@ name: flutter_phone_state_example description: Demonstrates how to use the flutter_phone_state plugin. publish_to: 'none' +version: 2.0.0 environment: - sdk: ">=2.2.2 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter - intl: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 + cupertino_icons: + intl: dev_dependencies: flutter_test: sdk: flutter - flutter_phone_state: path: ../ + pedantic: ^1.11.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index c878c7d..a4797e1 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -18,8 +18,7 @@ void main() { // 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/lib/extensions.dart b/lib/extensions.dart deleted file mode 100644 index f010a4f..0000000 --- a/lib/extensions.dart +++ /dev/null @@ -1,45 +0,0 @@ -//extension DateTimeExt on DateTime { -// Duration sinceNow() => -(this.difference(DateTime.now())); -//} -// -//extension IterableExtension on List { -// X find([bool filter(X input)]) { -// return this?.firstWhere(filter, orElse: () => null); -// } -// -// X lastOrNull([bool filter(X input)]) { -// return this?.lastWhere(filter, orElse: () => null); -// } -// -// X firstOrNull([bool filter(X input)]) { -// return this?.firstWhere(filter, orElse: () => null); -// } -//} -// -//extension StringExt on String { -// String truncate(int length) { -// if (this == null) return this; -// if (this.length <= length) { -// return this; -// } else { -// return this.substring(0, length); -// } -// } -// -// bool get isNullOrEmpty => this?.isNotEmpty != true; -// -// bool get isNotNullOrEmpty => !isNullOrEmpty; -// -// bool get isNullOrBlank => this == null || this.trim().isEmpty == true; -// -// bool get isNotNullOrBlank => !isNullOrBlank; -// -// String orEmpty() { -// if (this == null) return ""; -// return this; -// } -//} -// -//extension EnumExtension on Object { -// String get value => "$this".replaceAll(RegExp(".*\\."), ""); -//} diff --git a/lib/extensions_static.dart b/lib/extensions_static.dart index 196a2e4..cb9b8b8 100644 --- a/lib/extensions_static.dart +++ b/lib/extensions_static.dart @@ -1,18 +1,26 @@ +import 'package:collection/collection.dart'; + Duration sinceNow(DateTime self) => -(self.difference(DateTime.now())); -X find(List self, [bool filter(X input)]) { - return self?.firstWhere(filter, orElse: () => null); +X? find(List? self, bool Function(X? input) filter) { + return self?.firstWhereOrNull( + filter, + ); } -X lastOrNull(List self, [bool filter(X input)]) { - return self?.lastWhere(filter, orElse: () => null); +X? lastOrNull(List? self, bool Function(X? input) filter) { + return self?.lastWhereOrNull( + filter, + ); } -X firstOrNull(List self, [bool filter(X input)]) { - return self?.firstWhere(filter, orElse: () => null); +X? firstOrNull(List? self, bool Function(X? input) filter) { + return self?.firstWhereOrNull( + filter, + ); } -String truncate(String self, int length) { +String? truncate(String? self, int length) { if (self == null) return self; if (self.length <= length) { return self; @@ -21,19 +29,19 @@ String truncate(String self, int length) { } } -bool isNullOrEmpty(String self) { +bool isNullOrEmpty(String? self) { return self?.isNotEmpty != true; } bool isNotNullOrEmpty(String self) => isNullOrEmpty(self); -bool isNullOrBlank(String self) => self == null || self.trim().isEmpty == true; +bool isNullOrBlank(String? self) => self == null || self.trim().isEmpty == true; bool isNotNullOrBlank(String self) => !isNullOrBlank(self); -String orEmpty(String self) { - if (self == null) return ""; +String orEmpty(String? self) { + if (self == null) return ''; return self; } -String value(self) => "$self".replaceAll(RegExp(".*\\."), ""); +String value(self) => '$self'.replaceAll(RegExp('.*\\.'), ''); diff --git a/lib/flutter_phone_state.dart b/lib/flutter_phone_state.dart index bb4aca1..fc292ae 100644 --- a/lib/flutter_phone_state.dart +++ b/lib/flutter_phone_state.dart @@ -1,12 +1,11 @@ import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_phone_state/extensions_static.dart'; -import 'package:flutter_phone_state/logging.dart'; -import 'package:flutter_phone_state/phone_event.dart'; import 'package:logging/logging.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'extensions_static.dart'; +import 'logging.dart'; +import 'phone_event.dart'; export 'package:flutter_phone_state/phone_event.dart'; @@ -14,22 +13,22 @@ export 'package:flutter_phone_state/phone_event.dart'; final _localEvents = StreamController.broadcast(); const MethodChannel _channel = MethodChannel('flutter_phone_state'); -final Logger _log = Logger("flutterPhoneState"); +final Logger _log = Logger('flutterPhoneState'); final _instance = FlutterPhoneState(); class FlutterPhoneState with WidgetsBindingObserver { /// Configures logging. FlutterPhoneState uses the [logging] plugin. - static void configureLogs({Level level, Logging onLog}) { + static void configureLogs({Level? level, Logging? onLog}) { configureLogging(logger: _log, level: level, onLog: onLog); } static Future get platformVersion async { - final String version = await _channel.invokeMethod('getPlatformVersion'); + final version = await _channel.invokeMethod('getPlatformVersion') as String; return version; } /// A broadcast stream of raw events from the underlying phone state. It's preferred to use [phoneCallEvents] - static Stream get rawPhoneEvents => _initializedNativeEvents; + static Stream get rawPhoneEvents => _initializedNativeEvents; /// A list of events associated to all calls. This includes events from the underlying OS, as well as our /// own cancellation and timeout errors @@ -47,7 +46,7 @@ class FlutterPhoneState with WidgetsBindingObserver { FlutterPhoneState() { configureLogging(logger: _log); - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance?.addObserver(this); _initializedNativeEvents.forEach(_handleRawPhoneEvent); } @@ -55,16 +54,17 @@ class FlutterPhoneState with WidgetsBindingObserver { /// This should add both calls, and track them separately as best we can. /// /// As a note, Android does not support listening to events from nested calls. - List _calls = []; + final List _calls = []; /// Finds a previously placed call that matches the incoming event - PhoneCall _findMatchingCall(RawPhoneEvent event) { + PhoneCall? _findMatchingCall(RawPhoneEvent event) { // Either the first matching, or the first one without an ID - PhoneCall matching; + PhoneCall? matching; if (event.id != null) { - matching = firstOrNull(_calls, (c) => c.callId == event.id); + matching = firstOrNull(_calls, (c) => c?.callId == event.id); } - matching ??= lastOrNull(_calls, (call) => call.canBeLinked(event)); + matching ??= + lastOrNull(_calls, (call) => call?.canBeLinked(event) ?? false); if (matching != null) { // Link them together for future reference matching.callId = event.id; @@ -72,15 +72,20 @@ class FlutterPhoneState with WidgetsBindingObserver { return matching; } + @override void didChangeAppLifecycleState(AppLifecycleState state) { - _log.info("Received application lifecycle state change: $state"); + _log.info('Received application lifecycle state change: $state'); if (state == AppLifecycleState.resumed) { /// We wait 1 second because ios has a short flash of resumed before the phone app opens Future.delayed(Duration(seconds: 1), () { - final expired = lastOrNull(_calls, (PhoneCall c) { - return c.status == PhoneCallStatus.dialing && - sinceNow(c.startTime).inSeconds < 30; + final expired = lastOrNull(_calls, (PhoneCall? c) { + if (c != null) { + return c.status == PhoneCallStatus.dialing && + sinceNow(c.startTime).inSeconds < 30; + } else { + return false; + } }); if (expired != null) { @@ -90,7 +95,7 @@ class FlutterPhoneState with WidgetsBindingObserver { } } - _openCallLink(PhoneCall call) async { + Future _openCallLink(PhoneCall call) async { /// Phone calls are weird in IOS. We need to initiate the phone call by using the link /// below, but the app doesn't give us any meaningful feedback, so we mark the phone interaction /// as "complete" (technically this just means the call was started) by either @@ -99,7 +104,7 @@ class FlutterPhoneState with WidgetsBindingObserver { /// (c) 5 seconds passes with no feedback (this will send back a result code of [cancelled], which /// means the call won't be logged try { - final link = "tel:${call.phoneNumber}"; + final link = 'tel:${call.phoneNumber}'; final status = await _openTelLink(link); if (status != LinkOpenResult.success) { @@ -130,15 +135,15 @@ class FlutterPhoneState with WidgetsBindingObserver { // create an event PhoneCallEvent event; if (call.events.any((e) => e.status == status)) { - _log.fine("Call ${truncate(call.id, 8)} already has status $status"); + _log.fine('Call ${truncate(call.id, 8)} already has status $status'); } if (status == PhoneCallStatus.disconnected || status == PhoneCallStatus.timedOut || status == PhoneCallStatus.error || status == PhoneCallStatus.cancelled) { - _log.info("Call is done: ${call.id}- Removing due to $status"); + _log.info('Call is done: ${call.id}- Removing due to $status'); call.complete(status).then((event) { - _localEvents.add(event); + _localEvents.add(event!); }); _calls.removeWhere((existing) => existing == call); } else { @@ -147,14 +152,16 @@ class FlutterPhoneState with WidgetsBindingObserver { } } - _handleRawPhoneEvent(RawPhoneEvent event) async { + Future _handleRawPhoneEvent(RawPhoneEvent? event) async { + if (event == null) return; + try { - _pruneCalls(); - PhoneCall matching = _findMatchingCall(event); + await _pruneCalls(); + var matching = _findMatchingCall(event); /// If no match was found? if (matching == null && event.isNewCall) { - _log.info("Adding a call to the stack: $event"); + _log.info('Adding a call to the stack: $event'); matching = PhoneCall.start( event.phoneNumber, event.type == RawEventType.inbound @@ -192,12 +199,12 @@ class FlutterPhoneState with WidgetsBindingObserver { break; } } catch (e, stack) { - _log.severe("Error handling phone call event: $e", e, stack); + _log.severe('Error handling phone call event: $e', e, stack); } } /// Looks for calls that weren't properly terminated and completes them - _pruneCalls() { + Future _pruneCalls() async { final expired = [..._calls.where((c) => c.isExpired)]; for (final expiring in expired) { _changeStatus(expiring, PhoneCallStatus.timedOut); @@ -210,48 +217,52 @@ final EventChannel _phoneStateCallEventChannel = EventChannel('co.sunnyapp/phone_events'); /// Native event stream, lazily created. See [nativeEvents] -Stream _nativeEvents; +Stream? _nativeEvents; /// A stream of [RawPhoneEvent] instances. The stream only contains null values if there was an error -Stream get _initializedNativeEvents { +Stream get _initializedNativeEvents { _nativeEvents ??= _phoneStateCallEventChannel.receiveBroadcastStream().map((dyn) { try { if (dyn == null) return null; if (dyn is! Map) { - _log.warning("Unexpected result type for phone event. " + _log.warning('Unexpected result type for phone event. ' "Expected Map but got ${dyn?.runtimeType ?? 'null'} "); } - final Map event = (dyn as Map).cast(); - final eventType = _parseEventType(event["type"] as String); + final event = Map.from(dyn as Map); return RawPhoneEvent( - event["id"] as String, event["phoneNumber"] as String, eventType); + (event['id'] is String) ? event['id'] as String : null, + (event['phoneNumber'] is String) + ? event['phoneNumber'] as String + : null, + _parseEventType(event['type'] as String), + ); } catch (e, stack) { - _log.severe("Error handling native event $e", e, stack); + _log.severe('Error handling native event $e', e, stack); return null; } }); - return _nativeEvents; + return _nativeEvents!; } RawEventType _parseEventType(String dyn) { switch (dyn) { - case "inbound": + case 'inbound': return RawEventType.inbound; - case "connected": + case 'connected': return RawEventType.connected; - case "outbound": + case 'outbound': return RawEventType.outbound; - case "disconnected": + case 'disconnected': return RawEventType.disconnected; default: - throw "Illegal raw event type: $dyn"; + throw 'Illegal raw event type: $dyn'; } } /// Removes all non-numeric characters String sanitizePhoneNumber(String input) { - String out = ""; + var out = ''; for (var i = 0; i < input.length; ++i) { var char = input[i]; @@ -262,14 +273,14 @@ String sanitizePhoneNumber(String input) { return out; } -bool _isNumeric(String str) { +bool _isNumeric(String? str) { if (str == null) { return false; } return double.tryParse(str) != null; } -Future _openTelLink(String appLink) async { +Future _openTelLink(String? appLink) async { if (appLink == null) { return LinkOpenResult.invalidInput; } diff --git a/lib/logging.dart b/lib/logging.dart index d0c918d..ecd91ea 100644 --- a/lib/logging.dart +++ b/lib/logging.dart @@ -7,10 +7,14 @@ import 'package:logging/logging.dart'; typedef Logging = void Function(LogRecord record); /// Keeps the old logging subscription, so we can cancel at reconfigure -StreamSubscription _subscription; +StreamSubscription? _subscription; /// Configures a single logger. By default will -configureLogging({Logger logger, Level level, Logging onLog}) async { +Future configureLogging({ + Logger? logger, + Level? level, + Logging? onLog, +}) async { level ??= Level.INFO; logger ??= Logger.root; onLog ??= defaultLogging(logger); diff --git a/lib/phone_event.dart b/lib/phone_event.dart index 872abfa..32fc3c6 100644 --- a/lib/phone_event.dart +++ b/lib/phone_event.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:flutter_phone_state/extensions_static.dart'; import 'package:logging/logging.dart'; import 'package:uuid/uuid.dart'; +import 'extensions_static.dart'; -final Logger _log = Logger("flutterPhoneState"); +final Logger _log = Logger('flutterPhoneState'); /// Represents phone events that surface from the device. These events can be subscribed to by /// using [FlutterPhoneState.rawEventStream] @@ -15,10 +15,10 @@ class RawPhoneEvent { /// android: always null /// ios: a uuid /// others: ?? - final String id; + final String? id; /// If available, the phone number being dialed. - final String phoneNumber; + final String? phoneNumber; /// The type of call event. final RawEventType type; @@ -26,8 +26,7 @@ class RawPhoneEvent { RawPhoneEvent(this.id, this.phoneNumber, this.type); /// Whether this event represents a new call - bool get isNewCall => - type == RawEventType.inbound || type == RawEventType.outbound; + bool get isNewCall => type == RawEventType.inbound || type == RawEventType.outbound; @override String toString() { @@ -61,15 +60,14 @@ class PhoneCallEvent { /// @non_null final DateTime timestamp; - PhoneCallEvent(this.call, this.status, [DateTime eventDate]) - : timestamp = eventDate ?? DateTime.now(); + PhoneCallEvent(this.call, this.status, [DateTime? eventDate]) : timestamp = eventDate ?? DateTime.now(); @override String toString() { return 'PhoneCallEvent{status: ${value(status)}, ' - 'id: ${truncate(call?.id, 12)} ' - 'callId: ${truncate(call?.callId, 12) ?? '-'}, ' - 'phoneNumber: ${call?.phoneNumber ?? '-'}}'; + 'id: ${truncate(call.id, 12)} ' + 'callId: ${truncate(call.callId, 12) ?? '-'}, ' + 'phoneNumber: ${call.phoneNumber ?? '-'}}'; } } @@ -82,38 +80,37 @@ class PhoneCall { /// An id assigned to the call by the underlying os /// @nullable - String callId; + String? callId; /// The phone number being dialed, or the inbound number /// @nullabe - String phoneNumber; + String? phoneNumber; /// The current status of the call /// @non_null - PhoneCallStatus status; + PhoneCallStatus status = PhoneCallStatus.disconnected; /// Whether the call is inbound or outbound /// @non_null - final PhoneCallPlacement placement; + late final PhoneCallPlacement placement; /// When the call was started - final DateTime startTime; + late final DateTime startTime; /// A list of events associated with this call - final List events; + late final List events; /// Whether or not this call is complete. see [isComplete] bool _isComplete = false; /// Used internally to track the call events, can be subscribed to, or awaited on. - StreamController _eventStream; + StreamController? _eventStream; /// The final call duration. See [duration] - Duration _duration; + Duration? _duration; - PhoneCall.start(this.phoneNumber, this.placement, [String id]) - : status = null, - id = id ?? Uuid().v4(), + PhoneCall.start(this.phoneNumber, this.placement, [String? id]) + : id = id ?? Uuid().v4(), events = [], startTime = DateTime.now(); @@ -126,18 +123,21 @@ class PhoneCall { /// Marks this call as complete, and returns the final event as a [FutureOr]. If the /// event stream has subscribers, it will first close, and then return - Future complete(PhoneCallStatus status) async { + Future complete(PhoneCallStatus status) async { if (_isComplete) { - throw "Illegal state: This call is already marked complete"; + throw 'Illegal state: This call is already marked complete'; } - this._duration = DateTime.now().difference(startTime); + _duration = DateTime.now().difference(startTime); final event = recordStatus(status); _isComplete = true; - if (_eventStream?.isClosed == false) { - await _eventStream.close(); - return event; - } else { - return event; + final stream = _eventStream; + if (stream != null) { + if (stream.isClosed == false) { + await stream.close(); + return event; + } else { + return event; + } } } @@ -156,7 +156,7 @@ class PhoneCall { FutureOr get done { if (_isComplete) return this; return _getOrCreateEventController().done.then((_) { - _log.info("Finished call. Status $status"); + _log.info('Finished call. Status $status'); return this; }); } @@ -165,10 +165,8 @@ class PhoneCall { /// - It's in a dialing state for more than 30 seconds /// - It's in an active state for more than 8 hours bool get isExpired { - if (status == PhoneCallStatus.dialing && sinceNow(startTime).inSeconds > 30) - return true; - if (status == PhoneCallStatus.connected && sinceNow(startTime).inHours > 8) - return true; + if (status == PhoneCallStatus.dialing && sinceNow(startTime).inSeconds > 30) return true; + if (status == PhoneCallStatus.connected && sinceNow(startTime).inHours > 8) return true; return false; } @@ -178,10 +176,8 @@ class PhoneCall { /// Whether this call can be linked to the provided event. This check is fairly loose, it makes sure that /// the values aren't for two disparate ids, phone numbers, and that the status is a subsequent status bool canBeLinked(RawPhoneEvent event) { - if (event.phoneNumber != null && - this.phoneNumber != null && - event.phoneNumber != this.phoneNumber) return false; - if (this.callId != null && this.callId != event.id) return false; + if (event.phoneNumber != null && phoneNumber != null && event.phoneNumber != phoneNumber) return false; + if (callId != null && callId != event.id) return false; if (isNotBefore(status, event.type)) return false; return true; @@ -189,8 +185,7 @@ class PhoneCall { @override bool operator ==(Object other) => - identical(this, other) || - other is PhoneCall && runtimeType == other.runtimeType && id == other.id; + identical(this, other) || other is PhoneCall && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; @@ -199,9 +194,10 @@ class PhoneCall { PhoneCallEvent recordStatus(PhoneCallStatus status) { this.status = status; final event = PhoneCallEvent(this, status); - this.events.add(event); + events.add(event); + if (_eventStream?.isClosed == true) { - throw "Illegal state for call ${truncate(id, 12)}: Received status event after closing stream"; + throw 'Illegal state for call ${truncate(id, 12)}: Received status event after closing stream'; } _eventStream?.add(event); return event; @@ -212,25 +208,12 @@ class PhoneCall { } enum RawEventType { inbound, outbound, connected, disconnected } -enum PhoneCallStatus { - ringing, - dialing, - cancelled, - error, - connecting, - connected, - timedOut, - disconnected -} +enum PhoneCallStatus { ringing, dialing, cancelled, error, connecting, connected, timedOut, disconnected } enum PhoneCallPlacement { inbound, outbound } const Map> priorStatuses = { RawEventType.outbound: {PhoneCallStatus.dialing}, - RawEventType.connected: { - PhoneCallStatus.connecting, - PhoneCallStatus.ringing, - PhoneCallStatus.dialing - }, + RawEventType.connected: {PhoneCallStatus.connecting, PhoneCallStatus.ringing, PhoneCallStatus.dialing}, RawEventType.inbound: {}, RawEventType.disconnected: { PhoneCallStatus.connecting, @@ -240,8 +223,7 @@ const Map> priorStatuses = { }, }; -bool isNotBefore(PhoneCallStatus status, RawEventType type) => - !isBefore(status, type); +bool isNotBefore(PhoneCallStatus status, RawEventType type) => !isBefore(status, type); bool isBefore(PhoneCallStatus status, RawEventType type) { return priorStatuses[type]?.contains(status) == true; diff --git a/pubspec.lock b/pubspec.lock index 06d0f90..2565649 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,63 +7,56 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.5.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" + version: "1.15.0" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "3.0.1" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" flutter: dependency: "direct main" description: flutter @@ -79,55 +72,55 @@ packages: description: flutter source: sdk version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" logging: dependency: "direct main" description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.1" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" - platform_detect: - dependency: transitive + version: "1.8.0" + pedantic: + dependency: "direct dev" description: - name: platform_detect + name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.11.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -139,105 +132,112 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.0" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" stream_transform: dependency: "direct main" description: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" + version: "0.2.19" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.5.0" + version: "6.0.6" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+1" + version: "2.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+7" + version: "2.0.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "2.0.3" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "2.0.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" uuid: dependency: "direct main" description: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.4" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0" sdks: - dart: ">=2.10.0-110 <2.11.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + dart: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3740f38..10c2b4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,24 +1,25 @@ name: flutter_phone_state description: This plugin provides an easy way to make phone calls, and track the state of the phone call -version: 0.5.9 +version: 2.0.0 homepage: "https://github.com/SunnyApp/flutter_phone_state.git" environment: - sdk: ">=2.5.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter - url_launcher: ^5.4.2 - logging: ^0.11.4 - uuid: ^2.0.4 - stream_transform: ^1.1.0 + logging: ^1.0.1 + stream_transform: ^2.0.0 + url_launcher: ^6.0.6 + uuid: ^3.0.4 dev_dependencies: flutter_test: sdk: flutter + pedantic: ^1.11.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/flutter_phone_state_test.dart b/test/flutter_phone_state_test.dart index 2a1ee5b..65b6996 100644 --- a/test/flutter_phone_state_test.dart +++ b/test/flutter_phone_state_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_phone_state/flutter_phone_state.dart'; void main() { - const MethodChannel channel = MethodChannel('flutter_phone_state'); + const channel = MethodChannel('flutter_phone_state'); TestWidgetsFlutterBinding.ensureInitialized();