Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/pasteboard/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
68 changes: 68 additions & 0 deletions packages/pasteboard/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
group = "one.mixin.pasteboard"
version = "1.0-SNAPSHOT"

buildscript {
ext.kotlin_version = "1.8.22"
repositories {
google()
mavenCentral()
}

dependencies {
classpath("com.android.tools.build:gradle:8.1.4")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}

allprojects {
repositories {
google()
mavenCentral()
}
}

apply plugin: "com.android.library"
apply plugin: "kotlin-android"

android {
if (project.android.hasProperty("namespace")) {
namespace = "one.mixin.pasteboard"
}

compileSdk = 34

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}

sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}

defaultConfig {
minSdk = 21
}

dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
}

testOptions {
unitTests.all {
useJUnitPlatform()

testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
1 change: 1 addition & 0 deletions packages/pasteboard/android/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'pasteboard'
4 changes: 4 additions & 0 deletions packages/pasteboard/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="one.mixin.pasteboard">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package one.mixin.pasteboard


import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.content.FileProvider
import io.flutter.embedding.engine.plugins.FlutterPlugin
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 java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.UUID
import kotlin.concurrent.thread

/** PasteboardPlugin */
class PasteboardPlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var context: Context
private lateinit var channel : MethodChannel

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "pasteboard")
channel.setMethodCallHandler(this)
context = flutterPluginBinding.applicationContext
}

override fun onMethodCall(call: MethodCall, result: Result) {
val manager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val cr = context.contentResolver
val first = manager.primaryClip?.getItemAt(0)
when (call.method) {
"image" -> {
first?.uri?.let {
val mime = cr.getType(it)
if (mime == null || !mime.startsWith("image")) return result.success(null)
result.success(cr.openInputStream(it).use { stream ->
stream?.buffered()?.readBytes()
})
}
result.success(null)
}
"files" -> {
manager.primaryClip?.run {
if (itemCount == 0) result.success(null)
val files: MutableList<String> = mutableListOf()
for (i in 0 until itemCount) {
getItemAt(i).uri?.let {
files.add(it.toString())
}
}
result.success(files)
}
}
"html" -> result.success(first?.htmlText)
"writeFiles" -> {
val args = call.arguments<List<String>>() ?: return result.error(
"NoArgs",
"Missing Arguments",
null,
)
val clip: ClipData? = null
for (i in args) {
val uri = Uri.parse(i)
clip ?: ClipData.newUri(cr, "files", uri)
clip?.addItem(ClipData.Item(uri))
}
clip?.let {
manager.setPrimaryClip(it)
}
result.success(null)
}
"writeImage" -> {
val image = call.arguments<ByteArray>() ?: return result.error(
"NoArgs",
"Missing Arguments",
null,
)
val out = ByteArrayOutputStream()
thread {
val bitmap = BitmapFactory.decodeByteArray(image, 0, image.size)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
}
val name = UUID.randomUUID().toString()
val file = File(context.cacheDir, name)
FileOutputStream(file).use {
out.writeTo(it)
}
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
val clip = ClipData.newUri(cr, "image.png", uri)
manager.setPrimaryClip(clip)
}
else -> result.notImplemented()
}
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external_files"
path="." />
</paths>
6 changes: 3 additions & 3 deletions packages/pasteboard/lib/src/pasteboard_platform_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class PasteboardPlatformIO implements PasteboardPlatform {

@override
Future<String?> get html async {
if (Platform.isWindows) {
if (Platform.isWindows || Platform.isAndroid) {
return await _channel.invokeMethod<Object>('html') as String?;
}
return null;
Expand All @@ -35,7 +35,7 @@ class PasteboardPlatformIO implements PasteboardPlatform {
if (image == null) {
return null;
}
if (Platform.isMacOS || Platform.isLinux || Platform.isIOS) {
if (Platform.isMacOS || Platform.isLinux || Platform.isIOS || Platform.isAndroid) {
return image as Uint8List;
} else if (Platform.isWindows) {
final file = File(image as String);
Expand All @@ -62,7 +62,7 @@ class PasteboardPlatformIO implements PasteboardPlatform {
if (image == null) {
return;
}
if (Platform.isIOS || Platform.isMacOS) {
if (Platform.isIOS || Platform.isMacOS || Platform.isAndroid) {
await _channel.invokeMethod<void>('writeImage', image);
} else if (Platform.isWindows) {
final file = await File(GetTempFileName()).create();
Expand Down
3 changes: 3 additions & 0 deletions packages/pasteboard/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ dev_dependencies:
flutter:
plugin:
platforms:
android:
package: one.mixin.pasteboard
pluginClass: PasteboardPlugin
macos:
pluginClass: PasteboardPlugin
windows:
Expand Down