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

Move _select encoding and decoding to threads. #33

Open
wants to merge 1 commit 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
Expand Up @@ -13,37 +13,33 @@ import androidx.core.content.ContextCompat
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.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.*
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.nio.ByteBuffer

public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, ActivityAware, ActivityResultListener, RequestPermissionsResultListener {
public class FlutterContactsPlugin : FlutterPlugin, BinaryMessenger.BinaryMessageHandler, EventChannel.StreamHandler, ActivityAware, ActivityResultListener, RequestPermissionsResultListener {
companion object {
private var activity: Activity? = null
private var context: Context? = null
private var resolver: ContentResolver? = null
private val permissionReadWriteCode: Int = 0
private val permissionReadOnlyCode: Int = 1
private var permissionResult: Result? = null
private var viewResult: Result? = null
private var editResult: Result? = null
private var pickResult: Result? = null
private var insertResult: Result? = null
private var permissionReply: BinaryMessenger.BinaryReply? = null
private var viewReply: BinaryMessenger.BinaryReply? = null
private var editReply: BinaryMessenger.BinaryReply? = null
private var pickReply: BinaryMessenger.BinaryReply? = null
private var insertReply: BinaryMessenger.BinaryReply? = null
}

// --- FlutterPlugin implementation ---

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "github.com/QuisApp/flutter_contacts")
flutterPluginBinding.binaryMessenger.setMessageHandler("github.com/QuisApp/flutter_contacts", FlutterContactsPlugin())
val eventChannel = EventChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "github.com/QuisApp/flutter_contacts/events")
channel.setMethodCallHandler(FlutterContactsPlugin())
eventChannel.setStreamHandler(FlutterContactsPlugin())
context = flutterPluginBinding.applicationContext
resolver = context!!.contentResolver
Expand Down Expand Up @@ -78,28 +74,28 @@ public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChan
): Boolean {
when (requestCode) {
FlutterContacts.REQUEST_CODE_VIEW ->
if (viewResult != null) {
viewResult!!.success(null)
viewResult = null
if (viewReply != null) {
success(viewReply!!, null)
viewReply = null
}
FlutterContacts.REQUEST_CODE_EDIT ->
if (editResult != null) {
if (editReply != null) {
// Result is of the form:
// content://com.android.contacts/contacts/lookup/<hash>/<id>
val id = intent?.getData()?.getLastPathSegment()
editResult!!.success(id)
editResult = null
success(editReply!!, id)
editReply = null
}
FlutterContacts.REQUEST_CODE_PICK ->
if (pickResult != null) {
if (pickReply != null) {
// Result is of the form:
// content://com.android.contacts/contacts/lookup/<hash>/<id>
val id = intent?.getData()?.getLastPathSegment()
pickResult!!.success(id)
pickResult = null
success(pickReply!!, id)
pickReply = null
}
FlutterContacts.REQUEST_CODE_INSERT ->
if (insertResult != null) {
if (insertReply != null) {
// Result is of the form:
// content://com.android.contacts/raw_contacts/<raw_id>
// So we need to get the ID from the raw ID.
Expand All @@ -119,14 +115,14 @@ public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChan
/*idIsRawContactId=*/true
)
if (contacts.isNotEmpty()) {
insertResult!!.success(contacts[0]["id"])
success(insertReply!!, contacts[0]["id"])
} else {
insertResult!!.success(null)
success(insertReply!!, null)
}
} else {
insertResult!!.success(null)
success(insertReply!!, null)
}
insertResult = null
insertReply = null
}
}
return true
Expand All @@ -145,10 +141,10 @@ public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChan
grantResults!!.size == 2 &&
grantResults!![0] == PackageManager.PERMISSION_GRANTED &&
grantResults!![1] == PackageManager.PERMISSION_GRANTED
if (permissionResult != null) {
if (permissionReply != null) {
GlobalScope.launch(Dispatchers.Main) {
permissionResult!!.success(granted)
permissionResult = null
success(permissionReply!!, granted)
permissionReply = null
}
}
return true
Expand All @@ -157,10 +153,10 @@ public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChan
val granted = grantResults != null &&
grantResults!!.size == 1 &&
grantResults!![0] == PackageManager.PERMISSION_GRANTED
if (permissionResult != null) {
if (permissionReply != null) {
GlobalScope.launch(Dispatchers.Main) {
permissionResult!!.success(granted)
permissionResult = null
success(permissionReply!!, granted)
permissionReply = null
}
}
return true
Expand All @@ -171,23 +167,35 @@ public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChan

// --- MethodCallHandler implementation ---

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
private fun success(reply: BinaryMessenger.BinaryReply, result: Any?) {
val codec = StandardMethodCodec.INSTANCE
reply.reply(codec.encodeSuccessEnvelope(result))
}

private fun error(reply: BinaryMessenger.BinaryReply, errorCode: String, errorMessage: String, errorDetails: Any) {
val codec = StandardMethodCodec.INSTANCE
reply.reply(codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails))
}

override fun onMessage(message: ByteBuffer?, reply: BinaryMessenger.BinaryReply) {
val codec = StandardMethodCodec.INSTANCE
val call = codec.decodeMethodCall(message)
when (call.method) {
// Requests permission to read/write contacts.
"requestPermission" ->
GlobalScope.launch(Dispatchers.IO) {
if (context == null) {
GlobalScope.launch(Dispatchers.Main) { result.success(false); }
GlobalScope.launch(Dispatchers.Main) { success(reply, false); }
} else {
val readonly = call.arguments as Boolean
val readPermission = Manifest.permission.READ_CONTACTS
val writePermission = Manifest.permission.WRITE_CONTACTS
if (ContextCompat.checkSelfPermission(context!!, readPermission) == PackageManager.PERMISSION_GRANTED &&
(readonly || ContextCompat.checkSelfPermission(context!!, writePermission) == PackageManager.PERMISSION_GRANTED)
) {
GlobalScope.launch(Dispatchers.Main) { result.success(true) }
GlobalScope.launch(Dispatchers.Main) { success(reply, true) }
} else if (activity != null) {
permissionResult = result
permissionReply = reply
if (readonly) {
ActivityCompat.requestPermissions(activity!!, arrayOf(readPermission), permissionReadOnlyCode)
} else {
Expand Down Expand Up @@ -223,7 +231,8 @@ public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChan
returnUnifiedContacts,
includeNonVisible
)
GlobalScope.launch(Dispatchers.Main) { result.success(contacts) }
val encodedContacts = codec.encodeSuccessEnvelope(contacts)
GlobalScope.launch(Dispatchers.Main) { reply.reply(encodedContacts) }
}
// Inserts a new contact and return it.
"insert" ->
Expand All @@ -234,9 +243,9 @@ public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChan
FlutterContacts.insert(resolver!!, contact)
GlobalScope.launch(Dispatchers.Main) {
if (insertedContact != null) {
result.success(insertedContact)
success(reply, insertedContact)
} else {
result.error("", "failed to create contact", "")
error(reply, "", "failed to create contact", "")
}
}
}
Expand All @@ -249,47 +258,48 @@ public class FlutterContactsPlugin : FlutterPlugin, MethodCallHandler, EventChan
FlutterContacts.update(resolver!!, contact)
GlobalScope.launch(Dispatchers.Main) {
if (updatedContact != null) {
result.success(updatedContact)
success(reply, updatedContact)
} else {
result.error("", "failed to update contact", "")
error(reply, "", "failed to update contact", "")
}
}
}
// Deletes contacts with given IDs.
"delete" ->
GlobalScope.launch(Dispatchers.IO) {
FlutterContacts.delete(resolver!!, call.arguments as List<String>)
GlobalScope.launch(Dispatchers.Main) { result.success(null) }
GlobalScope.launch(Dispatchers.Main) { success(reply, null) }
}
// Opens external contact app to view existing contact.
"openExternalView" ->
GlobalScope.launch(Dispatchers.IO) {
val args = call.arguments as List<Any>
val id = args[0] as String
FlutterContacts.openExternalViewOrEdit(activity, context, id, false)
viewResult = result
viewReply = reply
}
// Opens external contact app to edit existing contact.
"openExternalEdit" ->
GlobalScope.launch(Dispatchers.IO) {
val args = call.arguments as List<Any>
val id = args[0] as String
FlutterContacts.openExternalViewOrEdit(activity, context, id, true)
editResult = result
editReply = reply
}
// Opens external contact app to pick an existing contact.
"openExternalPick" ->
GlobalScope.launch(Dispatchers.IO) {
FlutterContacts.openExternalPickOrInsert(activity, context, false)
pickResult = result
pickReply = reply
}
// Opens external contact app to insert a new contact.
"openExternalInsert" ->
GlobalScope.launch(Dispatchers.IO) {
FlutterContacts.openExternalPickOrInsert(activity, context, true)
insertResult = result
insertReply = reply
}
else -> result.notImplemented()
// notImplemented
else -> reply.reply(null)
}
}

Expand Down
18 changes: 12 additions & 6 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@ class FlutterContactsExample extends StatefulWidget {

class _FlutterContactsExampleState extends State<FlutterContactsExample> {
List<Contact>? _contacts;
bool _notStarted = true;
bool _permissionDenied = false;

@override
void initState() {
super.initState();
_fetchContacts();
}

Future _fetchContacts() async {
if (!await FlutterContacts.requestPermission(readonly: true)) {
setState(() => _permissionDenied = true);
Expand All @@ -34,6 +29,17 @@ class _FlutterContactsExampleState extends State<FlutterContactsExample> {
body: _body()));

Widget _body() {
if (_notStarted) {
return Center(
child: ElevatedButton(
onPressed: () {
setState(() => _notStarted = false);
_fetchContacts();
},
child: Text('Start'),
),
);
}
if (_permissionDenied) return Center(child: Text('Permission denied'));
if (_contacts == null) return Center(child: CircularProgressIndicator());
return ListView.builder(
Expand Down
46 changes: 45 additions & 1 deletion lib/flutter_contacts.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_contacts/config.dart';
import 'package:flutter_contacts/contact.dart';
Expand All @@ -18,6 +19,48 @@ export 'properties/phone.dart';
export 'properties/social_media.dart';
export 'properties/website.dart';

/// From StandardMethodCodec.decodeEnvelope.
/// Needed to be able to call compute() with it.
dynamic decodeEnvelope(ByteData envelope) {
final messageCodec = const StandardMessageCodec();
// First byte is zero in success case, and non-zero otherwise.
if (envelope.lengthInBytes == 0) {
throw const FormatException('Expected envelope, got nothing');
}
final buffer = ReadBuffer(envelope);
if (buffer.getUint8() == 0) return messageCodec.readValue(buffer);
final errorCode = messageCodec.readValue(buffer);
final errorMessage = messageCodec.readValue(buffer);
final errorDetails = messageCodec.readValue(buffer);
final errorStacktrace =
(buffer.hasRemaining) ? messageCodec.readValue(buffer) as String? : null;
if (errorCode is String &&
(errorMessage == null || errorMessage is String) &&
!buffer.hasRemaining) {
throw PlatformException(
code: errorCode,
message: errorMessage as String?,
details: errorDetails,
stacktrace: errorStacktrace);
} else {
throw const FormatException('Invalid envelope');
}
}

/// Like MethodChannel.invokeMethod, but decode on a thread
Future<T?> _invokeMethodDecodeOnThread<T>(MethodChannel channel, String method,
[dynamic arguments]) async {
final result = await channel.binaryMessenger.send(
channel.name,
channel.codec.encodeMethodCall(MethodCall(method, arguments)),
);
if (result == null) {
throw MissingPluginException(
'No implementation found for method $method on channel ${channel.name}');
}
return await compute(decodeEnvelope, result) as T?;
}

class FlutterContacts {
static const _channel = MethodChannel('github.com/QuisApp/flutter_contacts');
static const _eventChannel =
Expand Down Expand Up @@ -280,7 +323,8 @@ class FlutterContacts {
}) async {
// removing the types makes it crash at runtime
// ignore: omit_local_variable_types
List untypedContacts = await _channel.invokeMethod('select', [
List untypedContacts =
await _invokeMethodDecodeOnThread(_channel, 'select', [
id,
withProperties,
withThumbnail,
Expand Down