Merge branch 'feat/notifications'

This commit is contained in:
PapaTutuWawa 2023-08-04 13:54:41 +02:00
commit 497ac279cc
Signed by: PapaTutuWawa
GPG Key ID: 56C749835F3CE824
64 changed files with 3811 additions and 449 deletions

14
.gitlint Normal file
View File

@ -0,0 +1,14 @@
[general]
ignore=B5,B6,B7,B8
[title-max-length]
line-length=72
[title-trailing-punctuation]
[title-hard-tab]
[title-match-regex]
regex=^(feat|fix|chore|refactor)\((android|ios|linux|windows|macos|interface|base|repo)(,(android|ios|linux|windows|macos|interface|base))*\): [A-Z0-9].*$
[body-trailing-whitespace]
[body-first-line-empty]

View File

@ -3,6 +3,60 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 2023-08-04
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`moxplatform` - `v0.1.17+2`](#moxplatform---v01172)
- [`moxplatform_android` - `v0.1.18`](#moxplatform_android---v0118)
- [`moxplatform_platform_interface` - `v0.1.18`](#moxplatform_platform_interface---v0118)
---
#### `moxplatform` - `v0.1.17+2`
- **FIX**: Format and lint.
#### `moxplatform_android` - `v0.1.18`
- **FIX**: Format and lint.
- **FIX**: Fix self-replies after receiving another message.
- **FIX**: Add payload to all intents.
- **FIX**: Fix images disappearing after replying.
- **FEAT**: Move recordSentMessage to pigeon.
- **FEAT**: Move the crypto APIs to pigeon.
- **FEAT**: Adjust to Moxxy changes.
- **FEAT**: Store the avatar path also in the shared preferences.
- **FEAT**: Allow the sender's data being null.
- **FEAT**: Allow attaching arbitrary data to the notification.
- **FEAT**: Allow showing regular notifications.
- **FEAT**: Make i18n data a bit more persistent.
- **FEAT**: Color in the notification silhouette.
- **FEAT**: Allow setting the self-avatar.
- **FEAT**: Take care of i18n.
#### `moxplatform_platform_interface` - `v0.1.18`
- **FIX**: Format and lint.
- **FIX**: Add payload to all intents.
- **FEAT**: Move recordSentMessage to pigeon.
- **FEAT**: Move the crypto APIs to pigeon.
- **FEAT**: Allow the sender's data being null.
- **FEAT**: Allow attaching arbitrary data to the notification.
- **FEAT**: Allow showing regular notifications.
- **FEAT**: Color in the notification silhouette.
- **FEAT**: Allow setting the self-avatar.
- **FEAT**: Take care of i18n.
## 2023-07-21
### Changes

View File

@ -9,9 +9,11 @@ This repo is based on [very_good_flutter_plugin](https://github.com/VeryGoodOpen
The development of this package is based on [melos](https://pub.dev/packages/melos).
To make all packages link to each other locally, begin by running `melos bootstrap`. After editing
the code and making your changes, please run `melos run analyze` to make sure that no linter warnings
are left inside the code.
the code and making your changes, please format the code using `melos run format` and lint using `melos run analyze`.
When done - and a version bump is appropriate - bump the version of all packages using `melos version` and
publish with `melos publish --no-dry-run --git-tag-version`.
## Acknowledgements
- [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). moxplatform_android is basically just a copy and paste of [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service).
- [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). moxplatform_android's service implementation is basically just a copy and paste of [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service).

View File

@ -1,4 +1,8 @@
include: package:very_good_analysis/analysis_options.yaml
analyzer:
exclude:
- lib/src/api.g.dart
linter:
rules:
public_member_api_docs: false

View File

@ -47,7 +47,7 @@ android {
applicationId "com.example.example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion flutter.minSdkVersion
minSdkVersion 26
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@ -34,5 +34,5 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

View File

@ -1,17 +1,73 @@
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
import 'package:permission_handler/permission_handler.dart';
/// The id of the notification channel.
const channelId = "me.polynom.moxplatform.testing3";
const otherChannelId = "me.polynom.moxplatform.testing4";
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
class Sender {
const Sender(this.name, this.jid);
final String name;
final String jid;
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
initStateAsync();
}
Future<void> initStateAsync() async {
await Permission.notification.request();
await MoxplatformPlugin.notifications.createNotificationChannel(
"Test notification channel",
"Test1",
channelId,
false,
);
await MoxplatformPlugin.notifications.createNotificationChannel(
"Test notification channel for warnings",
"Test2",
otherChannelId,
false,
);
await MoxplatformPlugin.notifications.setI18n(
NotificationI18nData(
reply: "答える",
markAsRead: "読みた",
you: "あなた",
),
);
MoxplatformPlugin.notifications.getEventStream().listen((event) {
// ignore: avoid_print
print(
'NotificationEvent(type: ${event.type}, jid: ${event.jid}, payload: ${event.payload}, extras: ${event.extra})',
);
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
@ -24,9 +80,25 @@ class MyApp extends StatelessWidget {
}
}
class MyHomePage extends StatelessWidget {
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
/// List of "Message senders".
final List<Sender> senders = const [
Sender('Mash Kyrielight', 'mash@example.org'),
Sender('Rio Tsukatsuki', 'rio@millenium'),
Sender('Raiden Shogun', 'raiden@tevhat'),
];
/// List of sent messages.
List<NotificationMessage> messages =
List<NotificationMessage>.empty(growable: true);
Future<void> _cryptoTest() async {
final result = await FilePicker.platform.pickFiles();
if (result == null) {
@ -46,10 +118,13 @@ class MyHomePage extends StatelessWidget {
final end = DateTime.now();
final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch;
// ignore: avoid_print
print('TIME: ${diff / 1000}s');
// ignore: avoid_print
print('DONE (${enc != null})');
final lengthEnc = await File('$path.enc').length();
final lengthOrig = await File(path).length();
// ignore: avoid_print
print('Encrypted file is $lengthEnc Bytes large (Orig $lengthOrig)');
await MoxplatformPlugin.crypto.decryptFile(
@ -60,9 +135,11 @@ class MyHomePage extends StatelessWidget {
CipherAlgorithm.aes256CbcPkcs7,
'SHA-256',
);
// ignore: avoid_print
print('DONE');
final lengthDec = await File('$path.dec').length();
// ignore: avoid_print
print('Decrypted file is $lengthDec Bytes large (Orig $lengthOrig)');
}
@ -73,9 +150,8 @@ class MyHomePage extends StatelessWidget {
title: const Text('Moxplatform Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
child: ListView(
children: [
ElevatedButton(
onPressed: _cryptoTest,
child: const Text('Test cryptography'),
@ -88,16 +164,113 @@ class MyHomePage extends StatelessWidget {
),
ElevatedButton(
onPressed: () {
MoxplatformPlugin.contacts.recordSentMessage('Person', 'Person', fallbackIcon: FallbackIconType.person);
MoxplatformPlugin.contacts.recordSentMessage('Person', 'Person',
fallbackIcon: FallbackIconType.person);
},
child: const Text('Test recordSentMessage (person fallback)'),
),
ElevatedButton(
onPressed: () {
MoxplatformPlugin.contacts.recordSentMessage('Notes', 'Notes', fallbackIcon: FallbackIconType.notes);
MoxplatformPlugin.contacts.recordSentMessage('Notes', 'Notes',
fallbackIcon: FallbackIconType.notes);
},
child: const Text('Test recordSentMessage (notes fallback)'),
),
ElevatedButton(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
);
// ignore: avoid_print
print('Picked file: ${result?.files.single.path}');
// Create a new message.
final senderIndex = Random().nextInt(senders.length);
final time = DateTime.now().millisecondsSinceEpoch;
messages.add(NotificationMessage(
jid: senders[senderIndex].jid,
sender: senders[senderIndex].name,
content: NotificationMessageContent(
body: result != null ? null : 'Message #${messages.length}',
mime: 'image/jpeg',
path: result?.files.single.path,
),
timestamp: time,
));
await Future<void>.delayed(const Duration(seconds: 4));
await MoxplatformPlugin.notifications.showMessagingNotification(
MessagingNotification(
id: 2343,
title: 'Test conversation',
messages: messages,
channelId: channelId,
jid: 'testjid',
isGroupchat: true,
extra: {
'jid': 'testjid',
'avatarPath': 'lol',
'rio': 'cute',
},
),
);
},
child: const Text('Show messaging notification'),
),
ElevatedButton(
onPressed: () {
MoxplatformPlugin.notifications.showNotification(
RegularNotification(
id: 4384,
title: 'Warning',
body: 'Something brokey',
channelId: otherChannelId,
icon: NotificationIcon.warning,
),
);
},
child: const Text('Show warning notification'),
),
ElevatedButton(
onPressed: () {
MoxplatformPlugin.notifications.showNotification(
RegularNotification(
id: 4384,
title: 'Error',
body: "Lol, you're on your own",
channelId: otherChannelId,
icon: NotificationIcon.error,
),
);
},
child: const Text('Show error notification'),
),
ElevatedButton(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
);
if (result == null) return;
MoxplatformPlugin.notifications
.setNotificationSelfAvatar(result.files.single.path!);
},
child: const Text('Set notification self-avatar'),
),
ElevatedButton(
onPressed: () async {
// ignore: avoid_print
print(await MoxplatformPlugin.platform.getPersistentDataPath());
},
child: const Text('Get data directory'),
),
ElevatedButton(
onPressed: () async {
// ignore: avoid_print
print(await MoxplatformPlugin.platform.getCacheDataPath());
},
child: const Text('Get cache directory'),
),
],
),
),

View File

@ -32,12 +32,14 @@ dependencies:
moxplatform:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.17+1
version: 0.1.17+2
moxplatform_android:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.17+1
version: 0.1.18
file_picker: 5.2.0+1
permission_handler: 10.4.3
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.

View File

@ -8,5 +8,7 @@ command:
usePubspecOverrides: true
scripts:
format:
exec: dart format .
analyze:
exec: dart analyze .
exec: flutter analyze

View File

@ -1,3 +1,7 @@
## 0.1.17+2
- **FIX**: Format and lint.
## 0.1.17+1
- Update a dependency to the latest release.

View File

@ -1,4 +1,6 @@
library moxplatform;
export 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
export 'src/plugin.dart';
export 'src/types.dart';

View File

@ -2,7 +2,9 @@ import 'package:moxplatform_platform_interface/moxplatform_platform_interface.da
class MoxplatformPlugin {
static IsolateHandler get handler => MoxplatformInterface.handler;
static MediaScannerImplementation get media => MoxplatformInterface.media;
static CryptographyImplementation get crypto => MoxplatformInterface.crypto;
static ContactsImplementation get contacts => MoxplatformInterface.contacts;
static NotificationsImplementation get notifications =>
MoxplatformInterface.notifications;
static PlatformImplementation get platform => MoxplatformInterface.platform;
}

View File

@ -1,6 +1,6 @@
name: moxplatform
description: Moxxy platform-specific code
version: 0.1.17+1
version: 0.1.17+2
publish_to: https://git.polynom.me/api/packages/Moxxy/pub
homepage: https://codeberg.org/moxxy/moxplatform
@ -26,10 +26,10 @@ dependencies:
moxplatform_android:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.17+1
version: ^0.1.18
moxplatform_platform_interface:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.17+1
version: ^0.1.18
dev_dependencies:
flutter_test:

View File

@ -1,3 +1,21 @@
## 0.1.18
- **FIX**: Format and lint.
- **FIX**: Fix self-replies after receiving another message.
- **FIX**: Add payload to all intents.
- **FIX**: Fix images disappearing after replying.
- **FEAT**: Move recordSentMessage to pigeon.
- **FEAT**: Move the crypto APIs to pigeon.
- **FEAT**: Adjust to Moxxy changes.
- **FEAT**: Store the avatar path also in the shared preferences.
- **FEAT**: Allow the sender's data being null.
- **FEAT**: Allow attaching arbitrary data to the notification.
- **FEAT**: Allow showing regular notifications.
- **FEAT**: Make i18n data a bit more persistent.
- **FEAT**: Color in the notification silhouette.
- **FEAT**: Allow setting the self-avatar.
- **FEAT**: Take care of i18n.
## 0.1.17+1
- **FIX**: Accidentally used the name as the target's key. Oops.

View File

@ -27,15 +27,17 @@ apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
android {
compileSdkVersion 31
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_14
targetCompatibility JavaVersion.VERSION_14
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 16
// What Moxxy currently uses
minSdkVersion 26
targetSdkVersion 33
}
}

View File

@ -1,11 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.polynom.moxplatform_android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application>
<provider
android:name="me.polynom.moxplatform_android.MoxplatformFileProvider"
android:authorities="me.polynom.moxplatform_android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:enabled="true"
android:exported="true"
@ -27,5 +38,6 @@
</intent-filter>
</receiver>
<receiver android:name=".NotificationReceiver" />
</application>
</manifest>

View File

@ -1,5 +1,7 @@
package me.polynom.moxplatform_android;
import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY;
import android.app.AlarmManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@ -85,10 +87,10 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
}
public static boolean isManuallyStopped(Context context) {
return context.getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE).getBoolean(manuallyStoppedKey, false);
return context.getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE).getBoolean(manuallyStoppedKey, false);
}
public void setManuallyStopped(Context context, boolean value) {
context.getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE)
context.getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE)
.edit()
.putBoolean(manuallyStoppedKey, value)
.apply();
@ -151,7 +153,7 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
FlutterInjector.instance().flutterLoader().startInitialization(getApplicationContext());
}
long entrypointHandle = getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE)
long entrypointHandle = getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE)
.getLong(MoxplatformAndroidPlugin.entrypointKey, 0);
FlutterInjector.instance().flutterLoader().ensureInitializationComplete(getApplicationContext(), null);
FlutterCallbackInformation callback = FlutterCallbackInformation.lookupCallbackInformation(entrypointHandle);

View File

@ -16,11 +16,11 @@ public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (MoxplatformAndroidPlugin.getStartAtBoot(context)) {
if (BackgroundService.wakeLock == null) {
Log.d(TAG, "Wakelock is null. Acquiring it...");
BackgroundService.getLock(context).acquire(MoxplatformConstants.WAKE_LOCK_DURATION);
Log.d(TAG, "Wakelock acquired...");
}
if (BackgroundService.wakeLock == null) {
Log.d(TAG, "Wakelock is null. Acquiring it...");
BackgroundService.getLock(context).acquire(MoxplatformConstants.WAKE_LOCK_DURATION);
Log.d(TAG, "Wakelock acquired...");
}
ContextCompat.startForegroundService(context, new Intent(context, BackgroundService.class));
}

View File

@ -6,6 +6,34 @@ const val TAG = "Moxplatform"
// The size of the buffer to hashing, encryption, and decryption in bytes.
const val BUFFER_SIZE = 8096
// The data key for text entered in the notification's reply field
const val REPLY_TEXT_KEY = "key_reply_text"
// The key for the notification id to mark as read
const val MARK_AS_READ_ID_KEY = "notification_id"
// Values for actions performed through the notification
const val REPLY_ACTION = "reply"
const val MARK_AS_READ_ACTION = "mark_as_read"
const val TAP_ACTION = "tap"
// Extra data keys for the intents that reach the NotificationReceiver
const val NOTIFICATION_EXTRA_JID_KEY = "jid"
const val NOTIFICATION_EXTRA_ID_KEY = "notification_id"
// Extra data keys for messages embedded inside the notification style
const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime"
const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path"
const val MOXPLATFORM_FILEPROVIDER_ID = "me.polynom.moxplatform_android.fileprovider"
// Shared preferences keys
const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android"
const val SHARED_PREFERENCES_YOU_KEY = "you"
const val SHARED_PREFERENCES_MARK_AS_READ_KEY = "mark_as_read"
const val SHARED_PREFERENCES_REPLY_KEY = "reply"
const val SHARED_PREFERENCES_AVATAR_KEY = "avatar_path"
// TODO: Maybe try again to rewrite the entire plugin in Kotlin
//const val METHOD_CHANNEL_KEY = "me.polynom.moxplatform_android"
//const val BACKGROUND_METHOD_CHANNEL_KEY = METHOD_CHANNEL_KEY + "_bg"

View File

@ -1,6 +1,8 @@
package me.polynom.moxplatform_android
import android.util.Log
import me.polynom.moxplatform_android.Api.CipherAlgorithm
import me.polynom.moxplatform_android.Api.CryptographyResult
import java.io.FileInputStream
import java.io.FileOutputStream
@ -10,6 +12,7 @@ import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.concurrent.thread
// A FileOutputStream that continuously hashes whatever it writes to the file.
private class HashedFileOutputStream(name: String, hashAlgorithm: String) : FileOutputStream(name) {
@ -30,115 +33,126 @@ private class HashedFileOutputStream(name: String, hashAlgorithm: String) : File
}
}
fun getCipherSpecFromInteger(algorithmType: Int): String {
return when (algorithmType) {
0 -> "AES_128/GCM/NoPadding"
1 -> "AES_256/GCM/NoPadding"
2 -> "AES_256/CBC/PKCS7PADDING"
else -> ""
fun getCipherSpecFromInteger(algorithm: CipherAlgorithm): String {
return when (algorithm) {
CipherAlgorithm.AES128GCM_NO_PADDING -> "AES_128/GCM/NoPadding"
CipherAlgorithm.AES256GCM_NO_PADDING -> "AES_256/GCM/NoPadding"
CipherAlgorithm.AES256CBC_PKCS7 -> "AES_256/CBC/PKCS7PADDING"
}
}
// Compute the hash, specified by @algorithm, of the file at path @srcFile. If an exception
// occurs, returns null. If everything went well, returns the raw hash of @srcFile.
fun hashFile(srcFile: String, algorithm: String): ByteArray? {
val buffer = ByteArray(BUFFER_SIZE)
try {
val digest = MessageDigest.getInstance(algorithm)
val fInputStream = FileInputStream(srcFile)
var length: Int
fun hashFile(srcFile: String, algorithm: String, result: Api.Result<ByteArray?>) {
thread(start = true) {
val buffer = ByteArray(BUFFER_SIZE)
try {
val digest = MessageDigest.getInstance(algorithm)
val fInputStream = FileInputStream(srcFile)
var length: Int
while (true) {
length = fInputStream.read()
if (length <= 0) break
while (true) {
length = fInputStream.read()
if (length <= 0) break
// Only update the digest if we read more than 0 bytes
digest.update(buffer, 0, length)
// Only update the digest if we read more than 0 bytes
digest.update(buffer, 0, length)
}
fInputStream.close()
result.success(digest.digest())
} catch (e: Exception) {
Log.e(TAG, "[hashFile]: " + e.stackTraceToString())
result.success(null)
}
fInputStream.close()
return digest.digest()
} catch (e: Exception) {
Log.e(TAG, "[hashFile]: " + e.stackTraceToString())
return null
}
}
// Encrypt the plaintext file at @src to @dest using the secret key @key and the IV @iv. The algorithm is chosen using @cipherAlgorithm. The file is additionally
// hashed before and after encryption using the hash algorithm specified by @hashAlgorithm.
fun encryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: String, hashAlgorithm: String): HashMap<String, ByteArray>? {
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherAlgorithm)
try {
val digest = MessageDigest.getInstance(hashAlgorithm)
val cipher = Cipher.getInstance(cipherAlgorithm)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
fun encryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: CipherAlgorithm, hashAlgorithm: String, result: Api.Result<CryptographyResult?>) {
thread(start = true) {
val cipherSpec = getCipherSpecFromInteger(cipherAlgorithm)
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherSpec)
try {
val digest = MessageDigest.getInstance(hashAlgorithm)
val cipher = Cipher.getInstance(cipherSpec)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
val fileInputStream = FileInputStream(src)
val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
val fileInputStream = FileInputStream(src)
val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
}
// Flush and close
cipherOutputStream.flush()
cipherOutputStream.close()
fileInputStream.close()
result.success(
CryptographyResult().apply {
plaintextHash = digest.digest()
ciphertextHash = fileOutputStream.digest()
}
)
} catch (e: Exception) {
Log.e(TAG, "[encryptAndHash]: " + e.stackTraceToString())
result.success(null)
}
// Flush and close
cipherOutputStream.flush()
cipherOutputStream.close()
fileInputStream.close()
return hashMapOf(
"plaintextHash" to digest.digest(),
"ciphertextHash" to fileOutputStream.digest(),
)
} catch (e: Exception) {
Log.e(TAG, "[encryptAndHash]: " + e.stackTraceToString())
return null
}
}
// Decrypt the ciphertext file at @src to @dest using the secret key @key and the IV @iv. The algorithm is chosen using @cipherAlgorithm. The file is additionally
// hashed before and after decryption using the hash algorithm specified by @hashAlgorithm.
fun decryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: String, hashAlgorithm: String): HashMap<String, ByteArray>? {
// Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherAlgorithm)
try {
val digest = MessageDigest.getInstance(hashAlgorithm)
val cipher = Cipher.getInstance(cipherAlgorithm)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
fun decryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: CipherAlgorithm, hashAlgorithm: String, result: Api.Result<CryptographyResult?>) {
thread(start = true) {
val cipherSpec = getCipherSpecFromInteger(cipherAlgorithm)
// Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherSpec)
try {
val digest = MessageDigest.getInstance(hashAlgorithm)
val cipher = Cipher.getInstance(cipherSpec)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
val fileInputStream = FileInputStream(src)
val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
val fileInputStream = FileInputStream(src)
val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
// Read, decrypt, and hash until we read 0 bytes
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
// Read, decrypt, and hash until we read 0 bytes
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
}
// Flush
cipherOutputStream.flush()
cipherOutputStream.close()
fileInputStream.close()
result.success(
CryptographyResult().apply {
plaintextHash = digest.digest()
ciphertextHash = fileOutputStream.digest()
}
)
} catch (e: Exception) {
Log.e(TAG, "[hashAndDecrypt]: " + e.stackTraceToString())
result.success(null)
}
// Flush
cipherOutputStream.flush()
cipherOutputStream.close()
fileInputStream.close()
return hashMapOf(
"plaintextHash" to digest.digest(),
"ciphertextHash" to fileOutputStream.digest(),
)
} catch (e: Exception) {
Log.e(TAG, "[hashAndDecrypt]: " + e.stackTraceToString())
return null
}
}

View File

@ -0,0 +1,6 @@
package me.polynom.moxplatform_android
import androidx.core.content.FileProvider
class MoxplatformFileProvider : FileProvider(R.xml.file_paths) {
}

View File

@ -1,9 +1,15 @@
package me.polynom.moxplatform_android;
import static me.polynom.moxplatform_android.RecordSentMessageKt.recordSentMessage;
import static androidx.core.content.ContextCompat.getSystemService;
import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY;
import static me.polynom.moxplatform_android.CryptoKt.*;
import static me.polynom.moxplatform_android.RecordSentMessageKt.*;
import me.polynom.moxplatform_android.Api.*;
import android.app.ActivityManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -12,254 +18,283 @@ import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.io.FileInputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.service.ServiceAware;
import io.flutter.embedding.engine.plugins.service.ServicePluginBinding;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.EventChannel.StreamHandler;
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 io.flutter.plugin.common.JSONMethodCodec;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware {
public static final String entrypointKey = "entrypoint_handle";
public static final String extraDataKey = "extra_data";
private static final String autoStartAtBootKey = "auto_start_at_boot";
public static final String sharedPrefKey = "me.polynom.moxplatform_android";
private static final String TAG = "moxplatform_android";
public static final String methodChannelKey = "me.polynom.moxplatform_android";
public static final String dataReceivedMethodName = "dataReceived";
public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, ServiceAware, MoxplatformApi {
public static final String entrypointKey = "entrypoint_handle";
public static final String extraDataKey = "extra_data";
private static final String autoStartAtBootKey = "auto_start_at_boot";
private static final String TAG = "moxplatform_android";
public static final String methodChannelKey = "me.polynom.moxplatform_android";
public static final String dataReceivedMethodName = "dataReceived";
private static final List<MoxplatformAndroidPlugin> _instances = new ArrayList<>();
private BackgroundService service;
private MethodChannel channel;
private Context context;
private static final List<MoxplatformAndroidPlugin> _instances = new ArrayList<>();
private BackgroundService service;
private MethodChannel channel;
private static EventChannel notificationChannel;
public static EventSink notificationSink;
public MoxplatformAndroidPlugin() {
_instances.add(this);
}
private Context context;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), methodChannelKey);
channel.setMethodCallHandler(this);
context = flutterPluginBinding.getApplicationContext();
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey));
Log.d(TAG, "Attached to engine");
}
static void registerWith(Registrar registrar) {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context());
final MoxplatformAndroidPlugin plugin = new MoxplatformAndroidPlugin();
localBroadcastManager.registerReceiver(plugin, new IntentFilter(methodChannelKey));
final MethodChannel channel = new MethodChannel(registrar.messenger(), "me.polynom/background_service_android", JSONMethodCodec.INSTANCE);
channel.setMethodCallHandler(plugin);
plugin.channel = channel;
Log.d(TAG, "Registered against registrar");
}
/// Store the entrypoint handle and extra data for the background service.
private void configure(long entrypointHandle, String extraData) {
SharedPreferences prefs = context.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE);
prefs.edit()
.putLong(entrypointKey, entrypointHandle)
.putString(extraDataKey, extraData)
.apply();
}
public static long getHandle(Context c) {
return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getLong(entrypointKey, 0);
}
public static String getExtraData(Context c) {
return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getString(extraDataKey, "");
}
public static void setStartAtBoot(Context c, boolean value) {
c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE)
.edit()
.putBoolean(autoStartAtBootKey, value)
.apply();
}
public static boolean getStartAtBoot(Context c) {
return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getBoolean(autoStartAtBootKey, false);
}
private boolean isRunning() {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo info : manager.getRunningServices(Integer.MAX_VALUE)) {
if (BackgroundService.class.getName().equals(info.service.getClassName())) {
return true;
}
public MoxplatformAndroidPlugin() {
_instances.add(this);
}
return false;
}
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), methodChannelKey);
channel.setMethodCallHandler(this);
context = flutterPluginBinding.getApplicationContext();
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "configure":
ArrayList args = (ArrayList) call.arguments;
long handle = (long) args.get(0);
String extraData = (String) args.get(1);
notificationChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "me.polynom/notification_stream");
notificationChannel.setStreamHandler(this);
configure(handle, extraData);
result.success(true);
break;
case "isRunning":
result.success(isRunning());
break;
case "start":
MoxplatformAndroidPlugin.setStartAtBoot(context, true);
BackgroundService.enqueue(context);
Intent intent = new Intent(context, BackgroundService.class);
ContextCompat.startForegroundService(context, intent);
Log.d(TAG, "Service started");
result.success(true);
break;
case "sendData":
for (MoxplatformAndroidPlugin plugin : _instances) {
if (plugin.service != null) {
plugin.service.receiveData((String) call.arguments);
break;
}
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey));
MoxplatformApi.setup(flutterPluginBinding.getBinaryMessenger(), this);
Log.d(TAG, "Attached to engine");
}
static void registerWith(Registrar registrar) {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context());
final MoxplatformAndroidPlugin plugin = new MoxplatformAndroidPlugin();
localBroadcastManager.registerReceiver(plugin, new IntentFilter(methodChannelKey));
final MethodChannel channel = new MethodChannel(registrar.messenger(), "me.polynom/background_service_android", JSONMethodCodec.INSTANCE);
channel.setMethodCallHandler(plugin);
plugin.channel = channel;
Log.d(TAG, "Registered against registrar");
}
@Override
public void onCancel(Object arguments) {
Log.d(TAG, "Removed listener");
notificationSink = null;
}
@Override
public void onListen(Object arguments, EventChannel.EventSink eventSink) {
Log.d(TAG, "Attached listener");
notificationSink = eventSink;
}
/// Store the entrypoint handle and extra data for the background service.
private void configure(long entrypointHandle, String extraData) {
SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE);
prefs.edit().putLong(entrypointKey, entrypointHandle).putString(extraDataKey, extraData).apply();
}
public static long getHandle(Context c) {
return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getLong(entrypointKey, 0);
}
public static String getExtraData(Context c) {
return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getString(extraDataKey, "");
}
public static void setStartAtBoot(Context c, boolean value) {
c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit().putBoolean(autoStartAtBootKey, value).apply();
}
public static boolean getStartAtBoot(Context c) {
return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getBoolean(autoStartAtBootKey, false);
}
private boolean isRunning() {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo info : manager.getRunningServices(Integer.MAX_VALUE)) {
if (BackgroundService.class.getName().equals(info.service.getClassName())) {
return true;
}
}
result.success(true);
break;
case "encryptFile":
Thread encryptionThread = new Thread(new Runnable() {
@Override
public void run() {
ArrayList args = (ArrayList) call.arguments;
String src = (String) args.get(0);
String dest = (String) args.get(1);
byte[] key = (byte[]) args.get(2);
byte[] iv = (byte[]) args.get(3);
int algorithm = (int) args.get(4);
String hashSpec = (String) args.get(5);
result.success(
encryptAndHash(
src,
dest,
key,
iv,
getCipherSpecFromInteger(algorithm),
hashSpec
)
);
}
});
encryptionThread.start();
break;
case "decryptFile":
Thread decryptionThread = new Thread(new Runnable() {
@Override
public void run() {
ArrayList args = (ArrayList) call.arguments;
String src = (String) args.get(0);
String dest = (String) args.get(1);
byte[] key = (byte[]) args.get(2);
byte[] iv = (byte[]) args.get(3);
int algorithm = (int) args.get(4);
String hashSpec = (String) args.get(5);
return false;
}
result.success(
decryptAndHash(
src,
dest,
key,
iv,
getCipherSpecFromInteger(algorithm),
hashSpec
)
);
}
});
decryptionThread.start();
break;
case "hashFile":
Thread hashingThread = new Thread(new Runnable() {
@Override
public void run() {
ArrayList args = (ArrayList) call.arguments;
String src = (String) args.get(0);
String hashSpec = (String) args.get(1);
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull io.flutter.plugin.common.MethodChannel.Result result) {
switch (call.method) {
case "configure":
ArrayList args = (ArrayList) call.arguments;
long handle = (long) args.get(0);
String extraData = (String) args.get(1);
result.success(hashFile(src, hashSpec));
}
});
hashingThread.start();
break;
case "recordSentMessage":
ArrayList rargs = (ArrayList) call.arguments;
recordSentMessage(
context,
(String) rargs.get(0),
(String) rargs.get(1),
(String) rargs.get(2),
(int) rargs.get(3)
configure(handle, extraData);
result.success(true);
break;
case "isRunning":
result.success(isRunning());
break;
case "start":
MoxplatformAndroidPlugin.setStartAtBoot(context, true);
BackgroundService.enqueue(context);
Intent intent = new Intent(context, BackgroundService.class);
ContextCompat.startForegroundService(context, intent);
Log.d(TAG, "Service started");
result.success(true);
break;
case "sendData":
for (MoxplatformAndroidPlugin plugin : _instances) {
if (plugin.service != null) {
plugin.service.receiveData((String) call.arguments);
break;
}
}
result.success(true);
break;
default:
result.notImplemented();
break;
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null) return;
if (intent.getAction().equalsIgnoreCase(methodChannelKey)) {
String data = intent.getStringExtra("data");
if (channel != null) {
channel.invokeMethod(dataReceivedMethodName, data);
}
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
localBroadcastManager.unregisterReceiver(this);
Log.d(TAG, "Detached from engine");
}
@Override
public void onAttachedToService(@NonNull ServicePluginBinding binding) {
Log.d(TAG, "Attached to service");
this.service = (BackgroundService) binding.getService();
}
@Override
public void onDetachedFromService() {
Log.d(TAG, "Detached from service");
this.service = null;
}
@Override
public void createNotificationChannel(@NonNull String title, @NonNull String description, @NonNull String id, @NonNull Boolean urgent) {
final NotificationChannel channel = new NotificationChannel(id, title, urgent ? NotificationManager.IMPORTANCE_HIGH : NotificationManager.IMPORTANCE_DEFAULT);
channel.enableVibration(true);
channel.enableLights(true);
channel.setDescription(description);
final NotificationManager manager = getSystemService(context, NotificationManager.class);
manager.createNotificationChannel(channel);
}
@Override
public void showMessagingNotification(@NonNull MessagingNotification notification) {
NotificationsKt.showMessagingNotification(context, notification);
}
@Override
public void showNotification(@NonNull RegularNotification notification) {
NotificationsKt.showNotification(context, notification);
}
@Override
public void dismissNotification(@NonNull Long id) {
NotificationManagerCompat.from(context).cancel(id.intValue());
}
@Override
public void setNotificationSelfAvatar(@NonNull String path) {
NotificationDataManager.INSTANCE.setAvatarPath(context, path);
}
@Override
public void setNotificationI18n(@NonNull NotificationI18nData data) {
// Configure i18n
NotificationDataManager.INSTANCE.setYou(context, data.getYou());
NotificationDataManager.INSTANCE.setReply(context, data.getReply());
NotificationDataManager.INSTANCE.setMarkAsRead(context, data.getMarkAsRead());
}
@NonNull
@Override
public String getPersistentDataPath() {
return context.getFilesDir().getPath();
}
@NonNull
@Override
public String getCacheDataPath() {
return context.getCacheDir().getPath();
}
@Override
public void recordSentMessage(@NonNull String name, @NonNull String jid, @Nullable String avatarPath, @NonNull FallbackIconType fallbackIcon) {
systemRecordSentMessage(context, name, jid, avatarPath, fallbackIcon);
}
@Override
public void encryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Api.Result<CryptographyResult> result) {
CryptoKt.encryptAndHash(
sourcePath,
destPath,
key,
iv,
algorithm,
hashSpec,
result
);
result.success(true);
break;
default:
result.notImplemented();
break;
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null) return;
if (intent.getAction().equalsIgnoreCase(methodChannelKey)) {
String data = intent.getStringExtra("data");
if (channel != null) {
channel.invokeMethod(dataReceivedMethodName, data);
}
@Override
public void decryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Api.Result<CryptographyResult> result) {
CryptoKt.decryptAndHash(
sourcePath,
destPath,
key,
iv,
algorithm,
hashSpec,
result
);
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
localBroadcastManager.unregisterReceiver(this);
@Override
public void hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Api.Result<byte[]> result) {
CryptoKt.hashFile(sourcePath, hashSpec, result);
}
Log.d(TAG, "Detached from engine");
}
@Override
public void onAttachedToService(@NonNull ServicePluginBinding binding) {
Log.d(TAG, "Attached to service");
this.service = (BackgroundService) binding.getService();
}
@Override
public void onDetachedFromService() {
Log.d(TAG, "Detached from service");
this.service = null;
}
@Override
public void eventStub(@NonNull NotificationEvent event) {
// Stub to trick pigeon into
}
}

