Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c05ed1c55 | |||
| 2125cefdac | |||
| 29be5a0e74 | |||
| 64e491805c | |||
| e12950c25c | |||
| 7552ac1942 | |||
| 282837d415 | |||
| 0b1f1a2121 | |||
| 10c6a8eb95 | |||
| 82276868c4 |
Generated
+8
@@ -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>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ android {
|
|||||||
minSdk = 22
|
minSdk = 22
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "0.10.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -57,5 +57,6 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
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.1")
|
||||||
|
implementation("com.google.android.exoplayer:exoplayer:2.19.1")
|
||||||
}
|
}
|
||||||
@@ -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,88 @@ 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)
|
||||||
|
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)
|
||||||
|
copyRawToFiles(R.raw.zombi_refrain, "zombi-refrain.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.nameWithoutExtension }
|
||||||
.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 +99,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 +199,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 +220,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 +261,6 @@ class MainActivity : Activity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
soundPool.release()
|
player?.release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.
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