merged with develop branch
This commit is contained in:
commit
d4ead49548
|
@ -41,7 +41,6 @@ jobs:
|
|||
name: unit-tests
|
||||
command: |
|
||||
./gradlew ciTest testDebugUnitTest
|
||||
./gradlew jacocoFullReport
|
||||
- run:
|
||||
name: lint
|
||||
command: ./gradlew :ultrasonic:lintRelease
|
||||
|
@ -61,8 +60,6 @@ jobs:
|
|||
- store_artifacts:
|
||||
path: subsonic-api/build/reports
|
||||
destination: reports
|
||||
- store_artifacts:
|
||||
path: build/reports/jacoco/jacocoFullReport/
|
||||
push_translations:
|
||||
docker:
|
||||
- image: cimg/python:3.6
|
||||
|
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
|
|
|
@ -17,7 +17,6 @@ buildscript {
|
|||
classpath libs.kotlin
|
||||
classpath libs.ktlintGradle
|
||||
classpath libs.detekt
|
||||
classpath libs.jacoco
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,8 +43,6 @@ allprojects {
|
|||
}
|
||||
}
|
||||
|
||||
apply from: 'gradle_scripts/jacoco.gradle'
|
||||
|
||||
wrapper {
|
||||
gradleVersion(libs.versions.gradle.get())
|
||||
distributionType("all")
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
apply from: bootstrap.androidModule
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
ext {
|
||||
jacocoExclude = [
|
||||
'**/domain/**'
|
||||
]
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.roomRuntime
|
||||
implementation libs.roomKtx
|
||||
|
|
|
@ -20,11 +20,3 @@ dependencies {
|
|||
testImplementation libs.mockWebServer
|
||||
testImplementation libs.apacheCodecs
|
||||
}
|
||||
|
||||
ext {
|
||||
// Excluding data classes
|
||||
jacocoExclude = [
|
||||
'**/models/**',
|
||||
'**/di/**'
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,10 +8,9 @@ androidxcore = "1.6.0"
|
|||
ktlint = "0.43.2"
|
||||
ktlintGradle = "10.2.0"
|
||||
detekt = "1.19.0"
|
||||
jacoco = "0.8.7"
|
||||
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"
|
||||
|
@ -49,7 +48,6 @@ gradle = { module = "com.android.tools.build:gradle", version.r
|
|||
kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
|
||||
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" }
|
||||
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" }
|
||||
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'jacoco'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
|
||||
android {
|
||||
|
@ -48,10 +47,6 @@ android {
|
|||
|
||||
tasks.withType(Test) {
|
||||
useJUnitPlatform()
|
||||
jacoco {
|
||||
includeNoLocationClasses = true
|
||||
excludes += jacocoExclude
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -61,11 +56,4 @@ dependencies {
|
|||
testRuntimeOnly libs.junitVintage
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion(libs.versions.jacoco.get())
|
||||
}
|
||||
|
||||
ext {
|
||||
jacocoExclude = ['jdk.internal.*']
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'jacoco'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
|
||||
sourceSets {
|
||||
|
@ -21,36 +20,8 @@ dependencies {
|
|||
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 {
|
||||
useJUnitPlatform()
|
||||
jacoco {
|
||||
excludes += jacocoExclude
|
||||
includeNoLocationClasses = true
|
||||
}
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
|
||||
tasks.register("ciTest") {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'jacoco'
|
||||
apply from: "../gradle_scripts/code_quality.gradle"
|
||||
|
||||
android {
|
||||
|
@ -135,36 +134,3 @@ dependencies {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
id="ObsoleteLintCustomCheck"
|
||||
message="Library lint checks out of date.

Lint found an issue registry (`androidx.annotation.experimental.lint.ExperimentalIssueRegistry`)
which was compiled against an older version of lint
than this one.

This often works just fine, but some basic verification
shows that the lint check jar references (for example)
the following API which is no longer valid in this
version of lint:
com.android.tools.lint.client.api.AnnotationLookup: org.jetbrains.uast.UAnnotation findRealAnnotation(com.intellij.psi.PsiAnnotation,com.intellij.psi.PsiClass,org.jetbrains.uast.UElement)
(Referenced from androidx/annotation/experimental/lint/ExperimentalDetector.class)

Recompile the checks against the latest version, or if
this is a check bundled with a third-party library, see
if there is a more recent version available.

Version of Lint API this lint check is using is 11.
The Lint API version currently running is 12 (7.2).">
|
||||
<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
|
||||
|
@ -62,7 +62,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="154"
|
||||
line="155"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
|
@ -73,7 +73,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="78"
|
||||
line="79"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
|
@ -209,6 +209,61 @@
|
|||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
|
||||
errorLine1="<vector android:height="48dp""
|
||||
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="<vector android:height="48dp""
|
||||
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="<vector android:height="32dp""
|
||||
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="<vector android:height="32dp""
|
||||
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="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/drawable/media3_notification_small_icon.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="IconDuplicates"
|
||||
message="The following unrelated icon files have identical contents: list_pressed_holo_dark.9.png, list_pressed_holo_light.9.png">
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
||||
<service android:name=".playback.PlaybackService"
|
||||
android:label="@string/common.appname"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
|
|
@ -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<MediaPlayerController>()
|
||||
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<MediaItem>
|
||||
): ListenableFuture<MutableList<MediaItem>> {
|
||||
|
||||
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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<CommandButton>,
|
||||
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<CommandButton>,
|
||||
playWhenReady: Boolean
|
||||
): MutableList<CommandButton> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue