diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d52377f1..963f73f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,18 +18,46 @@ By default Pull Request should be opened against **develop** branch, PR against ### Here are a few guidelines you should follow before submitting: 1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted. -Use `git commit --signoff` to acknowledge this. -2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests -should be written in this programming language. -3. **No Breakage:** New features or changes to existing ones must not degrade the user experience. -4. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms. +Use `git commit --signoff` to acknowledge this. +2. **No Breakage:** New features or changes to existing ones must not degrade the user experience. +3. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms. Refactoring existing messes is great, but watch out for breakage. -5. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review +4. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review and test. ### Pull Request Process +On each Pull Request Github runs a number of checks to make sure there are no problems. + +#### Signed commits +Commits must be signed. [See here how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) + +#### KtLint +This programm checks if the source code is formatted correctly. +You can run it yourself locally with + +`./gradlew -Pqc ktlintFormat` + +Running this command will fix common problems and will notify you of problems it couldn't fix automatically. + +#### Detekt + +Detekt is a static analyser. It helps to find potential bugs in our code. + +You can run it yourself locally with + +`./gradlew -Pqc detekt` + +There is a "baseline" file, in which errors which have been in the code base before are noted. +Sometimes it is necessary to regenerate this file by running: + +`./gradlew -Pqc detektBaseline` + +#### Lint +Lint looks for general problems in the code or unused resources etc. +You can run it with + +`./gradlew -Pqc lintRelease` + +If there is a need to regenerate the baseline, remove `ultrasonic/lint-baseline.xml` and rerun the command. + -1. Ensure [all commits are signed-off](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification). -2. Check tests for the new code are added. -3. Check code style is passing. -4. Check code static analysis is passing. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5edc6ce..dd18430b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ ktlintGradle = "10.2.0" detekt = "1.19.0" preferences = "1.1.1" media = "1.3.1" -media3 = "1.0.0-alpha03" +media3 = "1.0.0-beta01" androidSupport = "28.0.0" androidLegacySupport = "1.0.0" @@ -101,4 +101,3 @@ kluentAndroid = { module = "org.amshove.kluent:kluent-android", versio mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } - diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 521d62a7..4b9c04b2 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -55,7 +55,7 @@ errorLine2=" ~~~~~~~~"> @@ -66,7 +66,7 @@ errorLine2=" ~~~~~~~~"> @@ -77,18 +77,7 @@ errorLine2=" ~~~~~~~"> - - - - @@ -180,6 +169,61 @@ column="1"/> + + + + + + + + + + + + + + + + + + + + diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index d8ed9a5f..ab6384c3 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -67,6 +67,7 @@ diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index e2abb257..1f1b8e85 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -21,7 +21,6 @@ import androidx.media3.common.Player import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession -import androidx.media3.session.SessionResult import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -81,15 +80,12 @@ private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM" private const val DISPLAY_LIMIT = 100 private const val SEARCH_LIMIT = 10 -private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" -private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" - /** * MediaBrowserService implementation for e.g. Android Auto */ @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") class AutoMediaBrowserCallback(var player: Player) : - MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback, KoinComponent { + MediaLibraryService.MediaLibrarySession.Callback, KoinComponent { private val mediaPlayerController by inject() private val activeServerProvider: ActiveServerProvider by inject() @@ -181,39 +177,23 @@ class AutoMediaBrowserCallback(var player: Player) : return onLoadChildren(parentId) } - private fun setMediaItemFromSearchQuery(query: String) { - // Only accept query with pattern "play [Title]" or "[Title]" - // Where [Title]: must be exactly matched - // If no media with exact name found, play a random media instead - val mediaTitle = - if (query.startsWith("play ", ignoreCase = true)) { - query.drop(5) - } else { - query - } - - playFromMediaId(mediaTitle) - } - - override fun onSetMediaUri( - session: MediaSession, + /* + * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, + * and thereby customarily it is required to rebuild it.. + * See also: https://stackoverflow.com/questions/70096715/adding-mediaitem-when-using-the-media3-library-caused-an-error + */ + override fun onAddMediaItems( + mediaSession: MediaSession, controller: MediaSession.ControllerInfo, - uri: Uri, - extras: Bundle - ): Int { + mediaItems: MutableList + ): ListenableFuture> { - if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || - uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) - ) { - val searchQuery = - uri.getQueryParameter("query") - ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED - setMediaItemFromSearchQuery(searchQuery) - - return SessionResult.RESULT_SUCCESS - } else { - return SessionResult.RESULT_ERROR_NOT_SUPPORTED + val updatedMediaItems = mediaItems.map { mediaItem -> + mediaItem.buildUpon() + .setUri(mediaItem.requestMetadata.mediaUri) + .build() } + return Futures.immediateFuture(updatedMediaItems.toMutableList()) } @Suppress("ReturnCount", "ComplexMethod") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index 88d5dc13..77013e45 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -50,7 +50,7 @@ class LegacyPlaylistManager : KoinComponent { for (i in 0 until n) { val item = controller.getMediaItemAt(i) - val file = mediaItemCache[item.mediaMetadata.mediaUri.toString()] + val file = mediaItemCache[item.requestMetadata.toString()] if (file != null) _playlist.add(file) } @@ -59,11 +59,11 @@ class LegacyPlaylistManager : KoinComponent { } fun addToCache(item: MediaItem, file: DownloadFile) { - mediaItemCache.put(item.mediaMetadata.mediaUri.toString(), file) + mediaItemCache.put(item.requestMetadata.toString(), file) } fun updateCurrentPlaying(item: MediaItem?) { - currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()] + currentPlaying = mediaItemCache[item?.requestMetadata.toString()] } @Synchronized diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt index 5ecb4fe4..d9cd7c36 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt @@ -7,144 +7,38 @@ package org.moire.ultrasonic.playback -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context -import android.graphics.BitmapFactory -import android.os.Build -import android.os.Bundle import androidx.core.app.NotificationCompat -import androidx.core.graphics.drawable.IconCompat import androidx.media3.common.Player -import androidx.media3.common.util.Assertions import androidx.media3.common.util.UnstableApi -import androidx.media3.common.util.Util -import androidx.media3.session.MediaController +import androidx.media3.session.CommandButton +import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.MediaNotification -import androidx.media3.session.MediaNotification.ActionFactory -import org.moire.ultrasonic.R +import androidx.media3.session.MediaSession -/* -* This is a copy of DefaultMediaNotificationProvider.java with some small changes -* I have opened a bug https://github.com/androidx/media/issues/65 to make it easier to customize -* the icons and actions without creating our own copy of this class.. - */ @UnstableApi -/* package */ -internal class MediaNotificationProvider(context: Context) : - MediaNotification.Provider { - private val context: Context = context.applicationContext - private val notificationManager: NotificationManager = Assertions.checkStateNotNull( - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - ) +class MediaNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) { - @Suppress("LongMethod") - override fun createNotification( - mediaController: MediaController, - actionFactory: ActionFactory, - onNotificationChangedCallback: MediaNotification.Provider.Callback - ): MediaNotification { - ensureNotificationChannel() - val builder: NotificationCompat.Builder = NotificationCompat.Builder( - context, - NOTIFICATION_CHANNEL_ID - ) - // Skip to previous action. - builder.addAction( - actionFactory.createMediaAction( - IconCompat.createWithResource( - context, - R.drawable.media3_notification_seek_to_previous - ), - context.getString(R.string.media3_controls_seek_to_previous_description), - ActionFactory.COMMAND_SKIP_TO_PREVIOUS - ) - ) - if (mediaController.playbackState == Player.STATE_ENDED || - !mediaController.playWhenReady - ) { - // Play action. - builder.addAction( - actionFactory.createMediaAction( - IconCompat.createWithResource(context, R.drawable.media3_notification_play), - context.getString(R.string.media3_controls_play_description), - ActionFactory.COMMAND_PLAY - ) - ) - } else { - // Pause action. - builder.addAction( - actionFactory.createMediaAction( - IconCompat.createWithResource(context, R.drawable.media3_notification_pause), - context.getString(R.string.media3_controls_pause_description), - ActionFactory.COMMAND_PAUSE - ) - ) - } - // Skip to next action. - builder.addAction( - actionFactory.createMediaAction( - IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_next), - context.getString(R.string.media3_controls_seek_to_next_description), - ActionFactory.COMMAND_SKIP_TO_NEXT - ) - ) - - // Set metadata info in the notification. - val metadata = mediaController.mediaMetadata - builder.setContentTitle(metadata.title).setContentText(metadata.artist) - if (metadata.artworkData != null) { - val artworkBitmap = - BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData!!.size) - builder.setLargeIcon(artworkBitmap) - } - val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle() - .setShowActionsInCompactView(0, 1, 2) - val notification: Notification = builder - .setContentIntent(mediaController.sessionActivity) - .setOnlyAlertOnce(true) - .setSmallIcon(getSmallIconResId()) - .setStyle(mediaStyle) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setOngoing(false) - .build() - return MediaNotification( - NOTIFICATION_ID, - notification - ) + override fun addNotificationActions( + mediaSession: MediaSession, + mediaButtons: MutableList, + builder: NotificationCompat.Builder, + actionFactory: MediaNotification.ActionFactory + ): IntArray { + return super.addNotificationActions(mediaSession, mediaButtons, builder, actionFactory) } - override fun handleCustomAction( - mediaController: MediaController, - action: String, - extras: Bundle - ) { - // We don't handle custom commands. - } + override fun getMediaButtons( + playerCommands: Player.Commands, + customLayout: MutableList, + playWhenReady: Boolean + ): MutableList { + val commands = super.getMediaButtons(playerCommands, customLayout, playWhenReady) - private fun ensureNotificationChannel() { - if (Util.SDK_INT < Build.VERSION_CODES.O || - notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null - ) { - return + commands.forEachIndexed { index, command -> + command.extras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, index) } - val channel = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_NAME, - NotificationManager.IMPORTANCE_LOW - ) - channel.setShowBadge(false) - notificationManager.createNotificationChannel(channel) - } - - companion object { - private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic" - private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service" - private const val NOTIFICATION_ID = 3032 - private fun getSmallIconResId(): Int { - return R.drawable.ic_stat_ultrasonic - } + return commands } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 8404d0f9..1cc994d2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -11,9 +11,7 @@ import android.content.Intent import android.os.Build import androidx.media3.common.AudioAttributes import androidx.media3.common.C -import androidx.media3.common.C.CONTENT_TYPE_MUSIC import androidx.media3.common.C.USAGE_MEDIA -import androidx.media3.common.MediaItem import androidx.media3.datasource.DataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer @@ -38,29 +36,12 @@ class PlaybackService : MediaLibraryService(), KoinComponent { private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var apiDataSource: APIDataSource.Factory - private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback + private lateinit var librarySessionCallback: MediaLibrarySession.Callback private var rxBusSubscription = CompositeDisposable() private var isStarted = false - /* - * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, - * and thereby customarily it is required to rebuild it.. - */ - private class CustomMediaItemFiller : MediaSession.MediaItemFiller { - override fun fillInLocalConfiguration( - session: MediaSession, - controller: MediaSession.ControllerInfo, - mediaItem: MediaItem - ): MediaItem { - // Again, set the Uri, so that it will get a LocalConfiguration - return mediaItem.buildUpon() - .setUri(mediaItem.mediaMetadata.mediaUri) - .build() - } - } - override fun onCreate() { Timber.i("onCreate called") super.onCreate() @@ -134,7 +115,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent { // This will need to use the AutoCalls mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) - .setMediaItemFiller(CustomMediaItemFiller()) .setSessionActivity(getPendingIntentForContent()) .build() @@ -171,7 +151,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { private fun getAudioAttributes(): AudioAttributes { return AudioAttributes.Builder() .setUsage(USAGE_MEDIA) - .setContentType(CONTENT_TYPE_MUSIC) + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .build() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 749e30e0..73a5ef25 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -659,16 +659,21 @@ fun Track.toMediaItem(): MediaItem { val bitrate = Settings.maxBitRate val uri = "$id|$bitrate|$filePath" + val rmd = MediaItem.RequestMetadata.Builder() + .setMediaUri(uri.toUri()) + .build() + val metadata = MediaMetadata.Builder() metadata.setTitle(title) .setArtist(artist) .setAlbumTitle(album) - .setMediaUri(uri.toUri()) .setAlbumArtist(artist) + .build() val mediaItem = MediaItem.Builder() .setUri(uri) .setMediaId(id) + .setRequestMetadata(rmd) .setMediaMetadata(metadata.build()) return mediaItem.build() diff --git a/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml b/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml new file mode 100644 index 00000000..81e0ed96 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml @@ -0,0 +1,9 @@ + + +