|
|
@ -8,6 +8,7 @@ import android.content.ClipData |
|
|
import android.content.ClipboardManager |
|
|
import android.content.ClipboardManager |
|
|
import android.content.Context |
|
|
import android.content.Context |
|
|
import android.content.pm.PackageManager |
|
|
import android.content.pm.PackageManager |
|
|
|
|
|
import android.graphics.Rect |
|
|
import android.os.Build |
|
|
import android.os.Build |
|
|
import android.os.Bundle |
|
|
import android.os.Bundle |
|
|
import android.os.Handler |
|
|
import android.os.Handler |
|
|
@ -20,6 +21,7 @@ import android.text.TextWatcher |
|
|
import android.view.KeyEvent |
|
|
import android.view.KeyEvent |
|
|
import android.view.MotionEvent |
|
|
import android.view.MotionEvent |
|
|
import android.view.View |
|
|
import android.view.View |
|
|
|
|
|
import android.view.ViewTreeObserver |
|
|
import android.view.animation.AccelerateDecelerateInterpolator |
|
|
import android.view.animation.AccelerateDecelerateInterpolator |
|
|
import android.widget.ScrollView |
|
|
import android.widget.ScrollView |
|
|
import android.widget.TextView |
|
|
import android.widget.TextView |
|
|
@ -56,6 +58,7 @@ class MainActivity : AppCompatActivity() { |
|
|
private var isApplyingRemoteUpdate = false |
|
|
private var isApplyingRemoteUpdate = false |
|
|
private var lastLocalText = "" |
|
|
private var lastLocalText = "" |
|
|
private var lastLocalUserEditTime = 0L |
|
|
private var lastLocalUserEditTime = 0L |
|
|
|
|
|
private var isKeyboardCurrentlyVisible = false |
|
|
|
|
|
|
|
|
private val debounceHandler = Handler(Looper.getMainLooper()) |
|
|
private val debounceHandler = Handler(Looper.getMainLooper()) |
|
|
private var pendingSyncRunnable: Runnable? = null |
|
|
private var pendingSyncRunnable: Runnable? = null |
|
|
@ -81,11 +84,11 @@ class MainActivity : AppCompatActivity() { |
|
|
binding = ActivityMainBinding.inflate(layoutInflater) |
|
|
binding = ActivityMainBinding.inflate(layoutInflater) |
|
|
setContentView(binding.root) |
|
|
setContentView(binding.root) |
|
|
|
|
|
|
|
|
setupKeyboardInsetHandling() |
|
|
|
|
|
|
|
|
setupKeyboardVisibilityDetection() |
|
|
setupUI() |
|
|
setupUI() |
|
|
checkPermissions() |
|
|
checkPermissions() |
|
|
|
|
|
|
|
|
AppLogger.log("Main", "اپلیکیشن راهاندازی شد (همگامسازی بلادرنگ بدون باگ پاککردن)") |
|
|
|
|
|
|
|
|
AppLogger.log("Main", "اپلیکیشن راهاندازی شد (طراحی ریسپانسیو و فوکوس کامل اینپوتباکس روی کیبورد)") |
|
|
|
|
|
|
|
|
// Initialize Collaborative WebSocket Client |
|
|
// Initialize Collaborative WebSocket Client |
|
|
streamDictationClient = StreamDictationClient( |
|
|
streamDictationClient = StreamDictationClient( |
|
|
@ -156,21 +159,48 @@ class MainActivity : AppCompatActivity() { |
|
|
) |
|
|
) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private fun setupKeyboardInsetHandling() { |
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Dual-engine keyboard visibility detector (WindowInsets + OnGlobalLayoutListener) |
|
|
|
|
|
* Guarantees 100% detection on all Android versions and keyboards. |
|
|
|
|
|
*/ |
|
|
|
|
|
private fun setupKeyboardVisibilityDetection() { |
|
|
|
|
|
// Engine 1: Modern WindowInsets |
|
|
ViewCompat.setOnApplyWindowInsetsListener(binding.rootLayout) { _, insets -> |
|
|
ViewCompat.setOnApplyWindowInsetsListener(binding.rootLayout) { _, insets -> |
|
|
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) |
|
|
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) |
|
|
|
|
|
updateKeyboardUIMode(imeVisible) |
|
|
|
|
|
insets |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (imeVisible) { |
|
|
|
|
|
binding.bottomVoiceSection.visibility = View.GONE |
|
|
|
|
|
binding.tvSubtitle.visibility = View.GONE |
|
|
|
|
|
binding.btnMiniMic.visibility = View.VISIBLE |
|
|
|
|
|
} else { |
|
|
|
|
|
binding.bottomVoiceSection.visibility = View.VISIBLE |
|
|
|
|
|
binding.tvSubtitle.visibility = View.VISIBLE |
|
|
|
|
|
binding.btnMiniMic.visibility = View.GONE |
|
|
|
|
|
|
|
|
// Engine 2: Global Layout Frame Calculation (Fallback for OEM soft keyboards) |
|
|
|
|
|
binding.rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { |
|
|
|
|
|
private val r = Rect() |
|
|
|
|
|
override fun onGlobalLayout() { |
|
|
|
|
|
binding.rootLayout.getWindowVisibleDisplayFrame(r) |
|
|
|
|
|
val screenHeight = binding.rootLayout.rootView.height |
|
|
|
|
|
val keypadHeight = screenHeight - r.bottom |
|
|
|
|
|
val isKeyboardOpen = keypadHeight > screenHeight * 0.15 |
|
|
|
|
|
updateKeyboardUIMode(isKeyboardOpen) |
|
|
} |
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
insets |
|
|
|
|
|
|
|
|
private fun updateKeyboardUIMode(isKeyboardOpen: Boolean) { |
|
|
|
|
|
if (isKeyboardCurrentlyVisible == isKeyboardOpen) return |
|
|
|
|
|
isKeyboardCurrentlyVisible = isKeyboardOpen |
|
|
|
|
|
|
|
|
|
|
|
if (isKeyboardOpen) { |
|
|
|
|
|
// KEYBOARD OPEN: Hide big mic circle, hide action buttons & divider completely! |
|
|
|
|
|
// Give 100% of available space above keyboard exclusively to the huge text editor box! |
|
|
|
|
|
binding.bottomVoiceSection.visibility = View.GONE |
|
|
|
|
|
binding.actionDivider.visibility = View.GONE |
|
|
|
|
|
binding.actionButtonsRow.visibility = View.GONE |
|
|
|
|
|
binding.tvSubtitle.visibility = View.GONE |
|
|
|
|
|
} else { |
|
|
|
|
|
// KEYBOARD CLOSED: Restore spacious layout with large glowing mic button & action bar |
|
|
|
|
|
binding.bottomVoiceSection.visibility = View.VISIBLE |
|
|
|
|
|
binding.actionDivider.visibility = View.VISIBLE |
|
|
|
|
|
binding.actionButtonsRow.visibility = View.VISIBLE |
|
|
|
|
|
binding.tvSubtitle.visibility = View.VISIBLE |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -287,11 +317,6 @@ class MainActivity : AppCompatActivity() { |
|
|
binding.btnMic.setOnTouchListener { _, event -> |
|
|
binding.btnMic.setOnTouchListener { _, event -> |
|
|
handleMicTouch(event) |
|
|
handleMicTouch(event) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Touch listener for Compact Mini Mic Button (above keyboard) |
|
|
|
|
|
binding.btnMiniMic.setOnTouchListener { _, event -> |
|
|
|
|
|
handleMicTouch(event) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private fun handleMicTouch(event: MotionEvent): Boolean { |
|
|
private fun handleMicTouch(event: MotionEvent): Boolean { |
|
|
@ -413,7 +438,6 @@ class MainActivity : AppCompatActivity() { |
|
|
binding.tvInstruction.text = getString(R.string.release_to_type) |
|
|
binding.tvInstruction.text = getString(R.string.release_to_type) |
|
|
binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_red)) |
|
|
binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_red)) |
|
|
binding.btnMic.setBackgroundResource(R.drawable.bg_mic_button_active) |
|
|
binding.btnMic.setBackgroundResource(R.drawable.bg_mic_button_active) |
|
|
binding.btnMiniMic.setBackgroundColor(ContextCompat.getColor(this, R.color.accent_red)) |
|
|
|
|
|
|
|
|
|
|
|
startPulseAnimation() |
|
|
startPulseAnimation() |
|
|
streamDictationClient?.startRecording(voiceInsertionCursorStart) |
|
|
streamDictationClient?.startRecording(voiceInsertionCursorStart) |
|
|
@ -425,7 +449,6 @@ class MainActivity : AppCompatActivity() { |
|
|
|
|
|
|
|
|
stopPulseAnimation() |
|
|
stopPulseAnimation() |
|
|
binding.btnMic.setBackgroundResource(R.drawable.bg_mic_button) |
|
|
binding.btnMic.setBackgroundResource(R.drawable.bg_mic_button) |
|
|
binding.btnMiniMic.setBackgroundColor(ContextCompat.getColor(this, R.color.accent_blue)) |
|
|
|
|
|
binding.tvInstruction.text = "⏳ در حال درج و همگامسازی..." |
|
|
binding.tvInstruction.text = "⏳ در حال درج و همگامسازی..." |
|
|
binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_yellow)) |
|
|
binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_yellow)) |
|
|
|
|
|
|
|
|
|