Compare commits

..

49 Commits

Author SHA1 Message Date
42155d9e31 final commit 2023-09-09 00:30:56 +02:00
2813e72647 chore(base,interface,android): Bump version 2023-09-03 21:58:47 +02:00
4167227c7b feat(interface,android): Allow passing an initial locale to the service 2023-09-03 21:57:25 +02:00
81e819b4a7 chore(repo): Release new version 2023-09-03 13:11:12 +02:00
fe07b2a488 fix(repo): Remove notification examples 2023-09-03 13:09:44 +02:00
f2b140de18 chore(android,base,interface): Move notification stuff into Moxxy 2023-09-03 13:03:51 +02:00
7cc2d0e4be chore(repo): Fix linter issues 2023-08-30 20:46:38 +02:00
be58288025 feat(base,interface,android): Move more logic to Moxxy 2023-08-30 20:34:38 +02:00
27440067cd fix(android): Fix notification grouping with the foreground notification 2023-08-28 13:36:36 +02:00
11ea1fae12 fix(android): Fix creating notifications on Android 13 2023-08-26 23:28:07 +02:00
fe6d0a60c1 feat(android,interface): Implement video thumbnail generation 2023-08-24 20:06:31 +02:00
fb71ac330a chore(android,interface,base): Bump versions 2023-08-05 00:10:36 +02:00
2a9afdbb0c fix(android): Fix FileProvider id 2023-08-05 00:10:07 +02:00
73008c95b6 chore(android,interface,base): Bump versions 2023-08-05 00:08:22 +02:00
0d4f0c59cc feat(android,interface): Handle battery optimisation 2023-08-05 00:04:49 +02:00
497ac279cc
Merge branch 'feat/notifications' 2023-08-04 13:54:41 +02:00
3c773e5270 chore(repo): Introduce gitlint 2023-08-04 13:50:32 +02:00
173d5f5166 chore(release): publish packages
- moxplatform@0.1.17+2
 - moxplatform_android@0.1.18
 - moxplatform_platform_interface@0.1.18
2023-08-04 13:44:52 +02:00
960bad46d4 docs: Update docs a little 2023-08-04 13:43:42 +02:00
43e88af803 fix: Format and lint 2023-08-04 13:38:11 +02:00
b12e36da83 feat: Move recordSentMessage to pigeon 2023-08-03 21:27:13 +02:00
61de3cd565 feat: Move the crypto APIs to pigeon 2023-08-03 21:19:11 +02:00
271428219a feat: Adjust to Moxxy changes 2023-08-03 20:55:05 +02:00
6896b928e8 feat: Store the avatar path also in the shared preferences 2023-07-30 22:40:02 +02:00
79938aa177 fix: Fix self-replies after receiving another message
If the order of events is

- 1 or more messages received
- 1 reply
- 1 message received,

then the self-reply will be grouped together with the message
above it.
2023-07-30 22:28:16 +02:00
2f5a39416b feat: Allow the sender's data being null 2023-07-30 22:05:45 +02:00
2490a8ee9f fix: Add payload to all intents 2023-07-30 20:40:59 +02:00
c7ee2b6c6e feat: Allow attaching arbitrary data to the notification 2023-07-29 15:32:33 +02:00
6da35cd0ba feat: Allow showing regular notifications 2023-07-29 13:12:41 +02:00
30ef477999 feat: Make i18n data a bit more persistent 2023-07-29 12:50:50 +02:00
8f93821617 feat: Color in the notification silhouette 2023-07-29 12:34:40 +02:00
daf40aed0b feat: Allow setting the self-avatar 2023-07-28 21:46:47 +02:00
e975e749e4 fix: Fix images disappearing after replying 2023-07-28 21:06:50 +02:00
adb8ee88d1 feat: Take care of i18n 2023-07-28 17:32:14 +02:00
f90b3866ab Handle tapping the notification 2023-07-28 14:11:13 +02:00
fb9dab3d1e Implement streaming data into Flutter 2023-07-28 13:54:57 +02:00
da851a985b Cleanup 2023-07-28 12:46:02 +02:00
864b868f45 Rewrite the notification code in Kotlin 2023-07-28 00:46:19 +02:00
1771c0e1b6 Basic stuff 2023-07-27 20:45:09 +02:00
17642f9fab chore(release): publish packages
- moxplatform_android@0.1.17+1
 - moxplatform@0.1.17+1
 - moxplatform_platform_interface@0.1.17+1
2023-07-21 22:22:36 +02:00
d5fdbe736b fix: Accidentally used the name as the target's key. Oops 2023-07-21 22:22:25 +02:00
2486d846e8 fix: Fix minor things 2023-07-21 19:15:13 +02:00
3cd615aa3e fix: Add a pubspec file for melos 2023-07-21 17:38:55 +02:00
8c9724055f chore(release): publish packages
- moxplatform@0.1.17
 - moxplatform_android@0.1.17
 - moxplatform_platform_interface@0.1.17
2023-07-21 17:38:23 +02:00
f6e442fd4b fix: Fix example code 2023-07-21 17:31:43 +02:00
563b0386d6 feat: Improve code quality of the cryptography 2023-07-21 17:30:59 +02:00
eab467ee1d feat: Rewrite recordSentMessage in Kotlin 2023-07-21 13:23:11 +02:00
052a4e4700 feat: Add an API for creating direct share shortcuts 2023-07-21 13:04:44 +02:00
3bc880079c feat: Migrate to moxlib 0.2.0 2023-06-18 12:19:24 +02:00
74 changed files with 2899 additions and 635 deletions

14
.gitlint Normal file
View File

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

278
CHANGELOG.md Normal file
View File

@ -0,0 +1,278 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 2023-09-03
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`moxplatform_android` - `v0.1.22`](#moxplatform_android---v0122)
- [`moxplatform_platform_interface` - `v0.1.22`](#moxplatform_platform_interface---v0122)
- [`moxplatform` - `v0.1.17+6`](#moxplatform---v01176)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `moxplatform` - `v0.1.17+6`
---
#### `moxplatform_android` - `v0.1.22`
- **FIX**(android): Fix notification grouping with the foreground notification.
- **FIX**(android): Fix creating notifications on Android 13.
- **FEAT**(interface,android): Allow passing an initial locale to the service.
- **FEAT**(base,interface,android): Move more logic to Moxxy.
- **FEAT**(android,interface): Implement video thumbnail generation.
#### `moxplatform_platform_interface` - `v0.1.22`
- **FIX**(repo): Remove notification examples.
- **FEAT**(interface,android): Allow passing an initial locale to the service.
- **FEAT**(base,interface,android): Move more logic to Moxxy.
- **FEAT**(android,interface): Implement video thumbnail generation.
## 2023-09-03
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`moxplatform_android` - `v0.1.21`](#moxplatform_android---v0121)
- [`moxplatform_platform_interface` - `v0.1.21`](#moxplatform_platform_interface---v0121)
- [`moxplatform` - `v0.1.17+5`](#moxplatform---v01175)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `moxplatform` - `v0.1.17+5`
---
#### `moxplatform_android` - `v0.1.21`
- **FIX**(android): Fix notification grouping with the foreground notification.
- **FIX**(android): Fix creating notifications on Android 13.
- **FEAT**(base,interface,android): Move more logic to Moxxy.
- **FEAT**(android,interface): Implement video thumbnail generation.
#### `moxplatform_platform_interface` - `v0.1.21`
- **FIX**(repo): Remove notification examples.
- **FEAT**(base,interface,android): Move more logic to Moxxy.
- **FEAT**(android,interface): Implement video thumbnail generation.
## 2023-08-05
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`moxplatform_android` - `v0.1.20`](#moxplatform_android---v0120)
- [`moxplatform_platform_interface` - `v0.1.20`](#moxplatform_platform_interface---v0120)
- [`moxplatform` - `v0.1.17+4`](#moxplatform---v01174)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `moxplatform` - `v0.1.17+4`
---
#### `moxplatform_android` - `v0.1.20`
- **FIX**(android): Fix FileProvider id.
- **FEAT**(android,interface): Handle battery optimisation.
#### `moxplatform_platform_interface` - `v0.1.20`
- **FEAT**(android,interface): Handle battery optimisation.
## 2023-08-05
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`moxplatform_android` - `v0.1.19`](#moxplatform_android---v0119)
- [`moxplatform_platform_interface` - `v0.1.19`](#moxplatform_platform_interface---v0119)
- [`moxplatform` - `v0.1.17+3`](#moxplatform---v01173)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `moxplatform` - `v0.1.17+3`
---
#### `moxplatform_android` - `v0.1.19`
- **FEAT**(android,interface): Handle battery optimisation.
#### `moxplatform_platform_interface` - `v0.1.19`
- **FEAT**(android,interface): Handle battery optimisation.
## 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
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`moxplatform_android` - `v0.1.17+1`](#moxplatform_android---v01171)
- [`moxplatform` - `v0.1.17+1`](#moxplatform---v01171)
- [`moxplatform_platform_interface` - `v0.1.17+1`](#moxplatform_platform_interface---v01171)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `moxplatform` - `v0.1.17+1`
- `moxplatform_platform_interface` - `v0.1.17+1`
---
#### `moxplatform_android` - `v0.1.17+1`
- **FIX**: Accidentally used the name as the target's key. Oops.
- **FIX**: Fix minor things.
## 2023-07-21
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`moxplatform` - `v0.1.17`](#moxplatform---v0117)
- [`moxplatform_android` - `v0.1.17`](#moxplatform_android---v0117)
- [`moxplatform_platform_interface` - `v0.1.17`](#moxplatform_platform_interface---v0117)
---
#### `moxplatform` - `v0.1.17`
- **FIX**: Fix typecasting issue.
- **FEAT**: Add an API for creating direct share shortcuts.
- **FEAT**: Migrate to moxlib 0.2.0.
- **FEAT**: I forgot to bump dependency versions.
#### `moxplatform_android` - `v0.1.17`
- **FIX**: Fix typecasting issue.
- **FEAT**: Improve code quality of the cryptography.
- **FEAT**: Rewrite recordSentMessage in Kotlin.
- **FEAT**: Add an API for creating direct share shortcuts.
- **FEAT**: Migrate to moxlib 0.2.0.
- **FEAT**: I forgot to bump dependency versions.
- **FEAT**: Also hash the file on encryption and decryption.
#### `moxplatform_platform_interface` - `v0.1.17`
- **FIX**: Fix typecasting issue.
- **FEAT**: Add an API for creating direct share shortcuts.
- **FEAT**: Migrate to moxlib 0.2.0.
- **FEAT**: I forgot to bump dependency versions.
- **FEAT**: Also hash the file on encryption and decryption.

View File

@ -9,9 +9,11 @@ This repo is based on [very_good_flutter_plugin](https://github.com/VeryGoodOpen
The development of this package is based on [melos](https://pub.dev/packages/melos). 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 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 the code and making your changes, please format the code using `melos run format` and lint using `melos run analyze`.
are left inside the code.
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 ## Acknowledgements
- [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). moxplatform_android is basically just a copy and paste of [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). - [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). moxplatform_android's service implementation is basically just a copy and paste of [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service).

View File

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

View File

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

View File

@ -23,8 +23,8 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
@ -34,5 +34,5 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest> </manifest>

View File

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.6.10' ext.kotlin_version = '1.8.21'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

@ -3,59 +3,60 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; import 'package:path/path.dart';
/// The id of the notification channel.
const channelId = "me.polynom.moxplatform.testing3";
const otherChannelId = "me.polynom.moxplatform.testing4";
void main() { void main() {
runApp(const MyApp()); 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); const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application. @override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Moxplatform Demo',
theme: ThemeData( theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
), ),
home: const MyHomePage(title: 'Flutter Demo Home Page'), home: const MyHomePage(),
); );
} }
} }
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key); const MyHomePage({super.key});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override @override
State<MyHomePage> createState() => _MyHomePageState(); MyHomePageState createState() => MyHomePageState();
} }
class _MyHomePageState extends State<MyHomePage> { class MyHomePageState extends State<MyHomePage> {
int _counter = 0; /// 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'),
];
Future<void> _incrementCounter() async { Future<void> _cryptoTest() async {
final result = await FilePicker.platform.pickFiles(); final result = await FilePicker.platform.pickFiles();
if (result == null) { if (result == null) {
return; return;
@ -65,7 +66,7 @@ class _MyHomePageState extends State<MyHomePage> {
final path = result.files.single.path; final path = result.files.single.path;
final enc = await MoxplatformPlugin.crypto.encryptFile( final enc = await MoxplatformPlugin.crypto.encryptFile(
path!, path!,
path + '.enc', '$path.enc',
Uint8List.fromList(List.filled(32, 1)), Uint8List.fromList(List.filled(32, 1)),
Uint8List.fromList(List.filled(16, 2)), Uint8List.fromList(List.filled(16, 2)),
CipherAlgorithm.aes256CbcPkcs7, CipherAlgorithm.aes256CbcPkcs7,
@ -74,75 +75,165 @@ class _MyHomePageState extends State<MyHomePage> {
final end = DateTime.now(); final end = DateTime.now();
final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch; final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch;
// ignore: avoid_print
print('TIME: ${diff / 1000}s'); print('TIME: ${diff / 1000}s');
// ignore: avoid_print
print('DONE (${enc != null})'); print('DONE (${enc != null})');
final lengthEnc = await File(path + ".enc").length(); final lengthEnc = await File('$path.enc').length();
final lengthOrig = await File(path).length(); final lengthOrig = await File(path).length();
// ignore: avoid_print
print('Encrypted file is $lengthEnc Bytes large (Orig $lengthOrig)'); print('Encrypted file is $lengthEnc Bytes large (Orig $lengthOrig)');
await MoxplatformPlugin.crypto.decryptFile( await MoxplatformPlugin.crypto.decryptFile(
path + '.enc', '$path.enc',
path + '.dec', '$path.dec',
Uint8List.fromList(List.filled(32, 1)), Uint8List.fromList(List.filled(32, 1)),
Uint8List.fromList(List.filled(16, 2)), Uint8List.fromList(List.filled(16, 2)),
CipherAlgorithm.aes256CbcPkcs7, CipherAlgorithm.aes256CbcPkcs7,
'SHA-256', 'SHA-256',
); );
// ignore: avoid_print
print('DONE'); print('DONE');
final lengthDec = await File(path + ".dec").length(); final lengthDec = await File('$path.dec').length();
// ignore: avoid_print
print('Decrypted file is $lengthDec Bytes large (Orig $lengthOrig)'); print('Decrypted file is $lengthDec Bytes large (Orig $lengthOrig)');
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by title: const Text('Moxplatform Demo'),
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
), ),
body: Center( body: Center(
// Center is a layout widget. It takes a single child and positions it child: ListView(
// in the middle of the parent. children: [
child: Column( ElevatedButton(
// Column is also a layout widget. It takes a list of children and onPressed: _cryptoTest,
// arranges them vertically. By default, it sizes itself to fit its child: const Text('Test cryptography'),
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
), ),
Text( ElevatedButton(
'$_counter', onPressed: () {
style: Theme.of(context).textTheme.headline4, MoxplatformPlugin.contacts.recordSentMessage('Hallo', 'Welt');
},
child: const Text('Test recordSentMessage (no fallback)'),
),
ElevatedButton(
onPressed: () {
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);
},
child: const Text('Test recordSentMessage (notes fallback)'),
),
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'),
),
ElevatedButton(
onPressed: () async {
// ignore: avoid_print
print(await MoxplatformPlugin.platform
.isIgnoringBatteryOptimizations());
},
child: const Text('Is battery optimised?'),
),
ElevatedButton(
onPressed: () async {
await MoxplatformPlugin.platform
.openBatteryOptimisationSettings();
},
child: const Text('Open battery optimisation page'),
),
ElevatedButton(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.video,
);
if (result == null) return;
final path = result.files.single.path!;
final storagePath =
await MoxplatformPlugin.platform.getPersistentDataPath();
final mediaPath = join(storagePath, 'media');
if (!Directory(mediaPath).existsSync()) {
await Directory(mediaPath).create(recursive: true);
}
final internalPath = join(mediaPath, basename(path));
// ignore: avoid_print
print('Copying file');
await File(path).copy(internalPath);
// ignore: avoid_print
print('Generating thumbnail');
final thumbResult =
await MoxplatformPlugin.platform.generateVideoThumbnail(
internalPath,
'$internalPath.thumbnail.jpg',
720,
);
// ignore: avoid_print
print('Success: $thumbResult');
// ignore: use_build_context_synchronously
await showDialog<void>(
context: context,
builder: (context) => Image.file(
File('$internalPath.thumbnail.jpg'),
),
);
},
child: const Text('Thumbnail'),
),
ElevatedButton(
onPressed: () async {
final result = await MoxplatformPlugin.platform.pickFiles(FilePickerType.image, false);
print('Picked files $result');
},
child: const Text('Pick image'),
),
ElevatedButton(
onPressed: () async {
final result = await MoxplatformPlugin.platform.pickFiles(FilePickerType.image, true);
print('Picked files $result');
},
child: const Text('Pick multiple images'),
),
ElevatedButton(
onPressed: () async {
final result = await MoxplatformPlugin.platform.pickFiles(FilePickerType.imageAndVideo, true);
print('Picked files $result');
},
child: const Text('Pick multiple images and videos'),
),
ElevatedButton(
onPressed: () async {
final result = await MoxplatformPlugin.platform.pickFiles(FilePickerType.generic, true);
print('Picked files $result');
},
child: const Text('Pick multiple generic files'),
), ),
], ],
), ),
), ),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
); );
} }
} }

View File

@ -32,13 +32,15 @@ dependencies:
moxplatform: moxplatform:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.15 version: 0.1.17+6
moxplatform_android: moxplatform_android:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.15 version: 0.1.22
file_picker: 5.2.0+1 file_picker: 5.2.0+1
path: 1.8.3
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2

View File

@ -1,6 +1,66 @@
{ {
"nodes": { "nodes": {
"android-nixpkgs": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1689798050,
"narHash": "sha256-ZyFPra7N0MF803o55dYQQyX9b/BmXr6QTCyN7slRThY=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "9aa0e2990da86de8ca203af313668851dcb9ea6e",
"type": "github"
},
"original": {
"owner": "tadfisher",
"repo": "android-nixpkgs",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"android-nixpkgs",
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1688380630,
"narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=",
"owner": "numtide",
"repo": "devshell",
"rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": { "locked": {
"lastModified": 1649676176, "lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
@ -17,11 +77,27 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1660551188, "lastModified": 1689679375,
"narHash": "sha256-a1LARMMYQ8DPx1BgoI/UN4bXe12hhZkCNqdxNi6uS0g=", "narHash": "sha256-LHUC52WvyVDi9PwyL1QCpaxYWBqp4ir4iL6zgOkmcb8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "441dc5d512153039f19ef198e662e4f3dbb9fd65", "rev": "684c17c429c42515bafb3ad775d2a710947f3d67",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1689752456,
"narHash": "sha256-VOChdECcEI8ixz8QY+YC4JaNEFwQd1V8bA0G4B28Ki0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7f256d7da238cb627ef189d56ed590739f42f13b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -33,8 +109,39 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "android-nixpkgs": "android-nixpkgs",
"nixpkgs": "nixpkgs" "flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
} }
} }
}, },

View File

@ -3,9 +3,10 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
android-nixpkgs.url = "github:tadfisher/android-nixpkgs";
}; };
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let outputs = { self, nixpkgs, flake-utils, android-nixpkgs }: flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
config = { config = {
@ -13,29 +14,32 @@
allowUnfree = true; allowUnfree = true;
}; };
}; };
android = pkgs.androidenv.composeAndroidPackages { # Everything to make Flutter happy
# TODO: Find a way to pin these sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [
#toolsVersion = "26.1.1"; cmdline-tools-latest
#platformToolsVersion = "31.0.3"; build-tools-30-0-3
#buildToolsVersions = [ "31.0.0" ]; build-tools-33-0-2
#includeEmulator = true; build-tools-34-0-0
#emulatorVersion = "30.6.3"; platform-tools
platformVersions = [ "28" ]; emulator
includeSources = false; patcher-v4
includeSystemImages = true; platforms-android-30
systemImageTypes = [ "default" ]; platforms-android-31
abiVersions = [ "x86_64" ]; platforms-android-33
includeNDK = false; ]);
useGoogleAPIs = false; pinnedJDK = pkgs.jdk17;
useGoogleTVAddOns = false;
};
pinnedJDK = pkgs.jdk;
in { in {
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
flutter pinnedJDK android.platform-tools dart # Flutter # Android
gitlint jq # Code hygiene pinnedJDK
ripgrep # General utilities sdk
# Flutter
flutter dart
# Code hygiene
gitlint
# Flutter dependencies for linux desktop # Flutter dependencies for linux desktop
atk atk
@ -59,9 +63,13 @@
CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include"; CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include";
LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath [ atk cairo epoxy gdk-pixbuf glib gtk3 harfbuzz pango ]; LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath [ atk cairo epoxy gdk-pixbuf glib gtk3 harfbuzz pango ];
ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk"; ANDROID_HOME = "${sdk}/share/android-sdk";
ANDROID_SDK_ROOT = "${sdk}/share/android-sdk";
JAVA_HOME = pinnedJDK; JAVA_HOME = pinnedJDK;
ANDROID_AVD_HOME = (toString ./.) + "/.android/avd";
# Fix an issue with Flutter using an older version of aapt2, which does not know
# an used parameter.
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${sdk}/share/android-sdk/build-tools/34.0.0/aapt2";
}; };
}); });
} }

View File

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

View File

