diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 63577c2..0ef6ec2 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -23,8 +23,8 @@ - - + + { '$_counter', style: Theme.of(context).textTheme.headline4, ), + + ElevatedButton( + onPressed: () { + MoxplatformPlugin.contacts.recordSentMessage('Hallo', 'Welt'); + }, + child: Text('Test recordSentMessage (no fallback)'), + ), + ElevatedButton( + onPressed: () { + MoxplatformPlugin.contacts.recordSentMessage('Person', 'Person', fallbackIcon: FallbackIconType.person); + }, + child: Text('Test recordSentMessage (person fallback)'), + ), + ElevatedButton( + onPressed: () { + MoxplatformPlugin.contacts.recordSentMessage('Notes', 'Notes', fallbackIcon: FallbackIconType.notes); + }, + child: Text('Test recordSentMessage (notes fallback)'), + ), ], ), ), diff --git a/flake.lock b/flake.lock index a126c34..390e1fe 100644 --- a/flake.lock +++ b/flake.lock @@ -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" } } }, diff --git a/flake.nix b/flake.nix index f3277ab..f50d662 100644 --- a/flake.nix +++ b/flake.nix @@ -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"; }; }); } diff --git a/packages/moxplatform/lib/src/plugin.dart b/packages/moxplatform/lib/src/plugin.dart index eadc4c9..d081d70 100644 --- a/packages/moxplatform/lib/src/plugin.dart +++ b/packages/moxplatform/lib/src/plugin.dart @@ -4,4 +4,5 @@ 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; } diff --git a/packages/moxplatform_android/android/build.gradle b/packages/moxplatform_android/android/build.gradle index b476504..5bf9414 100644 --- a/packages/moxplatform_android/android/build.gradle +++ b/packages/moxplatform_android/android/build.gradle @@ -25,8 +25,8 @@ android { compileSdkVersion 31 compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_14 + targetCompatibility JavaVersion.VERSION_14 } defaultConfig { @@ -36,4 +36,5 @@ android { dependencies { implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' + implementation 'androidx.core:core:1.10.1' } \ No newline at end of file diff --git a/packages/moxplatform_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/moxplatform_android/android/gradle/wrapper/gradle-wrapper.properties index da9702f..ffed3a2 100644 --- a/packages/moxplatform_android/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/moxplatform_android/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index cd79e95..5e8e1e3 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java @@ -6,17 +6,28 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Icon; +import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.Person; import androidx.core.content.ContextCompat; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.drawable.IconCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import java.io.FileInputStream; import java.security.MessageDigest; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.crypto.Cipher; import javax.crypto.CipherOutputStream; @@ -191,12 +202,79 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt }); hashingThread.start(); break; + case "recordSentMessage": + ArrayList rargs = (ArrayList) call.arguments; + try { + recordSentMessage( + (String) rargs.get(0), + (String) rargs.get(1), + (String) rargs.get(2), + (int) rargs.get(3) + ); + } catch (ClassNotFoundException ex) { + Log.e(TAG, "Failed to get classname"); + Log.e(TAG, ex.getMessage()); + } + result.success(true); + break; default: result.notImplemented(); break; } } + private void recordSentMessage(String name, String jid, String avatarPath, int fallbackIconType) throws ClassNotFoundException { + // Very much inspired (or copied) from https://github.com/ShoutSocial/share_handler + final String pkgName = context.getPackageName(); + final Intent intent = new Intent(context, Class.forName(pkgName + ".MainActivity")); + intent.setAction(Intent.ACTION_SEND); + + // Compatibility with share_handler + intent.putExtra("conversationIdentifier", jid); + + final String shortcutTarget = pkgName + ".dynamic_share_target"; + final ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(context, name); + builder + .setShortLabel(name) + .setIsConversation() + .setCategories(Set.of(shortcutTarget)) + .setIntent(intent) + .setLongLived(true); + + // TODO: This is dumb. Maybe just raise the minimum Android version + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + final Person.Builder personBuilder = new Person.Builder() + .setKey(jid) + .setName(name); + + if (avatarPath != null) { + final Bitmap bitmap = BitmapFactory.decodeFile(avatarPath); + final IconCompat icon = IconCompat.createWithAdaptiveBitmap(bitmap); + builder.setIcon(icon); + personBuilder.setIcon(icon); + } else { + if (fallbackIconType == 0 || fallbackIconType == 1) { + final int id = switch (fallbackIconType) { + default: + case 0: yield R.mipmap.person; + case 1: yield R.mipmap.notes; + }; + final IconCompat personIcon = IconCompat.createWithResource( + context, + id + ); + builder.setIcon(personIcon); + personBuilder.setIcon(personIcon); + } + } + + builder.setPerson(personBuilder.build()); + } + + final ShortcutInfoCompat shortcut = builder.build(); + ShortcutManagerCompat.addDynamicShortcuts(context, List.of(shortcut)); + } + @Override public void onReceive(Context context, Intent intent) { if (intent.getAction() == null) return; diff --git a/packages/moxplatform_android/android/src/main/res/drawable/notes_foreground.xml b/packages/moxplatform_android/android/src/main/res/drawable/notes_foreground.xml new file mode 100644 index 0000000..54ce67b --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/drawable/notes_foreground.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/moxplatform_android/android/src/main/res/drawable/person_foreground.xml b/packages/moxplatform_android/android/src/main/res/drawable/person_foreground.xml new file mode 100644 index 0000000..af2529b --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/drawable/person_foreground.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/moxplatform_android/android/src/main/res/mipmap-anydpi-v26/notes.xml b/packages/moxplatform_android/android/src/main/res/mipmap-anydpi-v26/notes.xml new file mode 100644 index 0000000..7733c4b --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/mipmap-anydpi-v26/notes.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/res/mipmap-anydpi-v26/person.xml b/packages/moxplatform_android/android/src/main/res/mipmap-anydpi-v26/person.xml new file mode 100644 index 0000000..0e663b7 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/mipmap-anydpi-v26/person.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/res/mipmap-xhdpi/notes.png b/packages/moxplatform_android/android/src/main/res/mipmap-xhdpi/notes.png new file mode 100644 index 0000000..070bb9f Binary files /dev/null and b/packages/moxplatform_android/android/src/main/res/mipmap-xhdpi/notes.png differ diff --git a/packages/moxplatform_android/android/src/main/res/mipmap-xhdpi/person.png b/packages/moxplatform_android/android/src/main/res/mipmap-xhdpi/person.png new file mode 100644 index 0000000..cff4b38 Binary files /dev/null and b/packages/moxplatform_android/android/src/main/res/mipmap-xhdpi/person.png differ diff --git a/packages/moxplatform_android/android/src/main/res/values/notes_background.xml b/packages/moxplatform_android/android/src/main/res/values/notes_background.xml new file mode 100644 index 0000000..40c90a7 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/values/notes_background.xml @@ -0,0 +1,4 @@ + + + #CF4AFF + \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/res/values/person_background.xml b/packages/moxplatform_android/android/src/main/res/values/person_background.xml new file mode 100644 index 0000000..da3e5eb --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/values/person_background.xml @@ -0,0 +1,4 @@ + + + #CF4AFF + \ No newline at end of file diff --git a/packages/moxplatform_android/lib/src/contacts_android.dart b/packages/moxplatform_android/lib/src/contacts_android.dart new file mode 100644 index 0000000..6671897 --- /dev/null +++ b/packages/moxplatform_android/lib/src/contacts_android.dart @@ -0,0 +1,32 @@ +import 'package:flutter/services.dart'; +import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; + +class AndroidContactsImplementation extends ContactsImplementation { + final _methodChannel = const MethodChannel('me.polynom.moxplatform_android'); + + @override + Future 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', + ); + } + + await _methodChannel.invokeMethod( + 'recordSentMessage', + [ + name, + jid, + avatarPath, + fallbackIcon.id, + ], + ); + } +} diff --git a/packages/moxplatform_android/lib/src/plugin_android.dart b/packages/moxplatform_android/lib/src/plugin_android.dart index 525bd80..a024c23 100644 --- a/packages/moxplatform_android/lib/src/plugin_android.dart +++ b/packages/moxplatform_android/lib/src/plugin_android.dart @@ -1,3 +1,4 @@ +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'; @@ -7,9 +8,10 @@ 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.crypto = AndroidCryptographyImplementation(); } @override diff --git a/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart b/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart index 88c3336..9b2a14a 100644 --- a/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart +++ b/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart @@ -1,5 +1,7 @@ library moxplatform_platform_interface; +export 'src/contacts.dart'; +export 'src/contacts_stub.dart'; export 'src/crypto.dart'; export 'src/crypto_stub.dart'; export 'src/interface.dart'; diff --git a/packages/moxplatform_platform_interface/lib/src/contacts.dart b/packages/moxplatform_platform_interface/lib/src/contacts.dart new file mode 100644 index 0000000..3ee6a22 --- /dev/null +++ b/packages/moxplatform_platform_interface/lib/src/contacts.dart @@ -0,0 +1,22 @@ +// The type of icon to use when no avatar path is provided. +enum FallbackIconType { + none(-1), + person(0), + notes(1); + + const FallbackIconType(this.id); + + // The ID of the fallback icon. + final int id; +} + +// Wrapper around various contact APIs. +// ignore: one_member_abstracts +abstract class ContactsImplementation { + Future recordSentMessage( + String name, + String jid, { + String? avatarPath, + FallbackIconType fallbackIcon = FallbackIconType.none, + }); +} diff --git a/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart b/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart new file mode 100644 index 0000000..51a6dae --- /dev/null +++ b/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart @@ -0,0 +1,11 @@ +import 'package:moxplatform_platform_interface/src/contacts.dart'; + +class StubContactsImplementation extends ContactsImplementation { + @override + Future recordSentMessage( + String name, + String jid, { + String? avatarPath, + FallbackIconType fallbackIcon = FallbackIconType.none, + }) async {} +} diff --git a/packages/moxplatform_platform_interface/lib/src/interface.dart b/packages/moxplatform_platform_interface/lib/src/interface.dart index ca4ac6c..6005fb7 100644 --- a/packages/moxplatform_platform_interface/lib/src/interface.dart +++ b/packages/moxplatform_platform_interface/lib/src/interface.dart @@ -1,3 +1,5 @@ +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'; @@ -14,6 +16,7 @@ abstract class MoxplatformInterface extends PlatformInterface { static IsolateHandler handler = StubIsolateHandler(); static MediaScannerImplementation media = StubMediaScannerImplementation(); static CryptographyImplementation crypto = StubCryptographyImplementation(); + static ContactsImplementation contacts = StubContactsImplementation(); /// Return the current platform name. Future getPlatformName(); diff --git a/packages/moxplatform_platform_interface/packages/moxplatform_android/lib/src/contacts_android.dart b/packages/moxplatform_platform_interface/packages/moxplatform_android/lib/src/contacts_android.dart new file mode 100644 index 0000000..e69de29 diff --git a/packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts.dart b/packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts.dart new file mode 100644 index 0000000..e69de29 diff --git a/packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart b/packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart new file mode 100644 index 0000000..e69de29