Compare commits

...

59 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
7a999cf860 fix: Fix typecasting issue 2022-10-05 16:38:15 +02:00
7990ef2aec feat: I forgot to bump dependency versions 2022-10-05 15:29:38 +02:00
e891eb0ef0 release: Bump version 2022-10-05 15:06:58 +02:00
31f33a0413 feat: Also hash the file on encryption and decryption 2022-10-05 15:05:43 +02:00
35138e102e meta: Bump version 2022-10-04 23:41:13 +02:00
98c3355b44 meta: Implement access to AES encryption (but fast) 2022-10-04 23:37:32 +02:00
4a89958351 chore(release): publish packages
- moxplatform_android@0.1.11+2
 - moxplatform_platform_interface@0.1.11+2
 - moxplatform@0.1.11+2
2022-09-06 14:22:29 +02:00
bf49dfc0d7 style: Make melos happy 2022-09-06 14:22:15 +02:00
52acfeb3d1 chore(release): publish packages
- moxplatform@0.1.11+1
 - moxplatform_android@0.1.11+1
 - moxplatform_platform_interface@0.1.11+1
2022-09-06 14:19:53 +02:00
e1494cda4c refactor: Make version constraints looser 2022-09-06 14:19:16 +02:00
97 changed files with 3587 additions and 295 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).
To make all packages link to each other locally, begin by running `melos bootstrap`. After editing
the code and making your changes, please run `melos run analyze` to make sure that no linter warnings
are left inside the code.
the code and making your changes, please format the code using `melos run format` and lint using `melos run analyze`.
When done - and a version bump is appropriate - bump the version of all packages using `melos version` and
publish with `melos publish --no-dry-run --git-tag-version`.
## Acknowledgements
- [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). moxplatform_android is basically just a copy and paste of [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service).
- [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). moxplatform_android's service implementation is basically just a copy and paste of [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service).

View File

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

47
example/.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
example/.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: 85684f9300908116a78138ea4c6036c35c9a1236
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: android
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
example/README.md Normal file
View File

@@ -0,0 +1,16 @@
# example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
example/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,71 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 33
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 26
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,38 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<application
android:label="example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

View File

@@ -0,0 +1,6 @@
package com.example.example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.8.21'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip

View File

@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

239
example/lib/main.dart Normal file
View File

@@ -0,0 +1,239 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:moxplatform/moxplatform.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() {
runApp(const MyApp());
}
class Sender {
const Sender(this.name, this.jid);
final String name;
final String jid;
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Moxplatform Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
/// List of "Message senders".
final List<Sender> senders = const [
Sender('Mash Kyrielight', 'mash@example.org'),
Sender('Rio Tsukatsuki', 'rio@millenium'),
Sender('Raiden Shogun', 'raiden@tevhat'),
];
Future<void> _cryptoTest() async {
final result = await FilePicker.platform.pickFiles();
if (result == null) {
return;
}
final start = DateTime.now();
final path = result.files.single.path;
final enc = await MoxplatformPlugin.crypto.encryptFile(
path!,
'$path.enc',
Uint8List.fromList(List.filled(32, 1)),
Uint8List.fromList(List.filled(16, 2)),
CipherAlgorithm.aes256CbcPkcs7,
'SHA-256',
);
final end = DateTime.now();
final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch;
// ignore: avoid_print
print('TIME: ${diff / 1000}s');
// ignore: avoid_print
print('DONE (${enc != null})');
final lengthEnc = await File('$path.enc').length();
final lengthOrig = await File(path).length();
// ignore: avoid_print
print('Encrypted file is $lengthEnc Bytes large (Orig $lengthOrig)');
await MoxplatformPlugin.crypto.decryptFile(
'$path.enc',
'$path.dec',
Uint8List.fromList(List.filled(32, 1)),
Uint8List.fromList(List.filled(16, 2)),
CipherAlgorithm.aes256CbcPkcs7,
'SHA-256',
);
// ignore: avoid_print
print('DONE');
final lengthDec = await File('$path.dec').length();
// ignore: avoid_print
print('Decrypted file is $lengthDec Bytes large (Orig $lengthOrig)');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Moxplatform Demo'),
),
body: Center(
child: ListView(
children: [
ElevatedButton(
onPressed: _cryptoTest,
child: const Text('Test cryptography'),
),
ElevatedButton(
onPressed: () {
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'),
),
],
),
),
);
}
}

