Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle tags on startup and resume with Android #56

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.flutter.plugins.nfcmanager

import android.app.Activity
import androidx.lifecycle.*
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
Expand All @@ -18,24 +20,93 @@ import android.os.Build
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.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter
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.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import io.flutter.plugin.common.EventChannel.StreamHandler
import java.io.IOException
import java.lang.Exception
import java.util.*

class NfcManagerPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
class NfcManagerPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, LifecycleObserver {
private lateinit var channel : MethodChannel
private lateinit var activity: Activity
private lateinit var tags: MutableMap<String, Tag>
private lateinit var lifecycle: Lifecycle

private var tagFromIntent: Tag? = null
private var sinkTagDiscoveredEvents = ArrayList<EventSink>()

private var adapter: NfcAdapter? = null
private var connectedTech: TagTechnology? = null

private lateinit var enableActivityReaderMode : () -> Unit
private lateinit var disableActivityReaderMode : () -> Unit

override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(binding.binaryMessenger, "plugins.flutter.io/nfc_manager")
val baseChannelName = "plugins.flutter.io/nfc_manager"

channel = MethodChannel(binding.binaryMessenger, baseChannelName)
channel.setMethodCallHandler(this)

EventChannel(binding.binaryMessenger,
baseChannelName + "/stream").setStreamHandler(
object : StreamHandler {
private lateinit var currentEvents : EventSink

override fun onListen(arguments: Any?, events: EventSink) {
if (sinkTagDiscoveredEvents.isEmpty()) {
enableActivityReaderMode = {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
events.error("unavailable", "Requires API level 19.", null)
} else {
val adapter = adapter

if (adapter != null) {
var argMaps = arguments as HashMap<String,Any?>
adapter.enableReaderMode(activity, NfcAdapter.ReaderCallback {
activity.runOnUiThread { broadcastPreparedTag(it) }
}, getFlags(argMaps["pollingOptions"] as List<String>), null)
} else {
events.error("unavailable", "NFC is not available for device.", null)
}
}
}

disableActivityReaderMode = {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
events.error("unavailable", "Requires API level 19.", null)
} else {
adapter?.disableReaderMode(activity)
}
}

enableActivityReaderMode()
}

currentEvents = events
sinkTagDiscoveredEvents.add(currentEvents)

tagFromIntent?.let {
currentEvents.success(prepareTag(it))
tagFromIntent = null
}
}

override fun onCancel(arguments: Any?) {
sinkTagDiscoveredEvents.remove(currentEvents)

if (sinkTagDiscoveredEvents.isEmpty()) {
disableActivityReaderMode()
}
}
}
)

adapter = NfcAdapter.getDefaultAdapter(binding.applicationContext)
tags = mutableMapOf()
}
Expand All @@ -45,19 +116,60 @@ class NfcManagerPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
initBinding(binding)
processIntent(activity.intent)
}

override fun onDetachedFromActivity() {
// no op
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
initBinding(binding)
autoEnableReaderMode()
}

override fun onDetachedFromActivityForConfigChanges() {
// no op
// autoDisableReaderMode()
// lifecycle.removeObserver(this)
}

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun autoEnableReaderMode() {
// For some device (OnePlus for example),
// the readerMode is not reenabled after paused
if (sinkTagDiscoveredEvents.isNotEmpty()) {
enableActivityReaderMode()
}
}

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
private fun autoDisableReaderMode() {
if (sinkTagDiscoveredEvents.isNotEmpty()) {
disableActivityReaderMode()
}
}

private fun initBinding(binding: ActivityPluginBinding) {
activity = binding.activity
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding)

lifecycle.addObserver(this)

binding.addOnNewIntentListener(fun(intent: Intent?): Boolean {
var tagProcessed = false

if (intent != null) {
val tag = processIntent(intent)


if (tag != null) {
broadcastPreparedTag(tag)
tagProcessed = true
}
}

return tagProcessed
})
}

override fun onMethodCall(call: MethodCall, result: Result) {
Expand Down Expand Up @@ -104,11 +216,11 @@ class NfcManagerPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
result.error("unavailable", "NFC is not available for device.", null)
return
}
adapter.enableReaderMode(activity, {
val handle = UUID.randomUUID().toString()
tags[handle] = it
activity.runOnUiThread { channel.invokeMethod("onDiscovered", getTagMap(it).toMutableMap().apply { put("handle", handle) }) }

adapter.enableReaderMode(activity, NfcAdapter.ReaderCallback {
activity.runOnUiThread { channel.invokeMethod("onDiscovered", prepareTag(it)) }
}, getFlags(call.argument<List<String>>("pollingOptions")!!), null)

result.success(null)
}
}
Expand Down Expand Up @@ -345,4 +457,24 @@ class NfcManagerPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
connectedTech = tech
}
}

private fun processIntent(intent: Intent) : Tag? {
tagFromIntent = intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG)

return tagFromIntent
}