View File

@ -0,0 +1,197 @@
package me.polynom.moxplatform_android
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.IconCompat
import me.polynom.moxplatform_android.Api.NotificationEvent
import java.io.File
import java.time.Instant
class NotificationReceiver : BroadcastReceiver() {
/*
* Dismisses the notification through which we received @intent.
* */
private fun dismissNotification(context: Context, intent: Intent) {
// Dismiss the notification
val notificationId = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt()
if (notificationId != -1) {
NotificationManagerCompat.from(context).cancel(
notificationId,
)
} else {
Log.e("NotificationReceiver", "No id specified. Cannot dismiss notification")
}
}
private fun findActiveNotification(context: Context, id: Int): Notification? {
return (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
.activeNotifications
.find { it.id == id }?.notification
}
private fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> {
val extras = mutableMapOf<String?, String?>()
intent.extras?.keySet()!!.forEach {
Log.d(TAG, "Checking $it -> ${intent.extras!!.get(it)}")
if (it.startsWith("payload_")) {
Log.d(TAG, "Adding $it")
extras[it.substring(8)] = intent.extras!!.getString(it)
}
}
return extras
}
private fun handleMarkAsRead(context: Context, intent: Intent) {
MoxplatformAndroidPlugin.notificationSink?.success(
NotificationEvent().apply {
id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1)
jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!
type = Api.NotificationEventType.MARK_AS_READ
payload = null
extra = extractPayloadMapFromIntent(intent)
}.toList()
)
NotificationManagerCompat.from(context).cancel(intent.getLongExtra(MARK_AS_READ_ID_KEY, -1).toInt())
dismissNotification(context, intent);
}
private fun handleReply(context: Context, intent: Intent) {
val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return
val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY)
MoxplatformAndroidPlugin.notificationSink?.success(
NotificationEvent().apply {
id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1)
jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!
type = Api.NotificationEventType.REPLY
payload = replyPayload.toString()
extra = extractPayloadMapFromIntent(intent)
}.toList()
)
val id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt()
if (id == -1) {
Log.e(TAG, "Failed to find notification id for reply")
return;
}
val notification = findActiveNotification(context, id)
if (notification == null) {
Log.e(TAG, "Failed to find notification for id $id")
return
}
// Thanks https://medium.com/@sidorovroman3/android-how-to-use-messagingstyle-for-notifications-without-caching-messages-c414ef2b816c
val recoveredStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification)!!
val newStyle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
Notification.MessagingStyle(
android.app.Person.Builder().apply {
setName(NotificationDataManager.getYou(context))
// Set an avatar, if we have one
val avatarPath = NotificationDataManager.getAvatarPath(context)
if (avatarPath != null) {
setIcon(
Icon.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(avatarPath)
)
)
}
}.build()
)
else Notification.MessagingStyle(NotificationDataManager.getYou(context))
newStyle.apply {
conversationTitle = recoveredStyle.conversationTitle
recoveredStyle.messages.forEach {
// Check if we have to request (or refresh) the content URI to be able to still
// see the embedded image.
val mime = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_MIME)
val path = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_PATH)
val message = Notification.MessagingStyle.Message(it.text, it.timestamp, it.sender)
if (mime != null && path != null) {
// Request a new URI from the file provider to ensure we can still see the image
// in the notification
val fileUri = FileProvider.getUriForFile(
context,
MOXPLATFORM_FILEPROVIDER_ID,
File(path),
)
message.setData(
mime,
fileUri,
)
// As we're creating a new message, also recreate the additional metadata
message.extras.apply {
putString(NOTIFICATION_MESSAGE_EXTRA_MIME, mime)
putString(NOTIFICATION_MESSAGE_EXTRA_PATH, path)
}
}
// Append the old message
addMessage(message)
}
}
// Append our new message
newStyle.addMessage(
Notification.MessagingStyle.Message(
replyPayload!!,
Instant.now().toEpochMilli(),
null as CharSequence?
)
)
// Post the new notification
val recoveredBuilder = Notification.Builder.recoverBuilder(context, notification).apply {
style = newStyle
setOnlyAlertOnce(true)
}
NotificationManagerCompat.from(context).notify(id, recoveredBuilder.build())
}
private fun handleTap(context: Context, intent: Intent) {
MoxplatformAndroidPlugin.notificationSink?.success(
NotificationEvent().apply {
id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1)
jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!
type = Api.NotificationEventType.OPEN
payload = null
extra = extractPayloadMapFromIntent(intent)
}.toList()
)
// Bring the app into the foreground
val tapIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!
context.startActivity(tapIntent)
// Dismiss the notification
dismissNotification(context, intent)
}
override fun onReceive(context: Context, intent: Intent) {
// TODO: We need to be careful to ensure that the Flutter engine is running.
// If it's not, we have to start it. However, that's only an issue when we expect to
// receive notifications while not running, i.e. Push Notifications.
when (intent.action) {
MARK_AS_READ_ACTION -> handleMarkAsRead(context, intent)
REPLY_ACTION -> handleReply(context, intent)
TAP_ACTION -> handleTap(context, intent)
}
}
}

