merged with develop branch

This commit is contained in:
Holger Müller 2022-06-21 08:50:42 +02:00
commit d4ead49548
No known key found for this signature in database
GPG Key ID: 016F07C42F7AC2B1
18 changed files with 153 additions and 391 deletions

View File

@ -41,7 +41,6 @@ jobs:
name: unit-tests name: unit-tests
command: | command: |
./gradlew ciTest testDebugUnitTest ./gradlew ciTest testDebugUnitTest
./gradlew jacocoFullReport
- run: - run:
name: lint name: lint
command: ./gradlew :ultrasonic:lintRelease command: ./gradlew :ultrasonic:lintRelease
@ -61,8 +60,6 @@ jobs:
- store_artifacts: - store_artifacts:
path: subsonic-api/build/reports path: subsonic-api/build/reports
destination: reports destination: reports
- store_artifacts:
path: build/reports/jacoco/jacocoFullReport/
push_translations: push_translations:
docker: docker:
- image: cimg/python:3.6 - image: cimg/python:3.6

View File

@ -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: ### 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. 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

@ -17,7 +17,6 @@ buildscript {
classpath libs.kotlin classpath libs.kotlin
classpath libs.ktlintGradle classpath libs.ktlintGradle
classpath libs.detekt classpath libs.detekt
classpath libs.jacoco
} }
} }
@ -44,8 +43,6 @@ allprojects {
} }
} }
apply from: 'gradle_scripts/jacoco.gradle'
wrapper { wrapper {
gradleVersion(libs.versions.gradle.get()) gradleVersion(libs.versions.gradle.get())
distributionType("all") distributionType("all")

View File

@ -1,12 +1,6 @@
apply from: bootstrap.androidModule apply from: bootstrap.androidModule
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
ext {
jacocoExclude = [
'**/domain/**'
]
}
dependencies { dependencies {
implementation libs.roomRuntime implementation libs.roomRuntime
implementation libs.roomKtx implementation libs.roomKtx

View File

@ -20,11 +20,3 @@ dependencies {
testImplementation libs.mockWebServer testImplementation libs.mockWebServer
testImplementation libs.apacheCodecs testImplementation libs.apacheCodecs
} }
ext {
// Excluding data classes
jacocoExclude = [
'**/models/**',
'**/di/**'
]
}

View File

@ -8,10 +8,9 @@ androidxcore = "1.6.0"
ktlint = "0.43.2" ktlint = "0.43.2"
ktlintGradle = "10.2.0" ktlintGradle = "10.2.0"
detekt = "1.19.0" detekt = "1.19.0"
jacoco = "0.8.7"
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"
@ -49,7 +48,6 @@ gradle = { module = "com.android.tools.build:gradle", version.r
kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" } ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
jacoco = { module = "org.jacoco:org.jacoco.core", version.ref = "jacoco" }
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" } core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" } support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" }
@ -103,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

@ -3,7 +3,6 @@
*/ */
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'jacoco'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
android { android {
@ -48,10 +47,6 @@ android {
tasks.withType(Test) { tasks.withType(Test) {
useJUnitPlatform() useJUnitPlatform()
jacoco {
includeNoLocationClasses = true
excludes += jacocoExclude
}
} }
dependencies { dependencies {
@ -61,11 +56,4 @@ dependencies {
testRuntimeOnly libs.junitVintage testRuntimeOnly libs.junitVintage
} }
jacoco {
toolVersion(libs.versions.jacoco.get())
}
ext {
jacocoExclude = ['jdk.internal.*']
}

View File

@ -1,92 +0,0 @@
apply plugin: 'jacoco'
jacoco {
toolVersion(libs.versions.jacoco.get())
}
def mergedJacocoExec = file("${project.buildDir}/jacoco/jacocoMerged.exec")
def merge = tasks.register('jacocoMergeReports', JacocoMerge) {
group = "Reporting"
description = "Merge all jacoco reports from projects into one."
ListProperty<File> jacocoFiles = project.objects.listProperty(File.class)
project.subprojects { subproject ->
subproject.plugins.withId("jacoco") {
project.logger.info("${subproject.name} has Jacoco plugin applied")
subproject.tasks.withType(Test) { task ->
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
if (destFile.exists() && !task.name.contains("Release")) {
jacocoFiles.add(destFile)
}
}
}
}
executionData(jacocoFiles)
destinationFile(mergedJacocoExec)
}
tasks.register('jacocoFullReport', JacocoReport) {
dependsOn merge
group = "Reporting"
description = "Generate full Jacoco coverage report including all modules."
getClassDirectories().setFrom(files())
getSourceDirectories().setFrom(files())
getExecutionData().setFrom(files())
reports {
xml.enabled = true
html.enabled = true
csv.enabled = false
}
// Always run merging, as all input calculation is done in doFirst {}
outputs.upToDateWhen { false }
// Task will run anyway even if initial inputs are empty
onlyIf = { true }
project.subprojects { subproject ->
subproject.plugins.withId("jacoco") {
project.logger.info("${subproject.name} has Jacoco plugin applied")
subproject.plugins.withId("kotlin-android") {
project.logger.info("${subproject.name} is android project")
def mainSources = subproject.extensions.findByName("android").sourceSets['main']
project.logger.info("Android sources: ${mainSources.java.srcDirs}")
mainSources.java.srcDirs.forEach {
additionalSourceDirs(it)
}
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
additionalClassDirs(fileTree(
dir: "${subproject.buildDir}/tmp/kotlin-classes/debug",
excludes: subproject.jacocoExclude
))
}
subproject.plugins.withId("kotlin") { plugin ->
project.logger.info("${subproject.name} is common kotlin project")
SourceDirectorySet mainSources = subproject.extensions.getByName("kotlin")
.sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
.kotlin
mainSources.srcDirs.forEach {
project.logger.debug("Adding sources: $it")
additionalSourceDirs(it)
}
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
additionalClassDirs(fileTree(
dir: "${subproject.buildDir}/classes/kotlin/main",
excludes: subproject.jacocoExclude
))
}
subproject.tasks.withType(Test) { task ->
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
if (destFile.exists() && !task.name.contains("Release")) {
project.logger.info("Adding execution data: $destFile")
executionData(destFile)
}
}
}
}
}

View File

@ -3,7 +3,6 @@
*/ */
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
sourceSets { sourceSets {
@ -21,36 +20,8 @@ dependencies {
testRuntimeOnly libs.junitVintage testRuntimeOnly libs.junitVintage
} }
jacoco {
toolVersion(libs.versions.jacoco.get())
}
ext {
// override it in the module
jacocoExclude = ['jdk.internal.*']
}
jacocoTestReport {
reports {
html.required = true
xml.required = false
csv.required = false
}
afterEvaluate {
getClassDirectories().setFrom(files(classDirectories.files.collect {
fileTree(dir: it, excludes: jacocoExclude)
}))
}
}
tasks.named("test").configure { tasks.named("test").configure {
useJUnitPlatform() useJUnitPlatform()
jacoco {
excludes += jacocoExclude
includeNoLocationClasses = true
}
finalizedBy jacocoTestReport
} }
tasks.register("ciTest") { tasks.register("ciTest") {

View File

@ -1,7 +1,6 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply from: "../gradle_scripts/code_quality.gradle" apply from: "../gradle_scripts/code_quality.gradle"
android { android {
@ -135,36 +134,3 @@ dependencies {
implementation libs.timber implementation libs.timber
} }
jacoco {
toolVersion(libs.versions.jacoco.get())
}
// Excluding all java classes and stuff that should not be covered
ext {
jacocoExclude = [
'**/activity/**',
'**/audiofx/**',
'**/fragment/**',
'**/provider/**',
'**/receiver/**',
'**/service/**',
'**/Test/**',
'**/util/**',
'**/view/**',
'**/R$*.class',
'**/R.class',
'**/BuildConfig.class',
'**/di/**',
'jdk.internal.*'
]
}
jacoco {
toolVersion(libs.versions.jacoco.get())
}
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
jacoco.excludes += jacocoExclude
}

View File

@ -5,7 +5,7 @@
id="ObsoleteLintCustomCheck" id="ObsoleteLintCustomCheck"
message="Library lint checks out of date.&#xA;&#xA;Lint found an issue registry (`androidx.annotation.experimental.lint.ExperimentalIssueRegistry`)&#xA;which was compiled against an older version of lint&#xA;than this one.&#xA;&#xA;This often works just fine, but some basic verification&#xA;shows that the lint check jar references (for example)&#xA;the following API which is no longer valid in this&#xA;version of lint:&#xA;com.android.tools.lint.client.api.AnnotationLookup: org.jetbrains.uast.UAnnotation findRealAnnotation(com.intellij.psi.PsiAnnotation,com.intellij.psi.PsiClass,org.jetbrains.uast.UElement)&#xA;(Referenced from androidx/annotation/experimental/lint/ExperimentalDetector.class)&#xA;&#xA;Recompile the checks against the latest version, or if&#xA;this is a check bundled with a third-party library, see&#xA;if there is a more recent version available.&#xA;&#xA;Version of Lint API this lint check is using is 11.&#xA;The Lint API version currently running is 12 (7.2)."> message="Library lint checks out of date.&#xA;&#xA;Lint found an issue registry (`androidx.annotation.experimental.lint.ExperimentalIssueRegistry`)&#xA;which was compiled against an older version of lint&#xA;than this one.&#xA;&#xA;This often works just fine, but some basic verification&#xA;shows that the lint check jar references (for example)&#xA;the following API which is no longer valid in this&#xA;version of lint:&#xA;com.android.tools.lint.client.api.AnnotationLookup: org.jetbrains.uast.UAnnotation findRealAnnotation(com.intellij.psi.PsiAnnotation,com.intellij.psi.PsiClass,org.jetbrains.uast.UElement)&#xA;(Referenced from androidx/annotation/experimental/lint/ExperimentalDetector.class)&#xA;&#xA;Recompile the checks against the latest version, or if&#xA;this is a check bundled with a third-party library, see&#xA;if there is a more recent version available.&#xA;&#xA;Version of Lint API this lint check is using is 11.&#xA;The Lint API version currently running is 12 (7.2).">
<location <location
file="../../../../.gradle/caches/transforms-3/41c4bb138622423228a0087a50b102c6/transformed/jetified-annotation-experimental-1.2.0/jars/lint.jar"/> file="../../../../.gradle/caches/transforms-3/0939f771fd60f77a6733c1fbba02a5be/transformed/jetified-annotation-experimental-1.2.0/jars/lint.jar"/>
</issue> </issue>
<issue <issue
@ -62,7 +62,7 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/AndroidManifest.xml" file="src/main/AndroidManifest.xml"
line="154" line="155"
column="10"/> column="10"/>
</issue> </issue>
@ -73,7 +73,7 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/AndroidManifest.xml" file="src/main/AndroidManifest.xml"
line="78" line="79"
column="10"/> column="10"/>
</issue> </issue>
@ -209,6 +209,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)
}
private fun ensureNotificationChannel() { commands.forEachIndexed { index, command ->
if (Util.SDK_INT < Build.VERSION_CODES.O || command.extras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, index)
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) return commands
}
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>