99
example/pubspec.yaml Normal file
View File

@@ -0,0 +1,99 @@
name: example
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
sdk: ">=2.17.5 <3.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
moxplatform:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.17+6
moxplatform_android:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.22
file_picker: 5.2.0+1
path: 1.8.3
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View File

@@ -0,0 +1,8 @@
# melos_managed_dependency_overrides: moxplatform,moxplatform_android,moxplatform_platform_interface
dependency_overrides:
moxplatform:
path: ../packages/moxplatform
moxplatform_android:
path: ../packages/moxplatform_android
moxplatform_platform_interface:
path: ../packages/moxplatform_platform_interface

View File

@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

117
flake.lock generated
View File

@@ -1,6 +1,66 @@
{
"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": {
"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": {
"lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
@@ -17,11 +77,27 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1660551188,
"narHash": "sha256-a1LARMMYQ8DPx1BgoI/UN4bXe12hhZkCNqdxNi6uS0g=",
"lastModified": 1689679375,
"narHash": "sha256-LHUC52WvyVDi9PwyL1QCpaxYWBqp4ir4iL6zgOkmcb8=",
"owner": "NixOS",
"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"
},
"original": {
@@ -33,8 +109,39 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"android-nixpkgs": "android-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 = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
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 {
inherit system;
config = {
@@ -13,29 +14,32 @@
allowUnfree = true;
};
};
android = pkgs.androidenv.composeAndroidPackages {
# TODO: Find a way to pin these
#toolsVersion = "26.1.1";
#platformToolsVersion = "31.0.3";
#buildToolsVersions = [ "31.0.0" ];
#includeEmulator = true;
#emulatorVersion = "30.6.3";
platformVersions = [ "28" ];
includeSources = false;
includeSystemImages = true;
systemImageTypes = [ "default" ];
abiVersions = [ "x86_64" ];
includeNDK = false;
useGoogleAPIs = false;
useGoogleTVAddOns = false;
};
pinnedJDK = pkgs.jdk;
# Everything to make Flutter happy
sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [
cmdline-tools-latest
build-tools-30-0-3
build-tools-33-0-2
build-tools-34-0-0
platform-tools
emulator
patcher-v4
platforms-android-30
platforms-android-31
platforms-android-33
]);
pinnedJDK = pkgs.jdk17;
in {
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
flutter pinnedJDK android.platform-tools dart # Flutter
gitlint jq # Code hygiene
ripgrep # General utilities
# Android
pinnedJDK
sdk
# Flutter
flutter dart
# Code hygiene
gitlint
# Flutter dependencies for linux desktop
atk
@@ -59,9 +63,13 @@
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 ];
ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk";
ANDROID_HOME = "${sdk}/share/android-sdk";
ANDROID_SDK_ROOT = "${sdk}/share/android-sdk";
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
scripts:
format:
exec: dart format .
analyze:
exec: dart analyze .
exec: flutter analyze

View File

@@ -1,3 +1,42 @@
## 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
- Update a dependency to the latest release.
## 0.1.11+1
- **REFACTOR**: Make version constraints looser.
## 0.1.0
* Initial release of this plugin.

View File

@@ -0,0 +1,3 @@
# moxplatform
The general package for using moxplatform.

View File

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

View File

@@ -2,5 +2,7 @@ import 'package:moxplatform_platform_interface/moxplatform_platform_interface.da
class MoxplatformPlugin {
static IsolateHandler get handler => MoxplatformInterface.handler;
static MediaScannerImplementation get media => MoxplatformInterface.media;
static CryptographyImplementation get crypto => MoxplatformInterface.crypto;
static ContactsImplementation get contacts => MoxplatformInterface.contacts;
static 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 BackgroundEvent implements JsonImplementation {}

View File

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

View File

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

View File

@@ -1,3 +1,68 @@
## 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
- **REFACTOR**: Make version constraints looser.
## 0.1.11+1
- **REFACTOR**: Make version constraints looser.
## 0.1.0
* Initial release of this plugin.

View File

@@ -2,13 +2,17 @@ group 'me.polynom.moxplatform_android'
version '1.0'
buildscript {
ext {
kotlin_version = '1.8.21'
}
repositories {
google()
mavenCentral()
}
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: 'org.jetbrains.kotlin.android'
android {
compileSdkVersion 31
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -30,10 +35,14 @@ android {
}
defaultConfig {
minSdkVersion 16
// What Moxxy currently uses
minSdkVersion 26
targetSdkVersion 33
}
}
dependencies {
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
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
zipStorePath=wrapper/dists

View File

@@ -1,8 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.polynom.moxplatform_android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application>
@@ -26,6 +28,5 @@
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
</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;
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.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
@@ -85,29 +91,15 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
}
public static boolean isManuallyStopped(Context context) {
return context.getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE).getBoolean(manuallyStoppedKey, false);
return context.getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE).getBoolean(manuallyStoppedKey, false);
}
public void setManuallyStopped(Context context, boolean value) {
context.getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE)
context.getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE)
.edit()
.putBoolean(manuallyStoppedKey, value)
.apply();
}
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() {
String packageName = getApplicationContext().getPackageName();
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))
);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, "FOREGROUND_DEFAULT")
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, "foreground_service")
.setSmallIcon(R.drawable.ic_service_icon)
.setAutoCancel(true)
.setOngoing(true)
@@ -151,7 +143,7 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
FlutterInjector.instance().flutterLoader().startInitialization(getApplicationContext());
}
long entrypointHandle = getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE)
long entrypointHandle = getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE)
.getLong(MoxplatformAndroidPlugin.entrypointKey, 0);
FlutterInjector.instance().flutterLoader().ensureInitializationComplete(getApplicationContext(), null);
FlutterCallbackInformation callback = FlutterCallbackInformation.lookupCallbackInformation(entrypointHandle);
@@ -237,7 +229,6 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa
public void onCreate() {
super.onCreate();
createNotificationChannel();
notificationBody = "Preparing...";
updateNotificationInfo();
}

