From ed77c100f0c403b8a937cb6a2fbe6a86a7331951 Mon Sep 17 00:00:00 2001 From: Harsh Bhikadia Date: Fri, 23 Apr 2021 14:06:07 +0530 Subject: [PATCH] initial working functionality - `pupsec`: added `>=2.12.0` constraints to `environment.sdk` - `ReceivedIntent`: added - `ReceiveIntent`: `getInitialIntent`, `receivedIntentStream` and `giveResult` added - `example`: simple working example added - `ReceiveIntentPlugin`: handling `getInitialIntent` and `giveResult` methods and sending "newIntent" events --- .gitignore | 3 + .../receive_intent/ReceiveIntentPlugin.kt | 136 ++++++++++++--- .../com/bhikadia/receive_intent/Utils.kt | 96 +++++++++++ .../android/app/src/main/AndroidManifest.xml | 4 + example/lib/main.dart | 53 +++--- example/pubspec.lock | 161 ------------------ lib/receive_intent.dart | 53 +++++- pubspec.lock | 147 ---------------- pubspec.yaml | 2 +- test/receive_intent_test.dart | 7 +- 10 files changed, 302 insertions(+), 360 deletions(-) create mode 100644 android/src/main/kotlin/com/bhikadia/receive_intent/Utils.kt delete mode 100644 example/pubspec.lock delete mode 100644 pubspec.lock diff --git a/.gitignore b/.gitignore index e9dc58d..f1c9014 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ .pub/ build/ + +# Flutter related +**/pubspec.lock diff --git a/android/src/main/kotlin/com/bhikadia/receive_intent/ReceiveIntentPlugin.kt b/android/src/main/kotlin/com/bhikadia/receive_intent/ReceiveIntentPlugin.kt index 27bd8ec..70fddc9 100644 --- a/android/src/main/kotlin/com/bhikadia/receive_intent/ReceiveIntentPlugin.kt +++ b/android/src/main/kotlin/com/bhikadia/receive_intent/ReceiveIntentPlugin.kt @@ -1,36 +1,130 @@ package com.bhikadia.receive_intent +import android.app.Activity +import android.content.Context +import android.content.Intent import androidx.annotation.NonNull - import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink 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 + /** ReceiveIntentPlugin */ -class ReceiveIntentPlugin: FlutterPlugin, MethodCallHandler { - /// 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 lateinit var channel : MethodChannel +class ReceiveIntentPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, 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 lateinit var methodChannel: MethodChannel + private lateinit var eventChannel: EventChannel + private var eventSink: EventSink? = null - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "receive_intent") - channel.setMethodCallHandler(this) - } + private lateinit var context: Context + private var activity: Activity? = null; - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - if (call.method == "getPlatformVersion") { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() + + private var initialIntentMap: Map? = null + private var latestIntentMap: Map? = null + private var initialIntent = true + + private fun intentToMap(intent: Intent, fromPackageName: String?): Map { + return mapOf( + "fromPackageName" to fromPackageName, + "fromSignatures" to fromPackageName?.let { getApplicationSignature(context, it) }, + "action" to intent.action, + "data" to intent.dataString, + "categories" to intent.categories, + "extra" to intent.extras?.let { bundleToMap(it) } + ) } - } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } + private fun handleIntent(intent: Intent, fromPackageName: String?) { + if (initialIntent) { + initialIntentMap = intentToMap(intent, fromPackageName) + initialIntent = false + } + latestIntentMap = intentToMap(intent, fromPackageName) + eventSink?.success(latestIntentMap) + } + + private fun giveResult(result: Result, resultCode: Int?, data: Map?) { + if (resultCode != null) { + if (data == null) + activity?.setResult(resultCode) + else + activity?.setResult(resultCode, mapToIntent(data)) + result.success(null) + } + result.error("InvalidArg", "resultCode can not be null", null) + } + + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + + methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "receive_intent") + methodChannel.setMethodCallHandler(this) + + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "receive_intent/event") + eventChannel.setStreamHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + when (call.method) { + "getInitialIntent" -> { + result.success(initialIntentMap) + } + "giveResult" -> { + giveResult(result, call.argument("resultCode"), call.argument("data")) + } + else -> { + result.notImplemented() + } + } + } + + override fun onListen(arguments: Any?, events: EventSink?) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + methodChannel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + binding.addOnNewIntentListener(fun(intent: Intent?): Boolean { + intent?.let { handleIntent(it, binding.activity.callingActivity?.packageName) } + return false; + }) + handleIntent(binding.activity.intent, binding.activity.callingActivity?.packageName) + } + + override fun onDetachedFromActivityForConfigChanges() { + activity = null; + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + binding.addOnNewIntentListener(fun(intent: Intent?): Boolean { + intent?.let { handleIntent(it, binding.activity.callingActivity?.packageName) } + return false; + }) + handleIntent(binding.activity.intent, binding.activity.callingActivity?.packageName) + } + + override fun onDetachedFromActivity() { + activity = null; + } } diff --git a/android/src/main/kotlin/com/bhikadia/receive_intent/Utils.kt b/android/src/main/kotlin/com/bhikadia/receive_intent/Utils.kt new file mode 100644 index 0000000..1cea08e --- /dev/null +++ b/android/src/main/kotlin/com/bhikadia/receive_intent/Utils.kt @@ -0,0 +1,96 @@ +package com.bhikadia.receive_intent + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import java.security.MessageDigest + +fun mapToBundle(map: Map): Bundle { + val bundle = Bundle(); + map.forEach { + val k = it.key + val v = it.value + when (v) { + is Byte -> bundle.putByte(k, v) + is ByteArray -> bundle.putByteArray(k, v) + is Char -> bundle.putChar(k, v) + is CharArray -> bundle.putCharArray(k, v) + is CharSequence -> bundle.putCharSequence(k, v) + is Float -> bundle.putFloat(k, v) + is FloatArray -> bundle.putFloatArray(k, v) + is Parcelable -> bundle.putParcelable(k, v) + is Short -> bundle.putShort(k, v) + is ShortArray -> bundle.putShortArray(k, v) + else -> throw IllegalArgumentException("$v is of a type that is not currently supported") + } + } + return bundle; +} + +fun mapToIntent(map: Map): Intent = Intent().apply { + putExtras(mapToBundle(map)) +} + + +fun bundleToMap(extras: Bundle): Map { + val map: MutableMap = HashMap() + val ks = extras.keySet() + val iterator: Iterator = ks.iterator() + while (iterator.hasNext()) { + val key = iterator.next() + map[key] = extras.get(key) + } + return map +} + +fun getApplicationSignature(context: Context, packageName: String): List { + val signatureList: List + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // New signature + val sig = context.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES).signingInfo + signatureList = if (sig.hasMultipleSigners()) { + // Send all with apkContentsSigners + sig.apkContentsSigners.map { + val digest = MessageDigest.getInstance("SHA") + digest.update(it.toByteArray()) + bytesToHex(digest.digest()) + } + } else { + // Send one with signingCertificateHistory + sig.signingCertificateHistory.map { + val digest = MessageDigest.getInstance("SHA") + digest.update(it.toByteArray()) + bytesToHex(digest.digest()) + } + } + } else { + val sig = context.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures + signatureList = sig.map { + val digest = MessageDigest.getInstance("SHA") + digest.update(it.toByteArray()) + bytesToHex(digest.digest()) + } + } + + return signatureList + } catch (e: Exception) { + // Handle error + } + return emptyList() +} + +fun bytesToHex(bytes: ByteArray): String { + val hexArray = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') + val hexChars = CharArray(bytes.size * 2) + var v: Int + for (j in bytes.indices) { + v = bytes[j].toInt() and 0xFF + hexChars[j * 2] = hexArray[v.ushr(4)] + hexChars[j * 2 + 1] = hexArray[v and 0x0F] + } + return String(hexChars) +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 6433a16..c11d76d 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -31,6 +31,10 @@ + + + + diff --git a/example/lib/main.dart b/example/lib/main.dart index 6a16fa2..c95b63c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; import 'dart:async'; -import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; import 'package:receive_intent/receive_intent.dart'; void main() { @@ -14,34 +13,39 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _platformVersion = 'Unknown'; + ReceivedIntent _initialIntent; @override void initState() { super.initState(); - initPlatformState(); + _init(); } - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String platformVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - try { - platformVersion = await ReceiveIntent.platformVersion; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; - } + Future _init() async { + final receivedIntent = await ReceiveIntent.getInitialIntent(); - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. if (!mounted) return; setState(() { - _platformVersion = platformVersion; + _initialIntent = receivedIntent; }); } + Widget _buildFromIntent(String label, ReceivedIntent intent) { + return Center( + child: Column( + children: [ + Text(label), + Text( + "fromPackage: ${intent?.fromPackageName}\nfromSignatures: ${_initialIntent?.fromSignatures}"), + Text( + 'action: ${_initialIntent?.action}\ndata: ${_initialIntent?.data}\ncategories: ${_initialIntent?.categories}'), + Text("extras: ${_initialIntent?.extra}") + ], + ), + ); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -49,8 +53,19 @@ class _MyAppState extends State { appBar: AppBar( title: const Text('Plugin example app'), ), - body: Center( - child: Text('Running on: $_platformVersion\n'), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildFromIntent("INITIAL", _initialIntent), + StreamBuilder( + stream: ReceiveIntent.receivedIntentStream, + builder: (context, snapshot) => + _buildFromIntent("STREAMED", snapshot.data), + ) + ], + ), ), ), ); diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index dd54202..0000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,161 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - receive_intent: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.0.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.19" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" -sdks: - dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/lib/receive_intent.dart b/lib/receive_intent.dart index 90912aa..7316ad7 100644 --- a/lib/receive_intent.dart +++ b/lib/receive_intent.dart @@ -1,14 +1,53 @@ - import 'dart:async'; import 'package:flutter/services.dart'; -class ReceiveIntent { - static const MethodChannel _channel = - const MethodChannel('receive_intent'); +class ReceivedIntent { + final String? fromPackageName; + final List? fromSignatures; + final String action; + final String? data; + final List? categories; + final Map? extra; - static Future get platformVersion async { - final String version = await _channel.invokeMethod('getPlatformVersion'); - return version; + const ReceivedIntent({ + this.fromPackageName, + this.fromSignatures, + required this.action, + this.data, + this.categories, + this.extra, + }); + + factory ReceivedIntent.fromMap(Map? map) => ReceivedIntent( + fromPackageName: map?["fromPackageName"], + fromSignatures: map?["fromSignatures"], + action: map?["action"], + data: map?["data"], + categories: map?["categories"], + extra: (map?["extra"] as Map?)?.map( + (key, value) => MapEntry(key.toString(), value)), + ); +} + +class ReceiveIntent { + static const MethodChannel _methodChannel = + const MethodChannel('receive_intent'); + static const EventChannel _eventChannel = + const EventChannel("receive_intent/event"); + + static Future getInitialIntent() async { + final renameMap = await _methodChannel.invokeMapMethod('getInitialIntent'); + print("result: $renameMap"); + return ReceivedIntent.fromMap(renameMap); + } + + static Stream receivedIntentStream = _eventChannel + .receiveBroadcastStream() + .map((event) => ReceivedIntent.fromMap(event as Map?)); + + static Future giveResult(int resultCode, {Map? data}) async { + await _methodChannel.invokeMethod('giveResult', + {"resultCode": resultCode, "data": data}); } } diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index ef01c9f..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,147 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.19" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" -sdks: - dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index a743ecf..ffa9f3d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ author: homepage: environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" flutter: ">=1.20.0" dependencies: diff --git a/test/receive_intent_test.dart b/test/receive_intent_test.dart index 91b0d78..91dee97 100644 --- a/test/receive_intent_test.dart +++ b/test/receive_intent_test.dart @@ -1,6 +1,5 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:receive_intent/receive_intent.dart'; void main() { const MethodChannel channel = MethodChannel('receive_intent'); @@ -17,7 +16,7 @@ void main() { channel.setMockMethodCallHandler(null); }); - test('getPlatformVersion', () async { - expect(await ReceiveIntent.platformVersion, '42'); - }); + // test('getPlatformVersion', () async { + // expect(await ReceiveIntent.platformVersion, '42'); + // }); }