View File

@ -0,0 +1,285 @@
package me.polynom.moxplatform_android
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.IconCompat
import java.io.File
import java.lang.Exception
/*
* Holds "persistent" data for notifications, like i18n strings. While not useful now, this is
* useful for when the app is dead and we receive a notification.
* */
object NotificationDataManager {
private var you: String? = null
private var markAsRead: String? = null
private var reply: String? = null
private var fetchedAvatarPath = false
private var avatarPath: String? = null
private fun getString(context: Context, key: String, fallback: String): String {
return context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)!!.getString(key, fallback)!!
}
private fun setString(context: Context, key: String, value: String) {
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
prefs.edit()
.putString(key, value)
.apply()
}
fun getYou(context: Context): String {
if (you == null) you = getString(context, SHARED_PREFERENCES_YOU_KEY, "You")
return you!!
}
fun setYou(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_YOU_KEY, value)
you = value
}
fun getMarkAsRead(context: Context): String {
if (markAsRead == null) markAsRead = getString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, "Mark as read")
return markAsRead!!
}
fun setMarkAsRead(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, value)
markAsRead = value
}
fun getReply(context: Context): String {
if (reply != null) reply = getString(context, SHARED_PREFERENCES_REPLY_KEY, "Reply")
return reply!!
}
fun setReply(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_REPLY_KEY, value)
reply = value
}
fun getAvatarPath(context: Context): String? {
if (avatarPath == null && !fetchedAvatarPath) {
val path = getString(context, SHARED_PREFERENCES_AVATAR_KEY, "")
if (path.isNotEmpty()) {
avatarPath = path
}
}
return avatarPath
}
fun setAvatarPath(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_AVATAR_KEY, value)
fetchedAvatarPath = true
avatarPath = value
}
}
/// Show a messaging style notification described by @notification.
fun showMessagingNotification(context: Context, notification: Api.MessagingNotification) {
// Build the actions
// -> Reply action
val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply {
setLabel(NotificationDataManager.getReply(context))
}.build()
val replyIntent = Intent(context, NotificationReceiver::class.java).apply {
action = REPLY_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
}
val replyPendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT,
)
val replyAction = NotificationCompat.Action.Builder(
R.drawable.reply,
NotificationDataManager.getReply(context),
replyPendingIntent,
).apply {
addRemoteInput(remoteInput)
setAllowGeneratedReplies(true)
}.build()
// -> Mark as read action
val markAsReadIntent = Intent(context, NotificationReceiver::class.java).apply {
action = MARK_AS_READ_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
}
val markAsReadPendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
markAsReadIntent,
PendingIntent.FLAG_UPDATE_CURRENT,
)
val markAsReadAction = NotificationCompat.Action.Builder(
R.drawable.mark_as_read,
NotificationDataManager.getMarkAsRead(context),
markAsReadPendingIntent,
).build()
// -> Tap action
// Thanks https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java#L246
val tapIntent = Intent(context, NotificationReceiver::class.java).apply {
action = TAP_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
}
val tapPendingIntent = PendingIntent.getBroadcast(
context,
notification.id.toInt(),
tapIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
// Build the notification
val selfPerson = Person.Builder().apply {
setName(NotificationDataManager.getYou(context))
// Set an avatar, if we have one
val avatarPath = NotificationDataManager.getAvatarPath(context)
if (avatarPath != null) {
setIcon(
IconCompat.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(avatarPath),
),
)
}
}.build()
val style = NotificationCompat.MessagingStyle(selfPerson);
style.isGroupConversation = notification.isGroupchat
if (notification.isGroupchat) {
style.conversationTitle = notification.title
}
for (i in notification.messages.indices) {
val message = notification.messages[i]
// Build the sender
// NOTE: Note that we set it to null if message.sender == null because otherwise this results in
// a bogus Person object which messes with the "self-message" display as Android expects
// null in that case.
val sender = if (message.sender == null)
null
else Person.Builder().apply {
setName(message.sender)
setKey(message.jid)
// Set the avatar, if available
if (message.avatarPath != null) {
try {
setIcon(
IconCompat.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(message.avatarPath),
),
)
} catch (ex: Throwable) {
Log.w(TAG, "Failed to open avatar at ${message.avatarPath}")
}
}
}.build()
// Build the message
val body = message.content.body ?: ""
val msg = NotificationCompat.MessagingStyle.Message(
body,
message.timestamp,
sender,
)
// 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,
MOXPLATFORM_FILEPROVIDER_ID,
File(message.content.path),
)
msg.apply {
setData(message.content.mime, fileUri)
extras.apply {
putString(NOTIFICATION_MESSAGE_EXTRA_MIME, message.content.mime)
putString(NOTIFICATION_MESSAGE_EXTRA_PATH, message.content.path)
}
}
}
// Append the message
style.addMessage(msg)
}
// Assemble the notification
val finalNotification = NotificationCompat.Builder(context, notification.channelId).apply {
setStyle(style)
// NOTE: It's okay to use the service icon here as I cannot get Android to display the
// actual logo. So we'll have to make do with the silhouette and the color purple.
setSmallIcon(R.drawable.ic_service_icon)
color = Color.argb(255, 207, 74, 255)
setColorized(true)
// Tap action
setContentIntent(tapPendingIntent)
// Notification actions
addAction(replyAction)
addAction(markAsReadAction)
// Groupchat title
if (notification.isGroupchat) {
setContentTitle(notification.title)
}
setAllowSystemGeneratedContextualActions(true)
setCategory(Notification.CATEGORY_MESSAGE)
// Prevent no notification when we replied before
setOnlyAlertOnce(false)
}.build()
// Post the notification
NotificationManagerCompat.from(context).notify(
notification.id.toInt(),
finalNotification,
)
}
fun showNotification(context: Context, notification: Api.RegularNotification) {
val builtNotification = NotificationCompat.Builder(context, notification.channelId).apply {
setContentTitle(notification.title)
setContentText(notification.body)
when (notification.icon) {
Api.NotificationIcon.ERROR -> setSmallIcon(R.drawable.error)
Api.NotificationIcon.WARNING -> setSmallIcon(R.drawable.warning)
Api.NotificationIcon.NONE -> {}
}
}.build()
// Post the notification
NotificationManagerCompat.from(context).notify(notification.id.toInt(), builtNotification)
}