View File

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

View File

@@ -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,178 +1,318 @@
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.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
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.Size;
import androidx.activity.result.PickVisualMediaRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.service.ServiceAware;
import io.flutter.embedding.engine.plugins.service.ServicePluginBinding;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.EventChannel.StreamHandler;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugin.common.PluginRegistry;
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 static final String entrypointKey = "entrypoint_handle";
public static final String extraDataKey = "extra_data";
private static final String autoStartAtBootKey = "auto_start_at_boot";
public static final String sharedPrefKey = "me.polynom.moxplatform_android";
private static final String TAG = "moxplatform_android";
public static final String methodChannelKey = "me.polynom.moxplatform_android";
public static final String dataReceivedMethodName = "dataReceived";
public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware, ActivityAware, PluginRegistry.ActivityResultListener, MoxplatformApi {
public static final String entrypointKey = "entrypoint_handle";
public static final String extraDataKey = "extra_data";
private static final String autoStartAtBootKey = "auto_start_at_boot";
private static final String TAG = "moxplatform_android";
public static final String methodChannelKey = "me.polynom.moxplatform_android";
public static final String dataReceivedMethodName = "dataReceived";
private static final List<MoxplatformAndroidPlugin> _instances = new ArrayList<>();
private BackgroundService service;
private MethodChannel channel;
private Context context;
private static final List<MoxplatformAndroidPlugin> _instances = new ArrayList<>();
private BackgroundService service;
private MethodChannel channel;
public MoxplatformAndroidPlugin() {
_instances.add(this);
}
public static Activity activity;
private Context context;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), methodChannelKey);
channel.setMethodCallHandler(this);
context = flutterPluginBinding.getApplicationContext();
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey));
Log.d(TAG, "Attached to engine");
}
static void registerWith(Registrar registrar) {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context());
final MoxplatformAndroidPlugin plugin = new MoxplatformAndroidPlugin();
localBroadcastManager.registerReceiver(plugin, new IntentFilter(methodChannelKey));
final MethodChannel channel = new MethodChannel(registrar.messenger(), "me.polynom/background_service_android", JSONMethodCodec.INSTANCE);
channel.setMethodCallHandler(plugin);
plugin.channel = channel;
Log.d(TAG, "Registered against registrar");
}
/// Store the entrypoint handle and extra data for the background service.
private void configure(long entrypointHandle, String extraData) {
SharedPreferences prefs = context.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE);
prefs.edit()
.putLong(entrypointKey, entrypointHandle)
.putString(extraDataKey, extraData)
.apply();
}
public static long getHandle(Context c) {
return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getLong(entrypointKey, 0);
}
public static String getExtraData(Context c) {
return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getString(extraDataKey, "");
}
public static void setStartAtBoot(Context c, boolean value) {
c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE)
.edit()
.putBoolean(autoStartAtBootKey, value)
.apply();
}
public static boolean getStartAtBoot(Context c) {
return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getBoolean(autoStartAtBootKey, false);
}
private boolean isRunning() {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo info : manager.getRunningServices(Integer.MAX_VALUE)) {
if (BackgroundService.class.getName().equals(info.service.getClassName())) {
return true;
}
public MoxplatformAndroidPlugin() {
_instances.add(this);
}
return false;
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
return onActivityResultImpl(context, requestCode, resultCode, data);
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "configure":
ArrayList args = (ArrayList) call.arguments;
long handle = (long) args.get(0);
String extraData = (String) args.get(1);
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), methodChannelKey);
channel.setMethodCallHandler(this);
context = flutterPluginBinding.getApplicationContext();
configure(handle, extraData);
result.success(true);
break;
case "isRunning":
result.success(isRunning());
break;
case "start":
MoxplatformAndroidPlugin.setStartAtBoot(context, true);
BackgroundService.enqueue(context);
Intent intent = new Intent(context, BackgroundService.class);
ContextCompat.startForegroundService(context, intent);
Log.d(TAG, "Service started");
result.success(true);
break;
case "sendData":
for (MoxplatformAndroidPlugin plugin : _instances) {
if (plugin.service != null) {
plugin.service.receiveData((String) call.arguments);
break;
}
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey));
MoxplatformApi.setup(flutterPluginBinding.getBinaryMessenger(), this);
Log.d(TAG, "Attached to engine");
}
static void registerWith(Registrar registrar) {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context());
final MoxplatformAndroidPlugin plugin = new MoxplatformAndroidPlugin();
localBroadcastManager.registerReceiver(plugin, new IntentFilter(methodChannelKey));
activity = registrar.activity();
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(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;
default:
result.notImplemented();
break;
return false;
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null) return;
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull io.flutter.plugin.common.MethodChannel.Result result) {
switch (call.method) {
case "configure":
ArrayList args = (ArrayList) call.arguments;
long handle = (long) args.get(0);
String extraData = (String) args.get(1);
if (intent.getAction().equalsIgnoreCase(methodChannelKey)) {
String data = intent.getStringExtra("data");
if (channel != null) {
channel.invokeMethod(dataReceivedMethodName, data);
}
configure(handle, extraData);
result.success(true);
break;
case "isRunning":
result.success(isRunning());
break;
case "start":
MoxplatformAndroidPlugin.setStartAtBoot(context, true);
BackgroundService.enqueue(context);
Intent intent = new Intent(context, BackgroundService.class);
ContextCompat.startForegroundService(context, intent);
Log.d(TAG, "Service started");
result.success(true);
break;
case "sendData":
for (MoxplatformAndroidPlugin plugin : _instances) {
if (plugin.service != null) {
plugin.service.receiveData((String) call.arguments);
break;
}
}
result.success(true);
break;
default:
result.notImplemented();
break;
}
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context);
localBroadcastManager.unregisterReceiver(this);
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null) return;
Log.d(TAG, "Detached from engine");
}
if (intent.getAction().equalsIgnoreCase(methodChannelKey)) {
String data = intent.getStringExtra("data");
@Override
public void onAttachedToService(@NonNull ServicePluginBinding binding) {
Log.d(TAG, "Attached to service");
this.service = (BackgroundService) binding.getService();
}
if (channel != null) {
channel.invokeMethod(dataReceivedMethodName, data);
}
}
}
@Override
public void onDetachedFromService() {
Log.d(TAG, "Detached from service");
this.service = null;
}
@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;
}
@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;
export 'src/isolate_android.dart';
export 'src/media_android.dart';
export 'src/plugin_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

