feat: Add an API for creating direct share shortcuts

This commit is contained in:
PapaTutuWawa 2023-07-21 13:04:44 +02:00
parent 3bc880079c
commit 052a4e4700
26 changed files with 368 additions and 36 deletions

View File

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

View File

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

View File

@ -135,6 +135,25 @@ class _MyHomePageState extends State<MyHomePage> {
'$_counter', '$_counter',
style: Theme.of(context).textTheme.headline4, 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)'),
),
], ],
), ),
), ),

View File

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

View File

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

View File

@ -4,4 +4,5 @@ class MoxplatformPlugin {
static IsolateHandler get handler => MoxplatformInterface.handler; static IsolateHandler get handler => MoxplatformInterface.handler;
static MediaScannerImplementation get media => MoxplatformInterface.media; static MediaScannerImplementation get media => MoxplatformInterface.media;
static CryptographyImplementation get crypto => MoxplatformInterface.crypto; static CryptographyImplementation get crypto => MoxplatformInterface.crypto;
static ContactsImplementation get contacts => MoxplatformInterface.contacts;
} }

View File

@ -25,8 +25,8 @@ android {
compileSdkVersion 31 compileSdkVersion 31
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_14
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_14
} }
defaultConfig { defaultConfig {
@ -36,4 +36,5 @@ android {
dependencies { dependencies {
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.core:core:1.10.1'
} }

View File

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

View File

@ -6,17 +6,28 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.Person;
import androidx.core.content.ContextCompat; 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 androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream; import javax.crypto.CipherOutputStream;
@ -191,12 +202,79 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
}); });
hashingThread.start(); hashingThread.start();
break; 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: default:
result.notImplemented(); result.notImplemented();
break; 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 @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null) return; if (intent.getAction() == null) return;

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 @@
<?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

@ -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<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',
);
}
await _methodChannel.invokeMethod<void>(
'recordSentMessage',
[
name,
jid,
avatarPath,
fallbackIcon.id,
],
);
}
}

View File

@ -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/crypto_android.dart';
import 'package:moxplatform_android/src/isolate_android.dart'; import 'package:moxplatform_android/src/isolate_android.dart';
import 'package:moxplatform_android/src/media_android.dart'; import 'package:moxplatform_android/src/media_android.dart';
@ -7,9 +8,10 @@ class MoxplatformAndroidPlugin extends MoxplatformInterface {
static void registerWith() { static void registerWith() {
// ignore: avoid_print // ignore: avoid_print
print('MoxplatformAndroidPlugin: Registering implementation'); print('MoxplatformAndroidPlugin: Registering implementation');
MoxplatformInterface.contacts = AndroidContactsImplementation();
MoxplatformInterface.crypto = AndroidCryptographyImplementation();
MoxplatformInterface.handler = AndroidIsolateHandler(); MoxplatformInterface.handler = AndroidIsolateHandler();
MoxplatformInterface.media = AndroidMediaScannerImplementation(); MoxplatformInterface.media = AndroidMediaScannerImplementation();
MoxplatformInterface.crypto = AndroidCryptographyImplementation();
} }
@override @override

View File

@ -1,5 +1,7 @@
library moxplatform_platform_interface; library moxplatform_platform_interface;
export 'src/contacts.dart';
export 'src/contacts_stub.dart';
export 'src/crypto.dart'; export 'src/crypto.dart';
export 'src/crypto_stub.dart'; export 'src/crypto_stub.dart';
export 'src/interface.dart'; export 'src/interface.dart';

View File

@ -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<void> recordSentMessage(
String name,
String jid, {
String? avatarPath,
FallbackIconType fallbackIcon = FallbackIconType.none,
});
}

View File

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

View File

@ -1,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.dart';
import 'package:moxplatform_platform_interface/src/crypto_stub.dart'; import 'package:moxplatform_platform_interface/src/crypto_stub.dart';
import 'package:moxplatform_platform_interface/src/isolate.dart'; import 'package:moxplatform_platform_interface/src/isolate.dart';
@ -14,6 +16,7 @@ abstract class MoxplatformInterface extends PlatformInterface {
static IsolateHandler handler = StubIsolateHandler(); static IsolateHandler handler = StubIsolateHandler();
static MediaScannerImplementation media = StubMediaScannerImplementation(); static MediaScannerImplementation media = StubMediaScannerImplementation();
static CryptographyImplementation crypto = StubCryptographyImplementation(); static CryptographyImplementation crypto = StubCryptographyImplementation();
static ContactsImplementation contacts = StubContactsImplementation();
/// Return the current platform name. /// Return the current platform name.
Future<String?> getPlatformName(); Future<String?> getPlatformName();