View File

@ -7,12 +7,15 @@ import androidx.core.app.Person
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import me.polynom.moxplatform_android.Api.FallbackIconType
/*
* Uses Android's direct share API to create dynamic share targets that are compatible
* with share_handler's media handling.
* NOTE: The "system" prefix is to prevent confusion between pigeon's abstract recordSentMessage
* method and this one.
* */
fun recordSentMessage(context: Context, name: String, jid: String, avatarPath: String?, fallbackIconType: Int) {
fun systemRecordSentMessage(context: Context, name: String, jid: String, avatarPath: String?, fallbackIcon: FallbackIconType) {
val pkgName = context.packageName
val intent = Intent(context, Class.forName("$pkgName.MainActivity")).apply {
action = Intent.ACTION_SEND
@ -43,9 +46,9 @@ fun recordSentMessage(context: Context, name: String, jid: String, avatarPath: S
shortcutBuilder.setIcon(icon)
personBuilder.setIcon(icon)
} else {
val resourceId = when(fallbackIconType) {
0 -> R.mipmap.person
1 -> R.mipmap.notes
val resourceId = when(fallbackIcon) {
FallbackIconType.PERSON -> R.mipmap.person
FallbackIconType.NOTES -> R.mipmap.notes
// "Fallthrough"
else -> R.mipmap.person
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17.34,20l-3.54,-3.54l1.41,-1.41l2.12,2.12l4.24,-4.24L23,14.34L17.34,20zM12,17c0,-3.87 3.13,-7 7,-7c1.08,0 2.09,0.25 3,0.68V4c0,-1.1 -0.9,-2 -2,-2H4C2.9,2 2,2.9 2,4v18l4,-4h6v0c0,-0.17 0.01,-0.33 0.03,-0.5C12.01,17.34 12,17.17 12,17z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View File

@ -0,0 +1,7 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- For testing -->
<cache-path name="file_picker" path="file_picker/"/>
<!-- Moxxy -->
<files-path name="media" path="media/" />
</paths>

View File

@ -1,6 +1,5 @@
library moxplatform_android;
export 'src/isolate_android.dart';
export 'src/media_android.dart';
export 'src/plugin_android.dart';
export 'src/service_android.dart';

View File

@ -1,8 +1,7 @@
import 'package:flutter/services.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidContactsImplementation extends ContactsImplementation {
final _methodChannel = const MethodChannel('me.polynom.moxplatform_android');
final MoxplatformApi _api = MoxplatformApi();
@override
Future<void> recordSentMessage(
@ -19,14 +18,11 @@ class AndroidContactsImplementation extends ContactsImplementation {
);
}
await _methodChannel.invokeMethod<void>(
'recordSentMessage',
[
name,
jid,
avatarPath,
fallbackIcon.id,
],
return _api.recordSentMessage(
name,
jid,
avatarPath,
fallbackIcon,
);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidCryptographyImplementation extends CryptographyImplementation {
final _methodChannel = const MethodChannel('me.polynom.moxplatform_android');
final MoxplatformApi _api = MoxplatformApi();
@override
Future<CryptographyResult?> encryptFile(
@ -13,22 +13,13 @@ class AndroidCryptographyImplementation extends CryptographyImplementation {
CipherAlgorithm algorithm,
String hashSpec,
) async {
final dynamic resultRaw =
await _methodChannel.invokeMethod<dynamic>('encryptFile', [
return _api.encryptFile(
sourcePath,
destPath,
key,
iv,
algorithm.value,
algorithm,
hashSpec,
]);
if (resultRaw == null) return null;
// ignore: argument_type_not_assignable
final result = Map<String, dynamic>.from(resultRaw);
return CryptographyResult(
result['plaintextHash']! as Uint8List,
result['ciphertextHash']! as Uint8List,
);
}
@ -41,35 +32,18 @@ class AndroidCryptographyImplementation extends CryptographyImplementation {
CipherAlgorithm algorithm,
String hashSpec,
) async {
final dynamic resultRaw =
await _methodChannel.invokeMethod<dynamic>('decryptFile', [
return _api.decryptFile(
sourcePath,
destPath,
key,
iv,
algorithm.value,
algorithm,
hashSpec,
]);
if (resultRaw == null) return null;
// ignore: argument_type_not_assignable
final result = Map<String, dynamic>.from(resultRaw);
return CryptographyResult(
result['plaintextHash']! as Uint8List,
result['ciphertextHash']! as Uint8List,
);
}
@override
Future<Uint8List?> hashFile(String path, String hashSpec) async {
final dynamic resultsRaw =
await _methodChannel.invokeMethod<dynamic>('hashFile', [
path,
hashSpec,
]);
if (resultsRaw == null) return null;
return resultsRaw as Uint8List;
Future<Uint8List?> hashFile(String sourcePath, String hashSpec) async {
return _api.hashFile(sourcePath, hashSpec);
}
}

View File

@ -7,7 +7,6 @@ import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_android/src/service_android.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
/// An [AwaitableDataSender] that uses flutter_background_service.
class BackgroundServiceDataSender

View File

@ -1,9 +0,0 @@
import 'package:media_scanner/media_scanner.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidMediaScannerImplementation extends MediaScannerImplementation {
@override
void scanFile(String path) {
MediaScanner.loadMedia(path: path);
}
}

View File

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidNotificationsImplementation extends NotificationsImplementation {
final MoxplatformApi _api = MoxplatformApi();
final EventChannel _channel =
const EventChannel('me.polynom/notification_stream');
@override
Future<void> createNotificationChannel(
String title,
String description,
String id,
bool urgent,
) async {
return _api.createNotificationChannel(title, description, id, urgent);
}
@override
Future<void> showMessagingNotification(
MessagingNotification notification,
) async {
return _api.showMessagingNotification(notification);
}
@override
Future<void> showNotification(RegularNotification notification) async {
return _api.showNotification(notification);
}
@override
Future<void> dismissNotification(int id) async {
return _api.dismissNotification(id);
}
@override
Future<void> setNotificationSelfAvatar(String path) async {
return _api.setNotificationSelfAvatar(path);
}
@override
Future<void> setI18n(NotificationI18nData data) {
return _api.setNotificationI18n(data);
}
@override
Stream<NotificationEvent> getEventStream() => _channel
.receiveBroadcastStream()
.cast<Object>()
.map(NotificationEvent.decode);
}

View File

@ -0,0 +1,13 @@
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidPlatformImplementation extends PlatformImplementation {
@override
Future<String> getCacheDataPath() {
return MoxplatformInterface.api.getCacheDataPath();
}
@override
Future<String> getPersistentDataPath() {
return MoxplatformInterface.api.getPersistentDataPath();
}
}

View File

@ -1,7 +1,8 @@
import 'package:moxplatform_android/src/contacts_android.dart';
import 'package:moxplatform_android/src/crypto_android.dart';
import 'package:moxplatform_android/src/isolate_android.dart';
import 'package:moxplatform_android/src/media_android.dart';
import 'package:moxplatform_android/src/notifications_android.dart';
import 'package:moxplatform_android/src/platform_android.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class MoxplatformAndroidPlugin extends MoxplatformInterface {
@ -11,7 +12,8 @@ class MoxplatformAndroidPlugin extends MoxplatformInterface {
MoxplatformInterface.contacts = AndroidContactsImplementation();
MoxplatformInterface.crypto = AndroidCryptographyImplementation();
MoxplatformInterface.handler = AndroidIsolateHandler();
MoxplatformInterface.media = AndroidMediaScannerImplementation();
MoxplatformInterface.notifications = AndroidNotificationsImplementation();
MoxplatformInterface.platform = AndroidPlatformImplementation();
}
@override

View File

@ -6,7 +6,6 @@ import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
import 'package:uuid/uuid.dart';
class AndroidBackgroundService extends BackgroundService {

View File

@ -1,6 +1,6 @@
name: moxplatform_android
description: Android implementation of moxplatform
version: 0.1.17+1
version: 0.1.18
homepage: https://codeberg.org/moxxy/moxplatform
publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@ -22,7 +22,6 @@ dependencies:
sdk: flutter
get_it: ^7.2.0
logging: ^1.0.2
media_scanner: ^2.0.0
meta: ^1.7.0
moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
@ -30,10 +29,10 @@ dependencies:
moxplatform:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.17+1
version: ^0.1.17+2
moxplatform_platform_interface:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.17+1
version: ^0.1.18
plugin_platform_interface: ^2.1.2
uuid: ^3.0.5
@ -41,4 +40,5 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
pigeon: 10.1.4
very_good_analysis: ^3.0.1

View File

@ -1,3 +1,16 @@
## 0.1.18
- **FIX**: Format and lint.
- **FIX**: Add payload to all intents.
- **FEAT**: Move recordSentMessage to pigeon.
- **FEAT**: Move the crypto APIs to pigeon.
- **FEAT**: Allow the sender's data being null.
- **FEAT**: Allow attaching arbitrary data to the notification.
- **FEAT**: Allow showing regular notifications.
- **FEAT**: Color in the notification silhouette.
- **FEAT**: Allow setting the self-avatar.
- **FEAT**: Take care of i18n.
## 0.1.17+1
- Update a dependency to the latest release.

View File

@ -1,5 +1,6 @@
library moxplatform_platform_interface;
export 'src/api.g.dart';
export 'src/contacts.dart';
export 'src/contacts_stub.dart';
export 'src/crypto.dart';
@ -7,6 +8,8 @@ export 'src/crypto_stub.dart';
export 'src/interface.dart';
export 'src/isolate.dart';
export 'src/isolate_stub.dart';
export 'src/media.dart';
export 'src/media_stub.dart';
export 'src/notifications.dart';
export 'src/notifications_stub.dart';
export 'src/platform.dart';
export 'src/platform_stub.dart';
export 'src/service.dart';

View File

@ -0,0 +1,733 @@
// Autogenerated from Pigeon (v10.1.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
enum NotificationIcon {
warning,
error,
none,
}
enum NotificationEventType {
markAsRead,
reply,
open,
}
enum CipherAlgorithm {
aes128GcmNoPadding,
aes256GcmNoPadding,
aes256CbcPkcs7,
}
enum FallbackIconType {
none,
person,
notes,
}
class NotificationMessageContent {
NotificationMessageContent({
this.body,
this.mime,
this.path,
});
/// The textual body of the message.
String? body;
/// The path and mime type of the media to show.
String? mime;
String? path;
Object encode() {
return <Object?>[
body,
mime,
path,
];
}
static NotificationMessageContent decode(Object result) {
result as List<Object?>;
return NotificationMessageContent(
body: result[0] as String?,
mime: result[1] as String?,
path: result[2] as String?,
);
}
}
class NotificationMessage {
NotificationMessage({
this.sender,
this.jid,
required this.content,
required this.timestamp,
this.avatarPath,
});
/// The sender of the message.
String? sender;
/// The jid of the sender.
String? jid;
/// The body of the message.
NotificationMessageContent content;
/// Milliseconds since epoch.
int timestamp;
/// The path to the avatar to use
String? avatarPath;
Object encode() {
return <Object?>[
sender,
jid,
content.encode(),
timestamp,
avatarPath,
];
}
static NotificationMessage decode(Object result) {
result as List<Object?>;
return NotificationMessage(
sender: result[0] as String?,
jid: result[1] as String?,
content: NotificationMessageContent.decode(result[2]! as List<Object?>),
timestamp: result[3]! as int,
avatarPath: result[4] as String?,
);
}
}
class MessagingNotification {
MessagingNotification({
required this.title,
required this.id,
required this.channelId,
required this.jid,
required this.messages,
required this.isGroupchat,
this.extra,
});
/// The title of the conversation.
String title;
/// The id of the notification.
int id;
/// The id of the notification channel the notification should appear on.
String channelId;
/// The JID of the chat in which the notifications happen.
String jid;
/// Messages to show.
List<NotificationMessage?> messages;
/// Flag indicating whether this notification is from a groupchat or not.
bool isGroupchat;
/// Additional data to include.
Map<String?, String?>? extra;
Object encode() {
return <Object?>[
title,
id,
channelId,
jid,
messages,
isGroupchat,
extra,
];
}
static MessagingNotification decode(Object result) {
result as List<Object?>;
return MessagingNotification(
title: result[0]! as String,
id: result[1]! as int,
channelId: result[2]! as String,
jid: result[3]! as String,
messages: (result[4] as List<Object?>?)!.cast<NotificationMessage?>(),
isGroupchat: result[5]! as bool,
extra: (result[6] as Map<Object?, Object?>?)?.cast<String?, String?>(),
);
}
}
class RegularNotification {
RegularNotification({
required this.title,
required this.body,
required this.channelId,
required this.id,
required this.icon,
});
/// The title of the notification.
String title;
/// The body of the notification.
String body;
/// The id of the channel to show the notification on.
String channelId;
/// The id of the notification.
int id;
/// The icon to use.
NotificationIcon icon;
Object encode() {
return <Object?>[
title,
body,
channelId,
id,
icon.index,
];
}
static RegularNotification decode(Object result) {
result as List<Object?>;
return RegularNotification(
title: result[0]! as String,
body: result[1]! as String,
channelId: result[2]! as String,
id: result[3]! as int,
icon: NotificationIcon.values[result[4]! as int],
);
}
}
class NotificationEvent {
NotificationEvent({
required this.id,
required this.jid,
required this.type,
this.payload,
this.extra,
});
/// The notification id.
int id;
/// The JID the notification was for.
String jid;
/// The type of event.
NotificationEventType type;
/// An optional payload.
/// - type == NotificationType.reply: The reply message text.
/// Otherwise: undefined.
String? payload;
/// Extra data. Only set when type == NotificationType.reply.
Map<String?, String?>? extra;
Object encode() {
return <Object?>[
id,
jid,
type.index,
payload,
extra,
];
}
static NotificationEvent decode(Object result) {
result as List<Object?>;
return NotificationEvent(
id: result[0]! as int,
jid: result[1]! as String,
type: NotificationEventType.values[result[2]! as int],
payload: result[3] as String?,
extra: (result[4] as Map<Object?, Object?>?)?.cast<String?, String?>(),
);
}
}
class NotificationI18nData {
NotificationI18nData({
required this.reply,
required this.markAsRead,
required this.you,
});
/// The content of the reply button.
String reply;
/// The content of the "mark as read" button.
String markAsRead;
/// The text to show when *you* reply.
String you;
Object encode() {
return <Object?>[
reply,
markAsRead,
you,
];
}
static NotificationI18nData decode(Object result) {
result as List<Object?>;
return NotificationI18nData(
reply: result[0]! as String,
markAsRead: result[1]! as String,
you: result[2]! as String,
);
}
}
class CryptographyResult {
CryptographyResult({
required this.plaintextHash,
required this.ciphertextHash,
});
Uint8List plaintextHash;
Uint8List ciphertextHash;
Object encode() {
return <Object?>[
plaintextHash,
ciphertextHash,
];
}
static CryptographyResult decode(Object result) {
result as List<Object?>;
return CryptographyResult(
plaintextHash: result[0]! as Uint8List,
ciphertextHash: result[1]! as Uint8List,
);
}
}
class _MoxplatformApiCodec extends StandardMessageCodec {
const _MoxplatformApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is CryptographyResult) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else if (value is MessagingNotification) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is NotificationEvent) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is NotificationI18nData) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is NotificationMessage) {
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else if (value is NotificationMessageContent) {
buffer.putUint8(133);
writeValue(buffer, value.encode());
} else if (value is RegularNotification) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return CryptographyResult.decode(readValue(buffer)!);
case 129:
return MessagingNotification.decode(readValue(buffer)!);
case 130:
return NotificationEvent.decode(readValue(buffer)!);
case 131:
return NotificationI18nData.decode(readValue(buffer)!);
case 132:
return NotificationMessage.decode(readValue(buffer)!);
case 133:
return NotificationMessageContent.decode(readValue(buffer)!);
case 134:
return RegularNotification.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
class MoxplatformApi {
/// Constructor for [MoxplatformApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
MoxplatformApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = _MoxplatformApiCodec();
/// Notification APIs
Future<void> createNotificationChannel(String arg_title,
String arg_description, String arg_id, bool arg_urgent) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_title, arg_description, arg_id, arg_urgent])
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;
}
}
Future<void> showMessagingNotification(
MessagingNotification arg_notification) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showMessagingNotification',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_notification]) 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;
}
}
Future<void> showNotification(RegularNotification arg_notification) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showNotification',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_notification]) 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;
}
}
Future<void> dismissNotification(int arg_id) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.dismissNotification',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_id]) 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;
}
}
Future<void> setNotificationSelfAvatar(String arg_path) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationSelfAvatar',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_path]) 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;
}
}
Future<void> setNotificationI18n(NotificationI18nData arg_data) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationI18n',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_data]) 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;
}
}
/// Platform APIs
Future<String> getPersistentDataPath() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getPersistentDataPath',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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 if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as String?)!;
}
}
Future<String> getCacheDataPath() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getCacheDataPath',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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 if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as String?)!;
}
}
/// Contacts APIs
Future<void> recordSentMessage(String arg_name, String arg_jid,
String? arg_avatarPath, FallbackIconType arg_fallbackIcon) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.recordSentMessage',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(<Object?>[
arg_name,
arg_jid,
arg_avatarPath,
arg_fallbackIcon.index
]) 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;
}
}
/// Cryptography APIs
Future<CryptographyResult?> encryptFile(
String arg_sourcePath,
String arg_destPath,
Uint8List arg_key,
Uint8List arg_iv,
CipherAlgorithm arg_algorithm,
String arg_hashSpec) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.encryptFile',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(<Object?>[
arg_sourcePath,
arg_destPath,
arg_key,
arg_iv,
arg_algorithm.index,
arg_hashSpec
]) 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 (replyList[0] as CryptographyResult?);
}
}
Future<CryptographyResult?> decryptFile(
String arg_sourcePath,
String arg_destPath,
Uint8List arg_key,
Uint8List arg_iv,
CipherAlgorithm arg_algorithm,
String arg_hashSpec) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.decryptFile',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(<Object?>[
arg_sourcePath,
arg_destPath,
arg_key,
arg_iv,
arg_algorithm.index,
arg_hashSpec
]) 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 (replyList[0] as CryptographyResult?);
}
}
Future<Uint8List?> hashFile(
String arg_sourcePath, String arg_hashSpec) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.hashFile',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_sourcePath, arg_hashSpec]) 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 (replyList[0] as Uint8List?);
}
}
/// Stubs
Future<void> eventStub(NotificationEvent arg_event) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.eventStub',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_event]) 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

