diff --git a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt index c593981..b31e990 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt @@ -8,7 +8,11 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.content.pm.PackageManager +import android.media.MediaPlayer import android.net.Uri +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ListView import android.content.res.ColorStateList import android.graphics.Color import android.graphics.PixelFormat @@ -67,6 +71,16 @@ class MainActivity : Activity(), SvxService.Listener { private lateinit var btnSettings: ImageButton private lateinit var btnMonitor: ImageButton private lateinit var btnRecord: ImageButton + // Список записей: воспроизведение/удаление + private var recPlayer: MediaPlayer? = null + private var recPlayingPath: String? = null + private var recLongPressFired = false + private var recListAdapter: ArrayAdapter? = null + private val recLongPressTask = Runnable { + recLongPressFired = true + Log.d(TAG, "recBtn long-press FIRED") + openRecordingsList() + } private val mainHandler = Handler(Looper.getMainLooper()) private var titleHoldRunnable: Runnable? = null @@ -155,6 +169,23 @@ class MainActivity : Activity(), SvxService.Listener { btnMonitor.setOnClickListener { toggleMonitoring() } btnRecord.setOnClickListener { toggleRecording() } + btnRecord.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + recLongPressFired = false + Log.d(TAG, "recBtn DOWN, scheduling long-press") + mainHandler.postDelayed(recLongPressTask, 3000) + false + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + Log.d(TAG, "recBtn UP/CANCEL, longFired=$recLongPressFired") + mainHandler.removeCallbacks(recLongPressTask) + // Если сработал долгий тап — гасим клик (иначе переключит запись) + recLongPressFired + } + else -> false + } + } updateMonitorButton() val testText = intent?.getStringExtra("node_toast_test") if (testText != null) { @@ -308,6 +339,8 @@ class MainActivity : Activity(), SvxService.Listener { hideNodeOverlay() aprsMeteo?.stop() aprsMeteo = null + mainHandler.removeCallbacks(recLongPressTask) + stopRecordingPlayback() mainHandler.removeCallbacksAndMessages(null) SplashActivity.mainAlive = false svc?.listener = null @@ -697,6 +730,103 @@ class MainActivity : Activity(), SvxService.Listener { btnRecord.contentDescription = getString(R.string.btn_record) } + // ---------- Список записей: воспроизведение / удаление ---------- + + private fun recordingsDir(): File = + File(getExternalFilesDir(null) ?: filesDir, "recordings") + + private fun openRecordingsList() { + Log.d(TAG, "openRecordingsList") + val dir = recordingsDir() + Log.d(TAG, "recordings dir exists=${dir.exists()} path=${dir.absolutePath}") + val files = (dir.listFiles { f -> f.extension.equals("ogg", true) } + ?: emptyArray()).toMutableList() + .apply { sortByDescending { it.name } } + Log.d(TAG, "recordings count=${files.size}") + if (files.isEmpty()) { + toast("Нет записей") + return + } + val dialog = AlertDialog.Builder(this) + .setTitle(R.string.recordings_title) + .create() + val listView = ListView(this).apply { + val adapter = object : ArrayAdapter(this@MainActivity, 0, files) { + override fun getView(pos: Int, convertView: View?, parent: ViewGroup): View { + val file = getItem(pos)!! + val row = convertView + ?: layoutInflater.inflate(R.layout.dialog_recording_item, parent, false) + row.findViewById(R.id.rec_name).text = file.name + val playBtn = row.findViewById(R.id.rec_play) + val delBtn = row.findViewById(R.id.rec_delete) + val playing = recPlayingPath == file.absolutePath && recPlayer?.isPlaying == true + playBtn.setImageResource(if (playing) R.drawable.ic_stop else R.drawable.ic_play) + playBtn.setOnClickListener { togglePlayRecording(file.absolutePath) } + delBtn.setOnClickListener { confirmDeleteRecording(file, this, dialog) } + return row + } + } + recListAdapter = adapter + this.adapter = adapter + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + (resources.displayMetrics.heightPixels * 0.6).toInt() + ) + } + dialog.setView(listView) + dialog.setOnDismissListener { stopRecordingPlayback() } + dialog.show() + } + + private fun togglePlayRecording(path: String) { + if (recPlayingPath == path && recPlayer?.isPlaying == true) { + stopRecordingPlayback() + refreshRecordingList() + return + } + stopRecordingPlayback() + try { + val mp = MediaPlayer().apply { + setDataSource(path) + setOnCompletionListener { stopRecordingPlayback(); refreshRecordingList() } + prepare() + start() + } + recPlayer = mp + recPlayingPath = path + refreshRecordingList() + } catch (e: Exception) { + toast(getString(R.string.record_play_failed)) + } + } + + private fun stopRecordingPlayback() { + try { recPlayer?.stop() } catch (_: Exception) {} + try { recPlayer?.release() } catch (_: Exception) {} + recPlayer = null + recPlayingPath = null + } + + private fun refreshRecordingList() { + recListAdapter?.notifyDataSetChanged() + } + + private fun confirmDeleteRecording(file: File, adapter: ArrayAdapter, dialog: AlertDialog) { + AlertDialog.Builder(this) + .setTitle(R.string.record_delete_title) + .setMessage(file.name) + .setPositiveButton(R.string.record_delete_yes) { _, _ -> + if (recPlayingPath == file.absolutePath) stopRecordingPlayback() + file.delete() + adapter.remove(file) + adapter.notifyDataSetChanged() + toast(getString(R.string.record_deleted)) + if (adapter.count == 0) dialog.dismiss() + } + .setNegativeButton(R.string.cancel, null) + .show() + } + /** Переключиться на другую группу: отправить серверу по живому соединению. */ private fun switchGroup(tg: Long) { settings = settings.copy(talkGroup = tg) diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..804fef0 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 0000000..e2eb1c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_stop.xml b/app/src/main/res/drawable/ic_stop.xml new file mode 100644 index 0000000..e8d04b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_stop.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/dialog_recording_item.xml b/app/src/main/res/layout/dialog_recording_item.xml new file mode 100644 index 0000000..fa5ba15 --- /dev/null +++ b/app/src/main/res/layout/dialog_recording_item.xml @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d6b4d2..4a2a158 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,6 +49,11 @@ Запись приёма начата Сохранено: %1$s Не удалось начать запись (формат OGG недоступен) + Записи приёма + Не удалось воспроизвести запись + Удалить запись? + Удалить + Удалено Мониторинг: %1$s Говорит TG%1$d RX