Skip to content

Commit 611bdda

Browse files
authored
feat(pasteboard): support android (#362)
* feat(pasteboard): support android * fix: handle some case * fix: add provider manifest * fix: wrong uri * revert: revert not handle content uri
1 parent c628747 commit 611bdda

File tree

11 files changed

+221
-3
lines changed

11 files changed

+221
-3
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea/workspace.xml
5+
/.idea/libraries
6+
.DS_Store
7+
/build
8+
/captures
9+
.cxx
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
group = "one.mixin.pasteboard"
2+
version = "1.0-SNAPSHOT"
3+
4+
buildscript {
5+
ext.kotlin_version = "1.8.22"
6+
repositories {
7+
google()
8+
mavenCentral()
9+
}
10+
11+
dependencies {
12+
classpath("com.android.tools.build:gradle:8.1.4")
13+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
14+
}
15+
}
16+
17+
allprojects {
18+
repositories {
19+
google()
20+
mavenCentral()
21+
}
22+
}
23+
24+
apply plugin: "com.android.library"
25+
apply plugin: "kotlin-android"
26+
27+
android {
28+
if (project.android.hasProperty("namespace")) {
29+
namespace = "one.mixin.pasteboard"
30+
}
31+
32+
compileSdk = 34
33+
34+
compileOptions {
35+
sourceCompatibility = JavaVersion.VERSION_1_8
36+
targetCompatibility = JavaVersion.VERSION_1_8
37+
}
38+
39+
kotlinOptions {
40+
jvmTarget = JavaVersion.VERSION_1_8
41+
}
42+
43+
sourceSets {
44+
main.java.srcDirs += "src/main/kotlin"
45+
test.java.srcDirs += "src/test/kotlin"
46+
}
47+
48+
defaultConfig {
49+
minSdk = 21
50+
}
51+
52+
dependencies {
53+
testImplementation("org.jetbrains.kotlin:kotlin-test")
54+
testImplementation("org.mockito:mockito-core:5.0.0")
55+
}
56+
57+
testOptions {
58+
unitTests.all {
59+
useJUnitPlatform()
60+
61+
testLogging {
62+
events "passed", "skipped", "failed", "standardOut", "standardError"
63+
outputs.upToDateWhen {false}
64+
showStandardStreams = true
65+
}
66+
}
67+
}
68+
}
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
4+
networkTimeout=10000
5+
validateDistributionUrl=true
6+
zipStoreBase=GRADLE_USER_HOME
7+
zipStorePath=wrapper/dists
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'pasteboard'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="one.mixin.pasteboard">
3+
4+
</manifest>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package one.mixin.pasteboard
2+
3+
4+
import android.content.ClipData
5+
import android.content.ClipboardManager
6+
import android.content.Context
7+
import android.graphics.Bitmap
8+
import android.graphics.BitmapFactory
9+
import android.net.Uri
10+
import androidx.core.content.FileProvider
11+
import io.flutter.embedding.engine.plugins.FlutterPlugin
12+
import io.flutter.plugin.common.MethodCall
13+
import io.flutter.plugin.common.MethodChannel
14+
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
15+
import io.flutter.plugin.common.MethodChannel.Result
16+
import java.io.ByteArrayOutputStream
17+
import java.io.File
18+
import java.io.FileInputStream
19+
import java.io.FileOutputStream
20+
import java.io.IOException
21+
import java.util.UUID
22+
import kotlin.concurrent.thread
23+
24+
/** PasteboardPlugin */
25+
class PasteboardPlugin: FlutterPlugin, MethodCallHandler {
26+
/// The MethodChannel that will the communication between Flutter and native Android
27+
///
28+
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
29+
/// when the Flutter Engine is detached from the Activity
30+
private lateinit var context: Context
31+
private lateinit var channel : MethodChannel
32+
33+
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
34+
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "pasteboard")
35+
channel.setMethodCallHandler(this)
36+
context = flutterPluginBinding.applicationContext
37+
}
38+
39+
override fun onMethodCall(call: MethodCall, result: Result) {
40+
val manager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
41+
val cr = context.contentResolver
42+
val first = manager.primaryClip?.getItemAt(0)
43+
when (call.method) {
44+
"image" -> {
45+
first?.uri?.let {
46+
val mime = cr.getType(it)
47+
if (mime == null || !mime.startsWith("image")) return result.success(null)
48+
result.success(cr.openInputStream(it).use { stream ->
49+
stream?.buffered()?.readBytes()
50+
})
51+
}
52+
result.success(null)
53+
}
54+
"files" -> {
55+
manager.primaryClip?.run {
56+
if (itemCount == 0) result.success(null)
57+
val files: MutableList<String> = mutableListOf()
58+
for (i in 0 until itemCount) {
59+
getItemAt(i).uri?.let {
60+
files.add(it.toString())
61+
}
62+
}
63+
result.success(files)
64+
}
65+
}
66+
"html" -> result.success(first?.htmlText)
67+
"writeFiles" -> {
68+
val args = call.arguments<List<String>>() ?: return result.error(
69+
"NoArgs",
70+
"Missing Arguments",
71+
null,
72+
)
73+
val clip: ClipData? = null
74+
for (i in args) {
75+
val uri = Uri.parse(i)
76+
clip ?: ClipData.newUri(cr, "files", uri)
77+
clip?.addItem(ClipData.Item(uri))
78+
}
79+
clip?.let {
80+
manager.setPrimaryClip(it)
81+
}
82+
result.success(null)
83+
}
84+
"writeImage" -> {
85+
val image = call.arguments<ByteArray>() ?: return result.error(
86+
"NoArgs",
87+
"Missing Arguments",
88+
null,
89+
)
90+
val out = ByteArrayOutputStream()
91+
thread {
92+
val bitmap = BitmapFactory.decodeByteArray(image, 0, image.size)
93+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
94+
}
95+
val name = UUID.randomUUID().toString()
96+
val file = File(context.cacheDir, name)
97+
FileOutputStream(file).use {
98+
out.writeTo(it)
99+
}
100+
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
101+
val clip = ClipData.newUri(cr, "image.png", uri)
102+
manager.setPrimaryClip(clip)
103+
}
104+
else -> result.notImplemented()
105+
}
106+
}
107+
108+
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
109+
channel.setMethodCallHandler(null)
110+
}
111+
}