private fun broadcastPreparedTag(tag: Tag) {
sinkTagDiscoveredEvents.forEach {
val preparedTag = prepareTag(tag)
it.success(preparedTag)
}
}

private fun prepareTag(tag: Tag): MutableMap<String, Any?> {
val handle = UUID.randomUUID().toString()
tags[handle] = tag

return getTagMap(tag).toMutableMap().apply { put("handle", handle) }
}
}
5 changes: 4 additions & 1 deletion lib/src/channel.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'package:flutter/services.dart';

const MethodChannel channel = MethodChannel('plugins.flutter.io/nfc_manager');
const baseChannelName = 'plugins.flutter.io/nfc_manager';

const MethodChannel channel = MethodChannel(baseChannelName);
const EventChannel eventChannel = EventChannel(baseChannelName + '/stream');
90 changes: 85 additions & 5 deletions lib/src/nfc_manager/nfc_manager.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:flutter/services.dart';

import '../channel.dart';
Expand All @@ -9,6 +11,19 @@ typedef NfcTagCallback = Future<void> Function(NfcTag tag);
/// Signature for `NfcManager.startSession` onError callback.
typedef NfcErrorCallback = Future<void> Function(NfcError error);

/// Handle the discovering nfc tag session
class NfcSessionHandler {
final StreamSubscription<NfcTag> _subscription;

const NfcSessionHandler._({required StreamSubscription<NfcTag> subscription})
: _subscription = subscription;

/// Stop the current session
Future<void> stop() {
return _subscription.cancel();
}
}

/// The entry point for accessing the NFC session.
class NfcManager {
NfcManager._() {
Expand All @@ -30,6 +45,64 @@ class NfcManager {
return channel.invokeMethod('Nfc#isAvailable').then((value) => value!);
}

/// (Android only)
/// Get the NFC on startup or resume of the application and start session
///
/// `pollingOptions` is used to specify the type of tags to be discovered. All types by default.
///
/// Think to stop session after discovering
/// ```dart
/// final session = NfcManager.instance.discover(onDiscovered: (tag) {
/// // Do something with tag
/// });
///
/// session.stop();
/// ```
///
/// To prevent application from being restarted when a NFC intent resume
/// the app, set `android:launchMode="singleTask"` on the main `activity` in
/// the `AndroidManifest.xml`
NfcSessionHandler discover({
required NfcTagCallback onDiscovered,
Set<NfcPollingOption>? pollingOptions,
String? alertMessage,
NfcErrorCallback? onError,
}) {
pollingOptions ??= NfcPollingOption.values.toSet();

// Cancellation handled by [NfcSessionManager.stop]
// ignore: cancel_subscriptions
final subscription = eventChannel
.receiveBroadcastStream({
'pollingOptions':
pollingOptions.map((e) => $NfcPollingOptionTable[e]).toList(),
})
.handleError((error) {
late NfcError nfcError;
if (error is PlatformException && error.code == 'unavailable') {
nfcError = NfcError(
type: NfcErrorType.unavailable,
message: error.message ?? 'Unknown',
details: error.details);
} else {
nfcError = $GetNfcError(error);
}

if (onError != null) {
onError(nfcError);
} else {
// To be handled by the framework
throw nfcError;
}
})
.map((tagMap) => $GetNfcTag(Map.from(tagMap)))
.listen((tag) {
_handleOnDiscovered(tag, onDiscovered);
}, cancelOnError: false);

return NfcSessionHandler._(subscription: subscription);
}

/// Start the session and register callbacks for tag discovery.
///
/// This uses the NFCTagReaderSession (on iOS) or NfcAdapter#enableReaderMode (on Android).
Expand Down Expand Up @@ -88,7 +161,8 @@ class NfcManager {
Future<void> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'onDiscovered':
_handleOnDiscovered(call);
_handleOnDiscovered(
$GetNfcTag(Map.from(call.arguments)), _onDiscovered);
break;
case 'onError':
_handleOnError(call);
Expand All @@ -99,10 +173,13 @@ class NfcManager {
}

// _handleOnDiscovered
void _handleOnDiscovered(MethodCall call) async {
final tag = $GetNfcTag(Map.from(call.arguments));
await _onDiscovered?.call(tag);
await _disposeTag(tag.handle);
Future<void> _handleOnDiscovered(
NfcTag tag, NfcTagCallback? onDiscovered) async {
try {
await onDiscovered?.call(tag);
} finally {
await _disposeTag(tag.handle);
}
}

// _handleOnError
Expand Down Expand Up @@ -188,6 +265,9 @@ enum NfcErrorType {
/// The user canceled the session.
userCanceled,

/// The NFC is unavailable (API, disabled, ...)
unavailable,

/// The session failed because the unexpected error has occurred.
unknown,
}
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ environment:
dependencies:
flutter:
sdk: flutter

flutter_plugin_android_lifecycle: ^2.0.3

dev_dependencies:
flutter_test:
Expand Down