diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 10da28382..20dcc6fc0 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("com.android.application") id("kotlin-android") id("androidx.navigation.safeargs.kotlin") - id("com.mikepenz.aboutlibraries.plugin") + id("com.mikepenz.aboutlibraries.plugin.android") id("kotlin-parcelize") id("dagger.hilt.android.plugin") id("com.google.firebase.crashlytics") @@ -239,7 +239,7 @@ android { } // About Libraries - implementation(libs.mikepenz.aboutlibrariesCore) + implementation(libs.mikepenz.aboutlibraries) // Billing implementation(libs.billingclient.billingKtx) diff --git a/android/app/src/main/assets/changelog.json b/android/app/src/main/assets/changelog.json index 796cc0884..815db1d90 100644 --- a/android/app/src/main/assets/changelog.json +++ b/android/app/src/main/assets/changelog.json @@ -1,4 +1,21 @@ [ + { + "versionName": "1.0.9", + "releaseDate": "15/01/2026", + "features": [ + + ], + "fixes": [ + "Fixed Chromecast reliability issues", + "Fixed a long-standing issue where the app could crash when playing from the background", + "Various stability improvements" + ], + "improvements": [ + ], + "notes": [ + "What's up? I want to make Shuttle great again. I just need more hours in the day, and more days in the week!" + ] + }, { "versionName": "1.0.8", "releaseDate": "10/01/2026", diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/appinitializers/PlaybackInitializer.kt b/android/app/src/main/java/com/simplecityapps/shuttle/appinitializers/PlaybackInitializer.kt index a4b1afd5f..15b07baf4 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/appinitializers/PlaybackInitializer.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/appinitializers/PlaybackInitializer.kt @@ -2,8 +2,10 @@ package com.simplecityapps.shuttle.appinitializers import android.annotation.SuppressLint import android.app.Application +import android.app.ForegroundServiceStartNotAllowedException import android.content.Context import android.content.Intent +import android.os.Build import androidx.core.content.ContextCompat import com.simplecityapps.mediaprovider.repository.songs.SongRepository import com.simplecityapps.playback.NoiseManager @@ -158,7 +160,15 @@ constructor( override fun onPlaybackStateChanged(playbackState: PlaybackState) { when (playbackState) { is PlaybackState.Playing -> { - ContextCompat.startForegroundService(context, Intent(context, PlaybackService::class.java)) + try { + ContextCompat.startForegroundService(context, Intent(context, PlaybackService::class.java)) + } catch (e: IllegalStateException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + Timber.w(e, "Cannot start foreground service from background - likely audio focus regained while app in background") + } else { + throw e + } + } } is PlaybackState.Paused -> { playbackPreferenceManager.playbackPosition = playbackManager.getProgress() diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt index d5b8e87b3..72a5a71dc 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt @@ -1,6 +1,7 @@ package com.simplecityapps.shuttle.ui import android.Manifest +import android.app.ForegroundServiceStartNotAllowedException import android.content.Intent import android.content.pm.PackageManager import android.os.Build @@ -22,6 +23,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withTimeout +import timber.log.Timber @AndroidEntryPoint class MainActivity : AppCompatActivity() { @@ -41,7 +43,7 @@ class MainActivity : AppCompatActivity() { @AppCoroutineScope lateinit var scope: CoroutineScope - lateinit var snowfallView: SnowfallView + var snowfallView: SnowfallView? = null // Lifecycle @@ -76,7 +78,7 @@ class MainActivity : AppCompatActivity() { withTimeout(5000) { remoteConfig.fetchAndActivate().await() } - snowfallView.setForecast(remoteConfig.getDouble("snow_forecast")) + snowfallView?.setForecast(remoteConfig.getDouble("snow_forecast")) } } @@ -104,15 +106,23 @@ class MainActivity : AppCompatActivity() { private fun handleSearchQuery(intent: Intent?) { if (intent?.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH) { - ContextCompat.startForegroundService( - this, - Intent(this, PlaybackService::class.java).apply { - action = PlaybackService.ACTION_SEARCH - intent.extras?.let { extras -> - putExtras(extras) + try { + ContextCompat.startForegroundService( + this, + Intent(this, PlaybackService::class.java).apply { + action = PlaybackService.ACTION_SEARCH + intent.extras?.let { extras -> + putExtras(extras) + } } + ) + } catch (e: IllegalStateException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + Timber.w(e, "Cannot start foreground service from search query - app may be in restricted state") + } else { + throw e } - ) + } } } } diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/ShortcutHandlerActivity.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/ShortcutHandlerActivity.kt index 0c55dabfb..87d63ae7d 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/ShortcutHandlerActivity.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/ShortcutHandlerActivity.kt @@ -1,11 +1,13 @@ package com.simplecityapps.shuttle.ui +import android.app.ForegroundServiceStartNotAllowedException import android.content.Intent import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.simplecityapps.playback.PlaybackService import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber @AndroidEntryPoint class ShortcutHandlerActivity : AppCompatActivity() { @@ -19,10 +21,18 @@ class ShortcutHandlerActivity : AppCompatActivity() { action = PlaybackService.ACTION_TOGGLE_PLAYBACK } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(serviceIntent) - } else { - startService(serviceIntent) + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } else { + startService(serviceIntent) + } + } catch (e: IllegalStateException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + Timber.w(e, "Cannot start foreground service from shortcut - app may be in restricted state") + } else { + throw e + } } } } diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/common/dialog/TagEditorAlertDialog.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/common/dialog/TagEditorAlertDialog.kt index 986d694bf..b4cbcfa3c 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/common/dialog/TagEditorAlertDialog.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/common/dialog/TagEditorAlertDialog.kt @@ -167,7 +167,9 @@ class TagEditorAlertDialog : } fun show(manager: FragmentManager) { - super.show(manager, TAG) + if (!manager.isStateSaved) { + super.show(manager, TAG) + } } // Private diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/albumartists/AlbumArtistListFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/albumartists/AlbumArtistListFragment.kt index 034b78d40..58d87ebe9 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/albumartists/AlbumArtistListFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/albumartists/AlbumArtistListFragment.kt @@ -46,6 +46,7 @@ import com.simplecityapps.shuttle.ui.screens.playlistmenu.PlaylistMenuView import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class AlbumArtistListFragment : @@ -336,13 +337,18 @@ class AlbumArtistListFragment : viewHolder: AlbumArtistBinder.ViewHolder ) { if (!contextualToolbarHelper.handleClick(albumArtist)) { - if (findNavController().currentDestination?.id != R.id.albumArtistDetailFragment) { - findNavController().navigate( - R.id.action_libraryFragment_to_albumArtistDetailFragment, - AlbumArtistDetailFragmentArgs(albumArtist).toBundle(), - null, - FragmentNavigatorExtras(viewHolder.imageView to viewHolder.imageView.transitionName) - ) + // Verify we're on libraryFragment (where the action is defined) before navigating + if (findNavController().currentDestination?.id == R.id.libraryFragment) { + try { + findNavController().navigate( + R.id.action_libraryFragment_to_albumArtistDetailFragment, + AlbumArtistDetailFragmentArgs(albumArtist).toBundle(), + null, + FragmentNavigatorExtras(viewHolder.imageView to viewHolder.imageView.transitionName) + ) + } catch (e: IllegalArgumentException) { + Timber.e(e, "Failed to navigate to album artist detail") + } } } } diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/main/MainFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/main/MainFragment.kt index 8e078c06f..7c2bf5a25 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/main/MainFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/main/MainFragment.kt @@ -165,7 +165,8 @@ class MainFragment : if (task.isSuccessful) { // We got the ReviewInfo object val reviewInfo = task.result - reviewManager.launchReviewFlow(requireActivity(), reviewInfo) + // Use nullable activity since fragment may be detached when async callback fires + activity?.let { reviewManager.launchReviewFlow(it, reviewInfo) } } else { // There was some problem, log or handle the error code. Timber.e(task.exception ?: Exception("Unknown"), "Failed to launch review flow") diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt index 6cf3cfa99..35e1522dc 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt @@ -196,7 +196,10 @@ class OnboardingParentFragment : generalPreferenceManager.hasOnboarded = true if (args.isOnboarding) { - findNavController().navigate(R.id.action_onboardingFragment_to_mainFragment) + // Guard against double-tap or race conditions where navigation already occurred + if (findNavController().currentDestination?.id == R.id.onboardingFragment) { + findNavController().navigate(R.id.action_onboardingFragment_to_mainFragment) + } } else { findNavController().popBackStack() } diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/playback/PlaybackFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/playback/PlaybackFragment.kt index ff0019061..6a2b9d799 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/playback/PlaybackFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/playback/PlaybackFragment.kt @@ -20,6 +20,7 @@ import com.google.android.gms.cast.framework.CastButtonFactory import com.simplecityapps.adapter.RecyclerAdapter import com.simplecityapps.adapter.RecyclerListener import com.simplecityapps.playback.PlaybackState +import com.simplecityapps.playback.chromecast.CastSessionManager import com.simplecityapps.playback.queue.QueueItem import com.simplecityapps.playback.queue.QueueManager import com.simplecityapps.shuttle.R @@ -64,6 +65,9 @@ class PlaybackFragment : @Inject lateinit var queueManager: QueueManager + @Inject + lateinit var castSessionManager: CastSessionManager + private var recyclerView: RecyclerView by autoCleared() private var adapter: RecyclerAdapter by autoCleared() @@ -224,7 +228,11 @@ class PlaybackFragment : presenter.setFavorite(favoriteButton.isChecked) } - CastButtonFactory.setUpMediaRouteButton(requireContext(), toolbar.menu, R.id.media_route_menu_item) + if (castSessionManager.isAvailable) { + CastButtonFactory.setUpMediaRouteButton(requireContext(), toolbar.menu, R.id.media_route_menu_item) + } else { + toolbar.menu.findItem(R.id.media_route_menu_item)?.isVisible = false + } savedInstanceState?.getParcelable(QueueFragment.ARG_RECYCLER_STATE)?.let { recyclerViewState = it } diff --git a/android/data/src/main/kotlin/com/simplecityapps/shuttle/parcel/LocalDateParceler.kt b/android/data/src/main/kotlin/com/simplecityapps/shuttle/parcel/LocalDateParceler.kt index 45d8ec410..ca6f94e04 100644 --- a/android/data/src/main/kotlin/com/simplecityapps/shuttle/parcel/LocalDateParceler.kt +++ b/android/data/src/main/kotlin/com/simplecityapps/shuttle/parcel/LocalDateParceler.kt @@ -5,7 +5,7 @@ import kotlinx.datetime.LocalDate import kotlinx.parcelize.Parceler object LocalDateParceler : Parceler { - override fun create(parcel: Parcel) = LocalDate.parse(parcel.readString()!!) + override fun create(parcel: Parcel): LocalDate? = parcel.readString()?.let { LocalDate.parse(it) } override fun LocalDate?.write( parcel: Parcel, diff --git a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackNotificationManager.kt b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackNotificationManager.kt index a273a34f8..68f5ce4a5 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackNotificationManager.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackNotificationManager.kt @@ -183,6 +183,10 @@ constructor( notificationManager.cancel(NOTIFICATION_ID) } + fun notify(notification: Notification) { + notificationManager.notify(NOTIFICATION_ID, notification) + } + private val playbackAction: NotificationCompat.Action get() { val intent = diff --git a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt index d168284f6..ade97806e 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt @@ -1,5 +1,7 @@ package com.simplecityapps.playback +import android.app.ForegroundServiceStartNotAllowedException +import android.app.Notification import android.app.SearchManager import android.app.Service import android.content.Intent @@ -91,7 +93,7 @@ class PlaybackService : Timber.v("onStartCommand() action: ${intent?.action}") - if (intent == null && (playbackManager.playbackState() != PlaybackState.Loading || playbackManager.playbackState() != PlaybackState.Playing)) { + if (intent == null && (playbackManager.playbackState() != PlaybackState.Loading && playbackManager.playbackState() != PlaybackState.Playing)) { stopForeground(true) return START_NOT_STICKY } @@ -104,8 +106,15 @@ class PlaybackService : intent?.let { when (intent.action) { ACTION_NOTIFICATION_DISMISS -> { - // The user has swiped away the notification. This is only possible when the service is no longer running in the foreground + // The user has swiped away the notification. This is only possible when the service is no longer running in the foreground. + // We still need to call startForeground() in case this was started via startForegroundService(). Timber.v("Stopping due to notification dismiss") + val notification = if (queueManager.getQueue().isEmpty()) { + notificationManager.displayQueueEmptyNotification() + } else { + notificationManager.displayPlaybackNotification() + } + startForegroundSafely(notification) stopSelf() return START_NOT_STICKY } @@ -123,16 +132,16 @@ class PlaybackService : This also gives S2 time to respond to pending commands. For example, if the command is 'loadFromSearch', we don't want to stop the service and allow the process to be killed while that we're in the middle of executing that command. */ - startForeground(PlaybackNotificationManager.NOTIFICATION_ID, notificationManager.displayQueueEmptyNotification()) + startForegroundSafely(notificationManager.displayQueueEmptyNotification()) postDelayedShutdown(10000) } else { Timber.v("startForeground() called. Showing notification: Playback") - startForeground(PlaybackNotificationManager.NOTIFICATION_ID, notificationManager.displayPlaybackNotification()) + startForegroundSafely(notificationManager.displayPlaybackNotification()) } processCommand(intent) } else { Timber.v("startForeground() called. Showing notification: Loading") - startForeground(PlaybackNotificationManager.NOTIFICATION_ID, notificationManager.displayLoadingNotification()) + startForegroundSafely(notificationManager.displayLoadingNotification()) pendingStartCommands.add(intent) } } @@ -178,6 +187,21 @@ class PlaybackService : // Private + private fun startForegroundSafely(notification: Notification) { + try { + startForeground(PlaybackNotificationManager.NOTIFICATION_ID, notification) + } catch (e: IllegalStateException) { + // ForegroundServiceStartNotAllowedException (API 31+) extends IllegalStateException + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + Timber.w(e, "Unable to start foreground service - likely started from background context (e.g., media button broadcast)") + // Still display the notification even if we can't start foreground + notificationManager.notify(notification) + } else { + throw e + } + } + } + private fun processCommand(intent: Intent) { Timber.v("processCommand()") MediaButtonReceiver.handleIntent(mediaSessionManager.mediaSession, intent) diff --git a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackWatcher.kt b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackWatcher.kt index c6fbce5f1..3915f533e 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackWatcher.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackWatcher.kt @@ -36,16 +36,16 @@ class PlaybackWatcher : PlaybackWatcherCallback { duration: Int, fromUser: Boolean ) { - callbacks.forEach { callback -> callback.onProgressChanged(position, duration, fromUser) } + callbacks.toList().forEach { callback -> callback.onProgressChanged(position, duration, fromUser) } } override fun onPlaybackStateChanged(playbackState: PlaybackState) { Timber.v("onPlaybackStateChanged(playbackState: $playbackState)") - callbacks.forEach { callback -> callback.onPlaybackStateChanged(playbackState) } + callbacks.toList().forEach { callback -> callback.onPlaybackStateChanged(playbackState) } } override fun onTrackEnded(song: Song) { Timber.v("onTrackEnded(song: ${song.name})") - callbacks.forEach { callback -> callback.onTrackEnded(song) } + callbacks.toList().forEach { callback -> callback.onTrackEnded(song) } } } diff --git a/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt b/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt index fbdd60c2d..ecafabec4 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt @@ -19,14 +19,25 @@ constructor( private val exoPlayerPlayback: ExoPlayerPlayback, private val mediaInfoProvider: MediaInfoProvider ) : SessionManagerListener { + var isAvailable: Boolean = false + private set + init { - val sessionManager = CastContext.getSharedInstance(applicationContext).sessionManager - sessionManager.addSessionManagerListener(this, CastSession::class.java) + try { + val sessionManager = CastContext.getSharedInstance(applicationContext).sessionManager + sessionManager.addSessionManagerListener(this, CastSession::class.java) + isAvailable = true + } catch (e: Exception) { + // Cast framework unavailable on this device (e.g., no Google Play Services) + Timber.w(e, "Failed to initialize Cast framework - Chromecast will be unavailable") + } } override fun onSessionStarting(castSession: CastSession) { Timber.d("onSessionStarting") - httpServer.start() + if (!httpServer.isAlive) { + httpServer.start() + } } override fun onSessionStarted( @@ -44,6 +55,7 @@ constructor( i: Int ) { Timber.e("onSessionStartFailed") + httpServer.stop() } override fun onSessionResuming( @@ -51,7 +63,7 @@ constructor( s: String ) { Timber.d("onSessionResuming") - if (!httpServer.wasStarted()) { + if (!httpServer.isAlive) { httpServer.start() } } @@ -74,6 +86,7 @@ constructor( i: Int ) { Timber.e("onSessionResumeFailed ($i)") + httpServer.stop() } override fun onSessionSuspended( diff --git a/android/playback/src/main/java/com/simplecityapps/playback/queue/QueueManager.kt b/android/playback/src/main/java/com/simplecityapps/playback/queue/QueueManager.kt index ac9f963e2..2424ae41d 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/queue/QueueManager.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/queue/QueueManager.kt @@ -398,10 +398,16 @@ class QueueManager( new: QueueItem ) { if (baseList.isNotEmpty()) { - baseList[baseList.indexOf(old)] = new + val baseIndex = baseList.indexOf(old) + if (baseIndex != -1) { + baseList[baseIndex] = new + } } if (shuffleList.isNotEmpty()) { - shuffleList[shuffleList.indexOf(old)] = new + val shuffleIndex = shuffleList.indexOf(old) + if (shuffleIndex != -1) { + shuffleList[shuffleIndex] = new + } } } diff --git a/buildSrc/src/main/kotlin/AppVersion.kt b/buildSrc/src/main/kotlin/AppVersion.kt index bf21f49fc..200b52e54 100644 --- a/buildSrc/src/main/kotlin/AppVersion.kt +++ b/buildSrc/src/main/kotlin/AppVersion.kt @@ -1,6 +1,6 @@ object AppVersion { const val versionMajor = 1 const val versionMinor = 0 - const val versionPatch = 8 + const val versionPatch = 9 val versionSuffix: String? = "" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 051555a15..977ef8d08 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -152,6 +152,7 @@ kotlinx-coroutinesPlayServices = { module = "org.jetbrains.kotlinx:kotlinx-corou kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary-android" } mikepenz-aboutlibrariesCore = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" } +mikepenz-aboutlibraries = { module = "com.mikepenz:aboutlibraries", version.ref = "aboutlibraries" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi-adapters" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi-kotlin" } @@ -175,6 +176,7 @@ vdurmont-semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" [plugins] aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } +aboutlibraries-android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlibraries" } android-application = { id = "com.android.application", version.ref = "agp" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }