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