From 12213cd7e1f5924bec8bb880ee311c07e6b1d63e Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 17 Jan 2026 10:28:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor(android):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E7=9A=84=E6=82=AC=E6=B5=AE=E7=AA=97=E5=92=8C?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E6=9D=83=E9=99=90=E7=9B=B8=E5=85=B3=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BB=A3=E7=A0=81=EF=BC=9B=EF=BC=88=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0=E5=B7=B2=E7=BB=8F=E8=BD=AC?= =?UTF-8?q?=E7=A7=BB=E5=88=B0=E6=9C=AC=E5=9C=B0=E6=8F=92=E4=BB=B6floating?= =?UTF-8?q?=5Fui=5Fplugin=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../saitong/voitrans/FloatingWindowPlugin.kt | 133 ------ .../saitong/voitrans/FloatingWindowService.kt | 378 ------------------ .../voitrans/OverlayPermissionPlugin.kt | 99 ----- 3 files changed, 610 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowPlugin.kt delete mode 100644 android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt delete mode 100644 android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowPlugin.kt b/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowPlugin.kt deleted file mode 100644 index 9e3e2c7f2..000000000 --- a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowPlugin.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.saitong.voitrans - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.provider.Settings -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 - -class FloatingWindowPlugin: FlutterPlugin, MethodCallHandler { - private lateinit var channel: MethodChannel - private lateinit var context: Context - - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "floating_window") - channel.setMethodCallHandler(this) - - // 设置回调通道 - FloatingWindowService.methodChannel = channel - } - - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "enableFloatingWindow" -> { - enableFloatingWindow(result) - } - "disableFloatingWindow" -> { - disableFloatingWindow(result) - } - "showFloatingWindow" -> { - showFloatingWindow(result) - } - "hideFloatingWindow" -> { - hideFloatingWindow(result) - } - "updateContent" -> { - updateContent(call, result) - } - "setPosition" -> { - setPosition(call, result) - } - else -> { - result.notImplemented() - } - } - } - - private fun enableFloatingWindow(result: Result) { - try { - val intent = Intent(context, FloatingWindowService::class.java) - context.startService(intent) - result.success(true) - } catch (e: Exception) { - result.error("ENABLE_ERROR", "Failed to enable floating window: ${e.message}", null) - } - } - - private fun disableFloatingWindow(result: Result) { - try { - val intent = Intent(context, FloatingWindowService::class.java) - context.stopService(intent) - result.success(true) - } catch (e: Exception) { - result.error("DISABLE_ERROR", "Failed to disable floating window: ${e.message}", null) - } - } - - private fun showFloatingWindow(result: Result) { - try { - // 服务已经在运行,不需要额外操作 - result.success(true) - } catch (e: Exception) { - result.error("SHOW_ERROR", "Failed to show floating window: ${e.message}", null) - } - } - - private fun hideFloatingWindow(result: Result) { - try { - FloatingWindowService.serviceInstance?.stopSelf() - result.success(true) - } catch (e: Exception) { - result.error("HIDE_ERROR", "Failed to hide floating window: ${e.message}", null) - } - } - - private fun updateContent(call: MethodCall, result: Result) { - try { - val args = call.arguments as? Map - if (args != null) { - val intent = Intent(context, FloatingWindowService::class.java).apply { - action = FloatingWindowService.ACTION_UPDATE_CONTENT - putExtra(FloatingWindowService.EXTRA_SOURCE_TEXT, args["sourceText"] as? String ?: "") - putExtra(FloatingWindowService.EXTRA_TRANSLATED_TEXT, args["translatedText"] as? String ?: "") - putExtra(FloatingWindowService.EXTRA_SOURCE_LANGUAGE, args["sourceLanguage"] as? String ?: "") - putExtra(FloatingWindowService.EXTRA_TARGET_LANGUAGE, args["targetLanguage"] as? String ?: "") - putExtra(FloatingWindowService.EXTRA_TIMESTAMP, (args["timestamp"] as? Number)?.toLong() ?: 0L) - putExtra(FloatingWindowService.EXTRA_IS_INTERMEDIATE, args["isIntermediate"] as? Boolean ?: false) - } - context.startService(intent) - } - result.success(true) - } catch (e: Exception) { - result.error("UPDATE_ERROR", "Failed to update content: ${e.message}", null) - } - } - - private fun setPosition(call: MethodCall, result: Result) { - try { - val args = call.arguments as? Map - if (args != null) { - val intent = Intent(context, FloatingWindowService::class.java).apply { - action = FloatingWindowService.ACTION_SET_POSITION - putExtra(FloatingWindowService.EXTRA_X, (args["x"] as? Number)?.toInt() ?: 0) - putExtra(FloatingWindowService.EXTRA_Y, (args["y"] as? Number)?.toInt() ?: 0) - } - context.startService(intent) - } - result.success(true) - } catch (e: Exception) { - result.error("POSITION_ERROR", "Failed to set position: ${e.message}", null) - } - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - FloatingWindowService.methodChannel = null - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt b/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt deleted file mode 100644 index 296310038..000000000 --- a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt +++ /dev/null @@ -1,378 +0,0 @@ -package com.saitong.voitrans - -import android.app.Service -import android.content.Intent -import android.graphics.PixelFormat -import android.os.Build -import android.os.IBinder -import android.view.* -import android.widget.TextView -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.FrameLayout -import androidx.cardview.widget.CardView -import io.flutter.plugin.common.MethodChannel -import java.text.SimpleDateFormat -import java.util.* - -class FloatingWindowService : Service() { - private var windowManager: WindowManager? = null - private var floatingView: View? = null - private var isMinimized = true - private lateinit var layoutParams: WindowManager.LayoutParams - - private lateinit var sourceTextView: TextView - private lateinit var translatedTextView: TextView - private lateinit var languageInfoView: TextView - private lateinit var timestampView: TextView - private lateinit var minimizedView: CardView - private lateinit var expandedView: CardView - - companion object { - var methodChannel: MethodChannel? = null - var serviceInstance: FloatingWindowService? = null - - const val ACTION_UPDATE_CONTENT = "UPDATE_CONTENT" - const val ACTION_SET_POSITION = "SET_POSITION" - - const val EXTRA_SOURCE_TEXT = "source_text" - const val EXTRA_TRANSLATED_TEXT = "translated_text" - const val EXTRA_SOURCE_LANGUAGE = "source_language" - const val EXTRA_TARGET_LANGUAGE = "target_language" - const val EXTRA_TIMESTAMP = "timestamp" - const val EXTRA_IS_INTERMEDIATE = "is_intermediate" - const val EXTRA_X = "x" - const val EXTRA_Y = "y" - } - - override fun onCreate() { - super.onCreate() - serviceInstance = this - createFloatingWindow() - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - when (intent?.action) { - ACTION_UPDATE_CONTENT -> updateContent(intent) - ACTION_SET_POSITION -> setPosition(intent) - } - return START_STICKY - } - - override fun onBind(intent: Intent?): IBinder? = null - - private fun createFloatingWindow() { - windowManager = getSystemService(WINDOW_SERVICE) as WindowManager - - // 创建布局 - floatingView = createFloatingLayout() - - // 初始化视图组件 - initViews() - - // 设置点击事件 - setupClickListeners() - - // 设置窗口参数 - layoutParams = WindowManager.LayoutParams().apply { - width = WindowManager.LayoutParams.WRAP_CONTENT - height = WindowManager.LayoutParams.WRAP_CONTENT - type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY - } else { - WindowManager.LayoutParams.TYPE_PHONE - } - flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or - WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - format = PixelFormat.TRANSLUCENT - gravity = Gravity.TOP or Gravity.START - x = 100 - y = 100 - } - - windowManager?.addView(floatingView, layoutParams) - - // 默认显示最小化状态 - updateViewState(true) - } - - private fun createFloatingLayout(): View { - val context = this - val density = resources.displayMetrics.density - - // 创建根布局 - val rootLayout = FrameLayout(context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - - // 创建最小化视图 - val minimizedCard = CardView(context).apply { - id = View.generateViewId() - layoutParams = FrameLayout.LayoutParams( - (60 * density).toInt(), - (60 * density).toInt() - ).apply { - setMargins((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt()) - } - radius = 30 * density - cardElevation = 8 * density - setCardBackgroundColor(0xFFE3F2FD.toInt()) - } - - val minimizedContent = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - gravity = Gravity.CENTER - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - } - - val icon = TextView(context).apply { - text = "译" - textSize = 14f - setTextColor(0xFF1976D2.toInt()) - gravity = Gravity.CENTER - } - - minimizedContent.addView(icon) - minimizedCard.addView(minimizedContent) - - // 创建展开视图 - val expandedCard = CardView(context).apply { - id = View.generateViewId() - layoutParams = FrameLayout.LayoutParams( - (320 * density).toInt(), - ViewGroup.LayoutParams.WRAP_CONTENT - ).apply { - setMargins((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt()) - } - radius = 16 * density - cardElevation = 12 * density - setCardBackgroundColor(0xFFFFFFFF.toInt()) - visibility = View.GONE - } - - val expandedContent = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - setPadding( - (16 * density).toInt(), - (16 * density).toInt(), - (16 * density).toInt(), - (16 * density).toInt() - ) - } - - // 标题栏 - val titleLayout = LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - } - - val titleText = TextView(context).apply { - text = "实时翻译" - textSize = 16f - setTextColor(0xFF333333.toInt()) - layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) - } - - val closeButton = TextView(context).apply { - id = View.generateViewId() - text = "✕" - textSize = 18f - setTextColor(0xFF666666.toInt()) - gravity = Gravity.CENTER - setPadding((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt()) - } - - titleLayout.addView(titleText) - titleLayout.addView(closeButton) - - // 语言信息 - val languageInfo = TextView(context).apply { - id = View.generateViewId() - text = "中文 → 英文" - textSize = 12f - setTextColor(0xFF666666.toInt()) - setPadding(0, (8 * density).toInt(), 0, (8 * density).toInt()) - } - - // 源文本 - val sourceText = TextView(context).apply { - id = View.generateViewId() - text = "正在识别..." - textSize = 14f - setTextColor(0xFF333333.toInt()) - setPadding(0, (8 * density).toInt(), 0, (4 * density).toInt()) - maxLines = 3 - } - - // 翻译文本 - val translatedText = TextView(context).apply { - id = View.generateViewId() - text = "翻译中..." - textSize = 14f - setTextColor(0xFF1976D2.toInt()) - setPadding(0, (4 * density).toInt(), 0, (8 * density).toInt()) - maxLines = 3 - } - - // 时间戳 - val timestamp = TextView(context).apply { - id = View.generateViewId() - text = "" - textSize = 10f - setTextColor(0xFF999999.toInt()) - gravity = Gravity.END - } - - expandedContent.addView(titleLayout) - expandedContent.addView(languageInfo) - expandedContent.addView(sourceText) - expandedContent.addView(translatedText) - expandedContent.addView(timestamp) - expandedCard.addView(expandedContent) - - rootLayout.addView(minimizedCard) - rootLayout.addView(expandedCard) - - // 保存引用 - minimizedView = minimizedCard - expandedView = expandedCard - sourceTextView = sourceText - translatedTextView = translatedText - languageInfoView = languageInfo - timestampView = timestamp - - return rootLayout - } - - private fun initViews() { - // Views are already initialized in createFloatingLayout - } - - private fun setupClickListeners() { - // 长按关闭 - floatingView!!.setOnLongClickListener { - closeFloatingWindow() - true - } - - // 拖拽功能 - setupDragListener() - } - - private fun setupDragListener() { - var initialX = 0 - var initialY = 0 - var initialTouchX = 0f - var initialTouchY = 0f - var dragging = false - val threshold = (4 * resources.displayMetrics.density).toInt() - floatingView!!.setOnTouchListener { _, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - initialX = layoutParams.x - initialY = layoutParams.y - initialTouchX = event.rawX - initialTouchY = event.rawY - dragging = false - true - } - MotionEvent.ACTION_MOVE -> { - val dx = (event.rawX - initialTouchX).toInt() - val dy = (event.rawY - initialTouchY).toInt() - if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) dragging = true - val dm = resources.displayMetrics - val maxX = dm.widthPixels - (floatingView?.width ?: 0) - val maxY = dm.heightPixels - (floatingView?.height ?: 0) - layoutParams.x = (initialX + dx).coerceIn(0, maxX) - layoutParams.y = (initialY + dy).coerceIn(0, maxY) - windowManager?.updateViewLayout(floatingView, layoutParams) - true - } - MotionEvent.ACTION_UP -> { - if (!dragging) { - isMinimized = !isMinimized - updateViewState(isMinimized) - notifyStateChange(if (isMinimized) "minimized" else "expanded") - } - dragging = false - true - } - else -> true - } - } - } - - private fun updateViewState(minimized: Boolean) { - if (minimized) { - minimizedView.visibility = View.VISIBLE - expandedView.visibility = View.GONE - } else { - minimizedView.visibility = View.GONE - expandedView.visibility = View.VISIBLE - } - } - - private fun updateContent(intent: Intent) { - val sourceText = intent.getStringExtra(EXTRA_SOURCE_TEXT) ?: "" - val translatedText = intent.getStringExtra(EXTRA_TRANSLATED_TEXT) ?: "" - val sourceLanguage = intent.getStringExtra(EXTRA_SOURCE_LANGUAGE) ?: "" - val targetLanguage = intent.getStringExtra(EXTRA_TARGET_LANGUAGE) ?: "" - val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, 0) - val isIntermediate = intent.getBooleanExtra(EXTRA_IS_INTERMEDIATE, false) - - // 更新UI - sourceTextView.text = if (sourceText.isNotEmpty()) sourceText else "正在识别..." - translatedTextView.text = if (translatedText.isNotEmpty()) translatedText else "翻译中..." - languageInfoView.text = "$sourceLanguage → $targetLanguage" - - if (timestamp > 0) { - val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) - timestampView.text = dateFormat.format(Date(timestamp)) - } - - // 如果是中间结果,可以添加一些视觉提示 - if (isIntermediate) { - translatedTextView.alpha = 0.7f - } else { - translatedTextView.alpha = 1.0f - } - } - - private fun setPosition(intent: Intent) { - val x = intent.getIntExtra(EXTRA_X, 0) - val y = intent.getIntExtra(EXTRA_Y, 0) - - floatingView?.let { - layoutParams.x = x - layoutParams.y = y - windowManager?.updateViewLayout(it, layoutParams) - } - } - - private fun closeFloatingWindow() { - stopSelf() - notifyFloatingWindowClosed() - } - - private fun notifyStateChange(state: String) { - methodChannel?.invokeMethod("onFloatingWindowStateChanged", mapOf("state" to state)) - } - - private fun notifyFloatingWindowClosed() { - methodChannel?.invokeMethod("onFloatingWindowClosed", null) - } - - override fun onDestroy() { - floatingView?.let { windowManager?.removeView(it) } - serviceInstance = null - methodChannel = null - super.onDestroy() - } -} diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt b/android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt deleted file mode 100644 index e8496d57a..000000000 --- a/android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.saitong.voitrans - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.content.pm.PackageManager -import android.provider.Settings -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 - -class OverlayPermissionPlugin: FlutterPlugin, MethodCallHandler { - private lateinit var channel: MethodChannel - private lateinit var context: Context - - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "overlay_permission") - channel.setMethodCallHandler(this) - } - - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "hasOverlayPermission" -> { - result.success(hasOverlayPermission()) - } - "requestOverlayPermission" -> { - requestOverlayPermission(result) - } - else -> { - result.notImplemented() - } - } - } - - private fun hasOverlayPermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Settings.canDrawOverlays(context) - } else { - true - } - } - - private fun requestOverlayPermission(result: Result) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (hasOverlayPermission()) { - result.success(true) - return - } - - val openedBySystem = startActivitySafe( - Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply { - data = Uri.parse("package:${context.packageName}") - } - ) - if (openedBySystem) { - result.success(true) - return - } - - val openedByAppDetails = startActivitySafe( - Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.parse("package:${context.packageName}") - } - ) - if (openedByAppDetails) { - result.success(true) - return - } - - result.success(false) - } else { - result.success(true) - } - } catch (e: Exception) { - result.error("REQUEST_ERROR", "Failed to request overlay permission: ${e.message}", null) - } - } - - private fun startActivitySafe(intent: Intent): Boolean { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val pm: PackageManager = context.packageManager - if (intent.resolveActivity(pm) == null) return false - return try { - context.startActivity(intent) - true - } catch (_: Exception) { - false - } - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } -}