Merge branch 'develop' into fix_#759

This commit is contained in:
Óscar García Amor 2022-06-21 08:24:52 +02:00 committed by GitHub
commit 737563bf6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 153 additions and 213 deletions

View File

@ -19,17 +19,45 @@ By default Pull Request should be opened against **develop** branch, PR against
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted. 1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
Use `git commit --signoff` to acknowledge this. Use `git commit --signoff` to acknowledge this.
2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests 2. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
should be written in this programming language. 3. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
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.
Refactoring existing messes is great, but watch out for breakage. 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. and test.
### Pull Request Process ### 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.

View File

@ -10,7 +10,7 @@ ktlintGradle = "10.2.0"
detekt = "1.19.0" detekt = "1.19.0"
preferences = "1.1.1" preferences = "1.1.1"
media = "1.3.1" media = "1.3.1"
media3 = "1.0.0-alpha03" media3 = "1.0.0-beta01"
androidSupport = "28.0.0" androidSupport = "28.0.0"
androidLegacySupport = "1.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" } mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" } apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }

View File

@ -55,7 +55,7 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/AndroidManifest.xml" file="src/main/AndroidManifest.xml"
line="151" line="155"
column="10"/> column="10"/>
</issue> </issue>
@ -66,7 +66,7 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/AndroidManifest.xml" file="src/main/AndroidManifest.xml"
line="75" line="79"
column="10"/> column="10"/>
</issue> </issue>
@ -77,18 +77,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/AndroidManifest.xml" file="src/main/AndroidManifest.xml"
line="65" line="68"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise. For launcher activities, this should be set to `true`."
errorLine1=" &lt;activity android:name=&quot;.activity.NavigationActivity&quot;"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="41"
column="10"/> column="10"/>
</issue> </issue>
@ -180,6 +169,61 @@
column="1"/> column="1"/>
</issue> </issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
errorLine1="&lt;vector android:height=&quot;48dp&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/media3_notification_pause.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.media3_notification_play` appears to be unused"
errorLine1="&lt;vector android:height=&quot;48dp&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/media3_notification_play.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.media3_notification_seek_to_next` appears to be unused"
errorLine1="&lt;vector android:height=&quot;32dp&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/media3_notification_seek_to_next.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.media3_notification_seek_to_previous` appears to be unused"
errorLine1="&lt;vector android:height=&quot;32dp&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/media3_notification_seek_to_previous.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/media3_notification_small_icon.xml"
line="1"
column="1"/>
</issue>
<issue <issue
id="IconDuplicates" id="IconDuplicates"
message="The following unrelated icon files have identical contents: list_pressed_holo_dark.9.png, list_pressed_holo_light.9.png"> message="The following unrelated icon files have identical contents: list_pressed_holo_dark.9.png, list_pressed_holo_light.9.png">

View File

@ -67,6 +67,7 @@
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md --> <!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
<service android:name=".playback.PlaybackService" <service android:name=".playback.PlaybackService"
android:label="@string/common.appname" android:label="@string/common.appname"
android:foregroundServiceType="mediaPlayback"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>

View File

