diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..aa46aec --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ae388c2..0897082 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,15 @@ diff --git a/.idea/kotlinScripting.xml b/.idea/kotlinScripting.xml deleted file mode 100644 index 26da98e..0000000 --- a/.idea/kotlinScripting.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - 2147483647 - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b4..fe63bb6 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c7f825..8499f04 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -12,11 +13,28 @@ android { applicationId = "com.d4rk.musicsleeptimer.plus" minSdk = 26 targetSdk = 34 - versionCode = 26 - versionName = "1.0.0" + versionCode = 27 + versionName = "3.0.0" archivesName = "${applicationId}-v${versionName}" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - resourceConfigurations += listOf("en", "de", "es", "fr", "hi", "hu", "in", "it", "ja", "ro", "ru", "tr", "sv", "bg", "pl", "uk") + resourceConfigurations += listOf( + "en" , + "de" , + "es" , + "fr" , + "hi" , + "hu" , + "in" , + "it" , + "ja" , + "ro" , + "ru" , + "tr" , + "sv" , + "bg" , + "pl" , + "uk" + ) } buildTypes { release { @@ -24,14 +42,18 @@ android { isMinifyEnabled = true isShrinkResources = true isDebuggable = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt") , "proguard-rules.pro" + ) } debug { multiDexEnabled = true isMinifyEnabled = true isShrinkResources = true isDebuggable = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt") , "proguard-rules.pro" + ) } } compileOptions { @@ -52,12 +74,14 @@ android { } } dependencies { - implementation("com.google.firebase:firebase-crashlytics-ktx:18.5.1") - implementation("com.google.firebase:firebase-analytics-ktx:21.5.0") - implementation("com.google.firebase:firebase-perf:20.5.0") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("androidx.multidex:multidex:2.0.1") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.crashlytics.ktx) + implementation(libs.firebase.analytics.ktx) + implementation(libs.firebase.perf) + implementation(libs.appcompat) + implementation(libs.work.runtime.ktx) + implementation(libs.multidex) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7c515de..dda7cee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,51 +1,54 @@ - - - - - + + + + + + + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> - + + + android:value="true" /> + android:value="true" /> - + + android:value="true" /> + + android:value="true" /> + android:value="@integer/google_play_services_version" /> \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/notifications/SleepNotification.kt b/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/notifications/SleepNotification.kt index 0621ff4..cb98063 100644 --- a/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/notifications/SleepNotification.kt +++ b/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/notifications/SleepNotification.kt @@ -1,4 +1,5 @@ package com.d4rk.musicsleeptimer.plus.notifications + import android.app.Notification import android.app.Notification.CATEGORY_EVENT import android.app.Notification.VISIBILITY_PUBLIC @@ -22,57 +23,93 @@ import java.text.DateFormat.SHORT import java.util.Date import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.MINUTES + object SleepNotification { private val TIMEOUT_INITIAL_MILLIS = MINUTES.toMillis(30) private val TIMEOUT_INCREMENT_MILLIS = MINUTES.toMillis(10) private val TIMEOUT_DECREMENT_MILLIS = MINUTES.toMillis(10) - private enum class Action(private val value: String) { + + private enum class Action(private val value : String) { CANCEL("com.d4rk.musicsleeptimer.plus.action.CANCEL") { - override fun title(context: Context) = context.getText(android.R.string.cancel) - }, + override fun title(context : Context) = context.getText(android.R.string.cancel) + } , INCREMENT("com.d4rk.musicsleeptimer.plus.action.INCREMENT") { - override fun title(context: Context) = context.getString(R.string.notification_action_increment, MILLISECONDS.toMinutes(TIMEOUT_INCREMENT_MILLIS)) - }, + override fun title(context : Context) = context.getString( + R.string.notification_action_increment , + MILLISECONDS.toMinutes(TIMEOUT_INCREMENT_MILLIS) + ) + } , DECREMENT("com.d4rk.musicsleeptimer.plus.action.DECREMENT") { - override fun title(context: Context) = context.getString(R.string.notification_action_decrement, MILLISECONDS.toMinutes(TIMEOUT_DECREMENT_MILLIS)) - },; + override fun title(context : Context) = context.getString( + R.string.notification_action_decrement , + MILLISECONDS.toMinutes(TIMEOUT_DECREMENT_MILLIS) + ) + } , ; + companion object { - fun parse(value: String?): Action? = values().firstOrNull { it.value == value } + fun parse(value : String?) : Action? = entries.firstOrNull { it.value == value } } - fun intent(context: Context): Intent = Intent(context, SleepTileService::class.java).setAction(value) - fun pendingIntent(context: Context, cancel: Boolean = false): PendingIntent? = PendingIntent.getService(context, 0, intent(context), FLAG_IMMUTABLE).apply { if (cancel) cancel() } - fun action(context: Context, cancel: Boolean = false): Notification.Action.Builder = Notification.Action.Builder(Icon.createWithResource(context, 0), title(context), pendingIntent(context, cancel)) - abstract fun title(context: Context): CharSequence? + + fun intent(context : Context) : Intent = + Intent(context , SleepTileService::class.java).setAction(value) + + fun pendingIntent(context : Context , cancel : Boolean = false) : PendingIntent? = + PendingIntent.getService(context , 0 , intent(context) , FLAG_IMMUTABLE) + .apply { if (cancel) cancel() } + + fun action(context : Context , cancel : Boolean = false) : Notification.Action.Builder = + Notification.Action.Builder( + Icon.createWithResource(context , 0) , + title(context) , + pendingIntent(context , cancel) + ) + + abstract fun title(context : Context) : CharSequence? } - fun Context.notificationManager(): NotificationManager? = getSystemService(NotificationManager::class.java) - fun Context.find() = notificationManager()?.activeNotifications?.firstOrNull { it.id == R.id.notification_id }?.notification - fun Context.handle(intent: Intent?) = when (Action.parse(intent?.action)) { + + fun Context.notificationManager() : NotificationManager? = + getSystemService(NotificationManager::class.java) + + fun Context.find() = + notificationManager()?.activeNotifications?.firstOrNull { it.id == R.id.notification_id }?.notification + + fun Context.handle(intent : Intent?) = when (Action.parse(intent?.action)) { INCREMENT -> update(TIMEOUT_INCREMENT_MILLIS) - DECREMENT -> update(-TIMEOUT_DECREMENT_MILLIS) + DECREMENT -> update(- TIMEOUT_DECREMENT_MILLIS) CANCEL -> cancel() null -> Unit } + fun Context.toggle() = if (find() == null) show() else cancel() private fun Context.cancel() = notificationManager()?.cancel(R.id.notification_id) ?: Unit - private fun Context.update(timeout: Long) = find()?.let { + private fun Context.update(timeout : Long) = find()?.let { it.`when` - currentTimeMillis() + }?.let { + if (it > - timeout) it + timeout else it + }?.let { + show(it) } - ?.let { - if (it > -timeout) it + timeout else it - } - ?.let { show(it) - } - private fun Context.show(timeout: Long = TIMEOUT_INITIAL_MILLIS) { + + private fun Context.show(timeout : Long = TIMEOUT_INITIAL_MILLIS) { require(timeout > 0) val eta = currentTimeMillis() + timeout - val notification = Notification.Builder(this, getString(R.string.notification_channel_id)).setCategory(CATEGORY_EVENT).setVisibility(VISIBILITY_PUBLIC).setOnlyAlertOnce(true).setOngoing(true).setSmallIcon(R.drawable.ic_music_off).setSubText(DateFormat.getTimeInstance(SHORT).format(Date(eta))).setShowWhen(true).setWhen(eta).setUsesChronometer(true).setChronometerCountDown(true).setTimeoutAfter(timeout).setDeleteIntent(SleepAudioService.pendingIntent(this)).addAction(INCREMENT.action(this).build()).addAction(DECREMENT.action(this, cancel = timeout <= TIMEOUT_DECREMENT_MILLIS).build()).addAction(CANCEL.action(this).build()).build() + val notification = Notification.Builder(this , getString(R.string.notification_channel_id)) + .setCategory(CATEGORY_EVENT).setVisibility(VISIBILITY_PUBLIC).setOnlyAlertOnce(true) + .setOngoing(true).setSmallIcon(R.drawable.ic_music_off) + .setSubText(DateFormat.getTimeInstance(SHORT).format(Date(eta))).setShowWhen(true) + .setWhen(eta).setUsesChronometer(true).setChronometerCountDown(true) + .setTimeoutAfter(timeout).setDeleteIntent(SleepAudioService.pendingIntent(this)) + .addAction(INCREMENT.action(this).build()).addAction( + DECREMENT.action(this , cancel = timeout <= TIMEOUT_DECREMENT_MILLIS).build() + ).addAction(CANCEL.action(this).build()).build() createNotificationChannel() - notificationManager()?.notify(R.id.notification_id, notification) + notificationManager()?.notify(R.id.notification_id , notification) } + private fun Context.createNotificationChannel() { val id = getString(R.string.notification_channel_id) - val name: CharSequence = getString(R.string.app_name) - val channel = NotificationChannel(id, name, IMPORTANCE_LOW).apply { + val name : CharSequence = getString(R.string.app_name) + val channel = NotificationChannel(id , name , IMPORTANCE_LOW).apply { setBypassDnd(true) lockscreenVisibility = VISIBILITY_PUBLIC } diff --git a/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/services/SleepAudioService.kt b/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/services/SleepAudioService.kt index ef17737..8331651 100644 --- a/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/services/SleepAudioService.kt +++ b/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/services/SleepAudioService.kt @@ -1,4 +1,5 @@ package com.d4rk.musicsleeptimer.plus.services + import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE import android.content.Context @@ -13,27 +14,34 @@ import android.media.AudioManager.AUDIOFOCUS_GAIN import android.media.AudioManager.STREAM_MUSIC import com.d4rk.musicsleeptimer.plus.services.SleepTileService.Companion.requestTileUpdate import java.util.concurrent.TimeUnit.SECONDS + @Suppress("DEPRECATION") class SleepAudioService : android.app.IntentService("SleepAudioService") { companion object { private val FADE_STEP_MILLIS = SECONDS.toMillis(1) private val RESTORE_VOLUME_MILLIS = SECONDS.toMillis(2) - private fun intent(context: Context) = Intent(context, SleepAudioService::class.java) - fun pendingIntent(context: Context): PendingIntent? = PendingIntent.getService(context, 0, intent(context), FLAG_IMMUTABLE) + private fun intent(context : Context) = Intent(context , SleepAudioService::class.java) + fun pendingIntent(context : Context) : PendingIntent? = + PendingIntent.getService(context , 0 , intent(context) , FLAG_IMMUTABLE) } + @Deprecated("Deprecated in Java") - override fun onHandleIntent(intent: Intent?) = getSystemService(AudioManager::class.java)?.run { - val volumeIndex = getStreamVolume(STREAM_MUSIC) - do { - adjustStreamVolume(STREAM_MUSIC, ADJUST_LOWER, 0) - Thread.sleep(FADE_STEP_MILLIS) - } while (getStreamVolume(STREAM_MUSIC) > 0) - val attributes = AudioAttributes.Builder().setUsage(USAGE_MEDIA).setContentType(CONTENT_TYPE_MUSIC).build() - val focusRequest = AudioFocusRequest.Builder(AUDIOFOCUS_GAIN).setAudioAttributes(attributes).setOnAudioFocusChangeListener {}.build() - requestAudioFocus(focusRequest) - Thread.sleep(RESTORE_VOLUME_MILLIS) - setStreamVolume(STREAM_MUSIC, volumeIndex, 0) - abandonAudioFocusRequest(focusRequest) - requestTileUpdate() - } ?: Unit + override fun onHandleIntent(intent : Intent?) = + getSystemService(AudioManager::class.java)?.run { + val volumeIndex = getStreamVolume(STREAM_MUSIC) + do { + adjustStreamVolume(STREAM_MUSIC , ADJUST_LOWER , 0) + Thread.sleep(FADE_STEP_MILLIS) + } while (getStreamVolume(STREAM_MUSIC) > 0) + val attributes = AudioAttributes.Builder().setUsage(USAGE_MEDIA) + .setContentType(CONTENT_TYPE_MUSIC).build() + val focusRequest = + AudioFocusRequest.Builder(AUDIOFOCUS_GAIN).setAudioAttributes(attributes) + .setOnAudioFocusChangeListener {}.build() + requestAudioFocus(focusRequest) + Thread.sleep(RESTORE_VOLUME_MILLIS) + setStreamVolume(STREAM_MUSIC , volumeIndex , 0) + abandonAudioFocusRequest(focusRequest) + requestTileUpdate() + } ?: Unit } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/services/SleepTileService.kt b/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/services/SleepTileService.kt index 1c71017..6315ade 100644 --- a/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/services/SleepTileService.kt +++ b/app/src/main/kotlin/com/d4rk/musicsleeptimer/plus/services/SleepTileService.kt @@ -1,4 +1,5 @@ package com.d4rk.musicsleeptimer.plus.services + import android.content.ComponentName import android.content.Context import android.content.Intent @@ -16,37 +17,46 @@ import com.d4rk.musicsleeptimer.plus.notifications.SleepNotification.toggle import java.text.DateFormat.SHORT import java.text.DateFormat.getTimeInstance import java.util.Date + class SleepTileService : TileService() { companion object { - fun Context.requestTileUpdate() = requestListeningState(this, ComponentName(this, SleepTileService::class.java)) + fun Context.requestTileUpdate() = + requestListeningState(this , ComponentName(this , SleepTileService::class.java)) } + override fun onStartListening() = refreshTile() override fun onClick() = when (notificationManager()?.areNotificationsEnabled()) { true -> toggle().also { refreshTile() } else -> requestNotificationsPermission() } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + + override fun onStartCommand(intent : Intent? , flags : Int , startId : Int) : Int { handle(intent) requestTileUpdate() stopSelfResult(startId) return START_NOT_STICKY } + private fun refreshTile() = qsTile?.run { when (val notification = find()) { null -> { state = STATE_INACTIVE if (SDK_INT >= Q) subtitle = resources.getText(R.string.tile_subtitle) } + else -> { state = STATE_ACTIVE - if (SDK_INT >= Q) subtitle = getTimeInstance(SHORT).format(Date(notification.`when`)) + if (SDK_INT >= Q) subtitle = + getTimeInstance(SHORT).format(Date(notification.`when`)) } } updateTile() } ?: Unit + @Suppress("DEPRECATION") - private fun requestNotificationsPermission() = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - putExtra(Settings.EXTRA_APP_PACKAGE, packageName) - }.let(::startActivityAndCollapse) + private fun requestNotificationsPermission() = + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + putExtra(Settings.EXTRA_APP_PACKAGE , packageName) + }.let(::startActivityAndCollapse) } \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml b/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml index 626dcb7..0f8a155 100644 --- a/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml @@ -1,6 +1,20 @@ - - - - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_music_off.xml b/app/src/main/res/drawable-anydpi/ic_music_off.xml index d78f581..1a172e9 100644 --- a/app/src/main/res/drawable-anydpi/ic_music_off.xml +++ b/app/src/main/res/drawable-anydpi/ic_music_off.xml @@ -1,3 +1,10 @@ - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml index 27e87f2..90d9271 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -1,5 +1,5 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 660576b..474534f 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -1,4 +1,4 @@ - + sleep_timer \ No newline at end of file diff --git a/app/src/main/res/xml/config_locales.xml b/app/src/main/res/xml/config_locales.xml index 4a4f72b..e0f0e7c 100644 --- a/app/src/main/res/xml/config_locales.xml +++ b/app/src/main/res/xml/config_locales.xml @@ -1,19 +1,18 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index ddd1216..cf539d5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - id("com.android.application") version "8.1.3" apply false - id("com.android.library") version "8.1.3" apply false - id("org.jetbrains.kotlin.android") version "1.9.10" apply false - id("com.google.gms.google-services") version "4.4.0" apply false - id("com.google.firebase.crashlytics") version "2.9.2" apply false + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.jetbrainsKotlinAndroid) apply false + alias(libs.plugins.googlePlayServices) apply false + alias(libs.plugins.googleFirebase) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..30fc54d --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,31 @@ +[versions] +agp = "8.4.1" +appcompat = "1.7.0" +espressoCore = "3.5.1" +firebaseBom = "33.1.0" +kotlin = "1.9.23" +google-firebase-crashlytics = "3.0.1" +google-services = "4.4.2" +junit = "4.13.2" +junitVersion = "1.1.5" +multidex = "2.0.1" +workRuntimeKtx = "2.9.0" + +[libraries] +appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } +ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } +firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } +firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx" } +firebase-perf = { module = "com.google.firebase:firebase-perf" } +multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } +junit = { module = "junit:junit", version.ref = "junit" } +work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +googlePlayServices = { id = "com.google.gms.google-services", version.ref = "google-services" } +googleFirebase = { id = "com.google.firebase.crashlytics", version.ref = "google-firebase-crashlytics" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index de4a76c..cc54d81 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Nov 09 10:16:15 EET 2023 +#Tue Jun 04 08:10:55 EEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists