Merge branch 'feat/notifications'
This commit is contained in:
commit
497ac279cc
14
.gitlint
Normal file
14
.gitlint
Normal 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]
|
54
CHANGELOG.md
54
CHANGELOG.md
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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.
|
||||
|
@ -8,5 +8,7 @@ command:
|
||||
usePubspecOverrides: true
|
||||
|
||||
scripts:
|
||||
format:
|
||||
exec: dart format .
|
||||
analyze:
|
||||
exec: dart analyze .
|
||||
exec: flutter analyze
|
||||
|
@ -1,3 +1,7 @@
|
||||
## 0.1.17+2
|
||||
|
||||
- **FIX**: Format and lint.
|
||||
|
||||
## 0.1.17+1
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
@ -1,4 +1,6 @@
|
||||
library moxplatform;
|
||||
|
||||
export 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
|
||||
|
||||
export 'src/plugin.dart';
|
||||
export 'src/types.dart';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package me.polynom.moxplatform_android
|
||||
|
||||
import androidx.core.content.FileProvider
|
||||
|
||||
class MoxplatformFileProvider : FileProvider(R.xml.file_paths) {
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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';
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
13
packages/moxplatform_android/lib/src/platform_android.dart
Normal file
13
packages/moxplatform_android/lib/src/platform_android.dart
Normal 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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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';
|
||||
|
733
packages/moxplatform_platform_interface/lib/src/api.g.dart
Normal file
733
packages/moxplatform_platform_interface/lib/src/api.g.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import 'package:moxplatform_platform_interface/src/media.dart';
|
||||
|
||||
class StubMediaScannerImplementation extends MediaScannerImplementation {
|
||||
@override
|
||||
void scanFile(String path) {}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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 => '';
|
||||
}
|
@ -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
195
pigeons/api.dart
Normal 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);
|
||||
}
|
@ -4,3 +4,4 @@ environment:
|
||||
sdk: '>=2.18.0 <3.0.0'
|
||||
dev_dependencies:
|
||||
melos: ^3.1.1
|
||||
pigeon: 10.1.4
|
||||
|
Reference in New Issue
Block a user