mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-03 09:11:00 +03:00
Compare commits
No commits in common. "develop" and "4.6.1" have entirely different histories.
@ -1,2 +0,0 @@
|
|||||||
[*.{kt,kts}]
|
|
||||||
ktlint_code_style = android_studio
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,7 +18,6 @@ out/
|
|||||||
|
|
||||||
# Gradle files
|
# Gradle files
|
||||||
.gradle/
|
.gradle/
|
||||||
.kotlin/
|
|
||||||
build/
|
build/
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
# Local configuration file (sdk path, etc)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
default:
|
default:
|
||||||
image: registry.gitlab.com/ultrasonic/ci-android:1.2.0
|
image: registry.gitlab.com/ultrasonic/ci-android:1.1.0
|
||||||
cache: &global_cache
|
cache: &global_cache
|
||||||
key:
|
key:
|
||||||
files:
|
files:
|
||||||
@ -138,7 +138,7 @@ RoboTest:
|
|||||||
script:
|
script:
|
||||||
- curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
|
- curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
|
||||||
- gcloud auth activate-service-account --key-file .secure_files/firebase-key.json
|
- gcloud auth activate-service-account --key-file .secure_files/firebase-key.json
|
||||||
- gcloud firebase test android run --project ultrasonic-61089 --type robo --app ultrasonic-release/${PACKAGE_APK} --robo-directives click:button1= --device model=Nexus6,version=21,locale=en,orientation=portrait --device model=Pixel3,version=28,locale=fr,orientation=landscape
|
- gcloud firebase test android run --project ultrasonic-61089 --type robo --app ultrasonic-release/${PACKAGE_APK} --robo-directives click:button1= --device model=Nexus6,version=21,locale=en,orientation=portrait --device model=Pixel3,version=28,locale=fr,orientation=landscape --device model=Pixel5,version=30,locale=zh,orientation=portrait
|
||||||
rules:
|
rules:
|
||||||
# Run when releasing a new tag
|
# Run when releasing a new tag
|
||||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||||
|
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
@ -1,6 +1,11 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="XML">
|
<codeStyleSettings language="XML">
|
||||||
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="21" />
|
<bytecodeTargetLevel target="17" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
21
build.gradle
21
build.gradle
@ -1,5 +1,3 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
apply from: 'gradle/versions.gradle'
|
apply from: 'gradle/versions.gradle'
|
||||||
@ -12,8 +10,7 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
maven { url "https://plugins.gradle.org/m2/" }
|
||||||
maven { url = "https://plugins.gradle.org/m2/" }
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath libs.gradle
|
classpath libs.gradle
|
||||||
@ -29,32 +26,24 @@ allprojects {
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Kotlin JVM target to the same for all subprojects
|
// Set Kotlin JVM target to the same for all subprojects
|
||||||
tasks.withType(KotlinCompile).configureEach {
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "21"
|
jvmTarget = "17"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(JavaCompile).tap {
|
|
||||||
configureEach {
|
|
||||||
options.compilerArgs.add("-Xlint:deprecation")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper {
|
wrapper {
|
||||||
gradleVersion = libs.versions.gradle.get()
|
gradleVersion(libs.versions.gradle.get())
|
||||||
distributionType = "all"
|
distributionType("all")
|
||||||
}
|
}
|
@ -1,20 +1,13 @@
|
|||||||
plugins {
|
|
||||||
alias libs.plugins.ksp
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: bootstrap.androidModule
|
apply from: bootstrap.androidModule
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.core
|
implementation libs.core
|
||||||
implementation libs.roomRuntime
|
implementation libs.roomRuntime
|
||||||
implementation libs.roomKtx
|
implementation libs.roomKtx
|
||||||
ksp libs.room
|
kapt libs.room
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = 'org.moire.ultrasonic.subsonic.domain'
|
namespace 'org.moire.ultrasonic.subsonic.domain'
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_21
|
|
||||||
targetCompatibility JavaVersion.VERSION_21
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ data class Album(
|
|||||||
override var genre: String? = null,
|
override var genre: String? = null,
|
||||||
override var starred: Boolean = false,
|
override var starred: Boolean = false,
|
||||||
override var path: String? = null,
|
override var path: String? = null,
|
||||||
override var closeness: Int = 0
|
override var closeness: Int = 0,
|
||||||
) : MusicDirectory.Child() {
|
) : MusicDirectory.Child() {
|
||||||
override var isDirectory = true
|
override var isDirectory = true
|
||||||
override var isVideo = false
|
override var isVideo = false
|
||||||
|
@ -13,7 +13,10 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||||||
var name: String? = null
|
var name: String? = null
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun getChildren(includeDirs: Boolean = true, includeFiles: Boolean = true): List<Child> {
|
fun getChildren(
|
||||||
|
includeDirs: Boolean = true,
|
||||||
|
includeFiles: Boolean = true
|
||||||
|
): List<Child> {
|
||||||
if (includeDirs && includeFiles) {
|
if (includeDirs && includeFiles) {
|
||||||
return toList()
|
return toList()
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
plugins {
|
|
||||||
alias libs.plugins.ksp
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: bootstrap.kotlinModule
|
apply from: bootstrap.kotlinModule
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.retrofit
|
api libs.retrofit
|
||||||
api libs.jacksonConverter
|
api libs.jacksonConverter
|
||||||
|
@ -8,8 +8,7 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
|||||||
* Base class for integration tests for [SubsonicAPIClient] class.
|
* Base class for integration tests for [SubsonicAPIClient] class.
|
||||||
*/
|
*/
|
||||||
abstract class SubsonicAPIClientTest {
|
abstract class SubsonicAPIClientTest {
|
||||||
@JvmField @Rule
|
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
||||||
val mockWebServerRule = MockWebServerRule()
|
|
||||||
|
|
||||||
protected lateinit var config: SubsonicClientConfiguration
|
protected lateinit var config: SubsonicClientConfiguration
|
||||||
protected lateinit var client: SubsonicAPIClient
|
protected lateinit var client: SubsonicAPIClient
|
||||||
|
@ -11,8 +11,7 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
|||||||
* Base class for testing [okhttp3.Interceptor] implementations.
|
* Base class for testing [okhttp3.Interceptor] implementations.
|
||||||
*/
|
*/
|
||||||
abstract class BaseInterceptorTest {
|
abstract class BaseInterceptorTest {
|
||||||
@Rule @JvmField
|
@Rule @JvmField val mockWebServerRule = MockWebServerRule()
|
||||||
val mockWebServerRule = MockWebServerRule()
|
|
||||||
|
|
||||||
lateinit var client: OkHttpClient
|
lateinit var client: OkHttpClient
|
||||||
|
|
||||||
|
@ -92,13 +92,7 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkVersion(V1_4_0)
|
checkVersion(V1_4_0)
|
||||||
checkParamVersion(musicFolderId, V1_12_0)
|
checkParamVersion(musicFolderId, V1_12_0)
|
||||||
return api.search2(
|
return api.search2(
|
||||||
query,
|
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
||||||
artistCount,
|
|
||||||
artistOffset,
|
|
||||||
albumCount,
|
|
||||||
albumOffset,
|
|
||||||
songCount,
|
|
||||||
musicFolderId
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,13 +108,7 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkVersion(V1_8_0)
|
checkVersion(V1_8_0)
|
||||||
checkParamVersion(musicFolderId, V1_12_0)
|
checkParamVersion(musicFolderId, V1_12_0)
|
||||||
return api.search3(
|
return api.search3(
|
||||||
query,
|
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
||||||
artistCount,
|
|
||||||
artistOffset,
|
|
||||||
albumCount,
|
|
||||||
albumOffset,
|
|
||||||
songCount,
|
|
||||||
musicFolderId
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,13 +228,7 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkParamVersion(estimateContentLength, V1_8_0)
|
checkParamVersion(estimateContentLength, V1_8_0)
|
||||||
checkParamVersion(converted, V1_14_0)
|
checkParamVersion(converted, V1_14_0)
|
||||||
return api.stream(
|
return api.stream(
|
||||||
id,
|
id, maxBitRate, format, timeOffset, videoSize, estimateContentLength, converted
|
||||||
maxBitRate,
|
|
||||||
format,
|
|
||||||
timeOffset,
|
|
||||||
videoSize,
|
|
||||||
estimateContentLength,
|
|
||||||
converted
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,9 +335,8 @@ internal class ApiVersionCheckWrapper(
|
|||||||
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
||||||
// If it is true, it is probably the first call with this server
|
// If it is true, it is probably the first call with this server
|
||||||
if (!isRealProtocolVersion) return
|
if (!isRealProtocolVersion) return
|
||||||
if (currentApiVersion < expectedVersion) {
|
if (currentApiVersion < expectedVersion)
|
||||||
throw ApiNotSupportedException(currentApiVersion)
|
throw ApiNotSupportedException(currentApiVersion)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
||||||
|
@ -90,7 +90,10 @@ interface SubsonicAPIDefinition {
|
|||||||
): Call<SubsonicResponse>
|
): Call<SubsonicResponse>
|
||||||
|
|
||||||
@GET("setRating.view")
|
@GET("setRating.view")
|
||||||
fun setRating(@Query("id") id: String, @Query("rating") rating: Int): Call<SubsonicResponse>
|
fun setRating(
|
||||||
|
@Query("id") id: String,
|
||||||
|
@Query("rating") rating: Int
|
||||||
|
): Call<SubsonicResponse>
|
||||||
|
|
||||||
@GET("getArtist.view")
|
@GET("getArtist.view")
|
||||||
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
|
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
|
||||||
@ -155,7 +158,8 @@ interface SubsonicAPIDefinition {
|
|||||||
@Query("public") public: Boolean? = null,
|
@Query("public") public: Boolean? = null,
|
||||||
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
|
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
|
||||||
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
|
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
|
||||||
): Call<SubsonicResponse>
|
):
|
||||||
|
Call<SubsonicResponse>
|
||||||
|
|
||||||
@GET("getPodcasts.view")
|
@GET("getPodcasts.view")
|
||||||
fun getPodcasts(
|
fun getPodcasts(
|
||||||
@ -223,7 +227,10 @@ interface SubsonicAPIDefinition {
|
|||||||
|
|
||||||
@Streaming
|
@Streaming
|
||||||
@GET("getCoverArt.view")
|
@GET("getCoverArt.view")
|
||||||
fun getCoverArt(@Query("id") id: String, @Query("size") size: Long? = null): Call<ResponseBody>
|
fun getCoverArt(
|
||||||
|
@Query("id") id: String,
|
||||||
|
@Query("size") size: Long? = null
|
||||||
|
): Call<ResponseBody>
|
||||||
|
|
||||||
@Streaming
|
@Streaming
|
||||||
@GET("stream.view")
|
@GET("stream.view")
|
||||||
|
@ -29,12 +29,10 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
|||||||
V1_13_0("5.3", "1.13.0"),
|
V1_13_0("5.3", "1.13.0"),
|
||||||
V1_14_0("6.0", "1.14.0"),
|
V1_14_0("6.0", "1.14.0"),
|
||||||
V1_15_0("6.1", "1.15.0"),
|
V1_15_0("6.1", "1.15.0"),
|
||||||
V1_16_0("6.1.2", "1.16.0")
|
V1_16_0("6.1.2", "1.16.0");
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic @Throws(IllegalArgumentException::class)
|
||||||
@Throws(IllegalArgumentException::class)
|
|
||||||
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
|
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
|
||||||
val versionComponents = apiVersion.split(".")
|
val versionComponents = apiVersion.split(".")
|
||||||
|
|
||||||
@ -43,11 +41,8 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
|||||||
try {
|
try {
|
||||||
val majorVersion = versionComponents[0].toInt()
|
val majorVersion = versionComponents[0].toInt()
|
||||||
val minorVersion = versionComponents[1].toInt()
|
val minorVersion = versionComponents[1].toInt()
|
||||||
val patchVersion = if (versionComponents.size > 2) {
|
val patchVersion = if (versionComponents.size > 2) versionComponents[2].toInt()
|
||||||
versionComponents[2].toInt()
|
else 0
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
when (majorVersion) {
|
when (majorVersion) {
|
||||||
1 -> when {
|
1 -> when {
|
||||||
|
@ -48,10 +48,7 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
retrofit: Retrofit
|
retrofit: Retrofit
|
||||||
): Converter<*, RequestBody>? {
|
): Converter<*, RequestBody>? {
|
||||||
return jacksonConverterFactory?.requestBodyConverter(
|
return jacksonConverterFactory?.requestBodyConverter(
|
||||||
type,
|
type, parameterAnnotations, methodAnnotations, retrofit
|
||||||
parameterAnnotations,
|
|
||||||
methodAnnotations,
|
|
||||||
retrofit
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +63,7 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VersionAwareResponseBodyConverter<T>(
|
class VersionAwareResponseBodyConverter<T> (
|
||||||
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
||||||
private val adapter: ObjectReader
|
private val adapter: ObjectReader
|
||||||
) : Converter<ResponseBody, T> {
|
) : Converter<ResponseBody, T> {
|
||||||
|
@ -6,7 +6,6 @@ import okhttp3.Interceptor.Chain
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
|
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
|
||||||
|
|
||||||
// Allow 20 seconds extra timeout pear MB offset.
|
// Allow 20 seconds extra timeout pear MB offset.
|
||||||
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
||||||
|
|
||||||
|
@ -23,8 +23,7 @@ enum class AlbumListType(val typeName: String) {
|
|||||||
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
||||||
STARRED("starred"),
|
STARRED("starred"),
|
||||||
BY_YEAR("byYear"),
|
BY_YEAR("byYear"),
|
||||||
BY_GENRE("byGenre")
|
BY_GENRE("byGenre");
|
||||||
;
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return typeName
|
return typeName
|
||||||
|
@ -16,8 +16,7 @@ enum class JukeboxAction(val action: String) {
|
|||||||
CLEAR("clear"),
|
CLEAR("clear"),
|
||||||
REMOVE("remove"),
|
REMOVE("remove"),
|
||||||
SHUFFLE("shuffle"),
|
SHUFFLE("shuffle"),
|
||||||
SET_GAIN("setGain")
|
SET_GAIN("setGain");
|
||||||
;
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return action
|
return action
|
||||||
|
@ -10,8 +10,7 @@ class BookmarksResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("bookmarks")
|
@JsonProperty("bookmarks") private val bookmarksWrapper = BookmarkWrapper()
|
||||||
private val bookmarksWrapper = BookmarkWrapper()
|
|
||||||
|
|
||||||
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
|
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,7 @@ class ChatMessagesResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("chatMessages")
|
@JsonProperty("chatMessages") private val wrapper = ChatMessagesWrapper()
|
||||||
private val wrapper = ChatMessagesWrapper()
|
|
||||||
|
|
||||||
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
|
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,7 @@ class GenresResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("genres")
|
@JsonProperty("genres") private val genresWrapper = GenresWrapper()
|
||||||
private val genresWrapper = GenresWrapper()
|
|
||||||
val genresList: List<Genre> get() = genresWrapper.genresList
|
val genresList: List<Genre> get() = genresWrapper.genresList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,8 +11,7 @@ class GetAlbumList2Response(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("albumList2")
|
@JsonProperty("albumList2") private val albumWrapper2 = AlbumWrapper2()
|
||||||
private val albumWrapper2 = AlbumWrapper2()
|
|
||||||
|
|
||||||
val albumList: List<Album>
|
val albumList: List<Album>
|
||||||
get() = albumWrapper2.albumList
|
get() = albumWrapper2.albumList
|
||||||
|
@ -10,8 +10,7 @@ class GetAlbumListResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("albumList")
|
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
|
||||||
private val albumWrapper = AlbumWrapper()
|
|
||||||
|
|
||||||
val albumList: List<Album>
|
val albumList: List<Album>
|
||||||
get() = albumWrapper.albumList
|
get() = albumWrapper.albumList
|
||||||
|
@ -10,8 +10,7 @@ class GetPodcastsResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("podcasts")
|
@JsonProperty("podcasts") private val channelsWrapper = PodcastChannelWrapper()
|
||||||
private val channelsWrapper = PodcastChannelWrapper()
|
|
||||||
|
|
||||||
val podcastChannels: List<PodcastChannel>
|
val podcastChannels: List<PodcastChannel>
|
||||||
get() = channelsWrapper.channelsList
|
get() = channelsWrapper.channelsList
|
||||||
|
@ -10,8 +10,7 @@ class GetRandomSongsResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("randomSongs")
|
@JsonProperty("randomSongs") private val songsWrapper = RandomSongsWrapper()
|
||||||
private val songsWrapper = RandomSongsWrapper()
|
|
||||||
|
|
||||||
val songsList
|
val songsList
|
||||||
get() = songsWrapper.songsList
|
get() = songsWrapper.songsList
|
||||||
|
@ -10,8 +10,7 @@ class GetSongsByGenreResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("songsByGenre")
|
@JsonProperty("songsByGenre") private val songsByGenreList = SongsByGenreWrapper()
|
||||||
private val songsByGenreList = SongsByGenreWrapper()
|
|
||||||
|
|
||||||
val songsList get() = songsByGenreList.songsList
|
val songsList get() = songsByGenreList.songsList
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,11 @@ class JukeboxResponse(
|
|||||||
error: SubsonicError?,
|
error: SubsonicError?,
|
||||||
var jukebox: JukeboxStatus = JukeboxStatus()
|
var jukebox: JukeboxStatus = JukeboxStatus()
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonSetter("jukeboxStatus")
|
@JsonSetter("jukeboxStatus") fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
||||||
fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
|
||||||
this.jukebox = jukebox
|
this.jukebox = jukebox
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSetter("jukeboxPlaylist")
|
@JsonSetter("jukeboxPlaylist") fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
||||||
fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
|
||||||
this.jukebox = jukebox
|
this.jukebox = jukebox
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,7 @@ class MusicFoldersResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("musicFolders")
|
@JsonProperty("musicFolders") private val wrapper = MusicFoldersWrapper()
|
||||||
private val wrapper = MusicFoldersWrapper()
|
|
||||||
|
|
||||||
val musicFolders get() = wrapper.musicFolders
|
val musicFolders get() = wrapper.musicFolders
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,7 @@ class SharesResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("shares")
|
@JsonProperty("shares") private val wrappedShares = SharesWrapper()
|
||||||
private val wrappedShares = SharesWrapper()
|
|
||||||
|
|
||||||
val shares get() = wrappedShares.share
|
val shares get() = wrappedShares.share
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,7 @@ open class SubsonicResponse(
|
|||||||
) {
|
) {
|
||||||
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
||||||
enum class Status(val jsonValue: String) {
|
enum class Status(val jsonValue: String) {
|
||||||
OK("ok"),
|
OK("ok"), ERROR("failed");
|
||||||
ERROR("failed");
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getStatusFromJson(jsonValue: String) =
|
fun getStatusFromJson(jsonValue: String) =
|
||||||
|
@ -10,8 +10,7 @@ class VideosResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("videos")
|
@JsonProperty("videos") private val videosWrapper = VideosWrapper()
|
||||||
private val videosWrapper = VideosWrapper()
|
|
||||||
|
|
||||||
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
|
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,7 @@ class ProxyPasswordInterceptorTest {
|
|||||||
|
|
||||||
private val proxyInterceptor = ProxyPasswordInterceptor(
|
private val proxyInterceptor = ProxyPasswordInterceptor(
|
||||||
V1_12_0,
|
V1_12_0,
|
||||||
mockPasswordHexInterceptor,
|
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false
|
||||||
mockPasswordMd5Interceptor,
|
|
||||||
false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -42,10 +40,8 @@ class ProxyPasswordInterceptorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Should use hex password if forceHex is true`() {
|
fun `Should use hex password if forceHex is true`() {
|
||||||
val interceptor = ProxyPasswordInterceptor(
|
val interceptor = ProxyPasswordInterceptor(
|
||||||
V1_16_0,
|
V1_16_0, mockPasswordHexInterceptor,
|
||||||
mockPasswordHexInterceptor,
|
mockPasswordMd5Interceptor, true
|
||||||
mockPasswordMd5Interceptor,
|
|
||||||
true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
interceptor.intercept(mockChain)
|
interceptor.intercept(mockChain)
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
Features:
|
|
||||||
- Search is accessible through a new icon on the main screen
|
|
||||||
- Modernize Back Handling
|
|
||||||
- Reenable R8 Code minification
|
|
||||||
- Add a "Play Random Songs" shortcut
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
- Avoid triggering a bug in Supysonic
|
|
||||||
- Readd the "Star" button to the Now Playing screen
|
|
||||||
- Fix a rare crash when shuffling playlists with duplicate entries
|
|
||||||
- Fix a crash when choosing "Play next" on an empty playlist.
|
|
||||||
- Tracks buttons flash a scrollbar sometimes in Android 13
|
|
||||||
- Fix EndlessScrolling in genre listing
|
|
||||||
- Couldn't delete a track when shuffle was active
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
Features:
|
|
||||||
- Search is accessible through a new icon on the main screen
|
|
||||||
- Modernize Back Handling
|
|
||||||
- Reenable R8 Code minification
|
|
||||||
- Add a "Play Random Songs" shortcut
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
- Fix a few crashes
|
|
||||||
- Avoid triggering a bug in Supysonic
|
|
||||||
- Readd the "Star" button to the Now Playing screen
|
|
||||||
- Fix a rare crash when shuffling playlists with duplicate entries
|
|
||||||
- Fix a crash when choosing "Play next" on an empty playlist.
|
|
||||||
- Tracks buttons flash a scrollbar sometimes in Android 13
|
|
||||||
- Fix EndlessScrolling in genre listing
|
|
||||||
- Couldn't delete a track when shuffle was active
|
|
@ -1,9 +0,0 @@
|
|||||||
### Features
|
|
||||||
- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
|
|
||||||
- Properly handling nested directory structures (Android Auto)
|
|
||||||
- Add a toast when adding tracks to the playlist
|
|
||||||
- Allow pinning when offline
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- Update koin
|
|
||||||
- Update media3 to v1.1.0
|
|
@ -1,12 +0,0 @@
|
|||||||
### Fixes
|
|
||||||
- Fix a bug in 4.7.0 that repeat mode was activated by default.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
|
|
||||||
- Properly handling nested directory structures (Android Auto)
|
|
||||||
- Add a toast when adding tracks to the playlist
|
|
||||||
- Allow pinning when offline
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- Update koin
|
|
||||||
- Update media3 to v1.1.0
|
|
@ -1,5 +0,0 @@
|
|||||||
### Features
|
|
||||||
- Improved display of rating stars
|
|
||||||
- Completely modernize all older code parts
|
|
||||||
- Updates for Android 14
|
|
||||||
- Update dependencies
|
|
@ -1,46 +1,44 @@
|
|||||||
[versions]
|
[versions]
|
||||||
# You need to run ./gradlew wrapper after updating the version
|
# You need to run ./gradlew wrapper after updating the version
|
||||||
gradle = "8.13"
|
gradle = "8.1.1"
|
||||||
|
|
||||||
navigation = "2.8.9"
|
navigation = "2.6.0"
|
||||||
gradlePlugin = "8.9.1"
|
gradlePlugin = "8.0.2"
|
||||||
androidxcar = "1.4.0"
|
androidxcore = "1.10.1"
|
||||||
androidxcore = "1.16.0"
|
ktlint = "0.43.2"
|
||||||
ktlint = "1.0.1"
|
ktlintGradle = "11.4.2"
|
||||||
ktlintGradle = "12.2.0"
|
detekt = "1.23.0"
|
||||||
detekt = "1.23.8"
|
preferences = "1.2.0"
|
||||||
preferences = "1.2.1"
|
media3 = "1.0.2"
|
||||||
media3 = "1.6.1"
|
|
||||||
|
|
||||||
androidSupport = "1.9.1"
|
androidSupport = "1.6.0"
|
||||||
materialDesign = "1.12.0"
|
materialDesign = "1.9.0"
|
||||||
constraintLayout = "2.2.1"
|
constraintLayout = "2.1.4"
|
||||||
activity = "1.10.1"
|
|
||||||
multidex = "2.0.1"
|
multidex = "2.0.1"
|
||||||
room = "2.7.0"
|
room = "2.5.2"
|
||||||
kotlin = "2.1.20"
|
kotlin = "1.8.22"
|
||||||
ksp = "2.1.20-2.0.0"
|
kotlinxCoroutines = "1.7.1"
|
||||||
kotlinxCoroutines = "1.10.2"
|
viewModelKtx = "2.6.1"
|
||||||
viewModelKtx = "2.8.7"
|
|
||||||
swipeRefresh = "1.1.0"
|
swipeRefresh = "1.1.0"
|
||||||
|
|
||||||
retrofit = "2.11.0"
|
retrofit = "2.9.0"
|
||||||
jackson = "2.18.3"
|
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
|
||||||
okhttp = "4.12.0"
|
jackson = "2.13.5"
|
||||||
koin = "4.0.4"
|
okhttp = "4.11.0"
|
||||||
|
koin = "3.3.2"
|
||||||
picasso = "2.8"
|
picasso = "2.8"
|
||||||
|
|
||||||
junit4 = "4.13.2"
|
junit4 = "4.13.2"
|
||||||
junit5 = "5.12.2"
|
junit5 = "5.9.3"
|
||||||
mockito = "5.17.0"
|
mockito = "5.4.0"
|
||||||
mockitoKotlin = "5.4.0"
|
mockitoKotlin = "5.0.0"
|
||||||
kluent = "1.73"
|
kluent = "1.73"
|
||||||
apacheCodecs = "1.18.0"
|
apacheCodecs = "1.16.0"
|
||||||
robolectric = "4.14.1"
|
robolectric = "4.10.3"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
fastScroll = "2.0.1"
|
fastScroll = "2.0.1"
|
||||||
colorPicker = "2.3.0"
|
colorPicker = "2.2.4"
|
||||||
rxJava = "3.1.10"
|
rxJava = "3.1.6"
|
||||||
rxAndroid = "3.0.2"
|
rxAndroid = "3.0.2"
|
||||||
multiType = "4.3.0"
|
multiType = "4.3.0"
|
||||||
|
|
||||||
@ -50,7 +48,6 @@ kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin"
|
|||||||
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" }
|
||||||
|
|
||||||
car = { module = "androidx.car.app:app", version.ref = "androidxcar" }
|
|
||||||
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
|
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
|
||||||
design = { module = "com.google.android.material:material", version.ref = "materialDesign" }
|
design = { module = "com.google.android.material:material", version.ref = "materialDesign" }
|
||||||
annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
|
annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
|
||||||
@ -66,7 +63,6 @@ navigationFragmentKtx = { module = "androidx.navigation:navigation-fragment-kt
|
|||||||
navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
||||||
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
|
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
|
||||||
navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"}
|
navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"}
|
||||||
activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" }
|
|
||||||
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
|
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
|
||||||
media3common = { module = "androidx.media3:media3-common", version.ref = "media3" }
|
media3common = { module = "androidx.media3:media3-common", version.ref = "media3" }
|
||||||
media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||||
@ -104,6 +100,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" }
|
||||||
|
|
||||||
[plugins]
|
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
ext.versions = [
|
ext.versions = [
|
||||||
minSdk : 26,
|
minSdk : 21,
|
||||||
targetSdk : 33,
|
targetSdk : 33,
|
||||||
compileSdk : 35,
|
compileSdk : 33,
|
||||||
]
|
]
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,7 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'org.jetbrains.kotlin.android'
|
apply plugin: 'org.jetbrains.kotlin.android'
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
apply plugin: 'com.google.devtools.ksp'
|
apply plugin: 'org.jetbrains.kotlin.kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion versions.compileSdk
|
compileSdkVersion versions.compileSdk
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* This module provides a base for for pure kotlin modules
|
* This module provides a base for for pure kotlin modules
|
||||||
*/
|
*/
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
apply plugin: 'com.google.devtools.ksp'
|
apply plugin: 'org.jetbrains.kotlin.kapt'
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@ -12,6 +12,7 @@ sourceSets {
|
|||||||
test.resources.srcDirs += "${projectDir}/src/integrationTest/resources"
|
test.resources.srcDirs += "${projectDir}/src/integrationTest/resources"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kotlinStdlib
|
api libs.kotlinStdlib
|
||||||
|
|
||||||
|
26
gradlew
vendored
26
gradlew
vendored
@ -15,8 +15,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@ -57,7 +55,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@ -85,8 +83,7 @@ done
|
|||||||
# This is normally unused
|
# This is normally unused
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@ -133,13 +130,10 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
if ! command -v java >/dev/null 2>&1
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
@ -147,7 +141,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC2039,SC3045
|
# shellcheck disable=SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
@ -155,7 +149,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC2039,SC3045
|
# shellcheck disable=SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@ -204,11 +198,11 @@ fi
|
|||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command;
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
# and any embedded shellness will be escaped.
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# double quotes to make sure that they get re-expanded; and
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
22
gradlew.bat
vendored
22
gradlew.bat
vendored
@ -13,8 +13,6 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
|
|||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
plugins {
|
|
||||||
alias libs.plugins.ksp
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'org.jetbrains.kotlin.android'
|
apply plugin: 'org.jetbrains.kotlin.android'
|
||||||
|
apply plugin: 'org.jetbrains.kotlin.kapt'
|
||||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||||
apply from: "../gradle_scripts/code_quality.gradle"
|
apply from: "../gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
@ -12,8 +9,8 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.moire.ultrasonic"
|
applicationId "org.moire.ultrasonic"
|
||||||
versionCode 130
|
versionCode 124
|
||||||
versionName "4.8.0"
|
versionName "4.6.1"
|
||||||
|
|
||||||
minSdkVersion versions.minSdk
|
minSdkVersion versions.minSdk
|
||||||
targetSdkVersion versions.targetSdk
|
targetSdkVersion versions.targetSdk
|
||||||
@ -34,10 +31,10 @@ android {
|
|||||||
'minify/proguard-kotlin.pro'
|
'minify/proguard-kotlin.pro'
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled = false
|
minifyEnabled false
|
||||||
multiDexEnabled = true
|
multiDexEnabled true
|
||||||
testCoverageEnabled = true
|
testCoverageEnabled true
|
||||||
applicationIdSuffix = '.debug'
|
applicationIdSuffix '.debug'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,43 +50,42 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "21"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding true
|
||||||
dataBinding = true
|
dataBinding true
|
||||||
buildConfig = true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_21
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_21
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
ksp {
|
kapt {
|
||||||
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas")
|
arguments {
|
||||||
|
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
baseline = file("lint-baseline.xml")
|
baseline = file("lint-baseline.xml")
|
||||||
abortOnError = true
|
abortOnError true
|
||||||
warningsAsErrors = true
|
warningsAsErrors true
|
||||||
warning 'ImpliedQuantity'
|
|
||||||
disable 'IconMissingDensityFolder', 'VectorPath'
|
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||||
disable 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||||
|
warning 'ImpliedQuantity'
|
||||||
disable 'ObsoleteLintCustomCheck'
|
disable 'ObsoleteLintCustomCheck'
|
||||||
// We manage dependencies on Gitlab with RenovateBot
|
textReport true
|
||||||
disable 'GradleDependency'
|
checkDependencies true
|
||||||
disable 'AndroidGradlePluginVersion'
|
|
||||||
textReport = true
|
|
||||||
checkDependencies = true
|
|
||||||
}
|
}
|
||||||
namespace = 'org.moire.ultrasonic'
|
namespace 'org.moire.ultrasonic'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(Test).configureEach {
|
tasks.withType(Test) {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +97,6 @@ dependencies {
|
|||||||
exclude group: "com.android.support"
|
exclude group: "com.android.support"
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation libs.car
|
|
||||||
implementation libs.core
|
implementation libs.core
|
||||||
implementation libs.design
|
implementation libs.design
|
||||||
implementation libs.multidex
|
implementation libs.multidex
|
||||||
@ -134,7 +129,7 @@ dependencies {
|
|||||||
implementation libs.rxAndroid
|
implementation libs.rxAndroid
|
||||||
implementation libs.multiType
|
implementation libs.multiType
|
||||||
|
|
||||||
ksp libs.room
|
kapt libs.room
|
||||||
|
|
||||||
testImplementation libs.kotlinReflect
|
testImplementation libs.kotlinReflect
|
||||||
testImplementation libs.junit
|
testImplementation libs.junit
|
||||||
@ -146,5 +141,6 @@ dependencies {
|
|||||||
testImplementation libs.robolectric
|
testImplementation libs.robolectric
|
||||||
|
|
||||||
implementation libs.timber
|
implementation libs.timber
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<?xml version="1.0" ?>
|
<?xml version="1.0" ?>
|
||||||
<SmellBaseline>
|
<SmellBaseline>
|
||||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
<ManuallySuppressedIssues>
|
||||||
<CurrentIssues>
|
<ID>TooManyFunctions:PlaybackService.kt$PlaybackService : MediaLibraryServiceKoinComponentCoroutineScope</ID>
|
||||||
|
<ID>UnusedPrivateMember:UApp.kt$private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder</ID>
|
||||||
|
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
|
||||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
|
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
|
||||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
|
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
|
||||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
|
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
|
||||||
@ -9,11 +11,19 @@
|
|||||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
||||||
<ID>LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
<ID>LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||||
|
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken, additionalId: String? )</ID>
|
||||||
<ID>LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
<ID>LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||||
|
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID>
|
||||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||||
|
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
|
||||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>
|
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>
|
||||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||||
|
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||||
|
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
||||||
|
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable</ID>
|
||||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
|
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
|
||||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||||
</CurrentIssues>
|
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||||
|
</ManuallySuppressedIssues>
|
||||||
|
<CurrentIssues/>
|
||||||
</SmellBaseline>
|
</SmellBaseline>
|
||||||
|
@ -70,6 +70,50 @@
|
|||||||
column="1"/>
|
column="1"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="Autofill"
|
||||||
|
message="Missing `autofillHints` attribute"
|
||||||
|
errorLine1=" <EditText"
|
||||||
|
errorLine2=" ~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/res/layout/chat.xml"
|
||||||
|
line="33"
|
||||||
|
column="10"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="Autofill"
|
||||||
|
message="Missing `autofillHints` attribute"
|
||||||
|
errorLine1=" <EditText"
|
||||||
|
errorLine2=" ~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/res/layout/save_playlist.xml"
|
||||||
|
line="9"
|
||||||
|
column="6"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="Autofill"
|
||||||
|
message="Missing `autofillHints` attribute"
|
||||||
|
errorLine1=" <EditText"
|
||||||
|
errorLine2=" ~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/res/layout/share_details.xml"
|
||||||
|
line="29"
|
||||||
|
column="10"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="Autofill"
|
||||||
|
message="Missing `autofillHints` attribute"
|
||||||
|
errorLine1=" <EditText"
|
||||||
|
errorLine2=" ~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/res/layout/time_span_dialog.xml"
|
||||||
|
line="28"
|
||||||
|
column="10"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="LabelFor"
|
id="LabelFor"
|
||||||
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"
|
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="androidx.car.app" />
|
|
||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
android:largeScreens="true"
|
android:largeScreens="true"
|
||||||
@ -22,22 +20,20 @@
|
|||||||
android:xlargeScreens="true"/>
|
android:xlargeScreens="true"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".app.UApp"
|
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/backup_rules"
|
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true" tools:targetApi="q"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/common.appname"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:preserveLegacyExternalStorage="true"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="false"
|
|
||||||
android:theme="@style/Theme.Material3.DynamicColors.Dark"
|
android:theme="@style/Theme.Material3.DynamicColors.Dark"
|
||||||
|
android:name=".app.UApp"
|
||||||
|
android:label="@string/common.appname"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="UnusedAttribute"
|
android:supportsRtl="false"
|
||||||
tools:targetApi="q">
|
android:preserveLegacyExternalStorage="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
<!-- Add for API 34 android:enableOnBackInvokedCallBack="true" -->
|
<!-- Add for API 34 android:enableOnBackInvokedCallBack="true" -->
|
||||||
|
|
||||||
<meta-data android:name="com.google.android.gms.car.application"
|
<meta-data android:name="com.google.android.gms.car.application"
|
||||||
@ -74,7 +70,7 @@
|
|||||||
</service>
|
</service>
|
||||||
|
|
||||||
<!-- 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=".service.PlaybackService"
|
<service android:name=".playback.PlaybackService"
|
||||||
android:label="@string/common.appname"
|
android:label="@string/common.appname"
|
||||||
android:foregroundServiceType="mediaPlayback"
|
android:foregroundServiceType="mediaPlayback"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@ -109,12 +105,6 @@
|
|||||||
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED"/>
|
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name="androidx.media3.session.MediaButtonReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".provider.UltrasonicAppWidgetProvider"
|
android:name=".provider.UltrasonicAppWidgetProvider"
|
||||||
android:label="Ultrasonic"
|
android:label="Ultrasonic"
|
||||||
@ -127,6 +117,12 @@
|
|||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/appwidget_info"/>
|
android:resource="@xml/appwidget_info"/>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver android:name=".receiver.MediaButtonIntentReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter android:priority="2147483647">
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
<provider
|
<provider
|
||||||
android:name=".provider.SearchSuggestionProvider"
|
android:name=".provider.SearchSuggestionProvider"
|
||||||
android:authorities="${applicationId}.provider.SearchSuggestionProvider"
|
android:authorities="${applicationId}.provider.SearchSuggestionProvider"
|
||||||
@ -139,4 +135,5 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
tools:ignore="ExportedContentProvider" />
|
tools:ignore="ExportedContentProvider" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -0,0 +1,297 @@
|
|||||||
|
package org.moire.ultrasonic.fragment;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ListAdapter;
|
||||||
|
import android.widget.ListView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.R;
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||||
|
import org.moire.ultrasonic.domain.ChatMessage;
|
||||||
|
import org.moire.ultrasonic.service.MusicService;
|
||||||
|
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||||
|
import org.moire.ultrasonic.util.BackgroundTask;
|
||||||
|
import org.moire.ultrasonic.util.CancellationToken;
|
||||||
|
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||||
|
import org.moire.ultrasonic.util.Settings;
|
||||||
|
import org.moire.ultrasonic.util.Util;
|
||||||
|
import org.moire.ultrasonic.view.ChatAdapter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
import kotlin.Lazy;
|
||||||
|
|
||||||
|
import static org.koin.java.KoinJavaComponent.inject;
|
||||||
|
|
||||||
|
import com.google.android.material.button.MaterialButton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides online chat functionality
|
||||||
|
*/
|
||||||
|
public class ChatFragment extends Fragment {
|
||||||
|
|
||||||
|
private ListView chatListView;
|
||||||
|
private EditText messageEditText;
|
||||||
|
private MaterialButton sendButton;
|
||||||
|
private Timer timer;
|
||||||
|
private volatile static Long lastChatMessageTime = (long) 0;
|
||||||
|
private static final ArrayList<ChatMessage> messageList = new ArrayList<>();
|
||||||
|
private CancellationToken cancellationToken;
|
||||||
|
private SwipeRefreshLayout swipeRefresh;
|
||||||
|
|
||||||
|
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
Util.applyTheme(this.getContext());
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.chat, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
swipeRefresh = view.findViewById(R.id.chat_refresh);
|
||||||
|
swipeRefresh.setEnabled(false);
|
||||||
|
|
||||||
|
cancellationToken = new CancellationToken();
|
||||||
|
messageEditText = view.findViewById(R.id.chat_edittext);
|
||||||
|
sendButton = view.findViewById(R.id.chat_send);
|
||||||
|
|
||||||
|
sendButton.setOnClickListener(view1 -> sendMessage());
|
||||||
|
|
||||||
|
chatListView = view.findViewById(R.id.chat_entries_list);
|
||||||
|
chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
|
||||||
|
chatListView.setStackFromBottom(true);
|
||||||
|
|
||||||
|
String serverName = activeServerProvider.getValue().getActiveServer().getName();
|
||||||
|
String userName = activeServerProvider.getValue().getActiveServer().getUserName();
|
||||||
|
String title = String.format("%s [%s@%s]", getResources().getString(R.string.button_bar_chat), userName, serverName);
|
||||||
|
|
||||||
|
FragmentTitle.Companion.setTitle(this, title);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
|
messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
|
||||||
|
|
||||||
|
messageEditText.addTextChangedListener(new TextWatcher()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable editable)
|
||||||
|
{
|
||||||
|
sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
messageEditText.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN))
|
||||||
|
{
|
||||||
|
sendMessage();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
load();
|
||||||
|
timerMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||||
|
inflater.inflate(R.menu.chat, menu);
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Listen for option item selections so that we receive a notification
|
||||||
|
* when the user requests a refresh by selecting the refresh action bar item.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
// Check if user triggered a refresh:
|
||||||
|
if (item.getItemId() == R.id.menu_refresh) {
|
||||||
|
// Start the refresh background task.
|
||||||
|
load();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// User didn't trigger a refresh, let the superclass handle this action
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume()
|
||||||
|
{
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
if (!messageList.isEmpty())
|
||||||
|
{
|
||||||
|
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
|
||||||
|
chatListView.setAdapter(chatAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timer == null)
|
||||||
|
{
|
||||||
|
timerMethod();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause()
|
||||||
|
{
|
||||||
|
super.onPause();
|
||||||
|
|
||||||
|
if (timer != null)
|
||||||
|
{
|
||||||
|
timer.cancel();
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
cancellationToken.cancel();
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void timerMethod()
|
||||||
|
{
|
||||||
|
int refreshInterval = Settings.getChatRefreshInterval();
|
||||||
|
|
||||||
|
if (refreshInterval > 0)
|
||||||
|
{
|
||||||
|
timer = new Timer();
|
||||||
|
|
||||||
|
timer.schedule(new TimerTask()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
getActivity().runOnUiThread(() -> load());
|
||||||
|
}
|
||||||
|
}, refreshInterval, refreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage()
|
||||||
|
{
|
||||||
|
if (messageEditText != null)
|
||||||
|
{
|
||||||
|
final String message;
|
||||||
|
Editable text = messageEditText.getText();
|
||||||
|
|
||||||
|
if (text == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = text.toString();
|
||||||
|
|
||||||
|
if (!Util.isNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
messageEditText.setText("");
|
||||||
|
|
||||||
|
BackgroundTask<Void> task = new FragmentBackgroundTask<Void>(getActivity(), false, swipeRefresh, cancellationToken)
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground() throws Throwable
|
||||||
|
{
|
||||||
|
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||||
|
musicService.addChatMessage(message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void done(Void result)
|
||||||
|
{
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void load()
|
||||||
|
{
|
||||||
|
BackgroundTask<List<ChatMessage>> task = new FragmentBackgroundTask<List<ChatMessage>>(getActivity(), false, swipeRefresh, cancellationToken)
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected List<ChatMessage> doInBackground() throws Throwable
|
||||||
|
{
|
||||||
|
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||||
|
return musicService.getChatMessages(lastChatMessageTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void done(List<ChatMessage> result)
|
||||||
|
{
|
||||||
|
if (result != null && !result.isEmpty())
|
||||||
|
{
|
||||||
|
// Reset lastChatMessageTime if we have a newer message
|
||||||
|
for (ChatMessage message : result)
|
||||||
|
{
|
||||||
|
if (message.getTime() > lastChatMessageTime)
|
||||||
|
{
|
||||||
|
lastChatMessageTime = message.getTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse results to show them on the bottom
|
||||||
|
Collections.reverse(result);
|
||||||
|
messageList.addAll(result);
|
||||||
|
|
||||||
|
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
|
||||||
|
chatListView.setAdapter(chatAdapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void error(Throwable error) {
|
||||||
|
// Stop the timer in case of an error, otherwise it may repeat the error message forever
|
||||||
|
if (timer != null)
|
||||||
|
{
|
||||||
|
timer.cancel();
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
super.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.execute();
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* BluetoothIntentReceiver.kt
|
* BluetoothIntentReceiver.kt
|
||||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -10,6 +10,9 @@ package org.moire.ultrasonic.receiver
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.bluetooth.BluetoothA2dp
|
import android.bluetooth.BluetoothA2dp
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothDevice.ACTION_ACL_CONNECTED
|
||||||
|
import android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECTED
|
||||||
|
import android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED
|
||||||
import android.bluetooth.BluetoothProfile
|
import android.bluetooth.BluetoothProfile
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -19,6 +22,9 @@ import android.os.Build
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_A2DP
|
||||||
|
import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_ALL
|
||||||
|
import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_DISABLED
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -36,28 +42,27 @@ class BluetoothIntentReceiver : BroadcastReceiver() {
|
|||||||
Timber.d("Bluetooth device: $name; State: $state; Action: $action")
|
Timber.d("Bluetooth device: $name; State: $state; Action: $action")
|
||||||
|
|
||||||
// In these flags we store what kind of device (any or a2dp) has (dis)connected
|
// In these flags we store what kind of device (any or a2dp) has (dis)connected
|
||||||
var connectionStatus = Constants.PREFERENCE_VALUE_DISABLED
|
var connectionStatus = PREFERENCE_VALUE_DISABLED
|
||||||
var disconnectionStatus = Constants.PREFERENCE_VALUE_DISABLED
|
var disconnectionStatus = PREFERENCE_VALUE_DISABLED
|
||||||
|
|
||||||
// First check for general devices
|
// First check for general devices
|
||||||
when (action) {
|
when (action) {
|
||||||
BluetoothDevice.ACTION_ACL_CONNECTED -> {
|
ACTION_ACL_CONNECTED -> {
|
||||||
connectionStatus = Constants.PREFERENCE_VALUE_ALL
|
connectionStatus = PREFERENCE_VALUE_ALL
|
||||||
}
|
}
|
||||||
BluetoothDevice.ACTION_ACL_DISCONNECTED,
|
ACTION_ACL_DISCONNECTED,
|
||||||
BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED
|
ACTION_ACL_DISCONNECT_REQUESTED -> {
|
||||||
-> {
|
disconnectionStatus = PREFERENCE_VALUE_ALL
|
||||||
disconnectionStatus = Constants.PREFERENCE_VALUE_ALL
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check for A2DP devices
|
// Then check for A2DP devices
|
||||||
when (state) {
|
when (state) {
|
||||||
BluetoothA2dp.STATE_CONNECTED -> {
|
BluetoothA2dp.STATE_CONNECTED -> {
|
||||||
connectionStatus = Constants.PREFERENCE_VALUE_A2DP
|
connectionStatus = PREFERENCE_VALUE_A2DP
|
||||||
}
|
}
|
||||||
BluetoothA2dp.STATE_DISCONNECTED -> {
|
BluetoothA2dp.STATE_DISCONNECTED -> {
|
||||||
disconnectionStatus = Constants.PREFERENCE_VALUE_A2DP
|
disconnectionStatus = PREFERENCE_VALUE_A2DP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,20 +72,20 @@ class BluetoothIntentReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
// Now check the settings and set the appropriate flags
|
// Now check the settings and set the appropriate flags
|
||||||
when (Settings.resumeOnBluetoothDevice) {
|
when (Settings.resumeOnBluetoothDevice) {
|
||||||
Constants.PREFERENCE_VALUE_ALL -> {
|
PREFERENCE_VALUE_ALL -> {
|
||||||
shouldResume = (connectionStatus != Constants.PREFERENCE_VALUE_DISABLED)
|
shouldResume = (connectionStatus != PREFERENCE_VALUE_DISABLED)
|
||||||
}
|
}
|
||||||
Constants.PREFERENCE_VALUE_A2DP -> {
|
PREFERENCE_VALUE_A2DP -> {
|
||||||
shouldResume = (connectionStatus == Constants.PREFERENCE_VALUE_A2DP)
|
shouldResume = (connectionStatus == PREFERENCE_VALUE_A2DP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (Settings.pauseOnBluetoothDevice) {
|
when (Settings.pauseOnBluetoothDevice) {
|
||||||
Constants.PREFERENCE_VALUE_ALL -> {
|
PREFERENCE_VALUE_ALL -> {
|
||||||
shouldPause = (disconnectionStatus != Constants.PREFERENCE_VALUE_DISABLED)
|
shouldPause = (disconnectionStatus != PREFERENCE_VALUE_DISABLED)
|
||||||
}
|
}
|
||||||
Constants.PREFERENCE_VALUE_A2DP -> {
|
PREFERENCE_VALUE_A2DP -> {
|
||||||
shouldPause = (disconnectionStatus == Constants.PREFERENCE_VALUE_A2DP)
|
shouldPause = (disconnectionStatus == PREFERENCE_VALUE_A2DP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,24 +105,24 @@ class BluetoothIntentReceiver : BroadcastReceiver() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun BluetoothDevice?.getNameSafely(): String? {
|
private fun BluetoothDevice?.getNameSafely(): String? {
|
||||||
val logBluetoothName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
val logBluetoothName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||||
(
|
(
|
||||||
ActivityCompat.checkSelfPermission(
|
ActivityCompat.checkSelfPermission(
|
||||||
UApp.applicationContext(), Manifest.permission.BLUETOOTH_CONNECT
|
UApp.applicationContext(), Manifest.permission.BLUETOOTH_CONNECT
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (logBluetoothName) this?.name else "Unknown"
|
return if (logBluetoothName) this?.name else "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Intent.getBluetoothDevice(): BluetoothDevice? {
|
private fun Intent.getBluetoothDevice(): BluetoothDevice? {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package org.moire.ultrasonic.receiver;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
|
||||||
|
|
||||||
|
import kotlin.Lazy;
|
||||||
|
|
||||||
|
import static org.koin.java.KoinJavaComponent.inject;
|
||||||
|
|
||||||
|
public class UltrasonicIntentReceiver extends BroadcastReceiver
|
||||||
|
{
|
||||||
|
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent)
|
||||||
|
{
|
||||||
|
String intentAction = intent.getAction();
|
||||||
|
Timber.i("Received Ultrasonic Intent: %s", intentAction);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lifecycleSupport.getValue().receiveIntent(intent);
|
||||||
|
|
||||||
|
if (isOrderedBroadcast())
|
||||||
|
{
|
||||||
|
abortBroadcast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception x)
|
||||||
|
{
|
||||||
|
// Ignored.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package org.moire.ultrasonic.service;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.app.UApp;
|
||||||
|
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitors the state of the mobile's external storage
|
||||||
|
*/
|
||||||
|
public class ExternalStorageMonitor
|
||||||
|
{
|
||||||
|
private BroadcastReceiver ejectEventReceiver;
|
||||||
|
private boolean externalStorageAvailable = true;
|
||||||
|
|
||||||
|
public void onCreate(final Runnable ejectedCallback)
|
||||||
|
{
|
||||||
|
// Stop when SD card is ejected.
|
||||||
|
ejectEventReceiver = new BroadcastReceiver()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent)
|
||||||
|
{
|
||||||
|
externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction());
|
||||||
|
if (!externalStorageAvailable)
|
||||||
|
{
|
||||||
|
Timber.i("External media is ejecting. Stopping playback.");
|
||||||
|
ejectedCallback.run();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Timber.i("External media is available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
|
||||||
|
ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
|
||||||
|
ejectFilter.addDataScheme("file");
|
||||||
|
UApp.Companion.applicationContext().registerReceiver(ejectEventReceiver, ejectFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDestroy()
|
||||||
|
{
|
||||||
|
UApp.Companion.applicationContext().unregisterReceiver(ejectEventReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExternalStorageAvailable() { return externalStorageAvailable; }
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.moire.ultrasonic.service;
|
||||||
|
|
||||||
|
import timber.log.Timber;
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||||
|
import org.moire.ultrasonic.domain.Track;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrobbles played songs to Last.fm.
|
||||||
|
*
|
||||||
|
* @author Sindre Mehus
|
||||||
|
* @version $Id$
|
||||||
|
*/
|
||||||
|
public class Scrobbler
|
||||||
|
{
|
||||||
|
private String lastSubmission;
|
||||||
|
private String lastNowPlaying;
|
||||||
|
|
||||||
|
public void scrobble(final Track song, final boolean submission)
|
||||||
|
{
|
||||||
|
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return;
|
||||||
|
|
||||||
|
final String id = song.getId();
|
||||||
|
|
||||||
|
// Avoid duplicate registrations.
|
||||||
|
if (submission && id.equals(lastSubmission)) return;
|
||||||
|
|
||||||
|
if (!submission && id.equals(lastNowPlaying)) return;
|
||||||
|
|
||||||
|
if (submission) lastSubmission = id;
|
||||||
|
else lastNowPlaying = id;
|
||||||
|
|
||||||
|
new Thread(String.format("Scrobble %s", song))
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
MusicService service = MusicServiceFactory.getMusicService();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
service.scrobble(id, submission);
|
||||||
|
Timber.i("Scrobbled '%s' for %s", submission ? "submission" : "now playing", song);
|
||||||
|
}
|
||||||
|
catch (Exception x)
|
||||||
|
{
|
||||||
|
Timber.i(x, "Failed to scrobble'%s' for %s", submission ? "submission" : "now playing", song);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package org.moire.ultrasonic.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for supplying items to a consumer
|
||||||
|
* @param <T> The type of the item supplied
|
||||||
|
*/
|
||||||
|
public abstract class Supplier<T>
|
||||||
|
{
|
||||||
|
public abstract T get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Subsonic.
|
||||||
|
|
||||||
|
Subsonic is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Subsonic is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2009 (C) Sindre Mehus
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.util;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Handler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sindre Mehus
|
||||||
|
*/
|
||||||
|
public abstract class BackgroundTask<T> implements ProgressListener
|
||||||
|
{
|
||||||
|
private final Activity activity;
|
||||||
|
private final Handler handler;
|
||||||
|
|
||||||
|
public BackgroundTask(Activity activity)
|
||||||
|
{
|
||||||
|
this.activity = activity;
|
||||||
|
handler = new Handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Activity getActivity()
|
||||||
|
{
|
||||||
|
return activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Handler getHandler()
|
||||||
|
{
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void execute();
|
||||||
|
|
||||||
|
protected abstract T doInBackground() throws Throwable;
|
||||||
|
|
||||||
|
protected abstract void done(T result);
|
||||||
|
|
||||||
|
protected void error(Throwable error)
|
||||||
|
{
|
||||||
|
CommunicationError.handleError(error, activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getErrorMessage(Throwable error)
|
||||||
|
{
|
||||||
|
return CommunicationError.getErrorMessage(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract void updateProgress(final String message);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateProgress(int messageId)
|
||||||
|
{
|
||||||
|
updateProgress(activity.getResources().getString(messageId));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package org.moire.ultrasonic.util;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sindre Mehus
|
||||||
|
* @version $Id$
|
||||||
|
*/
|
||||||
|
public abstract class FragmentBackgroundTask<T> extends BackgroundTask<T>
|
||||||
|
{
|
||||||
|
private final boolean changeProgress;
|
||||||
|
private final SwipeRefreshLayout swipe;
|
||||||
|
private final CancellationToken cancel;
|
||||||
|
|
||||||
|
public FragmentBackgroundTask(Activity activity, boolean changeProgress,
|
||||||
|
SwipeRefreshLayout swipe, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
super(activity);
|
||||||
|
this.changeProgress = changeProgress;
|
||||||
|
this.swipe = swipe;
|
||||||
|
this.cancel = cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
if (changeProgress)
|
||||||
|
{
|
||||||
|
if (swipe != null) swipe.setRefreshing(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
new Thread()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final T result = doInBackground();
|
||||||
|
if (cancel.isCancellationRequested())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHandler().post(new Runnable()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
if (changeProgress)
|
||||||
|
{
|
||||||
|
if (swipe != null) swipe.setRefreshing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
done(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (final Throwable t)
|
||||||
|
{
|
||||||
|
if (cancel.isCancellationRequested())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getHandler().post(new Runnable()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
if (changeProgress)
|
||||||
|
{
|
||||||
|
if (swipe != null) swipe.setRefreshing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateProgress(final String message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package org.moire.ultrasonic.util;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sindre Mehus
|
||||||
|
* @version $Id$
|
||||||
|
*/
|
||||||
|
public abstract class LoadingTask<T> extends BackgroundTask<T>
|
||||||
|
{
|
||||||
|
private final SwipeRefreshLayout swipe;
|
||||||
|
private final CancellationToken cancel;
|
||||||
|
|
||||||
|
public LoadingTask(Activity activity, SwipeRefreshLayout swipe, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
super(activity);
|
||||||
|
this.swipe = swipe;
|
||||||
|
this.cancel = cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute()
|
||||||
|
{
|
||||||
|
swipe.setRefreshing(true);
|
||||||
|
|
||||||
|
new Thread()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final T result = doInBackground();
|
||||||
|
if (cancel.isCancellationRequested())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHandler().post(() -> {
|
||||||
|
swipe.setRefreshing(false);
|
||||||
|
done(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (final Throwable t)
|
||||||
|
{
|
||||||
|
if (cancel.isCancellationRequested())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHandler().post(() -> {
|
||||||
|
swipe.setRefreshing(false);
|
||||||
|
error(t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateProgress(final String message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Subsonic.
|
||||||
|
|
||||||
|
Subsonic is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Subsonic is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2009 (C) Sindre Mehus
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sindre Mehus
|
||||||
|
*/
|
||||||
|
public interface ProgressListener
|
||||||
|
{
|
||||||
|
void updateProgress(String message);
|
||||||
|
|
||||||
|
void updateProgress(int messageId);
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package org.moire.ultrasonic.util;
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.domain.Track;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Josh on 12/17/13.
|
||||||
|
*/
|
||||||
|
public class ShareDetails
|
||||||
|
{
|
||||||
|
public String Description;
|
||||||
|
public boolean ShareOnServer;
|
||||||
|
public long Expiration;
|
||||||
|
public List<Track> Entries;
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package org.moire.ultrasonic.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import androidx.preference.DialogPreference;
|
||||||
|
import org.moire.ultrasonic.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Joshua Bahnsen on 12/22/13.
|
||||||
|
*/
|
||||||
|
public class TimeSpanPreference extends DialogPreference
|
||||||
|
{
|
||||||
|
Context context;
|
||||||
|
|
||||||
|
public TimeSpanPreference(Context context, AttributeSet attrs)
|
||||||
|
{
|
||||||
|
super(context, attrs);
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
setPositiveButtonText(android.R.string.ok);
|
||||||
|
setNegativeButtonText(android.R.string.cancel);
|
||||||
|
|
||||||
|
setDialogIcon(null);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getText()
|
||||||
|
{
|
||||||
|
String persisted = getPersistedString("");
|
||||||
|
|
||||||
|
if (!"".equals(persisted))
|
||||||
|
{
|
||||||
|
return persisted.replace(':', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.context.getResources().getString(R.string.time_span_disabled);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.text.util.Linkify;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import org.moire.ultrasonic.R;
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||||
|
import org.moire.ultrasonic.domain.ChatMessage;
|
||||||
|
import org.moire.ultrasonic.imageloader.ImageLoader;
|
||||||
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import kotlin.Lazy;
|
||||||
|
|
||||||
|
import static org.koin.java.KoinJavaComponent.inject;
|
||||||
|
|
||||||
|
public class ChatAdapter extends ArrayAdapter<ChatMessage>
|
||||||
|
{
|
||||||
|
private final Context context;
|
||||||
|
private final List<ChatMessage> messages;
|
||||||
|
|
||||||
|
private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})";
|
||||||
|
private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
|
||||||
|
|
||||||
|
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
||||||
|
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
|
||||||
|
|
||||||
|
public ChatAdapter(Context context, List<ChatMessage> messages)
|
||||||
|
{
|
||||||
|
super(context, R.layout.chat_item, messages);
|
||||||
|
this.context = context;
|
||||||
|
this.messages = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areAllItemsEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled(int position) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount()
|
||||||
|
{
|
||||||
|
return messages.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, ViewGroup parent)
|
||||||
|
{
|
||||||
|
ChatMessage message = this.getItem(position);
|
||||||
|
|
||||||
|
ViewHolder holder;
|
||||||
|
int layout;
|
||||||
|
|
||||||
|
String messageUser = message.getUsername();
|
||||||
|
Date messageTime = new java.util.Date(message.getTime());
|
||||||
|
String messageText = message.getMessage();
|
||||||
|
|
||||||
|
String me = activeServerProvider.getValue().getActiveServer().getUserName();
|
||||||
|
|
||||||
|
layout = messageUser.equals(me) ? R.layout.chat_item_reverse : R.layout.chat_item;
|
||||||
|
|
||||||
|
if (convertView == null)
|
||||||
|
{
|
||||||
|
convertView = inflateView(layout, parent);
|
||||||
|
holder = createViewHolder(layout, convertView);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
holder = (ViewHolder) convertView.getTag();
|
||||||
|
|
||||||
|
if (!holder.chatMessage.equals(message))
|
||||||
|
{
|
||||||
|
convertView = inflateView(layout, parent);
|
||||||
|
holder = createViewHolder(layout, convertView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.chatMessage = message;
|
||||||
|
|
||||||
|
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context);
|
||||||
|
String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
|
||||||
|
|
||||||
|
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
|
||||||
|
|
||||||
|
if (holder.avatar != null && !TextUtils.isEmpty(messageUser))
|
||||||
|
{
|
||||||
|
imageLoader.loadAvatarImage(holder.avatar, messageUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.username.setText(messageUser);
|
||||||
|
holder.message.setText(messageText);
|
||||||
|
holder.time.setText(messageTimeFormatted);
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private View inflateView(int layout, ViewGroup parent)
|
||||||
|
{
|
||||||
|
return LayoutInflater.from(context).inflate(layout, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewHolder createViewHolder(int layout, View convertView)
|
||||||
|
{
|
||||||
|
ViewHolder holder = new ViewHolder();
|
||||||
|
holder.layout = layout;
|
||||||
|
|
||||||
|
TextView usernameView;
|
||||||
|
TextView timeView;
|
||||||
|
TextView messageView;
|
||||||
|
ImageView imageView;
|
||||||
|
|
||||||
|
if (convertView != null)
|
||||||
|
{
|
||||||
|
usernameView = (TextView) convertView.findViewById(R.id.chat_username);
|
||||||
|
timeView = (TextView) convertView.findViewById(R.id.chat_time);
|
||||||
|
messageView = (TextView) convertView.findViewById(R.id.chat_message);
|
||||||
|
imageView = (ImageView) convertView.findViewById(R.id.chat_avatar);
|
||||||
|
|
||||||
|
messageView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
Linkify.addLinks(messageView, Linkify.ALL);
|
||||||
|
Linkify.addLinks(messageView, phoneMatcher, "tel:");
|
||||||
|
|
||||||
|
holder.avatar = imageView;
|
||||||
|
holder.message = messageView;
|
||||||
|
holder.username = usernameView;
|
||||||
|
holder.time = timeView;
|
||||||
|
|
||||||
|
convertView.setTag(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ViewHolder
|
||||||
|
{
|
||||||
|
int layout;
|
||||||
|
ImageView avatar;
|
||||||
|
TextView message;
|
||||||
|
TextView username;
|
||||||
|
TextView time;
|
||||||
|
ChatMessage chatMessage;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Subsonic.
|
||||||
|
|
||||||
|
Subsonic is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Subsonic is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2010 (C) Sindre Mehus
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.SectionIndexer;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.R;
|
||||||
|
import org.moire.ultrasonic.domain.Genre;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sindre Mehus
|
||||||
|
*/
|
||||||
|
public class GenreAdapter extends ArrayAdapter<Genre> implements SectionIndexer
|
||||||
|
{
|
||||||
|
private final LayoutInflater layoutInflater;
|
||||||
|
// Both arrays are indexed by section ID.
|
||||||
|
private final Object[] sections;
|
||||||
|
private final Integer[] positions;
|
||||||
|
|
||||||
|
public GenreAdapter(@NonNull Context context, List<Genre> genres)
|
||||||
|
{
|
||||||
|
super(context, R.layout.list_item_generic, genres);
|
||||||
|
|
||||||
|
layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||||
|
|
||||||
|
Collection<String> sectionSet = new LinkedHashSet<String>(30);
|
||||||
|
List<Integer> positionList = new ArrayList<Integer>(30);
|
||||||
|
|
||||||
|
for (int i = 0; i < genres.size(); i++)
|
||||||
|
{
|
||||||
|
Genre genre = genres.get(i);
|
||||||
|
String index = genre.getIndex();
|
||||||
|
if (!sectionSet.contains(index))
|
||||||
|
{
|
||||||
|
sectionSet.add(index);
|
||||||
|
positionList.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sections = sectionSet.toArray(new Object[0]);
|
||||||
|
positions = positionList.toArray(new Integer[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||||
|
View rowView = convertView;
|
||||||
|
if (rowView == null) {
|
||||||
|
rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
((TextView) rowView).setText(getItem(position).getName());
|
||||||
|
|
||||||
|
return rowView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object[] getSections()
|
||||||
|
{
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPositionForSection(int section)
|
||||||
|
{
|
||||||
|
return positions[section];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSectionForPosition(int pos)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < sections.length - 1; i++)
|
||||||
|
{
|
||||||
|
if (pos < positions[i + 1])
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sections.length - 1;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.R;
|
||||||
|
import org.moire.ultrasonic.domain.Share;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sindre Mehus
|
||||||
|
*/
|
||||||
|
public class ShareAdapter extends ArrayAdapter<Share>
|
||||||
|
{
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public ShareAdapter(Context context, List<Share> Shares)
|
||||||
|
{
|
||||||
|
super(context, R.layout.share_list_item, Shares);
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, ViewGroup parent)
|
||||||
|
{
|
||||||
|
Share entry = getItem(position);
|
||||||
|
ShareView view;
|
||||||
|
|
||||||
|
if (convertView instanceof ShareView)
|
||||||
|
{
|
||||||
|
ShareView currentView = (ShareView) convertView;
|
||||||
|
|
||||||
|
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
|
||||||
|
view = currentView;
|
||||||
|
view.setViewHolder(viewHolder);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
view = new ShareView(context);
|
||||||
|
view.setLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setShare(entry);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder
|
||||||
|
{
|
||||||
|
TextView url;
|
||||||
|
TextView description;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Subsonic.
|
||||||
|
|
||||||
|
Subsonic is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Subsonic is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2009 (C) Sindre Mehus
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.R;
|
||||||
|
import org.moire.ultrasonic.domain.Share;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to display playlists in a {@code ListView}.
|
||||||
|
*
|
||||||
|
* @author Joshua Bahnsen
|
||||||
|
*/
|
||||||
|
public class ShareView extends LinearLayout
|
||||||
|
{
|
||||||
|
private final Context context;
|
||||||
|
private ShareAdapter.ViewHolder viewHolder;
|
||||||
|
|
||||||
|
public ShareView(Context context)
|
||||||
|
{
|
||||||
|
super(context);
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLayout()
|
||||||
|
{
|
||||||
|
LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true);
|
||||||
|
viewHolder = new ShareAdapter.ViewHolder();
|
||||||
|
viewHolder.url = findViewById(R.id.share_url);
|
||||||
|
viewHolder.description = findViewById(R.id.share_description);
|
||||||
|
setTag(viewHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setViewHolder(ShareAdapter.ViewHolder viewHolder)
|
||||||
|
{
|
||||||
|
this.viewHolder = viewHolder;
|
||||||
|
setTag(this.viewHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShare(Share share)
|
||||||
|
{
|
||||||
|
viewHolder.url.setText(share.getName());
|
||||||
|
viewHolder.description.setText(share.getDescription());
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* NavigationActivity.kt
|
* NavigationActivity.kt
|
||||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -13,6 +13,7 @@ import android.content.Intent
|
|||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
@ -22,6 +23,7 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@ -37,7 +39,6 @@ import androidx.media3.common.Player.STATE_READY
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.navigateUp
|
import androidx.navigation.ui.navigateUp
|
||||||
import androidx.navigation.ui.onNavDestinationSelected
|
import androidx.navigation.ui.onNavDestinationSelected
|
||||||
@ -49,7 +50,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.scope.ScopeActivity
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.NavigationGraphDirections
|
import org.moire.ultrasonic.NavigationGraphDirections
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
@ -63,6 +63,7 @@ import org.moire.ultrasonic.service.MediaPlayerManager
|
|||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.service.plusAssign
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
|
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.InfoDialog
|
import org.moire.ultrasonic.util.InfoDialog
|
||||||
import org.moire.ultrasonic.util.LocaleHelper
|
import org.moire.ultrasonic.util.LocaleHelper
|
||||||
@ -80,7 +81,7 @@ import timber.log.Timber
|
|||||||
* onCreate/onResume/onDestroy methods...
|
* onCreate/onResume/onDestroy methods...
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class NavigationActivity : ScopeActivity() {
|
class NavigationActivity : AppCompatActivity() {
|
||||||
private var videoMenuItem: MenuItem? = null
|
private var videoMenuItem: MenuItem? = null
|
||||||
private var chatMenuItem: MenuItem? = null
|
private var chatMenuItem: MenuItem? = null
|
||||||
private var bookmarksMenuItem: MenuItem? = null
|
private var bookmarksMenuItem: MenuItem? = null
|
||||||
@ -95,7 +96,6 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
private var drawerLayout: DrawerLayout? = null
|
private var drawerLayout: DrawerLayout? = null
|
||||||
private var host: NavHostFragment? = null
|
private var host: NavHostFragment? = null
|
||||||
private var selectServerButton: MaterialButton? = null
|
private var selectServerButton: MaterialButton? = null
|
||||||
private var selectServerDropdownImage: ImageView? = null
|
|
||||||
private var headerBackgroundImage: ImageView? = null
|
private var headerBackgroundImage: ImageView? = null
|
||||||
|
|
||||||
// We store the last search string in this variable.
|
// We store the last search string in this variable.
|
||||||
@ -165,7 +165,7 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
drawerLayout
|
drawerLayout
|
||||||
)
|
)
|
||||||
|
|
||||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
setupActionBar(navController, appBarConfiguration)
|
||||||
|
|
||||||
setupNavigationMenu(navController)
|
setupNavigationMenu(navController)
|
||||||
|
|
||||||
@ -204,11 +204,10 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
||||||
if (it.state == STATE_READY) {
|
if (it.state == STATE_READY)
|
||||||
showNowPlaying()
|
showNowPlaying()
|
||||||
} else {
|
else
|
||||||
hideNowPlaying()
|
hideNowPlaying()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
||||||
@ -227,7 +226,7 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
|
|
||||||
// Setup app shortcuts on supported devices, but not on first start, when the server
|
// Setup app shortcuts on supported devices, but not on first start, when the server
|
||||||
// is not configured yet.
|
// is not configured yet.
|
||||||
if (!UApp.instance!!.isFirstRun) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !UApp.instance!!.isFirstRun) {
|
||||||
ShortcutUtil.registerShortcuts(this)
|
ShortcutUtil.registerShortcuts(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,7 +306,7 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
Storage.reset()
|
Storage.reset()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
Storage.checkForErrorsWithCustomRoot()
|
Storage.ensureRootIsAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
setMenuForServerCapabilities()
|
setMenuForServerCapabilities()
|
||||||
@ -315,11 +314,8 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
// Lifecycle support's constructor registers some event receivers so it should be created early
|
// Lifecycle support's constructor registers some event receivers so it should be created early
|
||||||
lifecycleSupport.onCreate()
|
lifecycleSupport.onCreate()
|
||||||
|
|
||||||
if (!nowPlayingHidden) {
|
if (!nowPlayingHidden) showNowPlaying()
|
||||||
showNowPlaying()
|
else hideNowPlaying()
|
||||||
} else {
|
|
||||||
hideNowPlaying()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -333,31 +329,35 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNavigationHeaderForServer() {
|
private fun updateNavigationHeaderForServer() {
|
||||||
|
// Only show the vector graphic on Android 11 or earlier
|
||||||
|
val showVectorBackground = (Build.VERSION.SDK_INT < Build.VERSION_CODES.S)
|
||||||
|
|
||||||
val activeServer = activeServerProvider.getActiveServer()
|
val activeServer = activeServerProvider.getActiveServer()
|
||||||
|
|
||||||
if (cachedServerCount == 0) {
|
if (cachedServerCount == 0)
|
||||||
selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name)
|
selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name)
|
||||||
} else {
|
else selectServerButton?.text = activeServer.name
|
||||||
selectServerButton?.text = activeServer.name
|
|
||||||
}
|
|
||||||
|
|
||||||
val foregroundColor =
|
val foregroundColor =
|
||||||
ServerColor.getForegroundColor(this, activeServer.color)
|
ServerColor.getForegroundColor(this, activeServer.color, showVectorBackground)
|
||||||
val backgroundColor =
|
val backgroundColor =
|
||||||
ServerColor.getBackgroundColor(this, activeServer.color)
|
ServerColor.getBackgroundColor(this, activeServer.color)
|
||||||
|
|
||||||
if (activeServer.index == 0) {
|
if (activeServer.index == 0)
|
||||||
selectServerButton?.icon =
|
selectServerButton?.icon =
|
||||||
ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off)
|
ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off)
|
||||||
} else {
|
else
|
||||||
selectServerButton?.icon =
|
selectServerButton?.icon =
|
||||||
ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server)
|
ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server)
|
||||||
}
|
|
||||||
|
|
||||||
selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor)
|
selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor)
|
||||||
selectServerButton?.setTextColor(foregroundColor)
|
selectServerButton?.setTextColor(foregroundColor)
|
||||||
selectServerDropdownImage?.imageTintList = ColorStateList.valueOf(foregroundColor)
|
|
||||||
headerBackgroundImage?.setBackgroundColor(backgroundColor)
|
headerBackgroundImage?.setBackgroundColor(backgroundColor)
|
||||||
|
|
||||||
|
// Hide the vector graphic on Android 12 or later
|
||||||
|
if (!showVectorBackground) {
|
||||||
|
headerBackgroundImage?.setImageDrawable(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupNavigationMenu(navController: NavController) {
|
private fun setupNavigationMenu(navController: NavController) {
|
||||||
@ -402,23 +402,26 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
|
|
||||||
selectServerButton =
|
selectServerButton =
|
||||||
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
|
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
|
||||||
selectServerDropdownImage =
|
val dropDownButton: ImageView? =
|
||||||
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
|
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
|
||||||
|
|
||||||
val onClick: (View) -> Unit = {
|
val onClick: (View) -> Unit = {
|
||||||
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
|
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true)
|
||||||
this.drawerLayout?.closeDrawer(GravityCompat.START)
|
this.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||||
}
|
|
||||||
navController.navigate(R.id.serverSelectorFragment)
|
navController.navigate(R.id.serverSelectorFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectServerButton?.setOnClickListener(onClick)
|
selectServerButton?.setOnClickListener(onClick)
|
||||||
selectServerDropdownImage?.setOnClickListener(onClick)
|
dropDownButton?.setOnClickListener(onClick)
|
||||||
|
|
||||||
headerBackgroundImage =
|
headerBackgroundImage =
|
||||||
navigationView?.getHeaderView(0)?.findViewById(R.id.img_header_bg)
|
navigationView?.getHeaderView(0)?.findViewById(R.id.img_header_bg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupActionBar(navController: NavController, appBarConfig: AppBarConfiguration) {
|
||||||
|
setupActionBarWithNavController(navController, appBarConfig)
|
||||||
|
}
|
||||||
|
|
||||||
private val closeNavigationDrawerOnBack = object : OnBackPressedCallback(true) {
|
private val closeNavigationDrawerOnBack = object : OnBackPressedCallback(true) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
drawerLayout?.closeDrawer(GravityCompat.START)
|
drawerLayout?.closeDrawer(GravityCompat.START)
|
||||||
@ -435,30 +438,19 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
val navController = findNavController(R.id.nav_host_fragment)
|
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment)) ||
|
||||||
// Check if this item ID exists in the nav graph
|
|
||||||
val destinationExists = navController.graph.findNode(item.itemId) != null
|
|
||||||
return if (destinationExists) {
|
|
||||||
item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
|
|
||||||
} else {
|
|
||||||
// Let the fragments handle their own menu items
|
|
||||||
super.onOptionsItemSelected(item)
|
super.onOptionsItemSelected(item)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Why is this needed? Shouldn't it just work by default?
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
// This override is required by design when using setupActionBarWithNavController()
|
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
|
||||||
// with an AppBarConfiguration. It ensures that the Up button behavior is correctly
|
|
||||||
// delegated — either navigating "up" in the back stack, or opening the drawer if
|
|
||||||
// we're at a top-level destination.
|
|
||||||
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration) ||
|
|
||||||
super.onSupportNavigateUp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent?.action) {
|
||||||
Constants.INTENT_PLAY_RANDOM_SONGS -> {
|
Constants.INTENT_PLAY_RANDOM_SONGS -> {
|
||||||
playRandomSongs()
|
playRandomSongs()
|
||||||
}
|
}
|
||||||
@ -481,8 +473,7 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
|
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
|
||||||
val suggestions = SearchRecentSuggestions(
|
val suggestions = SearchRecentSuggestions(
|
||||||
this,
|
this,
|
||||||
SearchSuggestionProvider.AUTHORITY,
|
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
|
||||||
SearchSuggestionProvider.MODE
|
|
||||||
)
|
)
|
||||||
suggestions.saveRecentQuery(query, null)
|
suggestions.saveRecentQuery(query, null)
|
||||||
|
|
||||||
@ -494,19 +485,16 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
|
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
|
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
|
||||||
|
val downloadHandler: DownloadHandler by inject()
|
||||||
mediaPlayerManager.addToPlaylist(
|
downloadHandler.addTracksToMediaController(
|
||||||
songs = musicDirectory.getTracks(),
|
songs = musicDirectory.getTracks(),
|
||||||
|
append = false,
|
||||||
|
playNext = false,
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
shuffle = false,
|
shuffle = false,
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR
|
fragment = currentFragment,
|
||||||
|
playlistName = null
|
||||||
)
|
)
|
||||||
|
|
||||||
if (Settings.shouldTransitionOnPlayback) {
|
|
||||||
currentFragment.findNavController().popBackStack(R.id.playerFragment, true)
|
|
||||||
currentFragment.findNavController().navigate(R.id.playerFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -537,6 +525,7 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
|
|
||||||
private fun showWelcomeDialog() {
|
private fun showWelcomeDialog() {
|
||||||
if (!UApp.instance!!.setupDialogDisplayed) {
|
if (!UApp.instance!!.setupDialogDisplayed) {
|
||||||
|
|
||||||
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
||||||
|
|
||||||
InfoDialog.Builder(this)
|
InfoDialog.Builder(this)
|
||||||
|
@ -32,11 +32,11 @@ import org.moire.ultrasonic.util.LayoutType
|
|||||||
*/
|
*/
|
||||||
open class AlbumRowDelegate(
|
open class AlbumRowDelegate(
|
||||||
open val onItemClick: (Album) -> Unit,
|
open val onItemClick: (Album) -> Unit,
|
||||||
open val onContextMenuClick: (MenuItem, Album) -> Boolean
|
open val onContextMenuClick: (MenuItem, Album) -> Boolean,
|
||||||
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
|
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
|
||||||
|
|
||||||
private val starDrawable: Int = R.drawable.rating_star_full
|
private val starDrawable: Int = R.drawable.ic_star_full
|
||||||
private val starHollowDrawable: Int = R.drawable.rating_star_hollow
|
private val starHollowDrawable: Int = R.drawable.ic_star_hollow
|
||||||
|
|
||||||
open var layoutType = LayoutType.LIST
|
open var layoutType = LayoutType.LIST
|
||||||
|
|
||||||
@ -61,11 +61,8 @@ open class AlbumRowDelegate(
|
|||||||
val imageLoaderProvider: ImageLoaderProvider by inject()
|
val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||||
imageLoaderProvider.executeOn {
|
imageLoaderProvider.executeOn {
|
||||||
it.loadImage(
|
it.loadImage(
|
||||||
holder.coverArt,
|
holder.coverArt, item,
|
||||||
item,
|
false, 0, R.drawable.unknown_album
|
||||||
false,
|
|
||||||
0,
|
|
||||||
R.drawable.unknown_album
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,6 @@ import android.widget.TextView
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.drakeet.multitype.ItemViewBinder
|
import com.drakeet.multitype.ItemViewBinder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
@ -67,21 +63,16 @@ class ArtistRowBinder(
|
|||||||
|
|
||||||
if (showArtistPicture()) {
|
if (showArtistPicture()) {
|
||||||
holder.coverArt.visibility = View.VISIBLE
|
holder.coverArt.visibility = View.VISIBLE
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
imageLoaderProvider.executeOn {
|
||||||
|
it.loadImage(
|
||||||
withContext(Dispatchers.Main) {
|
view = holder.coverArt,
|
||||||
imageLoaderProvider.executeOn {
|
id = holder.coverArtId,
|
||||||
it.loadImage(
|
key = key,
|
||||||
view = holder.coverArt,
|
large = false,
|
||||||
id = holder.coverArtId,
|
size = 0,
|
||||||
key = key,
|
defaultResourceId = R.drawable.ic_contact_picture
|
||||||
large = false,
|
)
|
||||||
size = 0,
|
|
||||||
defaultResourceId = R.drawable.ic_contact_picture
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
holder.coverArt.visibility = View.GONE
|
holder.coverArt.visibility = View.GONE
|
||||||
|
@ -23,7 +23,10 @@ class DividerBinder : ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHo
|
|||||||
holder.textView.setText(item.stringId)
|
holder.textView.setText(item.stringId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
override fun onCreateViewHolder(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
parent: ViewGroup
|
||||||
|
): ViewHolder {
|
||||||
return ViewHolder(inflater.inflate(layout, parent, false))
|
return ViewHolder(inflater.inflate(layout, parent, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,10 +78,7 @@ class FolderSelectorBinder(context: Context) :
|
|||||||
val popup = PopupMenu(weakContext.get()!!, layout)
|
val popup = PopupMenu(weakContext.get()!!, layout)
|
||||||
|
|
||||||
var menuItem = popup.menu.add(
|
var menuItem = popup.menu.add(
|
||||||
MENU_GROUP_MUSIC_FOLDER,
|
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
R.string.select_artist_all_folders
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
|
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
|
||||||
|
@ -46,6 +46,7 @@ class HeaderViewBinder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
|
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
|
||||||
|
|
||||||
val context = weakContext.get() ?: return
|
val context = weakContext.get() ?: return
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
|
|
||||||
@ -97,8 +98,7 @@ class HeaderViewBinder(
|
|||||||
holder.yearView.text = year
|
holder.yearView.text = year
|
||||||
|
|
||||||
val songs = resources.getQuantityString(
|
val songs = resources.getQuantityString(
|
||||||
R.plurals.n_songs,
|
R.plurals.select_album_n_songs, item.childCount,
|
||||||
item.childCount,
|
|
||||||
item.childCount
|
item.childCount
|
||||||
)
|
)
|
||||||
holder.songCountView.text = songs
|
holder.songCountView.text = songs
|
||||||
|
@ -77,6 +77,7 @@ internal class ServerRowAdapter(
|
|||||||
*/
|
*/
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
||||||
|
|
||||||
var vi: View? = convertView
|
var vi: View? = convertView
|
||||||
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)
|
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)
|
||||||
|
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
package org.moire.ultrasonic.adapters
|
package org.moire.ultrasonic.adapters
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.LayerDrawable
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Checkable
|
import android.widget.Checkable
|
||||||
import android.widget.CheckedTextView
|
import android.widget.CheckedTextView
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.PopupMenu
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@ -36,7 +31,7 @@ import org.moire.ultrasonic.service.RxBus
|
|||||||
import org.moire.ultrasonic.service.plusAssign
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import org.moire.ultrasonic.util.Util.themeColor
|
import timber.log.Timber
|
||||||
|
|
||||||
const val INDICATOR_THICKNESS_INDEFINITE = 5
|
const val INDICATOR_THICKNESS_INDEFINITE = 5
|
||||||
const val INDICATOR_THICKNESS_DEFINITE = 10
|
const val INDICATOR_THICKNESS_DEFINITE = 10
|
||||||
@ -56,12 +51,17 @@ class TrackViewHolder(val view: View) :
|
|||||||
|
|
||||||
var entry: Track? = null
|
var entry: Track? = null
|
||||||
private set
|
private set
|
||||||
private var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
|
var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
|
||||||
|
|
||||||
var check: CheckedTextView = view.findViewById(R.id.song_check)
|
var check: CheckedTextView = view.findViewById(R.id.song_check)
|
||||||
var drag: ImageView = view.findViewById(R.id.song_drag)
|
var drag: ImageView = view.findViewById(R.id.song_drag)
|
||||||
var observableChecked = MutableLiveData(false)
|
var observableChecked = MutableLiveData(false)
|
||||||
|
|
||||||
|
private var rating: LinearLayout = view.findViewById(R.id.song_rating)
|
||||||
|
private var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
|
||||||
|
private var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
|
||||||
|
private var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
|
||||||
|
private var fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
|
||||||
|
private var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
|
||||||
private var star: ImageView = view.findViewById(R.id.song_star)
|
private var star: ImageView = view.findViewById(R.id.song_star)
|
||||||
private var track: TextView = view.findViewById(R.id.song_track)
|
private var track: TextView = view.findViewById(R.id.song_track)
|
||||||
private var title: TextView = view.findViewById(R.id.song_title)
|
private var title: TextView = view.findViewById(R.id.song_title)
|
||||||
@ -79,35 +79,20 @@ class TrackViewHolder(val view: View) :
|
|||||||
|
|
||||||
private var rxBusSubscription: CompositeDisposable? = null
|
private var rxBusSubscription: CompositeDisposable? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
Timber.v("New ViewHolder created")
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
fun setSong(song: Track, checkable: Boolean, draggable: Boolean, isSelected: Boolean = false) {
|
fun setSong(
|
||||||
|
song: Track,
|
||||||
|
checkable: Boolean,
|
||||||
|
draggable: Boolean,
|
||||||
|
isSelected: Boolean = false
|
||||||
|
) {
|
||||||
|
val useFiveStarRating = Settings.useFiveStarRating
|
||||||
entry = song
|
entry = song
|
||||||
|
|
||||||
// Create new Disposable for the new Subscriptions
|
|
||||||
rxBusSubscription = CompositeDisposable()
|
|
||||||
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
|
|
||||||
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
|
|
||||||
if (it.id != song.id) return@subscribe
|
|
||||||
updateStatus(it.state, it.progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for rating updates
|
|
||||||
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
// Ignore updates which are not for the current song
|
|
||||||
if (it.id != song.id) return@launch
|
|
||||||
|
|
||||||
if (it.rating is HeartRating) {
|
|
||||||
updateRatingDisplay(song.userRating, it.rating.isHeart)
|
|
||||||
} else if (it.rating is StarRating) {
|
|
||||||
updateRatingDisplay(it.rating.starRating.toInt(), song.starred)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val entryDescription = Util.readableEntryDescription(song)
|
val entryDescription = Util.readableEntryDescription(song)
|
||||||
|
|
||||||
artist.text = entryDescription.artist
|
artist.text = entryDescription.artist
|
||||||
@ -128,7 +113,7 @@ class TrackViewHolder(val view: View) :
|
|||||||
if (ActiveServerProvider.isOffline()) {
|
if (ActiveServerProvider.isOffline()) {
|
||||||
star.isGone = true
|
star.isGone = true
|
||||||
} else {
|
} else {
|
||||||
setupRating(song)
|
setupStarButtons(song, useFiveStarRating)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instead of blocking the UI thread while looking up the current state,
|
// Instead of blocking the UI thread while looking up the current state,
|
||||||
@ -140,12 +125,41 @@ class TrackViewHolder(val view: View) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRatingDisplay(entry!!.userRating, entry!!.starred)
|
if (useFiveStarRating) {
|
||||||
|
updateFiveStars(entry?.userRating ?: 0)
|
||||||
|
} else {
|
||||||
|
updateSingleStar(entry!!.starred)
|
||||||
|
}
|
||||||
|
|
||||||
if (song.isVideo) {
|
if (song.isVideo) {
|
||||||
artist.isGone = true
|
artist.isGone = true
|
||||||
progressIndicator.isGone = true
|
progressIndicator.isGone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new Disposable for the new Subscriptions
|
||||||
|
rxBusSubscription = CompositeDisposable()
|
||||||
|
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
|
||||||
|
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
|
||||||
|
if (it.id != song.id) return@subscribe
|
||||||
|
updateStatus(it.state, it.progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for rating updates
|
||||||
|
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
// Ignore updates which are not for the current song
|
||||||
|
if (it.id != song.id) return@launch
|
||||||
|
|
||||||
|
if (it.rating is HeartRating) {
|
||||||
|
updateSingleStar(it.rating.isHeart)
|
||||||
|
} else if (it.rating is StarRating) {
|
||||||
|
updateFiveStars(it.rating.starRating.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is called when the Holder is recycled and receives a new Song
|
// This is called when the Holder is recycled and receives a new Song
|
||||||
@ -162,10 +176,7 @@ class TrackViewHolder(val view: View) :
|
|||||||
if (isPlaying && !isPlayingCached) {
|
if (isPlaying && !isPlayingCached) {
|
||||||
isPlayingCached = true
|
isPlayingCached = true
|
||||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
playingIcon,
|
playingIcon, null, null, null
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
)
|
||||||
val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
|
val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
|
||||||
songLayout.setBackgroundColor(color)
|
songLayout.setBackgroundColor(color)
|
||||||
@ -173,98 +184,62 @@ class TrackViewHolder(val view: View) :
|
|||||||
} else if (!isPlaying && isPlayingCached) {
|
} else if (!isPlaying && isPlayingCached) {
|
||||||
isPlayingCached = false
|
isPlayingCached = false
|
||||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
0,
|
0, 0, 0, 0
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
)
|
)
|
||||||
songLayout.setBackgroundColor(Color.TRANSPARENT)
|
songLayout.setBackgroundColor(Color.TRANSPARENT)
|
||||||
songLayout.elevation = 0F
|
songLayout.elevation = 0F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRating(track: Track) {
|
private fun setupStarButtons(track: Track, useFiveStarRating: Boolean) {
|
||||||
star.isVisible = true
|
if (useFiveStarRating) {
|
||||||
updateRatingDisplay(track.userRating, track.starred)
|
// Hide single star
|
||||||
|
star.isGone = true
|
||||||
|
rating.isVisible = true
|
||||||
|
val rating = if (track.userRating == null) 0 else track.userRating!!
|
||||||
|
updateFiveStars(rating)
|
||||||
|
|
||||||
star.setOnClickListener { toggleHeart(track) }
|
// Five star rating has no click handler because in the
|
||||||
star.setOnLongClickListener { view -> showRatingPopup(view, track) }
|
// track view theres not enough space
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleHeart(track: Track) {
|
|
||||||
track.starred = !track.starred
|
|
||||||
updateRatingDisplay(track.userRating, track.starred)
|
|
||||||
RxBus.ratingSubmitter.onNext(
|
|
||||||
RatingUpdate(track.id, HeartRating(track.starred))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
private fun showRatingPopup(view: View, track: Track): Boolean {
|
|
||||||
val popup = PopupMenu(view.context, view)
|
|
||||||
val inflater: MenuInflater = popup.menuInflater
|
|
||||||
inflater.inflate(R.menu.rating, popup.menu)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) popup.setForceShowIcon(true)
|
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener {
|
|
||||||
val rating = when (it.itemId) {
|
|
||||||
R.id.popup_rate_1 -> 1
|
|
||||||
R.id.popup_rate_2 -> 2
|
|
||||||
R.id.popup_rate_3 -> 3
|
|
||||||
R.id.popup_rate_4 -> 4
|
|
||||||
R.id.popup_rate_5 -> 5
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
track.userRating = rating
|
|
||||||
updateRatingDisplay(track.userRating, track.starred)
|
|
||||||
RxBus.ratingSubmitter.onNext(
|
|
||||||
RatingUpdate(track.id, StarRating(5, rating.toFloat()))
|
|
||||||
)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
popup.show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
private fun updateRatingDisplay(rating: Int?, starred: Boolean) {
|
|
||||||
val ratingDrawable = when (rating) {
|
|
||||||
1 -> R.drawable.rating_star_1
|
|
||||||
2 -> R.drawable.rating_star_2
|
|
||||||
3 -> R.drawable.rating_star_3
|
|
||||||
4 -> R.drawable.rating_star_4
|
|
||||||
5 -> R.drawable.rating_star_5
|
|
||||||
else -> {
|
|
||||||
R.drawable.rating_star_0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val layers = if (starred) {
|
|
||||||
arrayOf(
|
|
||||||
ResourcesCompat.getDrawable(view.resources, ratingDrawable, null)!!,
|
|
||||||
ResourcesCompat.getDrawable(
|
|
||||||
view.resources,
|
|
||||||
R.drawable.rating_heart_mini_overlay,
|
|
||||||
null
|
|
||||||
)!!
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
arrayOf(
|
star.isVisible = true
|
||||||
ResourcesCompat.getDrawable(view.resources, ratingDrawable, null)!!
|
rating.isGone = true
|
||||||
)
|
updateSingleStar(track.starred)
|
||||||
|
star.setOnClickListener {
|
||||||
|
track.starred = !track.starred
|
||||||
|
updateSingleStar(track.starred)
|
||||||
|
RxBus.ratingSubmitter.onNext(
|
||||||
|
RatingUpdate(track.id, HeartRating(track.starred))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val ratingDisplay = LayerDrawable(layers)
|
@Suppress("MagicNumber")
|
||||||
ratingDisplay.getDrawable(0).setTint(
|
private fun updateFiveStars(rating: Int) {
|
||||||
view.context.themeColor(com.google.android.material.R.attr.colorOnBackground)
|
fiveStar1.setImageResource(
|
||||||
|
if (rating > 0) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
||||||
)
|
)
|
||||||
if (starred) {
|
fiveStar2.setImageResource(
|
||||||
ratingDisplay.getDrawable(1).setTint(
|
if (rating > 1) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
||||||
view.context.themeColor(com.google.android.material.R.attr.colorTertiary)
|
)
|
||||||
)
|
fiveStar3.setImageResource(
|
||||||
}
|
if (rating > 2) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
||||||
|
)
|
||||||
|
fiveStar4.setImageResource(
|
||||||
|
if (rating > 3) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
||||||
|
)
|
||||||
|
fiveStar5.setImageResource(
|
||||||
|
if (rating > 4) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
star.setImageDrawable(ratingDisplay)
|
private fun updateSingleStar(starred: Boolean) {
|
||||||
|
if (starred) {
|
||||||
|
star.setImageResource(R.drawable.ic_star_full)
|
||||||
|
} else {
|
||||||
|
star.setImageResource(R.drawable.ic_star_hollow)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStatus(status: DownloadState, progress: Int?) {
|
private fun updateStatus(status: DownloadState, progress: Int?) {
|
||||||
@ -287,8 +262,7 @@ class TrackViewHolder(val view: View) :
|
|||||||
showProgress()
|
showProgress()
|
||||||
}
|
}
|
||||||
DownloadState.RETRYING,
|
DownloadState.RETRYING,
|
||||||
DownloadState.QUEUED
|
DownloadState.QUEUED -> {
|
||||||
-> {
|
|
||||||
showIndefiniteProgress()
|
showIndefiniteProgress()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -114,8 +114,10 @@ private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder {
|
|||||||
detectLeakedClosableObjects()
|
detectLeakedClosableObjects()
|
||||||
detectLeakedRegistrationObjects()
|
detectLeakedRegistrationObjects()
|
||||||
detectFileUriExposure()
|
detectFileUriExposure()
|
||||||
detectContentUriWithoutPermission()
|
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
detectContentUriWithoutPermission()
|
||||||
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
detectCredentialProtectedWhileLocked()
|
detectCredentialProtectedWhileLocked()
|
||||||
}
|
}
|
||||||
|
@ -50,8 +50,7 @@ class EqualizerController : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
val settings = deserialize<EqualizerSettings>(
|
val settings = deserialize<EqualizerSettings>(
|
||||||
UApp.applicationContext(),
|
UApp.applicationContext(), "equalizer.dat"
|
||||||
"equalizer.dat"
|
|
||||||
)
|
)
|
||||||
settings?.apply(equalizer!!)
|
settings?.apply(equalizer!!)
|
||||||
} catch (all: Throwable) {
|
} catch (all: Throwable) {
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -51,8 +53,7 @@ class ActiveServerProvider(
|
|||||||
}
|
}
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
|
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
|
||||||
serverId,
|
serverId, cachedServer
|
||||||
cachedServer
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,30 +106,6 @@ class ActiveServerProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the Active Server by its unique id
|
|
||||||
* @param serverId: The id of the desired server
|
|
||||||
*/
|
|
||||||
fun setActiveServerById(serverId: Int) {
|
|
||||||
val oldServerId = Settings.activeServer
|
|
||||||
if (oldServerId == serverId) return
|
|
||||||
|
|
||||||
// Notify components about the change before actually resetting the MusicService
|
|
||||||
// so they can react by e.g. stopping playback on the old server
|
|
||||||
RxBus.activeServerChangingPublisher.onNext(oldServerId)
|
|
||||||
|
|
||||||
// Use a coroutine to post the server change to the end of the message queue
|
|
||||||
launch {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
resetMusicService()
|
|
||||||
Settings.activeServer = serverId
|
|
||||||
|
|
||||||
RxBus.activeServerChangedPublisher.onNext(getActiveServer(serverId))
|
|
||||||
Timber.i("setActiveServerById done, new id: %s", serverId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getActiveMetaDatabase(): MetaDatabase {
|
fun getActiveMetaDatabase(): MetaDatabase {
|
||||||
val activeServer = getActiveServerId()
|
val activeServer = getActiveServerId()
|
||||||
@ -159,7 +136,7 @@ class ActiveServerProvider(
|
|||||||
METADATA_DB + serverId
|
METADATA_DB + serverId
|
||||||
)
|
)
|
||||||
.addMigrations(META_MIGRATION_2_3)
|
.addMigrations(META_MIGRATION_2_3)
|
||||||
.fallbackToDestructiveMigrationOnDowngrade(true)
|
.fallbackToDestructiveMigrationOnDowngrade()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,6 +234,29 @@ class ActiveServerProvider(
|
|||||||
return Settings.activeServer
|
return Settings.activeServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Active Server by its unique id
|
||||||
|
* @param serverId: The id of the desired server
|
||||||
|
*/
|
||||||
|
fun setActiveServerById(serverId: Int) {
|
||||||
|
val oldServerId = Settings.activeServer
|
||||||
|
if (oldServerId == serverId) return
|
||||||
|
|
||||||
|
// Notify components about the change before actually resetting the MusicService
|
||||||
|
// so they can react by e.g. stopping playback on the old server
|
||||||
|
RxBus.activeServerChangingPublisher.onNext(oldServerId)
|
||||||
|
|
||||||
|
// Post the server change to the end of the message queue,
|
||||||
|
// so the cleanup have time to finish
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
resetMusicService()
|
||||||
|
Settings.activeServer = serverId
|
||||||
|
|
||||||
|
RxBus.activeServerChangedPublisher.onNext(serverId)
|
||||||
|
Timber.i("setActiveServerById done, new id: %s", serverId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries if Scrobbling is enabled
|
* Queries if Scrobbling is enabled
|
||||||
*/
|
*/
|
||||||
|
@ -1,12 +1,3 @@
|
|||||||
/*
|
|
||||||
* AppDatabase.kt
|
|
||||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
|
||||||
*
|
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:Suppress("ktlint:standard:max-line-length")
|
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
@ -40,8 +31,8 @@ val MIGRATION_1_2: Migration = object : Migration(1, 2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_2_1: Migration = object : Migration(2, 1) {
|
val MIGRATION_2_1: Migration = object : Migration(2, 1) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
@ -57,7 +48,7 @@ val MIGRATION_2_1: Migration = object : Migration(2, 1) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO ServerSettingMigration (
|
INSERT INTO ServerSettingMigration (
|
||||||
id, [index], name, url, userName, password, jukeboxByDefault,
|
id, [index], name, url, userName, password, jukeboxByDefault,
|
||||||
@ -69,10 +60,10 @@ val MIGRATION_2_1: Migration = object : Migration(2, 1) {
|
|||||||
FROM ServerSetting
|
FROM ServerSetting
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"DROP TABLE ServerSetting"
|
"DROP TABLE ServerSetting"
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -96,8 +87,8 @@ val MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_3_2: Migration = object : Migration(3, 2) {
|
val MIGRATION_3_2: Migration = object : Migration(3, 2) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
@ -114,7 +105,7 @@ val MIGRATION_3_2: Migration = object : Migration(3, 2) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO ServerSettingMigration (
|
INSERT INTO ServerSettingMigration (
|
||||||
id, [index], name, url, userName, password, jukeboxByDefault,
|
id, [index], name, url, userName, password, jukeboxByDefault,
|
||||||
@ -126,26 +117,26 @@ val MIGRATION_3_2: Migration = object : Migration(3, 2) {
|
|||||||
FROM ServerSetting
|
FROM ServerSetting
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"DROP TABLE ServerSetting"
|
"DROP TABLE ServerSetting"
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
|
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"ALTER TABLE ServerSetting ADD COLUMN color INTEGER"
|
"ALTER TABLE ServerSetting ADD COLUMN color INTEGER"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
@ -166,7 +157,7 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO ServerSettingMigration (
|
INSERT INTO ServerSettingMigration (
|
||||||
id, [index], name, url, userName, password, jukeboxByDefault,
|
id, [index], name, url, userName, password, jukeboxByDefault,
|
||||||
@ -180,18 +171,18 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
|||||||
FROM ServerSetting
|
FROM ServerSetting
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"DROP TABLE ServerSetting"
|
"DROP TABLE ServerSetting"
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
||||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
@ -213,7 +204,7 @@ val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO `_new_ServerSetting` (
|
INSERT INTO `_new_ServerSetting` (
|
||||||
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
||||||
@ -227,14 +218,14 @@ val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
|||||||
FROM `ServerSetting`
|
FROM `ServerSetting`
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL("DROP TABLE `ServerSetting`")
|
database.execSQL("DROP TABLE `ServerSetting`")
|
||||||
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
||||||
`id` INTEGER PRIMARY KEY NOT NULL,
|
`id` INTEGER PRIMARY KEY NOT NULL,
|
||||||
@ -256,7 +247,7 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL(
|
database.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO `_new_ServerSetting` (
|
INSERT INTO `_new_ServerSetting` (
|
||||||
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
||||||
@ -270,31 +261,25 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
|||||||
FROM `ServerSetting`
|
FROM `ServerSetting`
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
db.execSQL("DROP TABLE `ServerSetting`")
|
database.execSQL("DROP TABLE `ServerSetting`")
|
||||||
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* ktlint-disable max-line-length */
|
||||||
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
|
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)")
|
||||||
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)"
|
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
|
||||||
)
|
database.execSQL("DROP TABLE `ServerSetting`")
|
||||||
db.execSQL(
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
|
|
||||||
)
|
|
||||||
db.execSQL("DROP TABLE `ServerSetting`")
|
|
||||||
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val MIGRATION_6_5: Migration = object : Migration(6, 5) {
|
val MIGRATION_6_5: Migration = object : Migration(6, 5) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL(
|
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)")
|
||||||
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)"
|
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
|
||||||
)
|
database.execSQL("DROP TABLE `ServerSetting`")
|
||||||
db.execSQL(
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
|
|
||||||
)
|
|
||||||
db.execSQL("DROP TABLE `ServerSetting`")
|
|
||||||
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* ktlint-enable max-line-length */
|
||||||
|
@ -1,10 +1,3 @@
|
|||||||
/*
|
|
||||||
* BasicDaos.kt
|
|
||||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
|
||||||
*
|
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
@ -98,7 +91,7 @@ interface GenericDao<T> {
|
|||||||
*/
|
*/
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
@JvmSuppressWildcards
|
@JvmSuppressWildcards
|
||||||
fun insertIgnoring(obj: List<T>): List<Long>
|
fun insertIgnoring(obj: List<T>?): List<Long>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an object from the database.
|
* Update an object from the database.
|
||||||
@ -116,7 +109,7 @@ interface GenericDao<T> {
|
|||||||
*/
|
*/
|
||||||
@Update
|
@Update
|
||||||
@JvmSuppressWildcards
|
@JvmSuppressWildcards
|
||||||
fun update(obj: List<T>)
|
fun update(obj: List<T>?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an object from the database
|
* Delete an object from the database
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* MetaDatabase.kt
|
* MetaDatabase.kt
|
||||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:Suppress("ktlint:standard:max-line-length")
|
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
@ -39,7 +37,7 @@ import org.moire.ultrasonic.domain.Track
|
|||||||
AutoMigration(
|
AutoMigration(
|
||||||
from = 1,
|
from = 1,
|
||||||
to = 2
|
to = 2
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
version = 3
|
version = 3
|
||||||
@ -69,27 +67,19 @@ class Converters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ktlint-disable max-line-length */
|
||||||
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
db.execSQL("DROP TABLE `albums`")
|
database.execSQL("DROP TABLE `albums`")
|
||||||
db.execSQL("DROP TABLE `indexes`")
|
database.execSQL("DROP TABLE `indexes`")
|
||||||
db.execSQL("DROP TABLE `artists`")
|
database.execSQL("DROP TABLE `artists`")
|
||||||
db.execSQL("DROP TABLE `tracks`")
|
database.execSQL("DROP TABLE `tracks`")
|
||||||
db.execSQL("DROP TABLE `music_folders`")
|
database.execSQL("DROP TABLE `music_folders`")
|
||||||
db.execSQL(
|
database.execSQL("CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
|
||||||
"CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
|
database.execSQL("CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))")
|
||||||
)
|
database.execSQL("CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
|
||||||
db.execSQL(
|
database.execSQL("CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))")
|
||||||
"CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))"
|
database.execSQL("CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))")
|
||||||
)
|
|
||||||
db.execSQL(
|
|
||||||
"CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
|
|
||||||
)
|
|
||||||
db.execSQL(
|
|
||||||
"CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))"
|
|
||||||
)
|
|
||||||
db.execSQL(
|
|
||||||
"CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* ktlint-enable max-line-length */
|
||||||
|
@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
|
|||||||
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.module.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.data.AppDatabase
|
import org.moire.ultrasonic.data.AppDatabase
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
package org.moire.ultrasonic.di
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
import org.moire.ultrasonic.util.CacheCleaner
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Koin module contains the registration of general classes needed for Ultrasonic
|
* This Koin module contains the registration of general classes needed for Ultrasonic
|
||||||
*/
|
*/
|
||||||
val applicationModule = module {
|
val applicationModule = module {
|
||||||
single { ActiveServerProvider(get()) }
|
single { ActiveServerProvider(get()) }
|
||||||
single { ImageLoaderProvider() }
|
single { ImageLoaderProvider(androidContext()) }
|
||||||
single { CacheCleaner() }
|
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,15 @@ import org.moire.ultrasonic.service.ExternalStorageMonitor
|
|||||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
import org.moire.ultrasonic.service.PlaybackStateSerializer
|
import org.moire.ultrasonic.service.PlaybackStateSerializer
|
||||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
|
||||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Koin module contains the registration of classes related to the media player
|
* This Koin module contains the registration of classes related to the media player
|
||||||
*/
|
*/
|
||||||
val mediaPlayerModule = module {
|
val mediaPlayerModule = module {
|
||||||
|
single { MediaPlayerLifecycleSupport() }
|
||||||
// These are dependency-free
|
|
||||||
single { PlaybackStateSerializer() }
|
single { PlaybackStateSerializer() }
|
||||||
single { ExternalStorageMonitor() }
|
single { ExternalStorageMonitor() }
|
||||||
single { NetworkAndStorageChecker() }
|
|
||||||
single { ShareHandler() }
|
|
||||||
|
|
||||||
// These MUST be singletons, for the media playback must work headless (without an activity)
|
// TODO Ideally this can be cleaned up when all circular references are removed.
|
||||||
single { MediaPlayerManager(get(), get()) }
|
single { MediaPlayerManager(get(), get(), get()) }
|
||||||
single { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
@file:JvmName("MusicServiceModule")
|
@file:JvmName("MusicServiceModule")
|
||||||
|
|
||||||
package org.moire.ultrasonic.di
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.BuildConfig
|
import org.moire.ultrasonic.BuildConfig
|
||||||
@ -16,6 +16,9 @@ import org.moire.ultrasonic.service.CachedMusicService
|
|||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
import org.moire.ultrasonic.service.OfflineMusicService
|
import org.moire.ultrasonic.service.OfflineMusicService
|
||||||
import org.moire.ultrasonic.service.RESTMusicService
|
import org.moire.ultrasonic.service.RESTMusicService
|
||||||
|
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||||
|
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||||
|
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,4 +68,8 @@ val musicServiceModule = module {
|
|||||||
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
||||||
OfflineMusicService()
|
OfflineMusicService()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single { DownloadHandler(get(), get()) }
|
||||||
|
single { NetworkAndStorageChecker(androidContext()) }
|
||||||
|
single { ShareHandler(androidContext()) }
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIAlbumConverter")
|
@file:JvmName("APIAlbumConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Album
|
import org.moire.ultrasonic.api.subsonic.models.Album
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIArtistConverter")
|
@file:JvmName("APIArtistConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// Contains helper functions to convert from api ChatMessage entity to domain entity
|
// Contains helper functions to convert from api ChatMessage entity to domain entity
|
||||||
@file:JvmName("APIChatMessageConverter")
|
@file:JvmName("APIChatMessageConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.ChatMessage as ApiChatMessage
|
import org.moire.ultrasonic.api.subsonic.models.ChatMessage as ApiChatMessage
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// Collection of functions to convert api Genre entity to domain entity
|
// Collection of functions to convert api Genre entity to domain entity
|
||||||
@file:JvmName("ApiGenreConverter")
|
@file:JvmName("ApiGenreConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre
|
import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// Collection of function to convert subsonic api jukebox responses to app entities
|
// Collection of function to convert subsonic api jukebox responses to app entities
|
||||||
@file:JvmName("APIJukeboxConverter")
|
@file:JvmName("APIJukeboxConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus as ApiJukeboxStatus
|
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus as ApiJukeboxStatus
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Converts Lyrics entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Lyrics entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APILyricsConverter")
|
@file:JvmName("APILyricsConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Lyrics as APILyrics
|
import org.moire.ultrasonic.api.subsonic.models.Lyrics as APILyrics
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@file:JvmName("APIMusicDirectoryConverter")
|
@file:JvmName("APIMusicDirectoryConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@ -36,7 +35,10 @@ fun MusicDirectoryChild.toAlbumEntity(serverId: Int): Album = Album(id, serverId
|
|||||||
populateCommonProps(this, this@toAlbumEntity)
|
populateCommonProps(this, this@toAlbumEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateCommonProps(entry: MusicDirectory.Child, source: MusicDirectoryChild) {
|
private fun populateCommonProps(
|
||||||
|
entry: MusicDirectory.Child,
|
||||||
|
source: MusicDirectoryChild
|
||||||
|
) {
|
||||||
entry.parent = source.parent
|
entry.parent = source.parent
|
||||||
entry.isDirectory = source.isDir
|
entry.isDirectory = source.isDir
|
||||||
entry.title = source.title
|
entry.title = source.title
|
||||||
@ -61,7 +63,10 @@ private fun populateCommonProps(entry: MusicDirectory.Child, source: MusicDirect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateTrackProps(track: Track, source: MusicDirectoryChild) {
|
private fun populateTrackProps(
|
||||||
|
track: Track,
|
||||||
|
source: MusicDirectoryChild
|
||||||
|
) {
|
||||||
track.size = source.size
|
track.size = source.size
|
||||||
track.contentType = source.contentType
|
track.contentType = source.contentType
|
||||||
track.suffix = source.suffix
|
track.suffix = source.suffix
|
||||||
@ -79,11 +84,10 @@ fun List<MusicDirectoryChild>.toDomainEntityList(serverId: Int): List<MusicDirec
|
|||||||
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
|
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
|
||||||
|
|
||||||
forEach {
|
forEach {
|
||||||
if (it.isDir) {
|
if (it.isDir)
|
||||||
newList.add(it.toAlbumEntity(serverId))
|
newList.add(it.toAlbumEntity(serverId))
|
||||||
} else {
|
else
|
||||||
newList.add(it.toTrackEntity(serverId))
|
newList.add(it.toTrackEntity(serverId))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newList
|
return newList
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIMusicFolderConverter")
|
@file:JvmName("APIMusicFolderConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
|
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
|
||||||
@ -19,8 +18,9 @@ fun APIMusicFolder.toDomainEntity(serverId: Int): MusicFolder = MusicFolder(
|
|||||||
name = this.name
|
name = this.name
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> = this.map {
|
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> =
|
||||||
val item = it.toDomainEntity(serverId)
|
this.map {
|
||||||
item.serverId = serverId
|
val item = it.toDomainEntity(serverId)
|
||||||
item
|
item.serverId = serverId
|
||||||
}
|
item
|
||||||
|
}
|
||||||
|
@ -31,11 +31,8 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory =
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
||||||
this.id,
|
this.id, this.name, this.owner,
|
||||||
this.name,
|
this.comment, this.songCount.toString(),
|
||||||
this.owner,
|
|
||||||
this.comment,
|
|
||||||
this.songCount.toString(),
|
|
||||||
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
|
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
|
||||||
public
|
public
|
||||||
)
|
)
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
// Converts podcasts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts podcasts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIPodcastConverter")
|
@file:JvmName("APIPodcastConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.PodcastChannel
|
import org.moire.ultrasonic.api.subsonic.models.PodcastChannel
|
||||||
|
|
||||||
fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
|
fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
|
||||||
this.id,
|
this.id, this.title, this.url, this.description, this.status
|
||||||
this.title,
|
|
||||||
this.url,
|
|
||||||
this.description,
|
|
||||||
this.status
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this
|
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APISearchConverter")
|
@file:JvmName("APISearchConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
|
import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
|
||||||
@ -16,8 +15,7 @@ import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
|
|||||||
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
||||||
|
|
||||||
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
||||||
emptyList(),
|
emptyList(), emptyList(),
|
||||||
emptyList(),
|
|
||||||
this.matchList.map { it.toTrackEntity(serverId) }
|
this.matchList.map { it.toTrackEntity(serverId) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
// Contains helper method to convert subsonic api share to domain model
|
// Contains helper method to convert subsonic api share to domain model
|
||||||
@file:JvmName("APIShareConverter")
|
@file:JvmName("APIShareConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// Helper functions to convert User entity to domain entity
|
// Helper functions to convert User entity to domain entity
|
||||||
@file:JvmName("APIUserConverter")
|
@file:JvmName("APIUserConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.User
|
import org.moire.ultrasonic.api.subsonic.models.User
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* AboutFragment.kt
|
* AboutFragment.kt
|
||||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -8,16 +8,17 @@
|
|||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.fragment
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||||
import org.moire.ultrasonic.util.Util.applyTheme
|
import org.moire.ultrasonic.util.Util.applyTheme
|
||||||
import org.moire.ultrasonic.util.Util.getVersionName
|
import org.moire.ultrasonic.util.Util.getVersionName
|
||||||
|
|
||||||
@ -55,18 +56,18 @@ class AboutFragment : Fragment() {
|
|||||||
versionName
|
versionName
|
||||||
)
|
)
|
||||||
|
|
||||||
FragmentTitle.setTitle(this@AboutFragment, getString(R.string.menu_about))
|
setTitle(this@AboutFragment, getString(R.string.menu_about))
|
||||||
titleText?.text = title
|
titleText?.text = title
|
||||||
|
|
||||||
webPageButton?.setOnClickListener {
|
webPageButton?.setOnClickListener {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(Intent.ACTION_VIEW, getString(R.string.about_webpage_url).toUri())
|
Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_webpage_url)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
reportBugButton?.setOnClickListener {
|
reportBugButton?.setOnClickListener {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(Intent.ACTION_VIEW, getString(R.string.about_report_url).toUri())
|
Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_report_url)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* AlbumListFragment.kt
|
* AlbumListFragment.kt
|
||||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("NAME_SHADOWING")
|
||||||
|
|
||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.fragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -30,7 +32,6 @@ import org.moire.ultrasonic.domain.Album
|
|||||||
import org.moire.ultrasonic.model.AlbumListModel
|
import org.moire.ultrasonic.model.AlbumListModel
|
||||||
import org.moire.ultrasonic.util.LayoutType
|
import org.moire.ultrasonic.util.LayoutType
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
|
||||||
import org.moire.ultrasonic.view.FilterButtonBar
|
import org.moire.ultrasonic.view.FilterButtonBar
|
||||||
import org.moire.ultrasonic.view.SortOrder
|
import org.moire.ultrasonic.view.SortOrder
|
||||||
import org.moire.ultrasonic.view.ViewCapabilities
|
import org.moire.ultrasonic.view.ViewCapabilities
|
||||||
@ -65,17 +66,19 @@ class AlbumListFragment(
|
|||||||
/**
|
/**
|
||||||
* The central function to pass a query to the model and return a LiveData object
|
* The central function to pass a query to the model and return a LiveData object
|
||||||
*/
|
*/
|
||||||
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<Album>> {
|
override fun getLiveData(
|
||||||
|
refresh: Boolean,
|
||||||
|
append: Boolean
|
||||||
|
): LiveData<List<Album>> {
|
||||||
fetchAlbums(refresh)
|
fetchAlbums(refresh)
|
||||||
|
|
||||||
return listModel.list
|
return listModel.list
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) {
|
private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) {
|
||||||
listModel.viewModelScope.launch(
|
|
||||||
toastingExceptionHandler()
|
listModel.viewModelScope.launch(handler) {
|
||||||
) {
|
refreshListView?.isRefreshing = true
|
||||||
swipeRefresh?.isRefreshing = true
|
|
||||||
|
|
||||||
if (navArgs.byArtist) {
|
if (navArgs.byArtist) {
|
||||||
listModel.getAlbumsOfArtist(
|
listModel.getAlbumsOfArtist(
|
||||||
@ -92,7 +95,7 @@ class AlbumListFragment(
|
|||||||
refresh = refresh or append
|
refresh = refresh or append
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
swipeRefresh?.isRefreshing = false
|
refreshListView?.isRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,8 +185,8 @@ class AlbumListFragment(
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// Setup refresh handler
|
// Setup refresh handler
|
||||||
swipeRefresh = view.findViewById(refreshListId)
|
refreshListView = view.findViewById(refreshListId)
|
||||||
swipeRefresh?.setOnRefreshListener {
|
refreshListView?.setOnRefreshListener {
|
||||||
fetchAlbums(refresh = true)
|
fetchAlbums(refresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import org.moire.ultrasonic.NavigationGraphDirections
|
|||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
||||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||||
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
import org.moire.ultrasonic.domain.Index
|
import org.moire.ultrasonic.domain.Index
|
||||||
import org.moire.ultrasonic.model.ArtistListModel
|
import org.moire.ultrasonic.model.ArtistListModel
|
||||||
@ -42,7 +43,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
|||||||
* The central function to pass a query to the model and return a LiveData object
|
* The central function to pass a query to the model and return a LiveData object
|
||||||
*/
|
*/
|
||||||
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<ArtistOrIndex>> {
|
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<ArtistOrIndex>> {
|
||||||
return listModel.getItems(navArgs.refresh || refresh, swipeRefresh!!)
|
return listModel.getItems(navArgs.refresh || refresh, refreshListView!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@ -69,7 +70,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
|||||||
id = item.id,
|
id = item.id,
|
||||||
name = item.name,
|
name = item.name,
|
||||||
parentId = item.id,
|
parentId = item.id,
|
||||||
isArtist = false
|
isArtist = (item is Artist)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
NavigationGraphDirections.toAlbumList(
|
NavigationGraphDirections.toAlbumList(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user