|
|
|
@ -10,6 +10,8 @@ import android.content.Context |
|
|
|
import android.content.pm.PackageManager |
|
|
|
import android.os.Build |
|
|
|
import android.os.Bundle |
|
|
|
import android.os.Handler |
|
|
|
import android.os.Looper |
|
|
|
import android.os.VibrationEffect |
|
|
|
import android.os.Vibrator |
|
|
|
import android.os.VibratorManager |
|
|
|
@ -49,10 +51,14 @@ class MainActivity : AppCompatActivity() { |
|
|
|
// Authoritative Permanent Gateway Server on Linux (116.16.16.19:8999) |
|
|
|
private val gatewayHost = "116.16.16.19:8999" |
|
|
|
|
|
|
|
// Live Synchronized State (Google Docs / Figma style) |
|
|
|
// Live Synchronized State (Collaborative Engine) |
|
|
|
private var currentRevision: Long = 0L |
|
|
|
private var isApplyingRemoteUpdate = false |
|
|
|
private var lastLocalText = "" |
|
|
|
private var lastLocalUserEditTime = 0L |
|
|
|
|
|
|
|
private val debounceHandler = Handler(Looper.getMainLooper()) |
|
|
|
private var pendingSyncRunnable: Runnable? = null |
|
|
|
|
|
|
|
// Voice Insertion Anchor |
|
|
|
private var voiceInsertionCursorStart = 0 |
|
|
|
@ -79,7 +85,7 @@ class MainActivity : AppCompatActivity() { |
|
|
|
setupUI() |
|
|
|
checkPermissions() |
|
|
|
|
|
|
|
AppLogger.log("Main", "اپلیکیشن راهاندازی شد (همگامسازی بلادرنگ Google Docs/Figma Style با مک)") |
|
|
|
AppLogger.log("Main", "اپلیکیشن راهاندازی شد (همگامسازی بلادرنگ بدون باگ پاککردن)") |
|
|
|
|
|
|
|
// Initialize Collaborative WebSocket Client |
|
|
|
streamDictationClient = StreamDictationClient( |
|
|
|
@ -91,16 +97,23 @@ class MainActivity : AppCompatActivity() { |
|
|
|
) |
|
|
|
}, |
|
|
|
onSyncStateReceived = { state -> |
|
|
|
// Apply update from Mac if source != android and revision is newer |
|
|
|
// Drop echoes and do not interrupt active user editing on phone |
|
|
|
if (state.source != "android") { |
|
|
|
val timeSinceLocalEdit = System.currentTimeMillis() - lastLocalUserEditTime |
|
|
|
|
|
|
|
// If user is actively typing or backspacing right now on phone, do not override with remote echo |
|
|
|
if (timeSinceLocalEdit < 1000L && binding.etTranscript.hasFocus()) { |
|
|
|
return@StreamDictationClient |
|
|
|
} |
|
|
|
|
|
|
|
if (state.text != lastLocalText) { |
|
|
|
isApplyingRemoteUpdate = true |
|
|
|
lastLocalText = state.text |
|
|
|
currentRevision = state.revision |
|
|
|
|
|
|
|
// Preserve cursor safely |
|
|
|
val currentCursor = binding.etTranscript.selectionStart |
|
|
|
binding.etTranscript.setText(state.text) |
|
|
|
|
|
|
|
val targetCursor = if (binding.etTranscript.hasFocus() && currentCursor >= 0) { |
|
|
|
currentCursor.coerceIn(0, state.text.length) |
|
|
|
} else { |
|
|
|
@ -143,20 +156,15 @@ class MainActivity : AppCompatActivity() { |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Dynamically adjusts the layout when virtual keyboard opens or closes |
|
|
|
*/ |
|
|
|
private fun setupKeyboardInsetHandling() { |
|
|
|
ViewCompat.setOnApplyWindowInsetsListener(binding.rootLayout) { _, insets -> |
|
|
|
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) |
|
|
|
|
|
|
|
if (imeVisible) { |
|
|
|
// Keyboard OPEN: Hide huge bottom circle, expand editor box to full height directly above keyboard |
|
|
|
binding.bottomVoiceSection.visibility = View.GONE |
|
|
|
binding.tvSubtitle.visibility = View.GONE |
|
|
|
binding.btnMiniMic.visibility = View.VISIBLE |
|
|
|
} else { |
|
|
|
// Keyboard CLOSED: Restore spacious layout with large glowing mic button |
|
|
|
binding.bottomVoiceSection.visibility = View.VISIBLE |
|
|
|
binding.tvSubtitle.visibility = View.VISIBLE |
|
|
|
binding.btnMiniMic.visibility = View.GONE |
|
|
|
@ -166,9 +174,6 @@ class MainActivity : AppCompatActivity() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Slices speech text right at the exact cursor/selection location |
|
|
|
*/ |
|
|
|
private fun insertSpeechAtCursor(speechText: String) { |
|
|
|
val current = binding.etTranscript.text?.toString() ?: "" |
|
|
|
val start = voiceInsertionCursorStart.coerceIn(0, current.length) |
|
|
|
@ -188,18 +193,18 @@ class MainActivity : AppCompatActivity() { |
|
|
|
|
|
|
|
isApplyingRemoteUpdate = true |
|
|
|
lastLocalText = mergedText |
|
|
|
lastLocalUserEditTime = System.currentTimeMillis() |
|
|
|
binding.etTranscript.setText(mergedText) |
|
|
|
binding.etTranscript.setSelection(newCursor) |
|
|
|
isApplyingRemoteUpdate = false |
|
|
|
|
|
|
|
AppLogger.log("Main", "تزریق گفتار در نشانگر: '$speechText' (موقعیت جدید: $newCursor)") |
|
|
|
|
|
|
|
// Broadcast updated state to Mac immediately! |
|
|
|
streamDictationClient?.sendLocalSyncState(mergedText, newCursor) |
|
|
|
} |
|
|
|
|
|
|
|
private fun setupUI() { |
|
|
|
// Real-time TextWatcher for local keyboard typing |
|
|
|
// Real-time TextWatcher for local keyboard typing & backspacing |
|
|
|
binding.etTranscript.addTextChangedListener(object : TextWatcher { |
|
|
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} |
|
|
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { |
|
|
|
@ -207,11 +212,17 @@ class MainActivity : AppCompatActivity() { |
|
|
|
val wordCount = if (text.trim().isEmpty()) 0 else text.trim().split("\\s+".toRegex()).size |
|
|
|
binding.tvCharCount.text = "$wordCount کلمه" |
|
|
|
|
|
|
|
// If typed locally by user, broadcast to Mac immediately! |
|
|
|
if (!isApplyingRemoteUpdate && !isCurrentlyRecording) { |
|
|
|
lastLocalText = text |
|
|
|
lastLocalUserEditTime = System.currentTimeMillis() |
|
|
|
val cur = binding.etTranscript.selectionStart.coerceIn(0, text.length) |
|
|
|
streamDictationClient?.sendLocalSyncState(text, cur) |
|
|
|
|
|
|
|
// Debounce sending to Mac by 60ms to allow 120Hz lag-free backspacing and typing |
|
|
|
pendingSyncRunnable?.let { debounceHandler.removeCallbacks(it) } |
|
|
|
pendingSyncRunnable = Runnable { |
|
|
|
streamDictationClient?.sendLocalSyncState(text, cur) |
|
|
|
} |
|
|
|
debounceHandler.postDelayed(pendingSyncRunnable!!, 60) |
|
|
|
} |
|
|
|
} |
|
|
|
override fun afterTextChanged(s: Editable?) {} |
|
|
|
@ -222,6 +233,7 @@ class MainActivity : AppCompatActivity() { |
|
|
|
isApplyingRemoteUpdate = true |
|
|
|
binding.etTranscript.setText("") |
|
|
|
lastLocalText = "" |
|
|
|
lastLocalUserEditTime = System.currentTimeMillis() |
|
|
|
isApplyingRemoteUpdate = false |
|
|
|
streamDictationClient?.sendLocalSyncState("", 0) |
|
|
|
binding.tvInstruction.text = getString(R.string.hold_to_speak) |
|
|
|
|