packages/pasteboard/example/android/app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@
3030
<meta-data
3131
android:name="flutterEmbedding"
3232
android:value="2" />
33+
<provider
34+
android:name="androidx.core.content.FileProvider"
35+
android:authorities="${applicationId}.provider"
36+
android:exported="false"
37+
android:grantUriPermissions="true">
38+
<meta-data
39+
android:name="android.support.FILE_PROVIDER_PATHS"
40+
android:resource="@xml/provider_paths" />
41+
</provider>
3342
</application>
3443
<!-- Required to query activities that can process text, see:
3544
https://developer.android.com/training/package-visibility and
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<paths xmlns:android="http://schemas.android.com/apk/res/android">
3+
<external-path
4+
name="external_files"
5+
path="." />
6+
</paths>

packages/pasteboard/lib/src/pasteboard_platform_io.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class PasteboardPlatformIO implements PasteboardPlatform {
2222

2323
@override
2424
Future<String?> get html async {
25-
if (Platform.isWindows) {
25+
if (Platform.isWindows || Platform.isAndroid) {
2626
return await _channel.invokeMethod<Object>('html') as String?;
2727
}
2828
return null;
@@ -35,7 +35,7 @@ class PasteboardPlatformIO implements PasteboardPlatform {
3535
if (image == null) {
3636
return null;
3737
}
38-
if (Platform.isMacOS || Platform.isLinux || Platform.isIOS) {
38+
if (Platform.isMacOS || Platform.isLinux || Platform.isIOS || Platform.isAndroid) {
3939
return image as Uint8List;
4040
} else if (Platform.isWindows) {
4141
final file = File(image as String);
@@ -62,7 +62,7 @@ class PasteboardPlatformIO implements PasteboardPlatform {
6262
if (image == null) {
6363
return;
6464
}
65-
if (Platform.isIOS || Platform.isMacOS) {
65+
if (Platform.isIOS || Platform.isMacOS || Platform.isAndroid) {
6666
await _channel.invokeMethod<void>('writeImage', image);
6767
} else if (Platform.isWindows) {
6868
final file = await File(GetTempFileName()).create();

packages/pasteboard/pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ dev_dependencies:
2424
flutter:
2525
plugin:
2626
platforms:
27+
android:
28+
package: one.mixin.pasteboard
29+
pluginClass: PasteboardPlugin
2730
macos:
2831
pluginClass: PasteboardPlugin
2932
windows:

0 commit comments

Comments
 (0)