@ -1,14 +1,4 @@
// The type of icon to use when no avatar path is provided.
enum FallbackIconType {
none(-1),
person(0),
notes(1);
const FallbackIconType(this.id);
// The ID of the fallback icon.
final int id;
}
import 'package:moxplatform_platform_interface/src/api.g.dart';
// Wrapper around various contact APIs.
// ignore: one_member_abstracts

View File

@ -1,3 +1,4 @@
import 'package:moxplatform_platform_interface/src/api.g.dart';
import 'package:moxplatform_platform_interface/src/contacts.dart';
class StubContactsImplementation extends ContactsImplementation {

View File

@ -1,21 +1,5 @@
import 'dart:typed_data';
enum CipherAlgorithm {
aes128GcmNoPadding(0),
aes256GcmNoPadding(1),
aes256CbcPkcs7(2);
const CipherAlgorithm(this.value);
/// The "id" of the algorithm choice.
final int value;
}
class CryptographyResult {
const CryptographyResult(this.plaintextHash, this.ciphertextHash);
final Uint8List plaintextHash;
final Uint8List ciphertextHash;
}
import 'package:moxplatform_platform_interface/src/api.g.dart';
/// Wrapper around platform-native cryptography APIs
abstract class CryptographyImplementation {
@ -47,9 +31,9 @@ abstract class CryptographyImplementation {
String hashSpec,
);
/// Hashes the file at [path] using the Hash function with name [hashSpec].
/// Hashes the file at [sourcePath] using the Hash function with name [hashSpec].
/// Note that this function runs off-thread as to not block the UI thread.
///
/// Returns the hash of the file.
Future<Uint8List?> hashFile(String path, String hashSpec);
Future<Uint8List?> hashFile(String sourcePath, String hashSpec);
}

View File

@ -1,4 +1,5 @@
import 'dart:typed_data';
import 'package:moxplatform_platform_interface/src/api.g.dart';
import 'package:moxplatform_platform_interface/src/crypto.dart';
class StubCryptographyImplementation extends CryptographyImplementation {
@ -27,7 +28,7 @@ class StubCryptographyImplementation extends CryptographyImplementation {
}
@override
Future<Uint8List?> hashFile(String path, String hashSpec) async {
Future<Uint8List?> hashFile(String sourcePath, String hashSpec) async {
return null;
}
}

View File

@ -1,11 +1,14 @@
import 'package:moxplatform_platform_interface/src/api.g.dart';
import 'package:moxplatform_platform_interface/src/contacts.dart';
import 'package:moxplatform_platform_interface/src/contacts_stub.dart';
import 'package:moxplatform_platform_interface/src/crypto.dart';
import 'package:moxplatform_platform_interface/src/crypto_stub.dart';
import 'package:moxplatform_platform_interface/src/isolate.dart';
import 'package:moxplatform_platform_interface/src/isolate_stub.dart';
import 'package:moxplatform_platform_interface/src/media.dart';
import 'package:moxplatform_platform_interface/src/media_stub.dart';
import 'package:moxplatform_platform_interface/src/notifications.dart';
import 'package:moxplatform_platform_interface/src/notifications_stub.dart';
import 'package:moxplatform_platform_interface/src/platform.dart';
import 'package:moxplatform_platform_interface/src/platform_stub.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
abstract class MoxplatformInterface extends PlatformInterface {
@ -13,10 +16,14 @@ abstract class MoxplatformInterface extends PlatformInterface {
static final Object _token = Object();
static MoxplatformApi api = MoxplatformApi();
static IsolateHandler handler = StubIsolateHandler();
static MediaScannerImplementation media = StubMediaScannerImplementation();
static CryptographyImplementation crypto = StubCryptographyImplementation();
static ContactsImplementation contacts = StubContactsImplementation();
static NotificationsImplementation notifications =
StubNotificationsImplementation();
static PlatformImplementation platform = StubPlatformImplementation();
/// Return the current platform name.
Future<String?> getPlatformName();

View File

@ -1,6 +0,0 @@
/// Wrapper around platform-specific media scanning
// ignore: one_member_abstracts
abstract class MediaScannerImplementation {
/// Let the platform-specific media scanner scan the file at [path].
void scanFile(String path);
}

View File

@ -1,6 +0,0 @@
import 'package:moxplatform_platform_interface/src/media.dart';
class StubMediaScannerImplementation extends MediaScannerImplementation {
@override
void scanFile(String path) {}
}

View File

@ -0,0 +1,30 @@
import 'dart:async';
import 'package:moxplatform_platform_interface/src/api.g.dart';
abstract class NotificationsImplementation {
/// Creates a notification channel with the name [title] and id [id]. If [urgent] is true, then
/// it configures the channel as carrying urgent information.
Future<void> createNotificationChannel(
String title,
String description,
String id,
bool urgent,
);
/// Shows a notification [notification] in the messaging style with everyting it needs.
Future<void> showMessagingNotification(MessagingNotification notification);
/// Shows a regular notification [notification].
Future<void> showNotification(RegularNotification notification);
/// Dismisses the notification with id [id].
Future<void> dismissNotification(int id);
/// Sets the path to the self-avatar for in-notification replies.
Future<void> setNotificationSelfAvatar(String path);
/// Configures the i18n data for usage in notifications.
Future<void> setI18n(NotificationI18nData data);
Stream<NotificationEvent> getEventStream();
}

View File

@ -0,0 +1,35 @@
import 'dart:async';
import 'package:moxplatform_platform_interface/src/api.g.dart';
import 'package:moxplatform_platform_interface/src/notifications.dart';
class StubNotificationsImplementation extends NotificationsImplementation {
@override
Future<void> createNotificationChannel(
String title,
String description,
String id,
bool urgent,
) async {}
@override
Future<void> showMessagingNotification(
MessagingNotification notification,
) async {}
@override
Future<void> showNotification(RegularNotification notification) async {}
@override
Future<void> dismissNotification(int id) async {}
@override
Future<void> setNotificationSelfAvatar(String path) async {}
@override
Future<void> setI18n(NotificationI18nData data) async {}
@override
Stream<NotificationEvent> getEventStream() {
return StreamController<NotificationEvent>().stream;
}
}

View File

@ -0,0 +1,7 @@
abstract class PlatformImplementation {
/// Returns the path where persistent data should be stored.
Future<String> getPersistentDataPath();
/// Returns the path where cache data should be stored.
Future<String> getCacheDataPath();
}

View File

@ -0,0 +1,11 @@
import 'package:moxplatform_platform_interface/src/platform.dart';
class StubPlatformImplementation extends PlatformImplementation {
/// Returns the path where persistent data should be stored.
@override
Future<String> getPersistentDataPath() async => '';
/// Returns the path where cache data should be stored.
@override
Future<String> getCacheDataPath() async => '';
}

View File

@ -1,6 +1,6 @@
name: moxplatform_platform_interface
description: A common platform interface for the my_plugin plugin.
version: 0.1.17+1
version: 0.1.18
homepage: https://codeberg.org/moxxy/moxplatform
publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@ -17,7 +17,7 @@ dependencies:
version: ^0.2.0
moxplatform:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.17+1
version: ^0.1.17+2
plugin_platform_interface: ^2.1.2

195
pigeons/api.dart Normal file
View File

@ -0,0 +1,195 @@
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'packages/moxplatform_platform_interface/lib/src/api.g.dart',
//kotlinOut: 'packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.g.kt',
//kotlinOptions: KotlinOptions(
// package: 'me.polynom.moxplatform_android',
//),
javaOut: 'packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java',
javaOptions: JavaOptions(
package: 'me.polynom.moxplatform_android',
),
),
)
class NotificationMessageContent {
const NotificationMessageContent(
this.body,
this.mime,
this.path,
);
/// The textual body of the message.
final String? body;
/// The path and mime type of the media to show.
final String? mime;
final String? path;
}
class NotificationMessage {
const NotificationMessage(
this.sender,
this.content,
this.jid,
this.timestamp,
this.avatarPath,
);
/// The sender of the message.
final String? sender;
/// The jid of the sender.
final String? jid;
/// The body of the message.
final NotificationMessageContent content;
/// Milliseconds since epoch.
final int timestamp;
/// The path to the avatar to use
final String? avatarPath;
}
class MessagingNotification {
const MessagingNotification(this.title, this.id, this.jid, this.messages, this.channelId, this.isGroupchat, this.extra);
/// The title of the conversation.
final String title;
/// The id of the notification.
final int id;
/// The id of the notification channel the notification should appear on.
final String channelId;
/// The JID of the chat in which the notifications happen.
final String jid;
/// Messages to show.
final List<NotificationMessage?> messages;
/// Flag indicating whether this notification is from a groupchat or not.
final bool isGroupchat;
/// Additional data to include.
final Map<String?, String?>? extra;
}
enum NotificationIcon {
warning,
error,
none,
}
class RegularNotification {
const RegularNotification(this.title, this.body, this.channelId, this.id, this.icon);
/// The title of the notification.
final String title;
/// The body of the notification.
final String body;
/// The id of the channel to show the notification on.
final String channelId;
/// The id of the notification.
final int id;
/// The icon to use.
final NotificationIcon icon;
}
enum NotificationEventType {
markAsRead,
reply,
open,
}
class NotificationEvent {
const NotificationEvent(
this.id,
this.jid,
this.type,
this.payload,
this.extra,
);
/// The notification id.
final int id;
/// The JID the notification was for.
final String jid;
/// The type of event.
final NotificationEventType type;
/// An optional payload.
/// - type == NotificationType.reply: The reply message text.
/// Otherwise: undefined.
final String? payload;
/// Extra data. Only set when type == NotificationType.reply.
final Map<String?, String?>? extra;
}
class NotificationI18nData {
const NotificationI18nData(this.reply, this.markAsRead, this.you);
/// The content of the reply button.
final String reply;
/// The content of the "mark as read" button.
final String markAsRead;
/// The text to show when *you* reply.
final String you;
}
enum CipherAlgorithm {
aes128GcmNoPadding,
aes256GcmNoPadding,
aes256CbcPkcs7;
}
class CryptographyResult {
const CryptographyResult(this.plaintextHash, this.ciphertextHash);
final Uint8List plaintextHash;
final Uint8List ciphertextHash;
}
// The type of icon to use when no avatar path is provided.
enum FallbackIconType {
none,
person,
notes;
}
@HostApi()
abstract class MoxplatformApi {
/// Notification APIs
void createNotificationChannel(String title, String description, String id, bool urgent);
void showMessagingNotification(MessagingNotification notification);
void showNotification(RegularNotification notification);
void dismissNotification(int id);
void setNotificationSelfAvatar(String path);
void setNotificationI18n(NotificationI18nData data);
/// Platform APIs
String getPersistentDataPath();
String getCacheDataPath();
/// Contacts APIs
void recordSentMessage(String name, String jid, String? avatarPath, FallbackIconType fallbackIcon);
/// Cryptography APIs
@async CryptographyResult? encryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec);
@async CryptographyResult? decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec);
@async Uint8List? hashFile(String sourcePath, String hashSpec);
/// Stubs
void eventStub(NotificationEvent event);
}

View File

@ -4,3 +4,4 @@ environment:
sdk: '>=2.18.0 <3.0.0'
dev_dependencies:
melos: ^3.1.1
pigeon: 10.1.4