7 Commits

Author SHA1 Message Date
pi 64e491805c added dwarfs and removed filenameextension 2026-04-13 12:44:13 +02:00
pi e12950c25c neue musik 2026-04-12 10:25:20 +02:00
pi 7552ac1942 neue musik 2026-04-11 13:14:00 +02:00
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
14 changed files with 177 additions and 37 deletions
+8
View File
@@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<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>
</selectionStates>
</component>
+1
View File
@@ -58,4 +58,5 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
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:theme="@android:style/Theme.Light.NoTitleBar">
<activity android:name=".MainActivity"
android:exported="true">
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -2,51 +2,87 @@ package com.example.hairdryer
import android.app.Activity
import android.graphics.Color
import android.media.AudioAttributes
import android.media.SoundPool
//import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.TextView
import kotlin.math.pow
import android.widget.*
import android.content.pm.PackageManager
import java.io.File
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() {
private lateinit var soundPool: SoundPool
private var soundId = 0
private var streamId = 0
private var player: ExoPlayer? = null
private val handler = Handler()
private var isPlaying = false
private var fadeEnabled = false
private var fadeEnabled = true
private var totalMillis: Long = 0
private var startTime: Long = 0
private var fadeRunnable: Runnable? = null
private var countdownRunnable: Runnable? = null
private var audioFiles: List<File> = emptyList()
private var selectedFileIndex = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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)
copyRawToFiles(R.raw.feuer, "feuer.ogg", soundFolder)
copyRawToFiles(R.raw.dwarfs, "dwarfs.ogg", soundFolder)
copyRawToFiles(R.raw.meeresrauschen, "meeresrauschen.ogg", soundFolder)
copyRawToFiles(R.raw.regen, "regen.ogg", soundFolder)
copyRawToFiles(R.raw.zombi, "zombi.ogg", soundFolder)
val status = findViewById<TextView>(R.id.statusText)
val input = findViewById<EditText>(R.id.timerInput)
val fadeToggle = findViewById<TextView>(R.id.fadeToggle)
val volumeBar = findViewById<ProgressBar>(R.id.volumeBar)
val spinner = findViewById<Spinner>(R.id.mp3Spinner)
val attrs = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
audioFiles = getAudioFilesFromFolder(soundFolder)
.sortedWith(compareByDescending<File> { it.name == "hairdryer.ogg" }
.thenBy { it.name })
soundPool = SoundPool.Builder()
.setMaxStreams(1)
.setAudioAttributes(attrs)
.build()
val names = audioFiles.map { it.nameWithoutExtension }
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 {
fadeEnabled = !fadeEnabled
fadeToggle.text = "Fade-Out: " + if (fadeEnabled) "ON" else "OFF"
@@ -62,21 +98,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) {
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
startTime = System.currentTimeMillis()
if (minutes > 0) {
totalMillis = minutes * 60_000L
updateCountdown(minutes * 60, status)
// Countdown jede Sekunde
countdownRunnable = object : Runnable {
override fun run() {
val elapsedSec = ((System.currentTimeMillis() - startTime) / 1000).toInt()
val remainingSec = max(minutes * 60 - elapsedSec, 0)
updateCountdown(remainingSec, status)
if (remainingSec > 0) {
handler.postDelayed(this, 1000L)
} else {
@@ -86,16 +198,18 @@ class MainActivity : Activity() {
}
handler.postDelayed(countdownRunnable!!, 1000L)
// Fade-Out (optional)
if (fadeEnabled) {
fadeRunnable = object : Runnable {
override fun run() {
if (!isPlaying) return
val elapsed = System.currentTimeMillis() - startTime
val progress = (elapsed.toFloat() / totalMillis).coerceIn(0f, 1f)
val volume = (1 - progress).pow(2) // exponentielles Fade
soundPool.setVolume(streamId, volume, volume)
val volume = (1 - progress).pow(2)
player?.volume = volume
volumeBar.progress = (volume * 100).toInt()
if (progress < 1f) {
handler.postDelayed(this, 200L)
}
@@ -105,32 +219,38 @@ class MainActivity : Activity() {
} else {
volumeBar.progress = 100
}
} else {
// unendlich
updateUI(status, volumeBar, "Läuft: ∞", Color.GREEN, 1f)
updateUI(status, volumeBar, "Stopp: ∞", Color.RED, 1f)
volumeBar.progress = 100
}
}
// --- Countdown ---
private fun updateCountdown(remainingSec: Int, status: TextView) {
val minutes = remainingSec / 60
val seconds = remainingSec % 60
status.text = String.format("Läuft: %02d:%02d", minutes, seconds)
status.setBackgroundColor(Color.GREEN)
status.text = String.format("Stopp: %02d:%02d", minutes, seconds)
status.setBackgroundColor(Color.RED)
status.setTextColor(Color.BLACK)
}
// --- STOP ---
private fun stopSound(status: TextView, volumeBar: ProgressBar) {
if (isPlaying) {
soundPool.stop(streamId)
player?.release()
player = null
isPlaying = false
}
handler.removeCallbacks(countdownRunnable ?: Runnable {})
handler.removeCallbacks(fadeRunnable ?: Runnable {})
updateUI(status, volumeBar, "Gestoppt", Color.RED, 0f)
updateUI(status, volumeBar, "Start", Color.GREEN, 0f)
volumeBar.progress = 0
}
// --- UI ---
private fun updateUI(view: TextView, bar: ProgressBar, text: String, color: Int, volume: Float) {
view.text = text
view.setBackgroundColor(color)
@@ -140,6 +260,6 @@ class MainActivity : Activity() {
override fun onDestroy() {
super.onDestroy()
soundPool.release()
player?.release()
}
}
}
+12 -2
View File
@@ -19,9 +19,18 @@
<Space
android:layout_width="match_parent"
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
android:id="@+id/statusText"
android:text="Bereit"
android:text="Start"
android:textSize="26sp"
android:gravity="center"
android:textColor="#000000"
@@ -33,6 +42,7 @@
<EditText
android:id="@+id/timerInput"
android:hint="Sleep Timer (Minuten)"
android:text="20"
android:inputType="number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -40,7 +50,7 @@
<TextView
android:id="@+id/fadeToggle"
android:text="Fade-Out: OFF"
android:text="Fade-Out: ON"
android:gravity="center"
android:padding="12dp"
android:layout_marginTop="20dp"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.