diff --git a/packages/pasteboard/android/.gitignore b/packages/pasteboard/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/pasteboard/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/pasteboard/android/build.gradle b/packages/pasteboard/android/build.gradle new file mode 100644 index 00000000..2376a19f --- /dev/null +++ b/packages/pasteboard/android/build.gradle @@ -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 + } + } + } +} diff --git a/packages/pasteboard/android/gradle/wrapper/gradle-wrapper.jar b/packages/pasteboard/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/packages/pasteboard/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/pasteboard/android/gradle/wrapper/gradle-wrapper.properties b/packages/pasteboard/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/packages/pasteboard/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/packages/pasteboard/android/settings.gradle b/packages/pasteboard/android/settings.gradle new file mode 100644 index 00000000..2a9683e3 --- /dev/null +++ b/packages/pasteboard/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'pasteboard' diff --git a/packages/pasteboard/android/src/main/AndroidManifest.xml b/packages/pasteboard/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..39e27b24 --- /dev/null +++ b/packages/pasteboard/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/packages/pasteboard/android/src/main/kotlin/one/mixin/pasteboard/PasteboardPlugin.kt b/packages/pasteboard/android/src/main/kotlin/one/mixin/pasteboard/PasteboardPlugin.kt new file mode 100644 index 00000000..ca3e7158 --- /dev/null +++ b/packages/pasteboard/android/src/main/kotlin/one/mixin/pasteboard/PasteboardPlugin.kt @@ -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 = 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>() ?: 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() ?: 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) + } +} diff --git a/packages/pasteboard/example/android/app/src/main/AndroidManifest.xml b/packages/pasteboard/example/android/app/src/main/AndroidManifest.xml index 74a78b93..69310c00 100644 --- a/packages/pasteboard/example/android/app/src/main/AndroidManifest.xml +++ b/packages/pasteboard/example/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,15 @@ + + +