@@ -224,16 +224,14 @@ class DialogLogFragment : DialogFragment() {
224224 private var hasError = false
225225 private var lineCount = 0
226226
227+ // logBuffer giờ chỉ chạy trên luồng gọi (background thread của Executor)
227228 private val logBuffer = SpannableStringBuilder ()
228-
229- // Vị trí bắt đầu của dòng hiện tại (dòng chưa kết thúc bằng \n) trong logBuffer.
230229 private var lineStart = 0
231- // true nếu vừa gặp \r và đang chờ nội dung tiếp theo ghi đè lên dòng hiện tại
232- // (bắt đầu từ lineStart) thay vì nối thêm vào cuối.
233230 private var pendingOverwrite = false
234231
235232 init {
236- logView?.setText(logBuffer, TextView .BufferType .EDITABLE )
233+ // Không set trực tiếp editable ban đầu nữa, quản lý qua append tĩnh cho mượt
234+ logView?.text = " "
237235 }
238236
239237 private fun getColor (resId : Int ): Int {
@@ -263,13 +261,11 @@ class DialogLogFragment : DialogFragment() {
263261 if (prompt.isNotEmpty()) {
264262 input.hint = prompt
265263 }
266- input.post {
267- input.isFocusable = true
268- input.isFocusableInTouchMode = true
269- input.requestFocus()
270- val imm = context.getSystemService(Context .INPUT_METHOD_SERVICE ) as ? InputMethodManager
271- imm?.showSoftInput(input, InputMethodManager .SHOW_IMPLICIT )
272- }
264+ input.isFocusable = true
265+ input.isFocusableInTouchMode = true
266+ input.requestFocus()
267+ val imm = context.getSystemService(Context .INPUT_METHOD_SERVICE ) as ? InputMethodManager
268+ imm?.showSoftInput(input, InputMethodManager .SHOW_IMPLICIT )
273269 }
274270 }
275271 }
@@ -293,11 +289,15 @@ class DialogLogFragment : DialogFragment() {
293289
294290 private fun resetLogState () {
295291 AnsiColorParser .reset()
296- lineCount = 0
297- lineStart = 0
298- pendingOverwrite = false
299- logBuffer.clear()
300- logViewRef.get()?.text = logBuffer
292+ synchronized(logBuffer) {
293+ lineCount = 0
294+ lineStart = 0
295+ pendingOverwrite = false
296+ logBuffer.clear()
297+ }
298+ logViewRef.get()?.post {
299+ logViewRef.get()?.text = " "
300+ }
301301 }
302302
303303 override fun onStart (forceStop : Runnable ? ) {
@@ -357,9 +357,6 @@ class DialogLogFragment : DialogFragment() {
357357 var parsedLog: CharSequence = AnsiColorParser .parse(text)
358358
359359 if (forcedColor != null && ! text.contains(" \u001B [" )) {
360- // Tô màu toàn bộ nội dung; các ký tự điều khiển \r/\n (nếu có, ở bất kỳ vị trí
361- // nào trong chuỗi) sẽ được dispatchLogUpdate xử lý riêng, không bị vấn đề vì
362- // ForegroundColorSpan không ảnh hưởng đến cách ngắt dòng.
363360 val spannable = SpannableStringBuilder (parsedLog)
364361 spannable.setSpan(
365362 ForegroundColorSpan (forcedColor),
@@ -374,50 +371,48 @@ class DialogLogFragment : DialogFragment() {
374371 }
375372
376373 /* *
377- * Ghi nội dung mới vào logBuffer, xử lý đúng ngữ nghĩa \r (ghi đè dòng hiện tại,
378- * dùng cho progress %) và \n (xuống dòng thật), kể cả khi các ký tự này nằm
379- * ở giữa chuỗi chứ không chỉ ở cuối.
374+ * XỬ LÝ CHUỖI TẠI BACKGROUND THREAD ĐỂ GIẢM TẢI UI THREAD
380375 */
381376 private fun dispatchLogUpdate (formattedText : CharSequence ) {
382377 val logView = logViewRef.get() ? : return
378+
379+ // Biến cục bộ để xác định xem dòng này có yêu cầu ghi đè hay không (\r)
380+ var isOverwriteAction = false
381+ val snapshotToDisplay: CharSequence
383382
384- logView.post {
383+ synchronized(logBuffer) {
385384 var i = 0
386385 val len = formattedText.length
387386
388387 while (i < len) {
389- // Tìm ký tự điều khiển (\r hoặc \n) gần nhất kể từ i
390388 var j = i
391389 while (j < len && formattedText[j] != ' \r ' && formattedText[j] != ' \n ' ) j++
392390
393- // Phần nội dung thuần trước ký tự điều khiển (hoặc tới hết chuỗi)
394391 if (j > i) {
395392 val segment = formattedText.subSequence(i, j)
396393 if (pendingOverwrite) {
397394 if (lineStart in 0 .. logBuffer.length) {
398395 logBuffer.delete(lineStart, logBuffer.length)
399396 }
400397 pendingOverwrite = false
398+ isOverwriteAction = true
401399 }
402400 logBuffer.append(segment)
403401 }
404402
405403 if (j < len) {
406404 when (formattedText[j]) {
407405 ' \r ' -> {
408- // \r\n (CRLF chuẩn) chỉ là xuống dòng bình thường, bỏ qua \r
409406 val isCRLF = j + 1 < len && formattedText[j + 1 ] == ' \n '
410407 if (! isCRLF) {
411- // Đánh dấu: nội dung tiếp theo sẽ ghi đè từ đầu dòng hiện tại
412408 pendingOverwrite = true
413409 }
414410 }
415411 ' \n ' -> {
416- // Nếu đang chờ ghi đè mà gặp \n ngay (không có nội dung mới sau \r)
417- // thì coi như dòng đó bị xoá trước khi xuống dòng thật
418412 if (pendingOverwrite && lineStart in 0 .. logBuffer.length) {
419413 logBuffer.delete(lineStart, logBuffer.length)
420414 pendingOverwrite = false
415+ isOverwriteAction = true
421416 }
422417 logBuffer.append(' \n ' )
423418 lineCount++
@@ -429,27 +424,58 @@ class DialogLogFragment : DialogFragment() {
429424 i = j
430425 }
431426
432- (logView.editableText ? : return @post).replace(
433- 0 ,
434- logView.editableText.length,
435- logBuffer
436- )
437-
438- // Thực hiện cuộn và giữ focus cho ô nhập liệu
439- (logView.parent as ? ScrollView )?.let { scrollView ->
440- scrollView.post {
441- // 1. Cuộn ScrollView xuống cuối cùng
442- scrollView.fullScroll(ScrollView .FOCUS_DOWN )
443-
444- // 2. Nếu ô nhập liệu đang hiện, ép hệ thống giữ con trỏ tại đây
445- val inputRow = inputRowRef.get()
446- val input = shellInputRef.get()
447- if (inputRow != null && inputRow.visibility == View .VISIBLE && input != null ) {
448- input.post {
449- input.requestFocus()
427+ // Cắt tỉa log tối đa 5000 dòng ngay tại luồng nền
428+ if (lineCount > 5000 ) {
429+ var deleteEndIndex = 0
430+ var linesToTrim = lineCount - 5000
431+ val currentCharSequence = logBuffer.toString()
432+
433+ for (k in currentCharSequence.indices) {
434+ if (currentCharSequence[k] == ' \n ' ) {
435+ linesToTrim--
436+ if (linesToTrim <= 0 ) {
437+ deleteEndIndex = k + 1
438+ break
450439 }
451440 }
452441 }
442+ if (deleteEndIndex > 0 ) {
443+ logBuffer.delete(0 , deleteEndIndex)
444+ lineCount = 5000
445+ lineStart = (lineStart - deleteEndIndex).coerceAtLeast(0 )
446+ isOverwriteAction = true // Cần reload toàn bộ vì cấu trúc buffer đã dịch chuyển
447+ }
448+ }
449+
450+ // Chụp lại trạng thái text hiện tại để ném lên UI hiển thị
451+ snapshotToDisplay = SpannableStringBuilder (logBuffer)
452+ }
453+
454+ // ĐẨY LÊN UI THREAD ĐỂ HIỂN THỊ
455+ logView.post {
456+ if (isOverwriteAction) {
457+ // Nếu có lệnh xóa dòng cũ đè dòng mới (\r) hoặc vừa bị cắt tỉa log > 5000 dòng, bắt buộc phải đặt lại text
458+ logView.text = snapshotToDisplay
459+ } else {
460+ // NẾU LÀ GHI TIẾP (APPEND): Đây là chìa khóa giúp log mượt!
461+ // Thay vì nạp lại toàn bộ 5000 dòng, chỉ truyền phần text mới tinh vào.
462+ val oldLen = logView.text.length
463+ if (snapshotToDisplay.length > oldLen) {
464+ val newChunk = snapshotToDisplay.subSequence(oldLen, snapshotToDisplay.length)
465+ logView.append(newChunk)
466+ }
467+ }
468+
469+ // Tối ưu cuộn ScrollView xuống cuối
470+ (logView.parent as ? ScrollView )?.let { scrollView ->
471+ scrollView.fullScroll(ScrollView .FOCUS_DOWN )
472+
473+ // Giữ focus thông minh không cần lồng post{} nhiều tầng
474+ val inputRow = inputRowRef.get()
475+ val input = shellInputRef.get()
476+ if (inputRow != null && inputRow.visibility == View .VISIBLE && input != null ) {
477+ if (! input.isFocused) input.requestFocus()
478+ }
453479 }
454480 }
455481 }
0 commit comments