Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64e491805c | |||
| e12950c25c | |||
| 7552ac1942 | |||
| 282837d415 | |||
| 0b1f1a2121 | |||
| 10c6a8eb95 | |||
| 82276868c4 |
Generated
+8
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
Reference in New Issue
Block a user