1.0.12: список записей по долгому тапу на кнопку записи
- Long-press (3 с) кнопки записи открывает диалог со списком файлов recordings/*.ogg. - В диалоге: воспроизведение (MediaPlayer, переключатель play/stop) и удаление с подтверждением. - Пустой список — тост «Нет записей». - Иконки ic_play/ic_stop/ic_delete, разметка dialog_recording_item.
This commit is contained in:
@@ -8,7 +8,11 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.media.MediaPlayer
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.ListView
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
@@ -67,6 +71,16 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
private lateinit var btnSettings: ImageButton
|
private lateinit var btnSettings: ImageButton
|
||||||
private lateinit var btnMonitor: ImageButton
|
private lateinit var btnMonitor: ImageButton
|
||||||
private lateinit var btnRecord: ImageButton
|
private lateinit var btnRecord: ImageButton
|
||||||
|
// Список записей: воспроизведение/удаление
|
||||||
|
private var recPlayer: MediaPlayer? = null
|
||||||
|
private var recPlayingPath: String? = null
|
||||||
|
private var recLongPressFired = false
|
||||||
|
private var recListAdapter: ArrayAdapter<File>? = null
|
||||||
|
private val recLongPressTask = Runnable {
|
||||||
|
recLongPressFired = true
|
||||||
|
Log.d(TAG, "recBtn long-press FIRED")
|
||||||
|
openRecordingsList()
|
||||||
|
}
|
||||||
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
private var titleHoldRunnable: Runnable? = null
|
private var titleHoldRunnable: Runnable? = null
|
||||||
@@ -155,6 +169,23 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
|
|
||||||
btnMonitor.setOnClickListener { toggleMonitoring() }
|
btnMonitor.setOnClickListener { toggleMonitoring() }
|
||||||
btnRecord.setOnClickListener { toggleRecording() }
|
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()
|
updateMonitorButton()
|
||||||
val testText = intent?.getStringExtra("node_toast_test")
|
val testText = intent?.getStringExtra("node_toast_test")
|
||||||
if (testText != null) {
|
if (testText != null) {
|
||||||
@@ -308,6 +339,8 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
hideNodeOverlay()
|
hideNodeOverlay()
|
||||||
aprsMeteo?.stop()
|
aprsMeteo?.stop()
|
||||||
aprsMeteo = null
|
aprsMeteo = null
|
||||||
|
mainHandler.removeCallbacks(recLongPressTask)
|
||||||
|
stopRecordingPlayback()
|
||||||
mainHandler.removeCallbacksAndMessages(null)
|
mainHandler.removeCallbacksAndMessages(null)
|
||||||
SplashActivity.mainAlive = false
|
SplashActivity.mainAlive = false
|
||||||
svc?.listener = null
|
svc?.listener = null
|
||||||
@@ -697,6 +730,103 @@ class MainActivity : Activity(), SvxService.Listener {
|
|||||||
btnRecord.contentDescription = getString(R.string.btn_record)
|
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<File>(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<TextView>(R.id.rec_name).text = file.name
|
||||||
|
val playBtn = row.findViewById<ImageButton>(R.id.rec_play)
|
||||||
|
val delBtn = row.findViewById<ImageButton>(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<File>, 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) {
|
private fun switchGroup(tg: Long) {
|
||||||
settings = settings.copy(talkGroup = tg)
|
settings = settings.copy(talkGroup = tg)
|
||||||
|
|||||||
9
app/src/main/res/drawable/ic_delete.xml
Normal file
9
app/src/main/res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M6,7h12v12a1,1 0,0 1,-1 1H7a1,1 0,0 1,-1 -1V7zM9,9v9h1V9H9zM14,9v9h1V9h-1zM4,5h16v2H4V5z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_play.xml
Normal file
9
app/src/main/res/drawable/ic_play.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M8,5v14l11,-7z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_stop.xml
Normal file
9
app/src/main/res/drawable/ic_stop.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M7,7h10v10h-10z" />
|
||||||
|
</vector>
|
||||||
41
app/src/main/res/layout/dialog_recording_item.xml
Normal file
41
app/src/main/res/layout/dialog_recording_item.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/rec_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="middle" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/rec_play"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:tint="@color/accent"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:contentDescription="Воспроизвести" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/rec_delete"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@drawable/ic_delete"
|
||||||
|
android:tint="@color/danger"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:contentDescription="Удалить" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -49,6 +49,11 @@
|
|||||||
<string name="record_started">Запись приёма начата</string>
|
<string name="record_started">Запись приёма начата</string>
|
||||||
<string name="record_saved">Сохранено: %1$s</string>
|
<string name="record_saved">Сохранено: %1$s</string>
|
||||||
<string name="record_failed">Не удалось начать запись (формат OGG недоступен)</string>
|
<string name="record_failed">Не удалось начать запись (формат OGG недоступен)</string>
|
||||||
|
<string name="recordings_title">Записи приёма</string>
|
||||||
|
<string name="record_play_failed">Не удалось воспроизвести запись</string>
|
||||||
|
<string name="record_delete_title">Удалить запись?</string>
|
||||||
|
<string name="record_delete_yes">Удалить</string>
|
||||||
|
<string name="record_deleted">Удалено</string>
|
||||||
<string name="status_monitoring">Мониторинг: %1$s</string>
|
<string name="status_monitoring">Мониторинг: %1$s</string>
|
||||||
<string name="talker_now">Говорит TG%1$d</string>
|
<string name="talker_now">Говорит TG%1$d</string>
|
||||||
<string name="duplex_rx">RX</string>
|
<string name="duplex_rx">RX</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user