4 Commits

Author SHA1 Message Date
pi 282837d415 hairdryer is always #1 2026-04-11 12:49:28 +02:00
pi 0b1f1a2121 changed from playsound to exoplayer2 because of 5,6seconds limit 2026-04-11 12:48:30 +02:00
pi 10c6a8eb95 farben geändert, default werte fade on und 20 minuten 2025-12-06 22:44:14 +01:00
pi 82276868c4 drehen und weiterspielen funktioniert jetzt
fixes #1
closes #1
resolves #1
2025-11-29 16:23:27 +01:00
9 changed files with 172 additions and 37 deletions
+8
View File
@@ -4,6 +4,14 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-24T22:53:27.667021Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=000988cf644b8f" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>
+1
View File
@@ -58,4 +58,5 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.exoplayer:exoplayer:2.19.1")
} }
+3 -2
View File
@@ -8,9 +8,10 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.Light.NoTitleBar"> android:theme="@android:style/Theme.Light.NoTitleBar">
<activity android:name=".MainActivity" <activity android:name=".MainActivity"
android:exported="true"> android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
@@ -2,51 +2,82 @@ package com.example.hairdryer
import android.app.Activity import android.app.Activity
import android.graphics.Color import android.graphics.Color
import android.media.AudioAttributes //import android.os.Build
import android.media.SoundPool
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.widget.EditText import android.widget.*
import android.widget.ProgressBar import android.content.pm.PackageManager
import android.widget.TextView import java.io.File
import kotlin.math.pow
import kotlin.math.max import kotlin.math.max
import kotlin.math.pow
// 🔥 ExoPlayer Imports
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
class MainActivity : Activity() { class MainActivity : Activity() {
private lateinit var soundPool: SoundPool private var player: ExoPlayer? = null
private var soundId = 0
private var streamId = 0
private val handler = Handler() private val handler = Handler()
private var isPlaying = false private var isPlaying = false
private var fadeEnabled = false private var fadeEnabled = true
private var totalMillis: Long = 0 private var totalMillis: Long = 0
private var startTime: Long = 0 private var startTime: Long = 0
private var fadeRunnable: Runnable? = null private var fadeRunnable: Runnable? = null
private var countdownRunnable: Runnable? = null private var countdownRunnable: Runnable? = null
private var audioFiles: List<File> = emptyList()
private var selectedFileIndex = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
// if (!checkPermissions()) return
val soundFolder = File(getExternalFilesDir(null), "babysounds")
if (!soundFolder.exists()) soundFolder.mkdirs()
cleanOldSounds(soundFolder, keepFiles = listOf("hairdryer.ogg", "fezforgotten.ogg"))
copyRawToFiles(R.raw.hairdryer, "hairdryer.ogg", soundFolder)
copyRawToFiles(R.raw.fezforgotten, "fezforgotten.ogg", soundFolder)
val status = findViewById<TextView>(R.id.statusText) val status = findViewById<TextView>(R.id.statusText)
val input = findViewById<EditText>(R.id.timerInput) val input = findViewById<EditText>(R.id.timerInput)
val fadeToggle = findViewById<TextView>(R.id.fadeToggle) val fadeToggle = findViewById<TextView>(R.id.fadeToggle)
val volumeBar = findViewById<ProgressBar>(R.id.volumeBar) val volumeBar = findViewById<ProgressBar>(R.id.volumeBar)
val spinner = findViewById<Spinner>(R.id.mp3Spinner)
val attrs = AudioAttributes.Builder() audioFiles = getAudioFilesFromFolder(soundFolder)
.setUsage(AudioAttributes.USAGE_MEDIA) .sortedWith(compareByDescending<File> { it.name == "hairdryer.ogg" }
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .thenBy { it.name })
.build()
soundPool = SoundPool.Builder() val names = audioFiles.map { it.name }
.setMaxStreams(1)
.setAudioAttributes(attrs)
.build()
soundId = soundPool.load(this, R.raw.sound, 1) val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, names)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: android.view.View?,
position: Int,
id: Long
) {
selectedFileIndex = position
stopSound(status, volumeBar)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
updateUI(status, volumeBar, "Bereit", Color.GREEN, 1f)
updateUI(status, volumeBar, "Bereit", Color.LTGRAY, 1f)
fadeToggle.setOnClickListener { fadeToggle.setOnClickListener {
fadeEnabled = !fadeEnabled fadeEnabled = !fadeEnabled
fadeToggle.text = "Fade-Out: " + if (fadeEnabled) "ON" else "OFF" fadeToggle.text = "Fade-Out: " + if (fadeEnabled) "ON" else "OFF"
@@ -62,21 +93,97 @@ class MainActivity : Activity() {
} }
} }
// --- RAW -> files ---
private fun copyRawToFiles(rawId: Int, fileName: String, folder: File) {
if (!folder.exists()) folder.mkdirs()
val outFile = File(folder, fileName)
if (!outFile.exists()) {
resources.openRawResource(rawId).use { input ->
outFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
// --- Alte Sounds löschen ---
private fun cleanOldSounds(folder: File, keepFiles: List<String>) {
folder.listFiles { file ->
file.isFile &&
(file.extension.equals("mp3", true) || file.extension.equals("ogg", true)) &&
file.name !in keepFiles
}?.forEach { it.delete() }
}
// --- Audio-Dateien holen ---
private fun getAudioFilesFromFolder(folder: File): List<File> {
return folder.listFiles { file ->
file.isFile &&
(file.extension.equals("mp3", true) || file.extension.equals("ogg", true))
}?.toList() ?: emptyList()
}
// --- Permissions ---
/* private fun checkPermissions(): Boolean {
val permission = if (Build.VERSION.SDK_INT >= 33)
android.Manifest.permission.READ_MEDIA_AUDIO
else
android.Manifest.permission.READ_EXTERNAL_STORAGE
return if (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
true
} else {
requestPermissions(arrayOf(permission), 123)
false
}
}
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 123 &&
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
recreate()
}
}
// --- START ---
private fun startSound(minutes: Int, status: TextView, volumeBar: ProgressBar) { private fun startSound(minutes: Int, status: TextView, volumeBar: ProgressBar) {
stopSound(status, volumeBar) stopSound(status, volumeBar)
streamId = soundPool.play(soundId, 1f, 1f, 1, -1, 1f)
if (audioFiles.isEmpty()) return
val file = audioFiles[selectedFileIndex]
if (!file.exists()) return
player = ExoPlayer.Builder(this).build().apply {
val mediaItem = MediaItem.fromUri(file.toURI().toString())
setMediaItem(mediaItem)
repeatMode = Player.REPEAT_MODE_ONE // 🔥 GAPLESS LOOP
prepare()
play()
}
isPlaying = true isPlaying = true
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
if (minutes > 0) { if (minutes > 0) {
totalMillis = minutes * 60_000L totalMillis = minutes * 60_000L
updateCountdown(minutes * 60, status) updateCountdown(minutes * 60, status)
// Countdown jede Sekunde
countdownRunnable = object : Runnable { countdownRunnable = object : Runnable {
override fun run() { override fun run() {
val elapsedSec = ((System.currentTimeMillis() - startTime) / 1000).toInt() val elapsedSec = ((System.currentTimeMillis() - startTime) / 1000).toInt()
val remainingSec = max(minutes * 60 - elapsedSec, 0) val remainingSec = max(minutes * 60 - elapsedSec, 0)
updateCountdown(remainingSec, status) updateCountdown(remainingSec, status)
if (remainingSec > 0) { if (remainingSec > 0) {
handler.postDelayed(this, 1000L) handler.postDelayed(this, 1000L)
} else { } else {
@@ -86,16 +193,18 @@ class MainActivity : Activity() {
} }
handler.postDelayed(countdownRunnable!!, 1000L) handler.postDelayed(countdownRunnable!!, 1000L)
// Fade-Out (optional)
if (fadeEnabled) { if (fadeEnabled) {
fadeRunnable = object : Runnable { fadeRunnable = object : Runnable {
override fun run() { override fun run() {
if (!isPlaying) return if (!isPlaying) return
val elapsed = System.currentTimeMillis() - startTime val elapsed = System.currentTimeMillis() - startTime
val progress = (elapsed.toFloat() / totalMillis).coerceIn(0f, 1f) val progress = (elapsed.toFloat() / totalMillis).coerceIn(0f, 1f)
val volume = (1 - progress).pow(2) // exponentielles Fade val volume = (1 - progress).pow(2)
soundPool.setVolume(streamId, volume, volume)
player?.volume = volume
volumeBar.progress = (volume * 100).toInt() volumeBar.progress = (volume * 100).toInt()
if (progress < 1f) { if (progress < 1f) {
handler.postDelayed(this, 200L) handler.postDelayed(this, 200L)
} }
@@ -105,32 +214,38 @@ class MainActivity : Activity() {
} else { } else {
volumeBar.progress = 100 volumeBar.progress = 100
} }
} else { } else {
// unendlich updateUI(status, volumeBar, "Stopp: ∞", Color.RED, 1f)
updateUI(status, volumeBar, "Läuft: ∞", Color.GREEN, 1f)
volumeBar.progress = 100 volumeBar.progress = 100
} }
} }
// --- Countdown ---
private fun updateCountdown(remainingSec: Int, status: TextView) { private fun updateCountdown(remainingSec: Int, status: TextView) {
val minutes = remainingSec / 60 val minutes = remainingSec / 60
val seconds = remainingSec % 60 val seconds = remainingSec % 60
status.text = String.format("Läuft: %02d:%02d", minutes, seconds) status.text = String.format("Stopp: %02d:%02d", minutes, seconds)
status.setBackgroundColor(Color.GREEN) status.setBackgroundColor(Color.RED)
status.setTextColor(Color.BLACK) status.setTextColor(Color.BLACK)
} }
// --- STOP ---
private fun stopSound(status: TextView, volumeBar: ProgressBar) { private fun stopSound(status: TextView, volumeBar: ProgressBar) {
if (isPlaying) { if (isPlaying) {
soundPool.stop(streamId) player?.release()
player = null
isPlaying = false isPlaying = false
} }
handler.removeCallbacks(countdownRunnable ?: Runnable {}) handler.removeCallbacks(countdownRunnable ?: Runnable {})
handler.removeCallbacks(fadeRunnable ?: Runnable {}) handler.removeCallbacks(fadeRunnable ?: Runnable {})
updateUI(status, volumeBar, "Gestoppt", Color.RED, 0f)
updateUI(status, volumeBar, "Start", Color.GREEN, 0f)
volumeBar.progress = 0 volumeBar.progress = 0
} }
// --- UI ---
private fun updateUI(view: TextView, bar: ProgressBar, text: String, color: Int, volume: Float) { private fun updateUI(view: TextView, bar: ProgressBar, text: String, color: Int, volume: Float) {
view.text = text view.text = text
view.setBackgroundColor(color) view.setBackgroundColor(color)
@@ -140,6 +255,6 @@ class MainActivity : Activity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
soundPool.release() player?.release()
} }
} }
+12 -2
View File
@@ -19,9 +19,18 @@
<Space <Space
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" /> android:layout_height="50dp" />
<Spinner
android:id="@+id/mp3Spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Space
android:layout_width="match_parent"
android:layout_height="20dp" />
<TextView <TextView
android:id="@+id/statusText" android:id="@+id/statusText"
android:text="Bereit" android:text="Start"
android:textSize="26sp" android:textSize="26sp"
android:gravity="center" android:gravity="center"
android:textColor="#000000" android:textColor="#000000"
@@ -33,6 +42,7 @@
<EditText <EditText
android:id="@+id/timerInput" android:id="@+id/timerInput"
android:hint="Sleep Timer (Minuten)" android:hint="Sleep Timer (Minuten)"
android:text="20"
android:inputType="number" android:inputType="number"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -40,7 +50,7 @@
<TextView <TextView
android:id="@+id/fadeToggle" android:id="@+id/fadeToggle"
android:text="Fade-Out: OFF" android:text="Fade-Out: ON"
android:gravity="center" android:gravity="center"
android:padding="12dp" android:padding="12dp"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
Binary file not shown.
Binary file not shown.
Binary file not shown.