feat(android): Implement sharing internal files and text

This commit is contained in:
PapaTutuWawa 2023-09-18 17:58:16 +02:00
parent f971a0e078
commit 44187675c7
14 changed files with 891 additions and 633 deletions

View File

@ -4,7 +4,7 @@
<application>
<provider
android:name="org.moxxy.moxxy_native.content.MoxxyFileProvider"
android:authorities="org.moxxy.moxxyv2.fileprovider"
android:authorities="org.moxxy.moxxyv2.fileprovider2"
android:exported="false"
android:grantUriPermissions="true">
<meta-data

View File

@ -24,7 +24,7 @@ const val NOTIFICATION_EXTRA_ID_KEY = "notification_id"
const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime"
const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path"
const val MOXXY_FILEPROVIDER_ID = "org.moxxy.moxxyv2.fileprovider"
const val MOXXY_FILEPROVIDER_ID = "org.moxxy.moxxyv2.fileprovider2"
// Shared preferences keys
const val SHARED_PREFERENCES_KEY = "org.moxxy.moxxyv2"

View File

@ -1,6 +1,24 @@
package org.moxxy.moxxy_native.content
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID
import org.moxxy.moxxy_native.R
import java.io.File
class MoxxyFileProvider : FileProvider(R.xml.file_paths)
class MoxxyFileProvider : FileProvider(R.xml.file_paths) {
companion object {
/*
* Convert a path @path inside a sharable storage directory into a content URI, given
* the application's context @context.
* */
fun getUriForPath(context: Context, path: String): Uri {
return getUriForFile(
context,
MOXXY_FILEPROVIDER_ID,
File(path),
)
}
}
}

View File