@ -1,3 +1,34 @@
## 0.1.17+6
- Update a dependency to the latest release.
## 0.1.17+5
- Update a dependency to the latest release.
## 0.1.17+4
- Update a dependency to the latest release.
## 0.1.17+3
- Update a dependency to the latest release.
## 0.1.17+2
- **FIX**: Format and lint.
## 0.1.17+1
- Update a dependency to the latest release.
## 0.1.17
- **FIX**: Fix typecasting issue.
- **FEAT**: Add an API for creating direct share shortcuts.
- **FEAT**: Migrate to moxlib 0.2.0.
- **FEAT**: I forgot to bump dependency versions.
## 0.1.11+2 ## 0.1.11+2
- Update a dependency to the latest release. - Update a dependency to the latest release.

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import 'package:moxlib/awaitabledatasender.dart'; import 'package:moxlib/moxlib.dart';
abstract class BackgroundCommand implements JsonImplementation {} abstract class BackgroundCommand implements JsonImplementation {}
abstract class BackgroundEvent implements JsonImplementation {} abstract class BackgroundEvent implements JsonImplementation {}

View File

@ -1,11 +1,11 @@
name: moxplatform name: moxplatform
description: Moxxy platform-specific code description: Moxxy platform-specific code
version: 0.1.15 version: 0.1.17+6
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
homepage: https://codeberg.org/moxxy/moxplatform homepage: https://codeberg.org/moxxy/moxplatform
environment: environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
flutter: ">=2.10.0" flutter: ">=2.10.0"
flutter: flutter:
@ -22,17 +22,17 @@ dependencies:
moxlib: moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.4 version: ^0.2.0
moxplatform_android: moxplatform_android:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.15 version: ^0.1.22
moxplatform_platform_interface: moxplatform_platform_interface:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.15 version: ^0.1.22
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
plugin_platform_interface: ^2.1.2 plugin_platform_interface: ^2.1.2
very_good_analysis: ^2.4.0 very_good_analysis: ^3.0.1

View File

@ -0,0 +1 @@
pubspec_overrides.yaml

View File

@ -1,3 +1,60 @@
## 0.1.22
- **FIX**(android): Fix notification grouping with the foreground notification.
- **FIX**(android): Fix creating notifications on Android 13.
- **FEAT**(interface,android): Allow passing an initial locale to the service.
- **FEAT**(base,interface,android): Move more logic to Moxxy.
- **FEAT**(android,interface): Implement video thumbnail generation.
## 0.1.21
- **FIX**(android): Fix notification grouping with the foreground notification.
- **FIX**(android): Fix creating notifications on Android 13.
- **FEAT**(base,interface,android): Move more logic to Moxxy.
- **FEAT**(android,interface): Implement video thumbnail generation.
## 0.1.20
- **FIX**(android): Fix FileProvider id.
- **FEAT**(android,interface): Handle battery optimisation.
## 0.1.19
- **FEAT**(android,interface): Handle battery optimisation.
## 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.
- **FIX**: Fix minor things.
## 0.1.17
- **FIX**: Fix typecasting issue.
- **FEAT**: Improve code quality of the cryptography.
- **FEAT**: Rewrite recordSentMessage in Kotlin.
- **FEAT**: Add an API for creating direct share shortcuts.
- **FEAT**: Migrate to moxlib 0.2.0.
- **FEAT**: I forgot to bump dependency versions.
- **FEAT**: Also hash the file on encryption and decryption.
## 0.1.11+2 ## 0.1.11+2
- **REFACTOR**: Make version constraints looser. - **REFACTOR**: Make version constraints looser.

View File

@ -2,13 +2,17 @@ group 'me.polynom.moxplatform_android'
version '1.0' version '1.0'
buildscript { buildscript {
ext {
kotlin_version = '1.8.21'
}
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.0' classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
@ -20,9 +24,10 @@ rootProject.allprojects {
} }
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
android { android {
compileSdkVersion 31 compileSdkVersion 33
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -30,10 +35,14 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 16 // What Moxxy currently uses
minSdkVersion 26
targetSdkVersion 33
} }
} }
dependencies { dependencies {
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.core:core:1.10.1'
implementation 'androidx.activity:activity:1.7.2'
} }

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -1,8 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.polynom.moxplatform_android"> package="me.polynom.moxplatform_android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application> <application>
@ -26,6 +28,5 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,507 @@
// Autogenerated from Pigeon (v10.1.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package me.polynom.moxplatform_android;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.common.BasicMessageChannel;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MessageCodec;
import io.flutter.plugin.common.StandardMessageCodec;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Generated class from Pigeon. */
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
public class Api {
/** Error class for passing custom error details to Flutter via a thrown PlatformException. */
public static class FlutterError extends RuntimeException {
/** The error code. */
public final String code;
/** The error details. Must be a datatype supported by the api codec. */
public final Object details;
public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details)
{
super(message);
this.code = code;
this.details = details;
}
}
@NonNull
protected static ArrayList<Object> wrapError(@NonNull Throwable exception) {
ArrayList<Object> errorList = new ArrayList<Object>(3);
if (exception instanceof FlutterError) {
FlutterError error = (FlutterError) exception;
errorList.add(error.code);
errorList.add(error.getMessage());
errorList.add(error.details);
} else {
errorList.add(exception.toString());
errorList.add(exception.getClass().getSimpleName());
errorList.add(
"Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
}
return errorList;
}
public enum CipherAlgorithm {
AES128GCM_NO_PADDING(0),
AES256GCM_NO_PADDING(1),
AES256CBC_PKCS7(2);
final int index;
private CipherAlgorithm(final int index) {
this.index = index;
}
}
public enum FallbackIconType {
NONE(0),
PERSON(1),
NOTES(2);
final int index;
private FallbackIconType(final int index) {
this.index = index;
}
}
public enum FilePickerType {
IMAGE(0),
VIDEO(1),
IMAGE_AND_VIDEO(2),
GENERIC(3);
final int index;
private FilePickerType(final int index) {
this.index = index;
}
}
/** Generated class from Pigeon that represents data sent in messages. */
public static final class CryptographyResult {
private @NonNull byte[] plaintextHash;
public @NonNull byte[] getPlaintextHash() {
return plaintextHash;
}
public void setPlaintextHash(@NonNull byte[] setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"plaintextHash\" is null.");
}
this.plaintextHash = setterArg;
}
private @NonNull byte[] ciphertextHash;
public @NonNull byte[] getCiphertextHash() {
return ciphertextHash;
}
public void setCiphertextHash(@NonNull byte[] setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"ciphertextHash\" is null.");
}
this.ciphertextHash = setterArg;
}
/** Constructor is non-public to enforce null safety; use Builder. */
CryptographyResult() {}
public static final class Builder {
private @Nullable byte[] plaintextHash;
public @NonNull Builder setPlaintextHash(@NonNull byte[] setterArg) {
this.plaintextHash = setterArg;
return this;
}
private @Nullable byte[] ciphertextHash;
public @NonNull Builder setCiphertextHash(@NonNull byte[] setterArg) {
this.ciphertextHash = setterArg;
return this;
}
public @NonNull CryptographyResult build() {
CryptographyResult pigeonReturn = new CryptographyResult();
pigeonReturn.setPlaintextHash(plaintextHash);
pigeonReturn.setCiphertextHash(ciphertextHash);
return pigeonReturn;
}
}
@NonNull
ArrayList<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(2);
toListResult.add(plaintextHash);
toListResult.add(ciphertextHash);
return toListResult;
}
static @NonNull CryptographyResult fromList(@NonNull ArrayList<Object> list) {
CryptographyResult pigeonResult = new CryptographyResult();
Object plaintextHash = list.get(0);
pigeonResult.setPlaintextHash((byte[]) plaintextHash);
Object ciphertextHash = list.get(1);
pigeonResult.setCiphertextHash((byte[]) ciphertextHash);
return pigeonResult;
}
}
public interface Result<T> {
@SuppressWarnings("UnknownNullness")
void success(T result);
void error(@NonNull Throwable error);
}
private static class MoxplatformApiCodec extends StandardMessageCodec {
public static final MoxplatformApiCodec INSTANCE = new MoxplatformApiCodec();
private MoxplatformApiCodec() {}
@Override
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
switch (type) {
case (byte) 128:
return CryptographyResult.fromList((ArrayList<Object>) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
}
}
@Override
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
if (value instanceof CryptographyResult) {
stream.write(128);
writeValue(stream, ((CryptographyResult) value).toList());
} else {
super.writeValue(stream, value);
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface MoxplatformApi {
/** Platform APIs */
@NonNull
String getPersistentDataPath();
@NonNull
String getCacheDataPath();
void openBatteryOptimisationSettings();
@NonNull
Boolean isIgnoringBatteryOptimizations();
/** Contacts APIs */
void recordSentMessage(@NonNull String name, @NonNull String jid, @Nullable String avatarPath, @NonNull FallbackIconType fallbackIcon);
/** Cryptography APIs */
void encryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Result<CryptographyResult> result);
void decryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Result<CryptographyResult> result);
void hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Result<byte[]> result);
/** Media APIs */
@NonNull
Boolean generateVideoThumbnail(@NonNull String src, @NonNull String dest, @NonNull Long maxWidth);
/** Picker */
void pickFiles(@NonNull FilePickerType type, @NonNull Boolean pickMultiple, @NonNull Result<List<String>> result);
/** The codec used by MoxplatformApi. */
static @NonNull MessageCodec<Object> getCodec() {
return MoxplatformApiCodec.INSTANCE;
}
/**Sets up an instance of `MoxplatformApi` to handle messages through the `binaryMessenger`. */
static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable MoxplatformApi api) {
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getPersistentDataPath", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
String output = api.getPersistentDataPath();
wrapped.add(0, output);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getCacheDataPath", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
String output = api.getCacheDataPath();
wrapped.add(0, output);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.openBatteryOptimisationSettings", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
api.openBatteryOptimisationSettings();
wrapped.add(0, null);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.isIgnoringBatteryOptimizations", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
Boolean output = api.isIgnoringBatteryOptimizations();
wrapped.add(0, output);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.recordSentMessage", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String nameArg = (String) args.get(0);
String jidArg = (String) args.get(1);
String avatarPathArg = (String) args.get(2);
FallbackIconType fallbackIconArg = args.get(3) == null ? null : FallbackIconType.values()[(int) args.get(3)];
try {
api.recordSentMessage(nameArg, jidArg, avatarPathArg, fallbackIconArg);
wrapped.add(0, null);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.encryptFile", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String sourcePathArg = (String) args.get(0);
String destPathArg = (String) args.get(1);
byte[] keyArg = (byte[]) args.get(2);
byte[] ivArg = (byte[]) args.get(3);
CipherAlgorithm algorithmArg = args.get(4) == null ? null : CipherAlgorithm.values()[(int) args.get(4)];
String hashSpecArg = (String) args.get(5);
Result<CryptographyResult> resultCallback =
new Result<CryptographyResult>() {
public void success(CryptographyResult result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.encryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.decryptFile", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String sourcePathArg = (String) args.get(0);
String destPathArg = (String) args.get(1);
byte[] keyArg = (byte[]) args.get(2);
byte[] ivArg = (byte[]) args.get(3);
CipherAlgorithm algorithmArg = args.get(4) == null ? null : CipherAlgorithm.values()[(int) args.get(4)];
String hashSpecArg = (String) args.get(5);
Result<CryptographyResult> resultCallback =
new Result<CryptographyResult>() {
public void success(CryptographyResult result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.decryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.hashFile", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String sourcePathArg = (String) args.get(0);
String hashSpecArg = (String) args.get(1);
Result<byte[]> resultCallback =
new Result<byte[]>() {
public void success(byte[] result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.hashFile(sourcePathArg, hashSpecArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.generateVideoThumbnail", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String srcArg = (String) args.get(0);
String destArg = (String) args.get(1);
Number maxWidthArg = (Number) args.get(2);
try {
Boolean output = api.generateVideoThumbnail(srcArg, destArg, (maxWidthArg == null) ? null : maxWidthArg.longValue());
wrapped.add(0, output);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.pickFiles", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
FilePickerType typeArg = args.get(0) == null ? null : FilePickerType.values()[(int) args.get(0)];
Boolean pickMultipleArg = (Boolean) args.get(1);
Result<List<String>> resultCallback =
new Result<List<String>>() {
public void success(List<String> result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.pickFiles(typeArg, pickMultipleArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
}

View File

@ -1,7 +1,13 @@
package me.polynom.moxplatform_android; package me.polynom.moxplatform_android;
import static me.polynom.moxplatform_android.ConstantsKt.GROUP_KEY_FOREGROUND;
import static me.polynom.moxplatform_android.ConstantsKt.GROUP_KEY_MESSAGES;
import static me.polynom.moxplatform_android.ConstantsKt.GROUP_KEY_OTHER;
import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
@ -85,29 +91,15 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
} }
public static boolean isManuallyStopped(Context context) { 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) { public void setManuallyStopped(Context context, boolean value) {
context.getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE) context.getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE)
.edit() .edit()
.putBoolean(manuallyStoppedKey, value) .putBoolean(manuallyStoppedKey, value)
.apply(); .apply();
} }
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Moxxy Background Service";
String description = "Executing Moxxy in the background";
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel("FOREGROUND_DEFAULT", name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
protected void updateNotificationInfo() { protected void updateNotificationInfo() {
String packageName = getApplicationContext().getPackageName(); String packageName = getApplicationContext().getPackageName();
Intent i = getPackageManager().getLaunchIntentForPackage(packageName); Intent i = getPackageManager().getLaunchIntentForPackage(packageName);
@ -122,7 +114,7 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
| (PendingIntent.FLAG_MUTABLE & (aboveS ? PendingIntent.FLAG_MUTABLE : 0)) | (PendingIntent.FLAG_MUTABLE & (aboveS ? PendingIntent.FLAG_MUTABLE : 0))
); );
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, "FOREGROUND_DEFAULT") NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, "foreground_service")
.setSmallIcon(R.drawable.ic_service_icon) .setSmallIcon(R.drawable.ic_service_icon)
.setAutoCancel(true) .setAutoCancel(true)
.setOngoing(true) .setOngoing(true)
@ -151,7 +143,7 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
FlutterInjector.instance().flutterLoader().startInitialization(getApplicationContext()); FlutterInjector.instance().flutterLoader().startInitialization(getApplicationContext());
} }
long entrypointHandle = getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE) long entrypointHandle = getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE)
.getLong(MoxplatformAndroidPlugin.entrypointKey, 0); .getLong(MoxplatformAndroidPlugin.entrypointKey, 0);
FlutterInjector.instance().flutterLoader().ensureInitializationComplete(getApplicationContext(), null); FlutterInjector.instance().flutterLoader().ensureInitializationComplete(getApplicationContext(), null);
FlutterCallbackInformation callback = FlutterCallbackInformation.lookupCallbackInformation(entrypointHandle); FlutterCallbackInformation callback = FlutterCallbackInformation.lookupCallbackInformation(entrypointHandle);
@ -237,7 +229,6 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
createNotificationChannel();
notificationBody = "Preparing..."; notificationBody = "Preparing...";
updateNotificationInfo(); updateNotificationInfo();
} }

View File

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

View File

@ -0,0 +1,58 @@
package me.polynom.moxplatform_android
// The tag we use for logging.
const val TAG = "Moxplatform"
// The size of the buffer to hashing, encryption, and decryption in bytes.
const val BUFFER_SIZE = 8096
const val GROUP_KEY_FOREGROUND = "foreground"
const val GROUP_KEY_MESSAGES = "messages"
const val GROUP_KEY_OTHER = "other"
// 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"
// https://github.com/ekasetiawans/flutter_background_service/blob/e427f3b70138ec26f9671c2617f9061f25eade6f/packages/flutter_background_service_android/android/src/main/java/id/flutter/flutter_background_service/BootReceiver.java#L20
//const val WAKELOCK_DURATION = 10*60*1000L;
// The name of the wakelock the background service manager holds.
//const val SERVICE_WAKELOCK_NAME = "BackgroundService.Lock"
//const val DATA_RECEIVER_METHOD_NAME = "dataReceived"
// Shared preferences keys
//const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android"
//const val SP_MANUALLY_STOPPED_KEY = "manually_stopped"
//const val SP_ENTRYPOINT_KEY = "entrypoint_handle"
//const val SP_EXTRA_DATA_KEY = "extra_data"
//const val SP_AUTO_START_AT_BOOT_KEY = "auto_start_at_boot"

View File

@ -0,0 +1,158 @@
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
import java.lang.Exception
import java.security.MessageDigest
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) {
private val digest: MessageDigest
init {
this.digest = MessageDigest.getInstance(hashAlgorithm)
}
override fun write(buffer: ByteArray, offset: Int, length: Int) {
super.write(buffer, offset, length)
digest.update(buffer, offset, length)
}
fun digest() : ByteArray {
return digest.digest()
}
}
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, 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
// 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)
}
}
}
// 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: 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)
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
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)
}
}
}
// 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: 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)
// 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)
}
// 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)
}
}
}

View File

@ -1,34 +0,0 @@
package me.polynom.moxplatform_android;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class HashedFileOutputStream extends FileOutputStream {
public MessageDigest digest;
public HashedFileOutputStream(String name, String hashSpec) throws FileNotFoundException, NoSuchAlgorithmException {
super(name);
digest = MessageDigest.getInstance(hashSpec);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
super.write(b, off, len);
digest.update(b, off, len);
}
public String getHexHash() {
StringBuffer result = new StringBuffer();
for (byte b : digest.digest()) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
return result.toString();
}
public byte[] getHash() {
return digest.digest();
}
}

View File

@ -1,348 +1,318 @@
package me.polynom.moxplatform_android; package me.polynom.moxplatform_android;
import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
import static androidx.core.content.ContextCompat.getSystemService;
import static androidx.core.content.ContextCompat.startActivity;
import static me.polynom.moxplatform_android.ConstantsKt.MOXPLATFORM_FILEPROVIDER_ID;
import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY;
import static me.polynom.moxplatform_android.CryptoKt.*;
import static me.polynom.moxplatform_android.PickerKt.filePickerRequest;
import static me.polynom.moxplatform_android.PickerKt.onActivityResultImpl;
import static me.polynom.moxplatform_android.RecordSentMessageKt.*;
import static me.polynom.moxplatform_android.ThumbnailsKt.generateVideoThumbnailImplementation;
import me.polynom.moxplatform_android.Api.*;
import android.app.Activity;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
import android.util.Size;
import androidx.activity.result.PickVisualMediaRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.io.FileInputStream; import java.io.ByteArrayOutputStream;
import java.security.MessageDigest; import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.crypto.Cipher; import java.util.Set;
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.FlutterPlugin;
import io.flutter.embedding.engine.plugins.service.ServiceAware; import io.flutter.embedding.engine.plugins.service.ServiceAware;
import io.flutter.embedding.engine.plugins.service.ServicePluginBinding; 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.MethodCall;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.JSONMethodCodec; import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware { public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware, ActivityAware, PluginRegistry.ActivityResultListener, MoxplatformApi {
public static final String entrypointKey = "entrypoint_handle"; public static final String entrypointKey = "entrypoint_handle";
public static final String extraDataKey = "extra_data"; public static final String extraDataKey = "extra_data";
private static final String autoStartAtBootKey = "auto_start_at_boot"; 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";
private static final String TAG = "moxplatform_android"; public static final String methodChannelKey = "me.polynom.moxplatform_android";
public static final String methodChannelKey = "me.polynom.moxplatform_android"; public static final String dataReceivedMethodName = "dataReceived";
public static final String dataReceivedMethodName = "dataReceived";
private static final List<MoxplatformAndroidPlugin> _instances = new ArrayList<>(); private static final List<MoxplatformAndroidPlugin> _instances = new ArrayList<>();
private BackgroundService service; private BackgroundService service;
private MethodChannel channel; private MethodChannel channel;
private Context context;
public MoxplatformAndroidPlugin() { public static Activity activity;
_instances.add(this); private Context context;
}
@Override public MoxplatformAndroidPlugin() {
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { _instances.add(this);
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;
}
} }
return false; @Override
} public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
return onActivityResultImpl(context, requestCode, resultCode, data);
}
@Override @Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
switch (call.method) { channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), methodChannelKey);
case "configure": channel.setMethodCallHandler(this);
ArrayList args = (ArrayList) call.arguments; context = flutterPluginBinding.getApplicationContext();
long handle = (long) args.get(0);
String extraData = (String) args.get(1);
configure(handle, extraData); LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
result.success(true); localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey));
break;
case "isRunning": MoxplatformApi.setup(flutterPluginBinding.getBinaryMessenger(), this);
result.success(isRunning());
break; Log.d(TAG, "Attached to engine");
case "start": }
MoxplatformAndroidPlugin.setStartAtBoot(context, true);
BackgroundService.enqueue(context); static void registerWith(Registrar registrar) {
Intent intent = new Intent(context, BackgroundService.class); LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context());
ContextCompat.startForegroundService(context, intent); final MoxplatformAndroidPlugin plugin = new MoxplatformAndroidPlugin();
Log.d(TAG, "Service started"); localBroadcastManager.registerReceiver(plugin, new IntentFilter(methodChannelKey));
result.success(true); activity = registrar.activity();
break;
case "sendData": final MethodChannel channel = new MethodChannel(registrar.messenger(), "me.polynom/background_service_android", JSONMethodCodec.INSTANCE);
for (MoxplatformAndroidPlugin plugin : _instances) { channel.setMethodCallHandler(plugin);
if (plugin.service != null) { plugin.channel = channel;
plugin.service.receiveData((String) call.arguments);
break; 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(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(encryptFile(src, dest, key, iv, algorithm, hashSpec)); return false;
}
});
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);
result.success(decryptFile(src, dest, key, iv, 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);
result.success(hashFile(src, hashSpec));
}
});
hashingThread.start();
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;
}
private String getCipherSpecFromInteger(int algorithm) {
switch (algorithm) {
case 0: return "AES_128/GCM/NoPadding";
case 1: return "AES_256/GCM/NoPadding";
case 2: return "AES_256/CBC/PKCS7PADDING";
default:
Log.d(TAG, "INVALID ALGORITHM");
return "";
}
}
public HashMap<String, byte[]> encryptFile(String src, String dest, byte[] key, byte[] iv, int algorithm, String hashSpec) {
String spec = getCipherSpecFromInteger(algorithm);
if (spec.isEmpty()) {
return null;
} }
// Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3 @Override
byte[] buffer = new byte[8096]; public void onMethodCall(@NonNull MethodCall call, @NonNull io.flutter.plugin.common.MethodChannel.Result result) {
SecretKeySpec sk = new SecretKeySpec(key, spec); switch (call.method) {
try { case "configure":
MessageDigest md = MessageDigest.getInstance(hashSpec); ArrayList args = (ArrayList) call.arguments;
Cipher cipher = Cipher.getInstance(spec); long handle = (long) args.get(0);
cipher.init(Cipher.ENCRYPT_MODE, sk, new IvParameterSpec(iv)); String extraData = (String) args.get(1);
FileInputStream fin = new FileInputStream(src);
HashedFileOutputStream fout = new HashedFileOutputStream(dest, hashSpec); configure(handle, extraData);
CipherOutputStream cout = new CipherOutputStream(fout, cipher); result.success(true);
int len = 0; break;
int bufLen = 0; case "isRunning":
while (true) { result.success(isRunning());
len = fin.read(buffer); break;
if (len != 0 && len > 0) { case "start":
md.update(buffer, 0, len); MoxplatformAndroidPlugin.setStartAtBoot(context, true);
cout.write(buffer, 0, len); BackgroundService.enqueue(context);
} else { Intent intent = new Intent(context, BackgroundService.class);
break; 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;
} }
}
cout.flush();
cout.close();
fin.close();
return new HashMap<String, byte[]>() {{
put("plaintext_hash", md.digest());
put("ciphertext_hash", fout.getHash());
}};
} catch (Exception ex) {
Log.d(TAG, "ENC: " + ex.getMessage());
return null;
}
}
public HashMap<String, byte[]> decryptFile(String src, String dest, byte[] key, byte[] iv, int algorithm, String hashSpec) {
String spec = getCipherSpecFromInteger(algorithm);
if (spec.isEmpty()) {
return null;
} }
// Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3 @Override
byte[] buffer = new byte[8096]; public void onReceive(Context context, Intent intent) {
SecretKeySpec sk = new SecretKeySpec(key, spec); if (intent.getAction() == null) return;
try {
Cipher cipher = Cipher.getInstance(spec); if (intent.getAction().equalsIgnoreCase(methodChannelKey)) {
cipher.init(Cipher.DECRYPT_MODE, sk, new IvParameterSpec(iv)); String data = intent.getStringExtra("data");
FileInputStream fin = new FileInputStream(src);
HashedFileOutputStream fout = new HashedFileOutputStream(dest, hashSpec); if (channel != null) {
CipherOutputStream cout = new CipherOutputStream(fout, cipher); channel.invokeMethod(dataReceivedMethodName, data);
MessageDigest md = MessageDigest.getInstance(hashSpec); }
Log.d(TAG, "Reading from " + src + ", writing to " + dest);
int len = 0;
while (true) {
len = fin.read(buffer);
if (len != 0 && len > 0) {
cout.write(buffer, 0, len);
md.update(buffer, 0, len);
} else {
break;
} }
}
cout.flush();
cout.close();
fin.close();
return new HashMap<String, byte[]>() {{
put("plaintext_hash", md.digest());
put("ciphertext_hash", fout.getHash());
}};
} catch (Exception ex) {
Log.d(TAG, "DEC: " + ex.getMessage());
return null;
} }
}
public byte[] hashFile(String src, String algorithm) { @Override
byte[] buffer = new byte[8096]; public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
try { channel.setMethodCallHandler(null);
MessageDigest md = MessageDigest.getInstance(algorithm); LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
FileInputStream fin = new FileInputStream(src); localBroadcastManager.unregisterReceiver(this);
int len = 0;
while (true) {
len = fin.read(buffer);
if (len != 0 && len > 0) {
md.update(buffer, 0, len);
} else {
break;
}
}
return md.digest(); Log.d(TAG, "Detached from engine");
} catch (Exception ex) {
Log.d(TAG, "Hash: " + ex.getMessage());
return null;
} }
}
@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;
}
@NonNull
@Override
public String getPersistentDataPath() {
return context.getFilesDir().getPath();
}
@NonNull
@Override
public String getCacheDataPath() {
return context.getCacheDir().getPath();
}
@Override
public void openBatteryOptimisationSettings() {
final Uri packageUri = Uri.parse("package:" + context.getPackageName());
Log.d(TAG, packageUri.toString());
final Intent intent = new Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, packageUri);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
@NonNull
@Override
public Boolean isIgnoringBatteryOptimizations() {
final PowerManager pm = context.getSystemService(PowerManager.class);
return pm.isIgnoringBatteryOptimizations(context.getPackageName());
}
@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
);
}
@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 hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Api.Result<byte[]> result) {
CryptoKt.hashFile(sourcePath, hashSpec, result);
}
@NonNull
@Override
public Boolean generateVideoThumbnail(@NonNull String src, @NonNull String dest, @NonNull Long maxWidth) {
return generateVideoThumbnailImplementation(src, dest, maxWidth);
}
@Override
public void pickFiles(@NonNull FilePickerType type, @NonNull Boolean pickMultiple, @NonNull Api.Result<List<String>> result) {
filePickerRequest(context, activity, type, pickMultiple, result);
}
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
activity = binding.getActivity();
binding.addActivityResultListener(this);
Log.d(TAG, "Activity attached");
}
@Override
public void onDetachedFromActivity() {}
@Override
public void onDetachedFromActivityForConfigChanges() {}
@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {}
} }

View File

@ -0,0 +1,226 @@
package me.polynom.moxplatform_android
import android.app.Activity
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.UUID
object RequestTracker {
val requests: MutableMap<Int, Api.Result<Any>> = mutableMapOf()
}
const val PICK_FILE_REQUEST = 41;
const val PICK_FILES_REQUEST = 42;
fun genericFilePickerRequest(activity: Activity?, pickMultiple: Boolean, result: Api.Result<List<String>>) {
val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, pickMultiple);
}
RequestTracker.requests[PICK_FILE_REQUEST] = result as Api.Result<Any>;
activity?.startActivityForResult(pickIntent, PICK_FILE_REQUEST)
}
fun filePickerRequest(
context: Context,
activity: Activity?,
type: Api.FilePickerType,
pickMultiple: Boolean,
result: Api.Result<List<String>>
) {
if (type == Api.FilePickerType.GENERIC) {
return genericFilePickerRequest(activity, pickMultiple, result)
}
val pickerType = when (type) {
Api.FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
Api.FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly
Api.FilePickerType.IMAGE_AND_VIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
// TODO
Api.FilePickerType.GENERIC -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
}
val pick = when (pickMultiple) {
false -> ActivityResultContracts.PickVisualMedia()
true -> ActivityResultContracts.PickMultipleVisualMedia()
}
val requestCode = if (pickMultiple) PICK_FILES_REQUEST else PICK_FILE_REQUEST
val pickIntent = pick.createIntent(context, PickVisualMediaRequest(pickerType))
RequestTracker.requests[requestCode] = result as Api.Result<Any>
Log.d(TAG, "Tracked size ${RequestTracker.requests.size}")
if (activity == null) {
Log.w(TAG, "Activity is null")
}
activity?.startActivityForResult(pickIntent, requestCode);
}
/**
* Copies the file from the given content URI to a temporary directory, retaining the original
* file name if possible.
*
*
* Each file is placed in its own directory to avoid conflicts according to the following
* scheme: {cacheDir}/{randomUuid}/{fileName}
*
*
* File extension is changed to match MIME type of the file, if known. Otherwise, the extension
* is left unchanged.
*
*
* If the original file name is unknown, a predefined "image_picker" filename is used and the
* file extension is deduced from the mime type (with fallback to ".jpg" in case of failure).
*/
fun getPathFromUri(context: Context, uri: Uri): String? {
try {
context.contentResolver.openInputStream(uri).use { inputStream ->
val uuid = UUID.randomUUID().toString()
val targetDirectory = File(context.cacheDir, uuid)
targetDirectory.mkdir()
// TODO(SynSzakala) according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably
// just clear the picked files after the app startup.
targetDirectory.deleteOnExit()
var fileName = getImageName(context, uri)
var extension = getImageExtension(context, uri)
if (fileName == null) {
Log.w("FileUtils", "Cannot get file name for $uri")
if (extension == null) extension = ".jpg"
fileName = "image_picker$extension"
} else if (extension != null) {
fileName = getBaseName(fileName) + extension
}
val file = File(targetDirectory, fileName)
FileOutputStream(file).use { outputStream ->
copy(inputStream!!, outputStream)
return file.path
}
}
} catch (e: IOException) {
// If closing the output stream fails, we cannot be sure that the
// target file was written in full. Flushing the stream merely moves
// the bytes into the OS, not necessarily to the file.
return null
} catch (e: SecurityException) {
// Calling `ContentResolver#openInputStream()` has been reported to throw a
// `SecurityException` on some devices in certain circumstances. Instead of crashing, we
// return `null`.
//
// See https://github.com/flutter/flutter/issues/100025 for more details.
return null
}
}
/** @return extension of image with dot, or null if it's empty.
*/
private fun getImageExtension(context: Context, uriImage: Uri): String? {
val extension: String?
extension = try {
if (uriImage.scheme == ContentResolver.SCHEME_CONTENT) {
val mime = MimeTypeMap.getSingleton()
mime.getExtensionFromMimeType(context.contentResolver.getType(uriImage))
} else {
MimeTypeMap.getFileExtensionFromUrl(
Uri.fromFile(File(uriImage.path)).toString()
)
}
} catch (e: Exception) {
return null
}
return if (extension == null || extension.isEmpty()) {
null
} else ".$extension"
}
/** @return name of the image provided by ContentResolver; this may be null.
*/
private fun getImageName(context: Context, uriImage: Uri): String? {
queryImageName(context, uriImage).use { cursor ->
return if (cursor == null || !cursor.moveToFirst() || (cursor.columnCount < 1)) null else cursor.getString(
0
)
}
}
private fun queryImageName(context: Context, uriImage: Uri): Cursor? {
return context
.contentResolver
.query(uriImage, arrayOf(MediaStore.MediaColumns.DISPLAY_NAME), null, null, null)
}
@Throws(IOException::class)
private fun copy(`in`: InputStream, out: OutputStream) {
val buffer = ByteArray(4 * 1024)
var bytesRead: Int
while (`in`.read(buffer).also { bytesRead = it } != -1) {
out.write(buffer, 0, bytesRead)
}
out.flush()
}
private fun getBaseName(fileName: String): String {
val lastDotIndex = fileName.lastIndexOf('.')
return if (lastDotIndex < 0) {
fileName
} else fileName.substring(0, lastDotIndex)
// Basename is everything before the last '.'.
}
fun onActivityResultImpl(context: Context, requestCode: Int, resultCode: Int, data: Intent?): Boolean {
Log.d(TAG, "Got result for $requestCode with result $resultCode (${data?.action})")
if (requestCode == PICK_FILE_REQUEST || requestCode == PICK_FILES_REQUEST) {
Log.d(TAG, "Extra data ${data?.data}")
val result = RequestTracker.requests.remove(requestCode);
if (result == null) {
Log.w(TAG, "Untracked response.")
return false;
}
if (resultCode != Activity.RESULT_OK) {
// No files picked
result!!.success(listOf<String>())
return true;
}
val pickedMultiple = requestCode == PICK_FILES_REQUEST
val pickedFiles = mutableListOf<String>()
if (pickedMultiple) {
val intentUris = data!!.clipData
if (data!!.clipData != null) {
for (i in 0 until data!!.clipData!!.itemCount) {
val path = getPathFromUri(context, data!!.clipData!!.getItemAt(i).uri)
if (path != null) {
pickedFiles.add(path )
}
}
}
} else {
val path = getPathFromUri(context, data!!.data!!)
if (path != null) {
pickedFiles.add(path )
}
}
result!!.success(pickedFiles)
return true;
}
return false;
}

