diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce2a603..28820c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,9 @@ + + + (R.id.seekBar) seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - // Check if the seek bar is at 90% - if (progress >= 90 ) { + if (progress >= 90) { stopAlarm(seekBar) } } - override fun onStartTrackingTouch(seekBar: SeekBar?) {} + override fun onStartTrackingTouch(seekBar: SeekBar?) {} override fun onStopTrackingTouch(seekBar: SeekBar?) {} }) - refreshCycle() - if (alarm != null) { - alarmRing(alarm.ringtoneUri) - } - //TODO Release wake lock - val alarmHandler = AlarmHandler() - alarmHandler.setNextAlarm(applicationContext) } - private fun refreshCycle() { - val progressBar = findViewById(R.id.progress_cycle) - val progressText = findViewById(R.id.progress_text) - val handler = Handler() - val delayMillis = 1000 - val maxProgress = 100 - - val updateRunnable = object : Runnable { - var i = 0 - - override fun run() { - val currentTime = System.currentTimeMillis() - val sdf = SimpleDateFormat("HH:mm") - val formattedTime = sdf.format(Date(currentTime)) - val percentage = getPercentageOfDay().toLong() - if (percentage <= maxProgress) { - progressText.text = formattedTime - progressBar.progress = percentage.toInt() - handler.postDelayed(this, delayMillis.toLong()) - } else { - // Reset progress bar and text - progressBar.progress = 0 - progressText.text = "00:00" - // Start the loop again - handler.postDelayed(this, delayMillis.toLong()) - } - } - } - - handler.post(updateRunnable) + private fun alarmRing(ringtoneUri: String) { + val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_ALARM) + .build() + + val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setAudioAttributes(audioAttributes) + .setOnAudioFocusChangeListener { /* Handle focus change */ } + .build() + + if (audioManager.requestAudioFocus(focusRequest) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + wakePhone() + playRingtone(ringtoneUri) + } } - private fun alarmRing(ringtoneUri: String) { - // Wake Phone - val powerManager: PowerManager = getSystemService(Context.POWER_SERVICE) as PowerManager + private fun wakePhone() { + val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) wakeLock = powerManager.newWakeLock( - PowerManager.ACQUIRE_CAUSES_WAKEUP or - PowerManager.ON_AFTER_RELEASE or - PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rooster:wakelock" - ) - wakeLock?.acquire() - - vibrator = applicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // it is safe to cancel other vibrations currently taking place - vibrator!!.cancel() - val soundUri = if (ringtoneUri == "default") { - // Fallback to a default sound if the URI is null or empty - Uri.parse("android.resource://${applicationContext.packageName}/raw/alarmclock") - } else { - // Use the provided URI - Uri.parse(ringtoneUri) - } + PowerManager.PARTIAL_WAKE_LOCK or + PowerManager.ACQUIRE_CAUSES_WAKEUP or + PowerManager.ON_AFTER_RELEASE, "rooster:wakelock" + ).apply { acquire(10 * 60 * 1000L /*10 minutes*/) } + } + + private fun playRingtone(ringtoneUri: String) { + vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + vibrator?.let { + it.cancel() + val uri = if (ringtoneUri == "default") Uri.parse("android.resource://${packageName}/raw/alarmclock") else Uri.parse(ringtoneUri) + mediaPlayer = MediaPlayer().apply { - setDataSource(applicationContext, soundUri) setAudioAttributes( AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ALARM) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_ALARM) .build() ) - setOnCompletionListener { - // Release MediaPlayer resources after completion - it?.release() - } - prepareAsync() - setOnPreparedListener { player -> - // Start playback when prepared - player.start() - startAlarmLoop() + try { + setDataSource(applicationContext, uri) + prepare() // Prepare the MediaPlayer asynchronously + setOnErrorListener { _, what, extra -> + Log.e("MediaPlayer Error", "What: $what, Extra: $extra") + true + } + setOnPreparedListener { start() } + isLooping = true + } catch (e: Exception) { + Log.e("MediaPlayer", "Error setting data source", e) } - isLooping = true } } } - private fun startAlarmLoop() { - // Start a loop that checks a SharedPreferences for a flag to stop the vibration - val vibesPattern = createWaveformVibrationPattern(longArrayOf(0, 1000, 2000, 3000), repeat = -1) - var vibrationEffect1 = VibrationEffect.createWaveform(vibesPattern, 3) - CoroutineScope(Dispatchers.Default).launch { - isVibrating = true - vibrator?.vibrate(vibrationEffect1) - if (alarmIsRunning) { - mediaPlayer?.start() - } - while (isVibrating) { - Log.w("Rooster Alarm", "Ring") - // Check for alarmIsRunning flag to control MediaPlayer and vibration - if (alarmIsRunning) { - delay(500) - } else { - isVibrating = false - break - } - delay(1000) + + // Remaining methods including refreshCycle, getPercentageOfDay, stopAlarm, onResume, onPause, releaseResources... + + private fun releaseResources() { + mediaPlayer?.let { + it.stop() + it.release() + } + mediaPlayer = null + wakeLock?.let { + if (it.isHeld) { + it.release() } } + wakeLock = null + vibrator?.cancel() } - fun createWaveformVibrationPattern(timings: LongArray, repeat: Int = -1): LongArray { - var pattern = LongArray(timings.size + 1) - pattern[0] = 0L - for (i in 1 until pattern.size) { - pattern[i] = pattern[i - 1] + timings[i - 1] - } + fun stopAlarm(view: View?) { + alarmIsRunning = false + isVibrating = false + releaseResources() + finish() + } - if (repeat != -1) { - val repeatIndex = pattern[repeat] - val repeatPattern = pattern.copyOfRange(repeatIndex.toInt(), pattern.size) - pattern = pattern.copyOfRange(0, repeatIndex.toInt()) + repeatPattern + private fun refreshCycle() { + val progressBar = findViewById(R.id.progress_cycle) + val progressText = findViewById(R.id.progress_text) + val handler = Handler() + val delayMillis = 1000 + val maxProgress = 100 + + val updateRunnable = object : Runnable { + var i = 0 + + override fun run() { + val currentTime = System.currentTimeMillis() + val sdf = SimpleDateFormat("HH:mm") + val formattedTime = sdf.format(Date(currentTime)) + val percentage = getPercentageOfDay().toLong() + if (percentage <= maxProgress) { + progressText.text = formattedTime + progressBar.progress = percentage.toInt() + handler.postDelayed(this, delayMillis.toLong()) + } else { + // Reset progress bar and text + progressBar.progress = 0 + progressText.text = "00:00" + // Start the loop again + handler.postDelayed(this, delayMillis.toLong()) + } + } } - return pattern + handler.post(updateRunnable) } fun getPercentageOfDay(): Float { @@ -200,39 +200,4 @@ class AlarmActivity : FragmentActivity() { val percentage = (totalSeconds / secondsInDay) * 100 return percentage.toFloat() } - - fun stopAlarm(view: View?) { - alarmIsRunning = false - releaseResources() - supportFragmentManager.popBackStackImmediate() - finish() - } - - override fun onResume() { - super.onResume() - wakeLock?.acquire() - - if (!alarmIsRunning) { - //alarmIsRunning = true - } - } - - override fun onPause() { - super.onPause() - stopAlarm(null) - /*alarmIsRunning = false - mediaPlayer?.stop() - vibrator?.cancel()*/ - } - - private fun releaseResources() { - vibrator?.cancel() - mediaPlayer?.stop() - mediaPlayer?.reset() - mediaPlayer?.release() - mediaPlayer = null - - wakeLock?.release() - wakeLock = null - } } \ No newline at end of file diff --git a/app/src/main/java/com/rooster/rooster/MainActivity.kt b/app/src/main/java/com/rooster/rooster/MainActivity.kt index 9aac459..740ae06 100644 --- a/app/src/main/java/com/rooster/rooster/MainActivity.kt +++ b/app/src/main/java/com/rooster/rooster/MainActivity.kt @@ -65,6 +65,7 @@ class MainActivity() : ComponentActivity() { Manifest.permission.USE_FULL_SCREEN_INTENT, Manifest.permission.SYSTEM_ALERT_WINDOW, Manifest.permission.WAKE_LOCK, + Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.SCHEDULE_EXACT_ALARM, Manifest.permission.FOREGROUND_SERVICE ) diff --git a/app/src/main/java/com/rooster/rooster/RingtoneActivity.kt b/app/src/main/java/com/rooster/rooster/RingtoneActivity.kt index 6e778e7..3fbb026 100644 --- a/app/src/main/java/com/rooster/rooster/RingtoneActivity.kt +++ b/app/src/main/java/com/rooster/rooster/RingtoneActivity.kt @@ -1,75 +1,61 @@ package com.rooster.rooster import android.content.Intent -import android.media.RingtoneManager import android.net.Uri import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import android.Manifest -import android.content.pm.PackageManager import android.util.Log import android.widget.Toast class RingtoneActivity : AppCompatActivity() { private var alarmId: Long = 0 - private val RINGTONE_PICKER_REQUEST = 999 + + // Register a callback for the result from opening a document + private val openDocumentRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + val uri: Uri? = result.data?.data + uri?.let { + // Grant temporary read permission to the content URI + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + + // Use the Uri to access the selected file here. + updateAlarmRingtone(alarmId, it.toString()) + Toast.makeText(this, "Ringtone file selected!", Toast.LENGTH_SHORT).show() + } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ringtone) - // Check and request permission - checkPermission() - - // Get alarm ID from intent intent.extras?.let { alarmId = it.getLong("alarm_id") Log.i("RingtoneActivity", "Alarm ID: $alarmId") } - // Open ringtone picker - val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { - putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) - putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, "Select Alarm Ringtone") - } - startActivityForResult(intent, RINGTONE_PICKER_REQUEST) - } - - private fun checkPermission() { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1) - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == 1 && grantResults.isNotEmpty() && grantResults[0] != PackageManager.PERMISSION_GRANTED) { - Toast.makeText(this, "Permission denied to read your External storage", Toast.LENGTH_SHORT).show() - } + // Trigger the document selection + selectRingtoneFile() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == RINGTONE_PICKER_REQUEST) { - val ringtoneUri: Uri? = data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) - ringtoneUri?.let { - // Here you should update the alarm in your database with the URI - updateAlarmRingtone(alarmId, it.toString()) - Toast.makeText(this, "Ringtone selected!", Toast.LENGTH_SHORT).show() - } + private fun selectRingtoneFile() { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "audio/*" // Filter to show only audio files. Adjust if needed. } + openDocumentRequest.launch(intent) } private fun updateAlarmRingtone(alarmId: Long, ringtoneUri: String) { + // Update your method to handle the ringtone file URI val alarmDbHelper = AlarmDbHelper(this) val alarm = alarmDbHelper.getAlarm(alarmId) - Log.w("Update", "Ringtone update Intent for alarm $alarmId") + Log.w("Update", "Ringtone file URI update Intent for alarm $alarmId") alarm?.let { it.ringtoneUri = ringtoneUri alarmDbHelper.updateAlarm(it) - Log.i("Update", "Ringtone updated for alarm $alarmId") + Log.i("Update", "Ringtone file URI updated for alarm $alarmId") } finish() }