@ -21,7 +21,6 @@ import androidx.media3.common.Player
import androidx.media3.session.LibraryResult import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture 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 DISPLAY_LIMIT = 100
private const val SEARCH_LIMIT = 10 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 * MediaBrowserService implementation for e.g. Android Auto
*/ */
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
class AutoMediaBrowserCallback(var player: Player) : class AutoMediaBrowserCallback(var player: Player) :
MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback, KoinComponent { MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
private val mediaPlayerController by inject<MediaPlayerController>() private val mediaPlayerController by inject<MediaPlayerController>()
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
@ -181,39 +177,23 @@ class AutoMediaBrowserCallback(var player: Player) :
return onLoadChildren(parentId) return onLoadChildren(parentId)
} }
private fun setMediaItemFromSearchQuery(query: String) { /*
// Only accept query with pattern "play [Title]" or "[Title]" * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
// Where [Title]: must be exactly matched * and thereby customarily it is required to rebuild it..
// If no media with exact name found, play a random media instead * See also: https://stackoverflow.com/questions/70096715/adding-mediaitem-when-using-the-media3-library-caused-an-error
val mediaTitle = */
if (query.startsWith("play ", ignoreCase = true)) { override fun onAddMediaItems(
query.drop(5) mediaSession: MediaSession,
} else {
query
}
playFromMediaId(mediaTitle)
}
override fun onSetMediaUri(
session: MediaSession,
controller: MediaSession.ControllerInfo, controller: MediaSession.ControllerInfo,
uri: Uri, mediaItems: MutableList<MediaItem>
extras: Bundle ): ListenableFuture<MutableList<MediaItem>> {
): Int {
if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || val updatedMediaItems = mediaItems.map { mediaItem ->
uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) mediaItem.buildUpon()
) { .setUri(mediaItem.requestMetadata.mediaUri)
val searchQuery = .build()
uri.getQueryParameter("query")
?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED
setMediaItemFromSearchQuery(searchQuery)
return SessionResult.RESULT_SUCCESS
} else {
return SessionResult.RESULT_ERROR_NOT_SUPPORTED
} }
return Futures.immediateFuture(updatedMediaItems.toMutableList())
} }
@Suppress("ReturnCount", "ComplexMethod") @Suppress("ReturnCount", "ComplexMethod")

View File

@ -50,7 +50,7 @@ class LegacyPlaylistManager : KoinComponent {
for (i in 0 until n) { for (i in 0 until n) {
val item = controller.getMediaItemAt(i) val item = controller.getMediaItemAt(i)
val file = mediaItemCache[item.mediaMetadata.mediaUri.toString()] val file = mediaItemCache[item.requestMetadata.toString()]
if (file != null) if (file != null)
_playlist.add(file) _playlist.add(file)
} }
@ -59,11 +59,11 @@ class LegacyPlaylistManager : KoinComponent {
} }
fun addToCache(item: MediaItem, file: DownloadFile) { fun addToCache(item: MediaItem, file: DownloadFile) {
mediaItemCache.put(item.mediaMetadata.mediaUri.toString(), file) mediaItemCache.put(item.requestMetadata.toString(), file)
} }
fun updateCurrentPlaying(item: MediaItem?) { fun updateCurrentPlaying(item: MediaItem?) {
currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()] currentPlaying = mediaItemCache[item?.requestMetadata.toString()]
} }
@Synchronized @Synchronized

View File

@ -7,144 +7,38 @@
package org.moire.ultrasonic.playback package org.moire.ultrasonic.playback
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.Assertions
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util import androidx.media3.session.CommandButton
import androidx.media3.session.MediaController import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaNotification import androidx.media3.session.MediaNotification
import androidx.media3.session.MediaNotification.ActionFactory import androidx.media3.session.MediaSession
import org.moire.ultrasonic.R
/*
* 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 @UnstableApi
/* package */ class MediaNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) {
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
)
@Suppress("LongMethod") override fun addNotificationActions(
override fun createNotification( mediaSession: MediaSession,
mediaController: MediaController, mediaButtons: MutableList<CommandButton>,
actionFactory: ActionFactory, builder: NotificationCompat.Builder,
onNotificationChangedCallback: MediaNotification.Provider.Callback actionFactory: MediaNotification.ActionFactory
): MediaNotification { ): IntArray {
ensureNotificationChannel() return super.addNotificationActions(mediaSession, mediaButtons, builder, actionFactory)
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 handleCustomAction( override fun getMediaButtons(
mediaController: MediaController, playerCommands: Player.Commands,
action: String, customLayout: MutableList<CommandButton>,
extras: Bundle playWhenReady: Boolean
) { ): MutableList<CommandButton> {
// We don't handle custom commands. val commands = super.getMediaButtons(playerCommands, customLayout, playWhenReady)
commands.forEachIndexed { index, command ->
command.extras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, index)
} }
private fun ensureNotificationChannel() { return commands
if (Util.SDK_INT < Build.VERSION_CODES.O ||
notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null
) {
return
}
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
}
} }
} }

View File

@ -11,9 +11,7 @@ import android.content.Intent
import android.os.Build import android.os.Build
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.C.CONTENT_TYPE_MUSIC
import androidx.media3.common.C.USAGE_MEDIA import androidx.media3.common.C.USAGE_MEDIA
import androidx.media3.common.MediaItem
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
@ -38,29 +36,12 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var apiDataSource: APIDataSource.Factory private lateinit var apiDataSource: APIDataSource.Factory
private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback private lateinit var librarySessionCallback: MediaLibrarySession.Callback
private var rxBusSubscription = CompositeDisposable() private var rxBusSubscription = CompositeDisposable()
private var isStarted = false 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() { override fun onCreate() {
Timber.i("onCreate called") Timber.i("onCreate called")
super.onCreate() super.onCreate()
@ -134,7 +115,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
// This will need to use the AutoCalls // This will need to use the AutoCalls
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setMediaItemFiller(CustomMediaItemFiller())
.setSessionActivity(getPendingIntentForContent()) .setSessionActivity(getPendingIntentForContent())
.build() .build()
@ -171,7 +151,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
private fun getAudioAttributes(): AudioAttributes { private fun getAudioAttributes(): AudioAttributes {
return AudioAttributes.Builder() return AudioAttributes.Builder()
.setUsage(USAGE_MEDIA) .setUsage(USAGE_MEDIA)
.setContentType(CONTENT_TYPE_MUSIC) .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build() .build()
} }
} }

View File

@ -659,16 +659,21 @@ fun Track.toMediaItem(): MediaItem {
val bitrate = Settings.maxBitRate val bitrate = Settings.maxBitRate
val uri = "$id|$bitrate|$filePath" val uri = "$id|$bitrate|$filePath"
val rmd = MediaItem.RequestMetadata.Builder()
.setMediaUri(uri.toUri())
.build()
val metadata = MediaMetadata.Builder() val metadata = MediaMetadata.Builder()
metadata.setTitle(title) metadata.setTitle(title)
.setArtist(artist) .setArtist(artist)
.setAlbumTitle(album) .setAlbumTitle(album)
.setMediaUri(uri.toUri())
.setAlbumArtist(artist) .setAlbumArtist(artist)
.build()
val mediaItem = MediaItem.Builder() val mediaItem = MediaItem.Builder()
.setUri(uri) .setUri(uri)
.setMediaId(id) .setMediaId(id)
.setRequestMetadata(rmd)
.setMediaMetadata(metadata.build()) .setMediaMetadata(metadata.build())
return mediaItem.build() return mediaItem.build()

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFF"
android:pathData="m12,3.8438c-4.9703,0 -9,4.0292 -9,9l0,5.0625c0,1.2426 1.0074,2.25 2.25,2.25 1.2426,0 2.25,-1.0074 2.25,-2.25l0,-3.375c0,-1.2426 -1.0074,-2.25 -2.25,-2.25 -0.4067,0 -0.783,0.1164 -1.1121,0.3049C4.2752,8.3573 7.7379,4.9688 12,4.9688 16.2621,4.9688 19.7242,8.3573 19.8621,12.5861 19.5336,12.3977 19.1567,12.2813 18.75,12.2813c-1.2426,0 -2.25,1.0074 -2.25,2.25l0,3.375c0,1.2426 1.0074,2.25 2.25,2.25 1.2426,0 2.25,-1.0074 2.25,-2.25L21,12.8438C21,7.8729 16.9708,3.8438 12,3.8438ZM5.25,13.4063c0.621,0 1.125,0.504 1.125,1.125l0,3.375c0,0.621 -0.504,1.125 -1.125,1.125 -0.621,0 -1.125,-0.504 -1.125,-1.125l0,-3.375c0,-0.621 0.504,-1.125 1.125,-1.125zM19.875,17.9063c0,0.621 -0.504,1.125 -1.125,1.125 -0.621,0 -1.125,-0.504 -1.125,-1.125l0,-3.375c0,-0.621 0.504,-1.125 1.125,-1.125 0.621,0 1.125,0.504 1.125,1.125z"/>
</vector>