View File

@ -0,0 +1,65 @@
package me.polynom.moxplatform_android
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
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 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
// Compatibility with share_handler
putExtra("conversationIdentifier", jid)
}
val shortcutTarget = "$pkgName.dynamic_share_target"
val shortcutBuilder = ShortcutInfoCompat.Builder(context, jid).apply {
setShortLabel(name)
setIsConversation()
setCategories(setOf(shortcutTarget))
setIntent(intent)
setLongLived(true)
}
val personBuilder = Person.Builder().apply {
setKey(jid)
setName(name)
}
// Either set an avatar image OR a fallback icon
if (avatarPath != null) {
val icon = IconCompat.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(avatarPath),
)
shortcutBuilder.setIcon(icon)
personBuilder.setIcon(icon)
} else {
val resourceId = when(fallbackIcon) {
FallbackIconType.PERSON -> R.mipmap.person
FallbackIconType.NOTES -> R.mipmap.notes
// "Fallthrough"
else -> R.mipmap.person
}
val icon = IconCompat.createWithResource(context, resourceId)
shortcutBuilder.setIcon(icon)
personBuilder.setIcon(icon)
}
shortcutBuilder.setPerson(personBuilder.build())
ShortcutManagerCompat.addDynamicShortcuts(
context,
listOf(shortcutBuilder.build()),
)
}

View File

@ -0,0 +1,43 @@
package me.polynom.moxplatform_android
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.util.Log
import java.io.FileOutputStream
/*
* Generate a video thumbnail using the first frame of the video at @src. Afterwards, scale it
* down such that its width is equal to @maxWidth (while keeping the aspect ratio) and write it to
* @dest.
*
* If everything went well, returns true. If we're unable to generate the thumbnail, returns false.
* */
fun generateVideoThumbnailImplementation(src: String, dest: String, maxWidth: Long): Boolean {
try {
val mmr = MediaMetadataRetriever().apply {
setDataSource(src)
}
val unscaledThumbnail = mmr.getFrameAtTime(0) ?: return false
// Scale down the thumbnail while keeping the aspect ratio
val scalingFactor = maxWidth.toDouble() / unscaledThumbnail.width;
Log.d(TAG, "Scaling to $maxWidth from ${unscaledThumbnail.width} with scalingFactor $scalingFactor");
val thumbnail = Bitmap.createScaledBitmap(
unscaledThumbnail,
(unscaledThumbnail.width * scalingFactor).toInt(),
(unscaledThumbnail.height * scalingFactor).toInt(),
false,
)
// Write it to the destination file
val fos = FileOutputStream(dest)
thumbnail.compress(Bitmap.CompressFormat.JPEG, 75, fos)
fos.flush()
fos.close()
return true;
} catch (ex: Exception) {
Log.e(TAG, "Failed to create thumbnail for $src: ${ex.message}")
ex.printStackTrace()
return false;
}
}

View File

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

View File

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

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group android:scaleX="0.42988887"
android:scaleY="0.42988887"
android:translateX="6.8413334"
android:translateY="6.8413334">
<path
android:pathData="M3,18h12v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h18v-2L3,11v2z"
android:fillColor="#ffffff"/>
</group>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group android:scaleX="0.483625"
android:scaleY="0.483625"
android:translateX="6.1965"
android:translateY="6.1965">
<path
android:pathData="m12,12c2.21,0 4,-1.79 4,-4C16,5.79 14.21,4 12,4 9.79,4 8,5.79 8,8c0,2.21 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"
android:fillColor="#fffff9"/>
</group>
</vector>

View File

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

View File

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

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/notes_background"/>
<foreground android:drawable="@drawable/notes_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/person_background"/>
<foreground android:drawable="@drawable/person_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="notes_background">#CF4AFF</color>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="person_background">#CF4AFF</color>
</resources>

View File

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

View File

@ -0,0 +1,28 @@
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidContactsImplementation extends ContactsImplementation {
final MoxplatformApi _api = MoxplatformApi();
@override
Future<void> recordSentMessage(
String name,
String jid, {
String? avatarPath,
FallbackIconType fallbackIcon = FallbackIconType.none,
}) async {
// Ensure we always have an icon
if (avatarPath != null) {
assert(
fallbackIcon != FallbackIconType.none,
'If no avatar is specified, then a fallbackIcon must be set',
);
}
return _api.recordSentMessage(
name,
jid,
avatarPath,
fallbackIcon,
);
}
}

View File

@ -1,59 +1,49 @@
import 'dart:typed_data';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidCryptographyImplementation extends CryptographyImplementation { class AndroidCryptographyImplementation extends CryptographyImplementation {
final _methodChannel = const MethodChannel('me.polynom.moxplatform_android'); final MoxplatformApi _api = MoxplatformApi();
@override @override
Future<CryptographyResult?> encryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec) async { Future<CryptographyResult?> encryptFile(
final dynamic resultRaw = await _methodChannel.invokeMethod<dynamic>('encryptFile', [ String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
) async {
return _api.encryptFile(
sourcePath, sourcePath,
destPath, destPath,
key, key,
iv, iv,
algorithm.toInt(), algorithm,
hashSpec, hashSpec,
]);
if (resultRaw == null) return null;
// ignore: argument_type_not_assignable
final result = Map<String, dynamic>.from(resultRaw);
return CryptographyResult(
result['plaintext_hash']! as Uint8List,
result['ciphertext_hash']! as Uint8List,
); );
} }
@override @override
Future<CryptographyResult?> decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec) async { Future<CryptographyResult?> decryptFile(
final dynamic resultRaw = await _methodChannel.invokeMethod<dynamic>('decryptFile', [ String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
) async {
return _api.decryptFile(
sourcePath, sourcePath,
destPath, destPath,
key, key,
iv, iv,
algorithm.toInt(), algorithm,
hashSpec, hashSpec,
]);
if (resultRaw == null) return null;
// ignore: argument_type_not_assignable
final result = Map<String, dynamic>.from(resultRaw);
return CryptographyResult(
result['plaintext_hash']! as Uint8List,
result['ciphertext_hash']! as Uint8List,
); );
} }
@override @override
Future<Uint8List?> hashFile(String path, String hashSpec) async { Future<Uint8List?> hashFile(String sourcePath, String hashSpec) async {
final dynamic resultsRaw = await _methodChannel.invokeMethod<dynamic>('hashFile', [ return _api.hashFile(sourcePath, hashSpec);
path,
hashSpec,
]);
if (resultsRaw == null) return null;
return resultsRaw as Uint8List;
} }
} }

View File

@ -4,16 +4,16 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxlib/awaitabledatasender.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_android/src/service_android.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. /// An [AwaitableDataSender] that uses flutter_background_service.
class BackgroundServiceDataSender extends AwaitableDataSender<BackgroundCommand, BackgroundEvent> { class BackgroundServiceDataSender
extends AwaitableDataSender<BackgroundCommand, BackgroundEvent> {
BackgroundServiceDataSender() BackgroundServiceDataSender()
: _channel = const MethodChannel('me.polynom.moxplatform_android'), super(); : _channel = const MethodChannel('me.polynom.moxplatform_android'),
super();
final MethodChannel _channel; final MethodChannel _channel;
@override @override
@ -39,34 +39,40 @@ Future<void> androidEntrypoint() async {
); );
final data = jsonDecode(result!) as Map<String, dynamic>; final data = jsonDecode(result!) as Map<String, dynamic>;
final entrypointHandle = data['genericEntrypoint']! as int; final entrypointHandle = data['genericEntrypoint']! as int;
final entrypointCallbackHandle = CallbackHandle.fromRawHandle(entrypointHandle); final entrypointCallbackHandle =
final entrypoint = PluginUtilities.getCallbackFromHandle(entrypointCallbackHandle); CallbackHandle.fromRawHandle(entrypointHandle);
final entrypoint =
PluginUtilities.getCallbackFromHandle(entrypointCallbackHandle);
final handleUIEventHandle = data['eventHandle']! as int; final handleUIEventHandle = data['eventHandle']! as int;
final handleUIEventCallbackHandle = CallbackHandle.fromRawHandle(handleUIEventHandle); final handleUIEventCallbackHandle =
final handleUIEvent = PluginUtilities.getCallbackFromHandle(handleUIEventCallbackHandle); CallbackHandle.fromRawHandle(handleUIEventHandle);
final handleUIEvent =
PluginUtilities.getCallbackFromHandle(handleUIEventCallbackHandle);
final srv = AndroidBackgroundService(); final srv = AndroidBackgroundService();
GetIt.I.registerSingleton<BackgroundService>(srv); GetIt.I.registerSingleton<BackgroundService>(srv);
srv.init( srv.init(
entrypoint! as Future<void> Function(), entrypoint! as Future<void> Function(String),
handleUIEvent! as Future<void> Function(Map<String, dynamic>? data), handleUIEvent! as Future<void> Function(Map<String, dynamic>? data),
data['initialLocale']! as String,
); );
} }
/// The Android specific implementation of the [IsolateHandler]. /// The Android specific implementation of the [IsolateHandler].
class AndroidIsolateHandler extends IsolateHandler { class AndroidIsolateHandler extends IsolateHandler {
AndroidIsolateHandler() AndroidIsolateHandler()
: _channel = const MethodChannel('me.polynom.moxplatform_android'), : _channel = const MethodChannel('me.polynom.moxplatform_android'),
_dataSender = BackgroundServiceDataSender(), _dataSender = BackgroundServiceDataSender(),
_log = Logger('AndroidIsolateHandler'), _log = Logger('AndroidIsolateHandler'),
super(); super();
final BackgroundServiceDataSender _dataSender; final BackgroundServiceDataSender _dataSender;
final MethodChannel _channel; final MethodChannel _channel;
final Logger _log; final Logger _log;
@override @override
Future<void> attach(Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent) async { Future<void> attach(
Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent,
) async {
_channel.setMethodCallHandler((MethodCall call) async { _channel.setMethodCallHandler((MethodCall call) async {
final args = call.arguments as String; final args = call.arguments as String;
await handleIsolateEvent(jsonDecode(args) as Map<String, dynamic>); await handleIsolateEvent(jsonDecode(args) as Map<String, dynamic>);
@ -75,20 +81,25 @@ class AndroidIsolateHandler extends IsolateHandler {
@override @override
Future<void> start( Future<void> start(
Future<void> Function() entrypoint, Future<void> Function(String initialLocale) entrypoint,
Future<void> Function(Map<String, dynamic>? data) handleUIEvent, Future<void> Function(Map<String, dynamic>? data) handleUIEvent,
Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent, Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent,
String initialLocale,
) async { ) async {
_log.finest('Called start'); _log.finest('Called start');
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
final androidEntrypointHandle = PluginUtilities.getCallbackHandle(androidEntrypoint)!.toRawHandle(); final androidEntrypointHandle =
PluginUtilities.getCallbackHandle(androidEntrypoint)!.toRawHandle();
_log.finest('androidEntrypointHandle: $androidEntrypointHandle'); _log.finest('androidEntrypointHandle: $androidEntrypointHandle');
await _channel.invokeMethod<void>('configure', <dynamic>[ await _channel.invokeMethod<void>('configure', <dynamic>[
androidEntrypointHandle, androidEntrypointHandle,
jsonEncode({ jsonEncode({
'genericEntrypoint': PluginUtilities.getCallbackHandle(entrypoint)!.toRawHandle(), 'genericEntrypoint':
'eventHandle': PluginUtilities.getCallbackHandle(handleUIEvent)!.toRawHandle() PluginUtilities.getCallbackHandle(entrypoint)!.toRawHandle(),
'eventHandle':
PluginUtilities.getCallbackHandle(handleUIEvent)!.toRawHandle(),
'initialLocale': initialLocale,
}), }),
]); ]);

View File

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

View File

@ -0,0 +1,39 @@
import 'package:moxplatform/moxplatform.dart';
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();
}
@override
Future<bool> isIgnoringBatteryOptimizations() {
return MoxplatformInterface.api.isIgnoringBatteryOptimizations();
}
@override
Future<void> openBatteryOptimisationSettings() {
return MoxplatformInterface.api.openBatteryOptimisationSettings();
}
@override
Future<bool> generateVideoThumbnail(
String src,
String dest,
int width,
) async {
return MoxplatformInterface.api.generateVideoThumbnail(src, dest, width);
}
@override
Future<List<String>> pickFiles(FilePickerType type, bool pickMultiple) async {
final result = await MoxplatformInterface.api.pickFiles(type, pickMultiple);
return result.cast<String>();
}
}

View File

@ -1,15 +1,17 @@
import 'package:moxplatform_android/src/contacts_android.dart';
import 'package:moxplatform_android/src/crypto_android.dart'; import 'package:moxplatform_android/src/crypto_android.dart';
import 'package:moxplatform_android/src/isolate_android.dart'; import 'package:moxplatform_android/src/isolate_android.dart';
import 'package:moxplatform_android/src/media_android.dart'; import 'package:moxplatform_android/src/platform_android.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class MoxplatformAndroidPlugin extends MoxplatformInterface { class MoxplatformAndroidPlugin extends MoxplatformInterface {
static void registerWith() { static void registerWith() {
// ignore: avoid_print // ignore: avoid_print
print('MoxplatformAndroidPlugin: Registering implementation'); print('MoxplatformAndroidPlugin: Registering implementation');
MoxplatformInterface.handler = AndroidIsolateHandler(); MoxplatformInterface.contacts = AndroidContactsImplementation();
MoxplatformInterface.media = AndroidMediaScannerImplementation();
MoxplatformInterface.crypto = AndroidCryptographyImplementation(); MoxplatformInterface.crypto = AndroidCryptographyImplementation();
MoxplatformInterface.handler = AndroidIsolateHandler();
MoxplatformInterface.platform = AndroidPlatformImplementation();
} }
@override @override

View File

@ -4,19 +4,18 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/awaitabledatasender.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class AndroidBackgroundService extends BackgroundService { class AndroidBackgroundService extends BackgroundService {
AndroidBackgroundService() AndroidBackgroundService()
: _log = Logger('AndroidBackgroundService'), : _log = Logger('AndroidBackgroundService'),
super(); super();
@internal @internal
static const MethodChannel channel = MethodChannel('me.polynom.moxplatform_android_bg'); static const MethodChannel channel =
MethodChannel('me.polynom.moxplatform_android_bg');
final Logger _log; final Logger _log;
@override @override
@ -28,20 +27,21 @@ class AndroidBackgroundService extends BackgroundService {
} }
@override @override
void sendEvent(BackgroundEvent event, { String? id }) { void sendEvent(BackgroundEvent event, {String? id}) {
final data = DataWrapper( final data = DataWrapper(
id ?? const Uuid().v4(), id ?? const Uuid().v4(),
event, event,
); );
// NOTE: *S*erver to *F*oreground // NOTE: *S*erver to *F*oreground
_log.fine('S2F: ${data.toJson().toString()}'); _log.fine('S2F: ${data.toJson()}');
channel.invokeMethod<void>('sendData', jsonEncode(data.toJson())); channel.invokeMethod<void>('sendData', jsonEncode(data.toJson()));
} }
@override @override
void init( void init(
Future<void> Function() entrypoint, Future<void> Function(String initialLocale) entrypoint,
Future<void> Function(Map<String, dynamic>? data) handleEvent, Future<void> Function(Map<String, dynamic>? data) handleEvent,
String initialLocale,
) { ) {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -59,6 +59,6 @@ class AndroidBackgroundService extends BackgroundService {
_log.finest('Running...'); _log.finest('Running...');
entrypoint(); entrypoint(initialLocale);
} }
} }

View File

@ -1,11 +1,11 @@
name: moxplatform_android name: moxplatform_android
description: Android implementation of moxplatform description: Android implementation of moxplatform
version: 0.1.15 version: 0.1.22
homepage: https://codeberg.org/moxxy/moxplatform homepage: https://codeberg.org/moxxy/moxplatform
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
environment: environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.17.0 <4.0.0"
flutter: ">=2.10.0" flutter: ">=2.10.0"
flutter: flutter:
@ -22,18 +22,17 @@ dependencies:
sdk: flutter sdk: flutter
get_it: ^7.2.0 get_it: ^7.2.0
logging: ^1.0.2 logging: ^1.0.2
media_scanner: ^2.0.0
meta: ^1.7.0 meta: ^1.7.0
moxlib: moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.4 version: ^0.2.0
moxplatform: moxplatform:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.15 version: ^0.1.17+6
moxplatform_platform_interface: moxplatform_platform_interface:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.15 version: ^0.1.22
plugin_platform_interface: ^2.1.2 plugin_platform_interface: ^2.1.2
uuid: ^3.0.5 uuid: ^3.0.5
@ -41,4 +40,5 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
very_good_analysis: ^2.4.0 pigeon: 10.1.4
very_good_analysis: ^3.0.1

View File

@ -1,3 +1,49 @@
## 0.1.22
- **FIX**(repo): Remove notification examples.
- **FEAT**(interface,android): Allow passing an initial locale to the service.
- **FEAT**(base,interface,android): Move more logic to Moxxy.
- **FEAT**(android,interface): Implement video thumbnail generation.
## 0.1.21
- **FIX**(repo): Remove notification examples.
- **FEAT**(base,interface,android): Move more logic to Moxxy.
- **FEAT**(android,interface): Implement video thumbnail generation.
## 0.1.20
- **FEAT**(android,interface): Handle battery optimisation.
## 0.1.19
- **FEAT**(android,interface): Handle battery optimisation.
## 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.
## 0.1.17
- **FIX**: Fix typecasting issue.
- **FEAT**: Add an API for creating direct share shortcuts.
- **FEAT**: Migrate to moxlib 0.2.0.
- **FEAT**: I forgot to bump dependency versions.
- **FEAT**: Also hash the file on encryption and decryption.
## 0.1.11+2 ## 0.1.11+2
- **REFACTOR**: Make version constraints looser. - **REFACTOR**: Make version constraints looser.

View File

@ -1,10 +1,13 @@
library moxplatform_platform_interface; library moxplatform_platform_interface;
export 'src/api.g.dart';
export 'src/contacts.dart';
export 'src/contacts_stub.dart';
export 'src/crypto.dart'; export 'src/crypto.dart';
export 'src/crypto_stub.dart'; export 'src/crypto_stub.dart';
export 'src/interface.dart'; export 'src/interface.dart';
export 'src/isolate.dart'; export 'src/isolate.dart';
export 'src/isolate_stub.dart'; export 'src/isolate_stub.dart';
export 'src/media.dart'; export 'src/platform.dart';
export 'src/media_stub.dart'; export 'src/platform_stub.dart';
export 'src/service.dart'; export 'src/service.dart';

View File

@ -0,0 +1,338 @@
// 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 CipherAlgorithm {
aes128GcmNoPadding,
aes256GcmNoPadding,
aes256CbcPkcs7,
}
enum FallbackIconType {
none,
person,
notes,
}
enum FilePickerType {
image,
video,
imageAndVideo,
generic,
}
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 {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return CryptographyResult.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();
/// 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?)!;
}
}
Future<void> openBatteryOptimisationSettings() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.openBatteryOptimisationSettings', 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 {
return;
}
}
Future<bool> isIgnoringBatteryOptimizations() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.isIgnoringBatteryOptimizations', 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 bool?)!;
}
}
/// 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?);
}
}
/// Media APIs
Future<bool> generateVideoThumbnail(String arg_src, String arg_dest, int arg_maxWidth) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.generateVideoThumbnail', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_src, arg_dest, arg_maxWidth]) 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 bool?)!;
}
}
/// Picker
Future<List<String?>> pickFiles(FilePickerType arg_type, bool arg_pickMultiple) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.pickFiles', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_type.index, arg_pickMultiple]) 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 List<Object?>?)!.cast<String?>();
}
}
}

View File

@ -0,0 +1,12 @@
import 'package:moxplatform_platform_interface/src/api.g.dart';
// Wrapper around various contact APIs.
// ignore: one_member_abstracts
abstract class ContactsImplementation {
Future<void> recordSentMessage(
String name,
String jid, {
String? avatarPath,
FallbackIconType fallbackIcon = FallbackIconType.none,
});
}

View File

@ -0,0 +1,12 @@
import 'package:moxplatform_platform_interface/src/api.g.dart';
import 'package:moxplatform_platform_interface/src/contacts.dart';
class StubContactsImplementation extends ContactsImplementation {
@override
Future<void> recordSentMessage(
String name,
String jid, {
String? avatarPath,
FallbackIconType fallbackIcon = FallbackIconType.none,
}) async {}
}

View File

@ -1,27 +1,5 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:moxplatform_platform_interface/src/api.g.dart';
enum CipherAlgorithm {
aes128GcmNoPadding,
aes256GcmNoPadding,
aes256CbcPkcs7,
}
extension CipherAlgorithmToIntExtension on CipherAlgorithm {
int toInt() {
switch (this) {
case CipherAlgorithm.aes128GcmNoPadding: return 0;
case CipherAlgorithm.aes256GcmNoPadding: return 1;
case CipherAlgorithm.aes256CbcPkcs7: return 2;
}
}
}
class CryptographyResult {
const CryptographyResult(this.plaintextHash, this.ciphertextHash);
final Uint8List plaintextHash;
final Uint8List ciphertextHash;
}
/// Wrapper around platform-native cryptography APIs /// Wrapper around platform-native cryptography APIs
abstract class CryptographyImplementation { abstract class CryptographyImplementation {
@ -30,18 +8,32 @@ abstract class CryptographyImplementation {
/// Note that this function runs off-thread as to not block the UI thread. /// Note that this function runs off-thread as to not block the UI thread.
/// ///
/// Resolves to true if the encryption was successful. Resolves to fale on failure. /// Resolves to true if the encryption was successful. Resolves to fale on failure.
Future<CryptographyResult?> encryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec); Future<CryptographyResult?> encryptFile(
String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
);
/// Decrypt the file at [sourcePath] using [algorithm] and write the result back to /// Decrypt the file at [sourcePath] using [algorithm] and write the result back to
/// [destPath]. [hashSpec] is the name of the Hash function to use, i.e. "SHA-256". /// [destPath]. [hashSpec] is the name of the Hash function to use, i.e. "SHA-256".
/// Note that this function runs off-thread as to not block the UI thread. /// Note that this function runs off-thread as to not block the UI thread.
/// ///
/// Resolves to true if the encryption was successful. Resolves to fale on failure. /// Resolves to true if the encryption was successful. Resolves to fale on failure.
Future<CryptographyResult?> decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec); Future<CryptographyResult?> decryptFile(
String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
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. /// Note that this function runs off-thread as to not block the UI thread.
/// ///
/// Returns the hash of the file. /// Returns the hash of the file.
Future<Uint8List?> hashFile(String path, String hashSpec); Future<Uint8List?> hashFile(String sourcePath, String hashSpec);
} }

View File

@ -1,19 +1,34 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:moxplatform_platform_interface/src/api.g.dart';
import 'package:moxplatform_platform_interface/src/crypto.dart'; import 'package:moxplatform_platform_interface/src/crypto.dart';
class StubCryptographyImplementation extends CryptographyImplementation { class StubCryptographyImplementation extends CryptographyImplementation {
@override @override
Future<CryptographyResult?> encryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec) async { Future<CryptographyResult?> encryptFile(
String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
) async {
return null; return null;
} }
@override @override
Future<CryptographyResult?> decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec) async { Future<CryptographyResult?> decryptFile(
String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
) async {
return null; return null;
} }
@override @override
Future<Uint8List?> hashFile(String path, String hashSpec) async { Future<Uint8List?> hashFile(String sourcePath, String hashSpec) async {
return null; return null;
} }
} }

View File

@ -1,9 +1,12 @@
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.dart';
import 'package:moxplatform_platform_interface/src/crypto_stub.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.dart';
import 'package:moxplatform_platform_interface/src/isolate_stub.dart'; import 'package:moxplatform_platform_interface/src/isolate_stub.dart';
import 'package:moxplatform_platform_interface/src/media.dart'; import 'package:moxplatform_platform_interface/src/platform.dart';
import 'package:moxplatform_platform_interface/src/media_stub.dart'; import 'package:moxplatform_platform_interface/src/platform_stub.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart';
abstract class MoxplatformInterface extends PlatformInterface { abstract class MoxplatformInterface extends PlatformInterface {
@ -11,9 +14,12 @@ abstract class MoxplatformInterface extends PlatformInterface {
static final Object _token = Object(); static final Object _token = Object();
static MoxplatformApi api = MoxplatformApi();
static IsolateHandler handler = StubIsolateHandler(); static IsolateHandler handler = StubIsolateHandler();
static MediaScannerImplementation media = StubMediaScannerImplementation();
static CryptographyImplementation crypto = StubCryptographyImplementation(); static CryptographyImplementation crypto = StubCryptographyImplementation();
static ContactsImplementation contacts = StubContactsImplementation();
static PlatformImplementation platform = StubPlatformImplementation();
/// Return the current platform name. /// Return the current platform name.
Future<String?> getPlatformName(); Future<String?> getPlatformName();

View File

@ -1,4 +1,4 @@
import 'package:moxlib/awaitabledatasender.dart'; import 'package:moxlib/moxlib.dart';
/// A class abstracting the interaction between the UI isolate and the background /// A class abstracting the interaction between the UI isolate and the background
/// service, which is either a regular isolate or an Android foreground service. /// service, which is either a regular isolate or an Android foreground service.
@ -8,10 +8,12 @@ abstract class IsolateHandler {
/// [entrypoint] is the entrypoint that is run inside the new isolate. /// [entrypoint] is the entrypoint that is run inside the new isolate.
/// [handleUIEvent] is a handler function that is called when the isolate receives data from the UI. /// [handleUIEvent] is a handler function that is called when the isolate receives data from the UI.
/// [handleIsolateEvent] is a handler function that is called when the UI receives data from the service. /// [handleIsolateEvent] is a handler function that is called when the UI receives data from the service.
/// [initialLocale] the locale to pass to the background service when first starting.
Future<void> start( Future<void> start(
Future<void> Function() entrypoint, Future<void> Function(String initialLocale) entrypoint,
Future<void> Function(Map<String, dynamic>? data) handleUIEvent, Future<void> Function(Map<String, dynamic>? data) handleUIEvent,
Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent, Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent,
String initialLocale,
); );
/// Make sure that the UI event handler is registered without starting the isolate. /// Make sure that the UI event handler is registered without starting the isolate.

View File

@ -1,4 +1,4 @@
import 'package:moxlib/awaitabledatasender.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxplatform_platform_interface/src/isolate.dart'; import 'package:moxplatform_platform_interface/src/isolate.dart';
class StubDataSender extends AwaitableDataSender { class StubDataSender extends AwaitableDataSender {
@ -22,9 +22,10 @@ class StubIsolateHandler extends IsolateHandler {
@override @override
Future<void> start( Future<void> start(
Future<void> Function() entrypoint, Future<void> Function(String initialLocale) entrypoint,
Future<void> Function(Map<String, dynamic>? data) handleUIEvent, Future<void> Function(Map<String, dynamic>? data) handleUIEvent,
Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent, Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent,
String initialLocale,
) async { ) async {
// ignore: avoid_print // ignore: avoid_print
print('STUB STARTED!!!!!!'); print('STUB STARTED!!!!!!');

View File

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

View File

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

View File

@ -0,0 +1,24 @@
import 'package:moxplatform_platform_interface/src/api.g.dart';
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();
/// Returns whether the app is battery-optimised (false) or
/// excluded from battery savings (true).
Future<bool> isIgnoringBatteryOptimizations();
/// Opens the page for battery optimisations. If not supported on the
/// platform, does nothing.
Future<void> openBatteryOptimisationSettings();
/// Attempt to generate a thumbnail for the video file at [src], scale it, while keeping the
/// aspect ratio in tact to [width], and write it to [dest]. If we were successful, returns true.
/// If no thumbnail was generated, returns false.
Future<bool> generateVideoThumbnail(String src, String dest, int width);
Future<List<String>> pickFiles(FilePickerType type, bool pickMultiple);
}

View File

@ -0,0 +1,29 @@
import 'package:moxplatform_platform_interface/src/api.g.dart';
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 => '';
@override
Future<bool> isIgnoringBatteryOptimizations() async => false;
@override
Future<void> openBatteryOptimisationSettings() async {}
@override
Future<bool> generateVideoThumbnail(
String src,
String dest,
int width,
) async =>
false;
@override
Future<List<String>> pickFiles(FilePickerType type, bool pickMultiple) async => [];
}

View File

@ -5,13 +5,14 @@ abstract class BackgroundService {
void setNotification(String title, String body); void setNotification(String title, String body);
/// Send data from the background service to the UI. /// Send data from the background service to the UI.
void sendEvent(BackgroundEvent event, { String? id }); void sendEvent(BackgroundEvent event, {String? id});
/// Called before [entrypoint]. Sets up whatever it needs to set up. /// Called before [entrypoint]. Sets up whatever it needs to set up.
/// [handleEvent] is a function that is called whenever the service receives /// [handleEvent] is a function that is called whenever the service receives
/// data. /// data.
void init( void init(
Future<void> Function() entrypoint, Future<void> Function(String initialLocale) entrypoint,
Future<void> Function(Map<String, dynamic>? data) handleEvent, Future<void> Function(Map<String, dynamic>? data) handleEvent,
String initialLocale,
); );
} }

View File

@ -1,11 +1,11 @@
name: moxplatform_platform_interface name: moxplatform_platform_interface
description: A common platform interface for the my_plugin plugin. description: A common platform interface for the my_plugin plugin.
version: 0.1.15 version: 0.1.22
homepage: https://codeberg.org/moxxy/moxplatform homepage: https://codeberg.org/moxxy/moxplatform
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
environment: environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
flutter: ">=2.10.0" flutter: ">=2.10.0"
dependencies: dependencies:
@ -14,14 +14,14 @@ dependencies:
moxlib: moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.4 version: ^0.2.0
moxplatform: moxplatform:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.15 version: ^0.1.17+6
plugin_platform_interface: ^2.1.2 plugin_platform_interface: ^2.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
very_good_analysis: ^2.4.0 very_good_analysis: ^3.0.1

64
pigeons/api.dart Normal file
View File

@ -0,0 +1,64 @@
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',
),
),
)
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;
}
enum FilePickerType {
image,
video,
imageAndVideo,
generic,
}
@HostApi()
abstract class MoxplatformApi {
/// Platform APIs
String getPersistentDataPath();
String getCacheDataPath();
void openBatteryOptimisationSettings();
bool isIgnoringBatteryOptimizations();
/// 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);
/// Media APIs
bool generateVideoThumbnail(String src, String dest, int maxWidth);
/// Picker
@async
List<String> pickFiles(FilePickerType type, bool pickMultiple);
}

8
pubspec.yaml Normal file
View File

@ -0,0 +1,8 @@
name: moxplatform_workspace
environment:
sdk: '>=2.18.0 <3.0.0'
dev_dependencies:
melos: ^3.1.1
pigeon: 10.1.4
very_good_analysis: ^3.0.1