@ -20,13 +20,13 @@ private fun wrapError(exception: Throwable): List<Any?> {
return listOf(
exception.code,
exception.message,
exception.details
exception.details,
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
)
}
}
@ -40,13 +40,14 @@ private fun wrapError(exception: Throwable): List<Any?> {
class FlutterError(
val code: String,
override val message: String? = null,
val details: Any? = null
val details: Any? = null,
) : Throwable()
enum class NotificationIcon(val raw: Int) {
WARNING(0),
ERROR(1),
NONE(2);
NONE(2),
;
companion object {
fun ofRaw(raw: Int): NotificationIcon? {
@ -58,7 +59,8 @@ enum class NotificationIcon(val raw: Int) {
enum class NotificationEventType(val raw: Int) {
MARKASREAD(0),
REPLY(1),
OPEN(2);
OPEN(2),
;
companion object {
fun ofRaw(raw: Int): NotificationEventType? {
@ -70,7 +72,8 @@ enum class NotificationEventType(val raw: Int) {
enum class NotificationChannelImportance(val raw: Int) {
MIN(0),
HIGH(1),
DEFAULT(2);
DEFAULT(2),
;
companion object {
fun ofRaw(raw: Int): NotificationChannelImportance? {
@ -85,7 +88,7 @@ data class NotificationMessageContent (
val body: String? = null,
/** The path and mime type of the media to show. */
val mime: String? = null,
val path: String? = null
val path: String? = null,
) {
companion object {
@ -119,7 +122,7 @@ data class NotificationMessage (
/** Milliseconds since epoch. */
val timestamp: Long,
/** The path to the avatar to use */
val avatarPath: String? = null
val avatarPath: String? = null,
) {
companion object {
@ -163,7 +166,7 @@ data class MessagingNotification (
/** The id for notification grouping. */
val groupId: String? = null,
/** Additional data to include. */
val extra: Map<String?, String?>? = null
val extra: Map<String?, String?>? = null,
) {
companion object {
@ -207,7 +210,7 @@ data class RegularNotification (
/** The id of the notification. */
val id: Long,
/** The icon to use. */
val icon: NotificationIcon
val icon: NotificationIcon,
) {
companion object {
@ -249,7 +252,7 @@ data class NotificationEvent (
*/
val payload: String? = null,
/** Extra data. Only set when type == NotificationType.reply. */
val extra: Map<String?, String?>? = null
val extra: Map<String?, String?>? = null,
) {
companion object {
@ -281,7 +284,7 @@ data class NotificationI18nData (
/** The content of the "mark as read" button. */
val markAsRead: String,
/** The text to show when *you* reply. */
val you: String
val you: String,
) {
companion object {
@ -305,7 +308,7 @@ data class NotificationI18nData (
/** Generated class from Pigeon that represents data sent in messages. */
data class NotificationGroup(
val id: String,
val description: String
val description: String,
) {
companion object {
@ -333,7 +336,7 @@ data class NotificationChannel (
val showBadge: Boolean,
val groupId: String? = null,
val vibration: Boolean,
val enableLights: Boolean
val enableLights: Boolean,
) {
companion object {
@ -363,6 +366,7 @@ data class NotificationChannel (
)
}
}
@Suppress("UNCHECKED_CAST")
private object MoxxyNotificationsApiCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
@ -468,6 +472,7 @@ interface MoxxyNotificationsApi {
val codec: MessageCodec<Any?> by lazy {
MoxxyNotificationsApiCodec
}
/** Sets up an instance of `MoxxyNotificationsApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyNotificationsApi?) {

View File

@ -14,10 +14,8 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.app.TaskStackBuilder
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.IconCompat
import org.moxxy.moxxy_native.MARK_AS_READ_ACTION
import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME
@ -27,7 +25,7 @@ import org.moxxy.moxxy_native.REPLY_ACTION
import org.moxxy.moxxy_native.REPLY_TEXT_KEY
import org.moxxy.moxxy_native.TAG
import org.moxxy.moxxy_native.TAP_ACTION
import java.io.File
import org.moxxy.moxxy_native.content.MoxxyFileProvider
class NotificationsImplementation(private val context: Context) : MoxxyNotificationsApi {
override fun createNotificationGroups(groups: List<NotificationGroup>) {
@ -207,11 +205,7 @@ class NotificationsImplementation(private val context: Context) : MoxxyNotificat
)
// If we got an image, turn it into a content URI and set it
if (message.content.mime != null && message.content.path != null) {
val fileUri = FileProvider.getUriForFile(
context,
MOXXY_FILEPROVIDER_ID,
File(message.content.path),
)
val fileUri = MoxxyFileProvider.getUriForPath(context, message.content.path)
msg.apply {
setData(message.content.mime, fileUri)

View File

@ -8,6 +8,8 @@ import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
@ -41,17 +43,66 @@ class FlutterError(
val details: Any? = null,
) : Throwable()
/** Generated class from Pigeon that represents data sent in messages. */
data class ShareItem(
val path: String? = null,
val mime: String,
val text: String? = null,
) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromList(list: List<Any?>): ShareItem {
val path = list[0] as String?
val mime = list[1] as String
val text = list[2] as String?
return ShareItem(path, mime, text)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
path,
mime,
text,
)
}
}
@Suppress("UNCHECKED_CAST")
private object MoxxyPlatformApiCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
128.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ShareItem.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is ShareItem -> {
stream.write(128)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyPlatformApi {
fun getPersistentDataPath(): String
fun getCacheDataPath(): String
fun openBatteryOptimisationSettings()
fun isIgnoringBatteryOptimizations(): Boolean
fun shareItems(items: List<ShareItem>, genericMimeType: String)
companion object {
/** The codec used by MoxxyPlatformApi. */
val codec: MessageCodec<Any?> by lazy {
StandardMessageCodec()
MoxxyPlatformApiCodec
}
/** Sets up an instance of `MoxxyPlatformApi` to handle messages through the `binaryMessenger`. */
@ -122,6 +173,26 @@ interface MoxxyPlatformApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.shareItems", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val itemsArg = args[0] as List<ShareItem>
val genericMimeTypeArg = args[1] as String
var wrapped: List<Any?>
try {
api.shareItems(itemsArg, genericMimeTypeArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@ -5,6 +5,8 @@ import android.content.Intent
import android.net.Uri
import android.os.PowerManager
import android.provider.Settings
import androidx.core.app.ShareCompat
import org.moxxy.moxxy_native.content.MoxxyFileProvider
class PlatformImplementation(private val context: Context) : MoxxyPlatformApi {
override fun getPersistentDataPath(): String {
@ -27,4 +29,28 @@ class PlatformImplementation(private val context: Context) : MoxxyPlatformApi {
val pm = context.getSystemService(PowerManager::class.java)
return pm.isIgnoringBatteryOptimizations(context.packageName)
}
override fun shareItems(items: List<ShareItem>, genericMimeType: String) {
// Empty lists make no sense
assert(items.isNotEmpty())
// Convert the paths to content URIs
val builder = ShareCompat.IntentBuilder(context).setType(genericMimeType)
for (item in items) {
assert(item.text == null && item.path != null || item.text != null && item.path == null)
if (item.text != null) {
builder.setText(item.text)
} else if (item.path != null) {
builder.addStream(MoxxyFileProvider.getUriForPath(context, item.path))
}
}
// We cannot just use startChooser() because then Android complains that we're not attached
// to an Activity. So, we just ask it to start a new one.
val intent = builder.createChooserIntent().apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
}

View File

@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:moxxy_native/moxxy_native.dart';
import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart';
@pragma('vm:entrypoint')
@ -163,7 +164,63 @@ class MyAppState extends State<MyApp> {
awaitable: false,
);
},
child: const Text('Start foreground service')),
child: const Text('Start foreground service'),
),
TextButton(
onPressed: () async {
// Pick a file and copy it into the internal storage directory
final mediaDir = Directory(
p.join(
await MoxxyPlatformApi().getPersistentDataPath(),
'media',
),
);
if (!mediaDir.existsSync()) {
await mediaDir.create(recursive: true);
}
final pickResult = await MoxxyPickerApi()
.pickFiles(FilePickerType.image, true);
if (pickResult.isEmpty) return;
final shareItems = List<ShareItem>.empty(growable: true);
for (final result in pickResult) {
final mediaDirPath = p.join(
mediaDir.path,
p.basename(result!),
);
await File(result).copy(mediaDirPath);
shareItems.add(
ShareItem(
path: mediaDirPath,
mime: 'image/jpeg',
),
);
}
// Share with the system
await MoxxyPlatformApi().shareItems(
shareItems,
'image/*',
);
},
child: const Text('Share internal files'),
),
TextButton(
onPressed: () async {
// Share with the system
await MoxxyPlatformApi().shareItems(
[
ShareItem(
mime: 'text/plain',
text: 'Hello World!',
),
],
'text/*',
);
},
child: const Text('Share some text'),
),
],
),
),

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.3.0"
clock:
dependency: transitive
description:
@ -37,10 +37,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.17.1"
crypto:
dependency: transitive
description:
@ -95,10 +95,10 @@ packages:
dependency: transitive
description:
name: js
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.7"
lints:
dependency: transitive
description:
@ -119,10 +119,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.dev"
source: hosted
version: "0.12.13"
version: "0.12.15"
material_color_utilities:
dependency: transitive
description:
@ -135,10 +135,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
version: "1.9.1"
moxlib:
dependency: transitive
description:
@ -153,15 +153,15 @@ packages:
path: ".."
relative: true
source: path
version: "0.1.0"
version: "0.2.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.8.2"
version: "1.8.3"
permission_handler:
dependency: "direct main"
description:
@ -267,10 +267,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.dev"
source: hosted
version: "0.4.16"
version: "0.5.1"
typed_data:
dependency: transitive
description:
@ -296,5 +296,5 @@ packages:
source: hosted
version: "2.1.4"
sdks:
dart: ">=2.19.6 <3.0.0"
dart: ">=3.0.0-0 <4.0.0"
flutter: ">=2.8.0"

View File

@ -31,6 +31,7 @@ dependencies:
permission_handler: ^10.4.5
get_it: ^7.6.0
logging: ^1.2.0
path: ^1.8.3
dev_dependencies:
flutter_test:

View File

@ -44,7 +44,7 @@
]);
lib = pkgs.lib;
pinnedJDK = pkgs.jdk17;
flutterVersion = pkgs.flutter37;
flutterVersion = pkgs.flutter;
in {
devShell = pkgs.mkShell {
buildInputs = with pkgs; [

View File

@ -8,6 +8,60 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
class ShareItem {
ShareItem({
this.path,
required this.mime,
this.text,
});
String? path;
String mime;
String? text;
Object encode() {
return <Object?>[
path,
mime,
text,
];
}
static ShareItem decode(Object result) {
result as List<Object?>;
return ShareItem(
path: result[0] as String?,
mime: result[1]! as String,
text: result[2] as String?,
);
}
}
class _MoxxyPlatformApiCodec extends StandardMessageCodec {
const _MoxxyPlatformApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is ShareItem) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return ShareItem.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
class MoxxyPlatformApi {
/// Constructor for [MoxxyPlatformApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
@ -16,7 +70,7 @@ class MoxxyPlatformApi {
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = StandardMessageCodec();
static const MessageCodec<Object?> codec = _MoxxyPlatformApiCodec();
Future<String> getPersistentDataPath() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
@ -120,4 +174,27 @@ class MoxxyPlatformApi {
return (replyList[0] as bool?)!;
}
}
Future<void> shareItems(
List<ShareItem?> arg_items, String arg_genericMimeType) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.shareItems', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_items, arg_genericMimeType]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else {
return;
}
}
}

View File

@ -10,6 +10,13 @@ import 'package:pigeon/pigeon.dart';
),
),
)
class ShareItem {
const ShareItem(this.path, this.mime, this.text);
final String? path;
final String mime;
final String? text;
}
@HostApi()
abstract class MoxxyPlatformApi {
String getPersistentDataPath();
@ -19,4 +26,6 @@ abstract class MoxxyPlatformApi {
void openBatteryOptimisationSettings();
bool isIgnoringBatteryOptimizations();
void shareItems(List<ShareItem> items, String genericMimeType);
}