提交 9e872968 authored 作者: 祁增奎's avatar 祁增奎

增加Android端实现,未测试,慎用

上级 cbccb2ae
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.clx.clx_listener_screen">
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
</manifest>
package com.clx.clx_listener_screen
import android.app.Activity
import android.content.Context
import android.database.ContentObserver
import android.hardware.display.DisplayManager
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.view.Display
import android.view.WindowManager
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
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
/** ClxListenerScreenPlugin */
class ClxListenerScreenPlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, ActivityAware {
private lateinit var methodChannel : MethodChannel
private lateinit var eventChannel : EventChannel
private var context: Context? = null
private var activity: Activity? = null
private var eventSink: EventChannel.EventSink? = null
private var displayManager: DisplayManager? = null
private var contentObserver: ContentObserver? = null
private var lastRecordingStatus = 0
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "clx_flutter_visible")
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "clx_flutter_screen_listener")
eventChannel.setStreamHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "secureTextEntry") {
val arguments = call.arguments as? Map<String, Any>
val show = arguments?.get("show") as? Boolean ?: true
setSecureTextEntry(show)
result.success(true)
} else {
result.notImplemented()
}
}
private fun setSecureTextEntry(isSecure: Boolean) {
activity?.let { act ->
if (isSecure) {
act.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
act.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
context = null
}
// ActivityAware
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
registerScreenCaptureCallback()
}
override fun onDetachedFromActivityForConfigChanges() {
unregisterScreenCaptureCallback()
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
registerScreenCaptureCallback()
}
override fun onDetachedFromActivity() {
unregisterScreenCaptureCallback()
activity = null
}
private fun registerScreenCaptureCallback() {
if (Build.VERSION.SDK_INT >= 34) { // Android 14 (UPSIDE_DOWN_CAKE)
activity?.registerScreenCaptureCallback(
context!!.mainExecutor,
screenCaptureCallback
)
}
}
private fun unregisterScreenCaptureCallback() {
if (Build.VERSION.SDK_INT >= 34) {
activity?.unregisterScreenCaptureCallback(screenCaptureCallback)
}
}
private val screenCaptureCallback by lazy {
if (Build.VERSION.SDK_INT >= 34) {
Activity.ScreenCaptureCallback {
activity?.runOnUiThread {
eventSink?.success(mapOf("type" to "shot"))
}
}
} else {
throw IllegalStateException("API level too low")
}
}
// StreamHandler
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
startListening()
}
override fun onCancel(arguments: Any?) {
stopListening()
eventSink = null
}
private fun startListening() {
context?.let { ctx ->
// Screenshot Detection (File based, for < Android 14 or when FLAG_SECURE is OFF)
contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
handleContentChange(uri)
}
}
ctx.contentResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
true,
contentObserver!!
)
// Screen Recording Detection
displayManager = ctx.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager?.registerDisplayListener(displayListener, Handler(Looper.getMainLooper()))
// Initial check
checkRecordingStatus()
}
}
private fun stopListening() {
context?.let { ctx ->
contentObserver?.let {
ctx.contentResolver.unregisterContentObserver(it)
}
contentObserver = null
displayManager?.unregisterDisplayListener(displayListener)
displayManager = null
}
}
private fun handleContentChange(uri: Uri?) {
if (uri == null) return
val uriString = uri.toString().lowercase()
if (uriString.contains("screenshot")) {
eventSink?.success(mapOf("type" to "shot"))
return
}
val projection = arrayOf(MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATA)
try {
context?.contentResolver?.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
val dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val name = if (nameIndex >= 0) cursor.getString(nameIndex) else ""
val path = if (dataIndex >= 0) cursor.getString(dataIndex) else ""
if (name.contains("screenshot", ignoreCase = true) || path.contains("screenshot", ignoreCase = true)) {
eventSink?.success(mapOf("type" to "shot"))
}
}
}
} catch (e: Exception) {
// Ignore
}
}
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = checkRecordingStatus()
override fun onDisplayRemoved(displayId: Int) = checkRecordingStatus()
override fun onDisplayChanged(displayId: Int) = checkRecordingStatus()
}
private fun checkRecordingStatus() {
val isRecording = isScreenRecording()
val status = if (isRecording) 1 else 2
val statusName = if (isRecording) "开始录制" else "结束录制"
if (lastRecordingStatus != status) {
lastRecordingStatus = status
eventSink?.success(mapOf(
"type" to "record",
"status" to status,
"statusName" to statusName
))
}
}
private fun isScreenRecording(): Boolean {
val displays = displayManager?.displays ?: return false
for (display in displays) {
if (display.flags and Display.FLAG_PRESENTATION != 0) {
return true
}
}
return false
}
}
......@@ -45,7 +45,7 @@ android {
applicationId "com.clx.example.clx_listener_screen_example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
......
......@@ -22,3 +22,6 @@ flutter:
platforms:
ios:
pluginClass: CLXListenerScreenPlugin
android:
package: com.clx.clx_listener_screen
pluginClass: ClxListenerScreenPlugin
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论