@@ -0,0 +1,49 @@
import 'package:flutter/services.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidCryptographyImplementation extends CryptographyImplementation {
final MoxplatformApi _api = MoxplatformApi();
@override
Future<CryptographyResult?> encryptFile(
String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
) async {
return _api.encryptFile(
sourcePath,
destPath,
key,
iv,
algorithm,
hashSpec,
);
}
@override
Future<CryptographyResult?> decryptFile(
String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
) async {
return _api.decryptFile(
sourcePath,
destPath,
key,
iv,
algorithm,
hashSpec,
);
}
@override
Future<Uint8List?> hashFile(String sourcePath, String hashSpec) async {
return _api.hashFile(sourcePath, hashSpec);
}
}

View File

@@ -4,18 +4,18 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/awaitabledatasender.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_android/src/service_android.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
/// An [AwaitableDataSender] that uses flutter_background_service.
class BackgroundServiceDataSender extends AwaitableDataSender<BackgroundCommand, BackgroundEvent> {
class BackgroundServiceDataSender
extends AwaitableDataSender<BackgroundCommand, BackgroundEvent> {
BackgroundServiceDataSender()
: _channel = const MethodChannel('me.polynom.moxplatform_android'), super();
: _channel = const MethodChannel('me.polynom.moxplatform_android'),
super();
final MethodChannel _channel;
@override
Future<void> sendDataImpl(DataWrapper data) async {
await _channel.invokeMethod<void>('sendData', jsonEncode(data.toJson()));
@@ -39,56 +39,67 @@ Future<void> androidEntrypoint() async {
);
final data = jsonDecode(result!) as Map<String, dynamic>;
final entrypointHandle = data['genericEntrypoint']! as int;
final entrypointCallbackHandle = CallbackHandle.fromRawHandle(entrypointHandle);
final entrypoint = PluginUtilities.getCallbackFromHandle(entrypointCallbackHandle);
final entrypointCallbackHandle =
CallbackHandle.fromRawHandle(entrypointHandle);
final entrypoint =
PluginUtilities.getCallbackFromHandle(entrypointCallbackHandle);
final handleUIEventHandle = data['eventHandle']! as int;
final handleUIEventCallbackHandle = CallbackHandle.fromRawHandle(handleUIEventHandle);
final handleUIEvent = PluginUtilities.getCallbackFromHandle(handleUIEventCallbackHandle);
final handleUIEventCallbackHandle =
CallbackHandle.fromRawHandle(handleUIEventHandle);
final handleUIEvent =
PluginUtilities.getCallbackFromHandle(handleUIEventCallbackHandle);
final srv = AndroidBackgroundService();
GetIt.I.registerSingleton<BackgroundService>(srv);
srv.init(
entrypoint! as Future<void> Function(),
entrypoint! as Future<void> Function(String),
handleUIEvent! as Future<void> Function(Map<String, dynamic>? data),
data['initialLocale']! as String,
);
}
/// The Android specific implementation of the [IsolateHandler].
class AndroidIsolateHandler extends IsolateHandler {
AndroidIsolateHandler()
: _channel = const MethodChannel('me.polynom.moxplatform_android'),
_dataSender = BackgroundServiceDataSender(),
_log = Logger('AndroidIsolateHandler'),
super();
: _channel = const MethodChannel('me.polynom.moxplatform_android'),
_dataSender = BackgroundServiceDataSender(),
_log = Logger('AndroidIsolateHandler'),
super();
final BackgroundServiceDataSender _dataSender;
final MethodChannel _channel;
final Logger _log;
@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 {
final args = call.arguments as String;
await handleIsolateEvent(jsonDecode(args) as Map<String, dynamic>);
});
}
@override
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) handleIsolateEvent,
String initialLocale,
) async {
_log.finest('Called start');
WidgetsFlutterBinding.ensureInitialized();
final androidEntrypointHandle = PluginUtilities.getCallbackHandle(androidEntrypoint)!.toRawHandle();
final androidEntrypointHandle =
PluginUtilities.getCallbackHandle(androidEntrypoint)!.toRawHandle();
_log.finest('androidEntrypointHandle: $androidEntrypointHandle');
await _channel.invokeMethod<void>('configure', <dynamic>[
androidEntrypointHandle,
jsonEncode({
'genericEntrypoint': PluginUtilities.getCallbackHandle(entrypoint)!.toRawHandle(),
'eventHandle': PluginUtilities.getCallbackHandle(handleUIEvent)!.toRawHandle()
'genericEntrypoint':
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,13 +1,17 @@
import 'package:moxplatform_android/src/contacts_android.dart';
import 'package:moxplatform_android/src/crypto_android.dart';
import 'package:moxplatform_android/src/isolate_android.dart';
import 'package:moxplatform_android/src/media_android.dart';
import 'package:moxplatform_android/src/platform_android.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class MoxplatformAndroidPlugin extends MoxplatformInterface {
static void registerWith() {
// ignore: avoid_print
print('MoxplatformAndroidPlugin: Registering implementation');
MoxplatformInterface.contacts = AndroidContactsImplementation();
MoxplatformInterface.crypto = AndroidCryptographyImplementation();
MoxplatformInterface.handler = AndroidIsolateHandler();
MoxplatformInterface.media = AndroidMediaScannerImplementation();
MoxplatformInterface.platform = AndroidPlatformImplementation();
}
@override

View File

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

View File

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

View File

@@ -1,3 +1,57 @@
## 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
- **REFACTOR**: Make version constraints looser.
## 0.1.11+1
- **REFACTOR**: Make version constraints looser.
## 0.1.0
* Initial release.

View File

@@ -0,0 +1,3 @@
# moxplatform_platform_interface
The interface definitions for moxplatform.

View File

@@ -1,8 +1,13 @@
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_stub.dart';
export 'src/interface.dart';
export 'src/isolate.dart';
export 'src/isolate_stub.dart';
export 'src/media.dart';
export 'src/media_stub.dart';
export 'src/platform.dart';
export 'src/platform_stub.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

@@ -0,0 +1,39 @@
import 'dart:typed_data';
import 'package:moxplatform_platform_interface/src/api.g.dart';
/// Wrapper around platform-native cryptography APIs
abstract class CryptographyImplementation {
/// Encrypt 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".
/// 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.
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
/// [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.
///
/// 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,
);
/// Hashes the file at [sourcePath] using the Hash function with name [hashSpec].
/// Note that this function runs off-thread as to not block the UI thread.
///
/// Returns the hash of the file.
Future<Uint8List?> hashFile(String sourcePath, String hashSpec);
}

View File

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

View File

@@ -1,16 +1,25 @@
import 'package:moxplatform_platform_interface/src/api.g.dart';
import 'package:moxplatform_platform_interface/src/contacts.dart';
import 'package:moxplatform_platform_interface/src/contacts_stub.dart';
import 'package:moxplatform_platform_interface/src/crypto.dart';
import 'package:moxplatform_platform_interface/src/crypto_stub.dart';
import 'package:moxplatform_platform_interface/src/isolate.dart';
import 'package:moxplatform_platform_interface/src/isolate_stub.dart';
import 'package:moxplatform_platform_interface/src/media.dart';
import 'package:moxplatform_platform_interface/src/media_stub.dart';
import 'package:moxplatform_platform_interface/src/platform.dart';
import 'package:moxplatform_platform_interface/src/platform_stub.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
abstract class MoxplatformInterface extends PlatformInterface {
MoxplatformInterface() : super(token: _token);
static final Object _token = Object();
static MoxplatformApi api = MoxplatformApi();
static IsolateHandler handler = StubIsolateHandler();
static MediaScannerImplementation media = StubMediaScannerImplementation();
static CryptographyImplementation crypto = StubCryptographyImplementation();
static ContactsImplementation contacts = StubContactsImplementation();
static PlatformImplementation platform = StubPlatformImplementation();
/// Return the current platform name.
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
/// service, which is either a regular isolate or an Android foreground service.
@@ -8,17 +8,19 @@ abstract class IsolateHandler {
/// [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.
/// [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> Function() entrypoint,
Future<void> Function(String initialLocale) entrypoint,
Future<void> Function(Map<String, dynamic>? data) handleUIEvent,
Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent,
String initialLocale,
);
/// Make sure that the UI event handler is registered without starting the isolate.
Future<void> attach(
Future<void> Function(Map<String, dynamic>? data) handleIsolateEvent,
);
/// Return true if the background service is running. False if it's not.
Future<bool> isRunning();

View File

@@ -1,4 +1,4 @@
import 'package:moxlib/awaitabledatasender.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxplatform_platform_interface/src/isolate.dart';
class StubDataSender extends AwaitableDataSender {
@@ -19,12 +19,13 @@ class StubIsolateHandler extends IsolateHandler {
// ignore: avoid_print
print('STUB ATTACHED!!!!!!');
}
@override
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) handleIsolateEvent,
String initialLocale,
) async {
// ignore: avoid_print
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);
/// 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.
/// [handleEvent] is a function that is called whenever the service receives
/// data.
void init(
Future<void> Function() entrypoint,
Future<void> Function(String initialLocale) entrypoint,
Future<void> Function(Map<String, dynamic>? data) handleEvent,
String initialLocale,
);
}

View File

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