Compare commits
229 Commits
Author | SHA1 | Date |
---|---|---|
Óscar García Amor | 90205fe0fb | |
Óscar García Amor | 2165ce75b3 | |
Óscar García Amor | 8b3ee0a8d6 | |
birdbird | 695b2df63f | |
tzugen | 798d795e81 | |
tzugen | ecfce59e0f | |
tzugen | de0cb7713b | |
tzugen | 78bfab3753 | |
tzugen | b955d77152 | |
tzugen | b11694d6a2 | |
tzugen | 31a1fdace1 | |
tzugen | 5b03b632fd | |
tzugen | 152b1d261a | |
tzugen | 53a1a5545a | |
tzugen | ad54db5bcb | |
tzugen | 177329abcf | |
tzugen | 241e51015f | |
tzugen | 60dbe70ca5 | |
tzugen | 8490f7115d | |
tzugen | ee67f4c744 | |
tzugen | 3a3bd10fdb | |
birdbird | 3445576dc9 | |
tzugen | 8c40f662a1 | |
birdbird | 6c6227ce41 | |
tzugen | 240a2fa8f6 | |
tzugen | 7de775dc26 | |
birdbird | d034fc9c71 | |
birdbird | 05ada9297d | |
Maxence G | aa6c037b20 | |
Maxence G | b8c924be27 | |
Maxence G | 0929a6a1bd | |
Maxence G | fefee74a66 | |
Maxence G | 37e3ce09c1 | |
Maxence G | 16b3fcad32 | |
Maxence G | d6aebd9989 | |
Maxence G | 3f408600cb | |
Maxence G | 9014b47b74 | |
tzugen | ac489ae8b9 | |
tzugen | e7f8fa21cb | |
tzugen | b1c3cabfef | |
tzugen | 77865a143d | |
Óscar García Amor | ff9c7b2435 | |
Óscar García Amor | 737563bf6b | |
tzugen | 9a73d72fa4 | |
tzugen | 98ce519014 | |
tzugen | 83fc54d332 | |
Maxence G | a2b9c6b9a3 | |
Maxence G | 5ae56d26c5 | |
Maxence G | 4efb6dcb58 | |
tzugen | 8a90e98989 | |
tzugen | 46a8f4640d | |
tzugen | ab41966943 | |
tzugen | 00d7ce326c | |
Maxence G | bc4b0aa832 | |
Maxence G | 23fd336ffd | |
Maxence G | b57a973510 | |
Maxence G | 8796006ced | |
Maxence G | 545b65921e | |
Maxence G | cf367ead92 | |
Maxence G | 9961213f09 | |
tzugen | 5deb7d4d58 | |
tzugen | 5f31eaaffe | |
tzugen | cad6477cd9 | |
tzugen | b440821ea8 | |
Holger Müller | 8663b9d50e | |
Óscar García Amor | 2bae243be0 | |
Óscar García Amor | 139e810186 | |
tzugen | 66443ba018 | |
tzugen | f8b78a47d2 | |
tzugen | 4cda114f4c | |
tzugen | d8b5b774ee | |
tzugen | b6730f5a93 | |
tzugen | 87c160610f | |
tzugen | 70f8b75019 | |
tzugen | 147d7cd46e | |
tzugen | 59e37e62a6 | |
tzugen | 1e571e165c | |
tzugen | 5e0dd14c4f | |
tzugen | 53ae0cd232 | |
tzugen | 608f86ac5f | |
tzugen | 669b51c0d2 | |
tzugen | 6e1478d896 | |
Nite | d9e4b8b3d3 | |
Nite | f790e29add | |
Nite | faf07f2887 | |
i-do-cpp | 057644f592 | |
tzugen | 926081f84c | |
tzugen | 4a00494647 | |
Nite | 34e0178db3 | |
Nite | cbe3992b01 | |
Nite | 46846bd5c9 | |
tzugen | 707339b88b | |
tzugen | 827654c0c1 | |
tzugen | 1d236aa6e3 | |
tzugen | 9cdba9a27a | |
tzugen | 7ba599f58c | |
tzugen | 2e1e627b7a | |
tzugen | d550eabf88 | |
tzugen | dda86b42c7 | |
tzugen | b6e890b26c | |
tzugen | c2ac1d436f | |
tzugen | 2aaa3c2119 | |
tzugen | 5d4aff1f21 | |
tzugen | 6115ac995f | |
tzugen | 647435fe55 | |
tzugen | 81d24f6cbb | |
tzugen | 69c78f4c37 | |
tzugen | 3691428a68 | |
tzugen | 788538ee6a | |
tzugen | 762aeec5d3 | |
tzugen | a3a0c7f41d | |
tzugen | 1564379bd1 | |
tzugen | 7d33770fd6 | |
tzugen | 728afad00c | |
Óscar García Amor | f121e297df | |
Óscar García Amor | 3f2cfb131a | |
Óscar García Amor | b8b4b81726 | |
Óscar García Amor | 383089a409 | |
Óscar García Amor | 8d8a5f05ea | |
Óscar García Amor | 46a2e5d67b | |
tzugen | 92ef78a36a | |
tzugen | e5021959c3 | |
tzugen | 3ca25ed1c6 | |
tzugen | 6da83db9df | |
tzugen | 9779844620 | |
tzugen | f936ad690c | |
tzugen | 5230ce011d | |
Holger Müller | a98c9e2ffd | |
tzugen | 0128a8b29d | |
tzugen | 41f5520f1f | |
tzugen | fd34199c27 | |
tzugen | bb77216eff | |
tzugen | e1f4ee15d5 | |
tzugen | d0959ffcb5 | |
tzugen | 4c22c8b41b | |
tzugen | ba1a1c5538 | |
tzugen | 7742f67796 | |
tzugen | 1a69507e34 | |
tzugen | 46fb7664c3 | |
tzugen | dd65a12b53 | |
tzugen | 2f7f47783a | |
tzugen | b1c2d020b5 | |
tzugen | 5dc9fda7a4 | |
tzugen | 1313fb6c0c | |
tzugen | 5966dd7299 | |
tzugen | 1703f02aad | |
tzugen | 922022ab03 | |
tzugen | bfc11f9924 | |
tzugen | e77b5abd3e | |
tzugen | 988bf62acf | |
tzugen | 1a46f7e2c6 | |
tzugen | 1d88c585c4 | |
tzugen | 287169649a | |
tzugen | 020f67d5e6 | |
Óscar García Amor | fcc57ae316 | |
tzugen | 0fd17bfe8c | |
tzugen | 0c016bff41 | |
tzugen | 12435ed9ec | |
tzugen | c2226ba202 | |
tzugen | 892b441c0d | |
tzugen | e53da92dac | |
tzugen | 2de59b2206 | |
tzugen | 34c13d7908 | |
tzugen | 88918bd839 | |
tzugen | f49063664b | |
tzugen | 126efd35c6 | |
tzugen | a6a052781d | |
tzugen | 107b01fd91 | |
tzugen | d05ac1489e | |
Holger Müller | fc94d28862 | |
Holger Müller | 8bec74e66a | |
Holger Müller | acf6c5a681 | |
tzugen | 81a21ce8b7 | |
Holger Müller | cf86101de2 | |
Nite | 5a44fcfe29 | |
Nite | 2aa5174fbd | |
Nite | cf7cef9831 | |
Nite | 423957d954 | |
Holger Müller | 0944bd2217 | |
Holger Müller | 7b750c692c | |
Holger Müller | c247e930c4 | |
tzugen | 44d68a71da | |
tzugen | 7a51c271ba | |
Óscar García Amor | 4d91068535 | |
tzugen | fe3b713241 | |
Holger Müller | 2f5704548c | |
tzugen | 17850980e1 | |
tzugen | 34d2b45d71 | |
tzugen | 555ef5b7ff | |
tzugen | c269243a0d | |
tzugen | 2d8b93301f | |
tzugen | e4a41de3ef | |
tzugen | f0447105d2 | |
tzugen | 65c4f2b100 | |
tzugen | f6f9683a9c | |
tzugen | c2d62e8688 | |
tzugen | e153565086 | |
tzugen | f30a582c7b | |
Holger Müller | cc5f29ca98 | |
Holger Müller | 8f18192c36 | |
Holger Müller | ae2055e324 | |
Nite | dee4675715 | |
Nite | e8c31db90f | |
Nite | 34fb63c783 | |
Nite | 9ee03aae2f | |
Nite | ebfc06c423 | |
Nite | 0587f4d837 | |
Nite | 6bfd06c6a0 | |
Óscar García Amor | 6442bae882 | |
Nite | c81c685800 | |
Holger Müller | 5941e5ab87 | |
Holger Müller | cf52d76698 | |
Cem Eren | 273ac8f9b8 | |
Nite | 465c211017 | |
Nite | 4fbedc3d2b | |
Cem Eren | 0961f56a7d | |
Cem Eren | eb0fa67431 | |
Óscar García Amor | 3d65c0a90c | |
Nite | 7dd479c0d2 | |
Nite | b5bfd87fcc | |
Philippe Daouadi | d03b633eeb | |
Nite | 4ee4b70e09 | |
Nite | 8675f25668 | |
Nite | 00dc87d5df | |
Nite | b7d1e4acf6 | |
Óscar García Amor | baa0c92c7a | |
Nite | 7c9d51f758 | |
Nite | 9bf7e99abd | |
Nite | b5e606455e |
|
@ -1,11 +1,20 @@
|
||||||
version: 3
|
version: 2.1
|
||||||
|
parameters:
|
||||||
|
memory-config:
|
||||||
|
type: string
|
||||||
|
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g"
|
||||||
|
memory-config-debug:
|
||||||
|
type: string
|
||||||
|
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*"
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/android:api-30
|
- image: cimg/android:2022.06.1
|
||||||
working_directory: ~/ultrasonic
|
working_directory: ~/ultrasonic
|
||||||
environment:
|
environment:
|
||||||
JVM_OPTS: -Xmx3200m
|
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||||
|
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||||
|
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
|
@ -18,6 +27,7 @@ jobs:
|
||||||
command: |
|
command: |
|
||||||
sed -i '/^org.gradle.jvmargs/d' gradle.properties
|
sed -i '/^org.gradle.jvmargs/d' gradle.properties
|
||||||
sed -i 's/^org.gradle.daemon=true/org.gradle.daemon=false/g' gradle.properties
|
sed -i 's/^org.gradle.daemon=true/org.gradle.daemon=false/g' gradle.properties
|
||||||
|
cat gradle.properties
|
||||||
- run:
|
- run:
|
||||||
name: checkstyle
|
name: checkstyle
|
||||||
command: ./gradlew -Pqc ktlintCheck
|
command: ./gradlew -Pqc ktlintCheck
|
||||||
|
@ -31,7 +41,6 @@ jobs:
|
||||||
name: unit-tests
|
name: unit-tests
|
||||||
command: |
|
command: |
|
||||||
./gradlew ciTest testDebugUnitTest
|
./gradlew ciTest testDebugUnitTest
|
||||||
./gradlew jacocoFullReport
|
|
||||||
- run:
|
- run:
|
||||||
name: lint
|
name: lint
|
||||||
command: ./gradlew :ultrasonic:lintRelease
|
command: ./gradlew :ultrasonic:lintRelease
|
||||||
|
@ -44,18 +53,16 @@ jobs:
|
||||||
- save_cache:
|
- save_cache:
|
||||||
paths:
|
paths:
|
||||||
- ~/.gradle
|
- ~/.gradle
|
||||||
key: v1-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
key: v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: ultrasonic/build/reports
|
path: ultrasonic/build/reports
|
||||||
destination: reports
|
destination: reports
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: subsonic-api/build/reports
|
path: subsonic-api/build/reports
|
||||||
destination: reports
|
destination: reports
|
||||||
- store_artifacts:
|
|
||||||
path: build/reports/jacoco/jacocoFullReport/
|
|
||||||
push_translations:
|
push_translations:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/python:3.6
|
- image: cimg/python:3.6
|
||||||
working_directory: ~/ultrasonic
|
working_directory: ~/ultrasonic
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
@ -75,8 +82,12 @@ jobs:
|
||||||
tx push -s
|
tx push -s
|
||||||
generate_signed_apk:
|
generate_signed_apk:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/android:api-30
|
- image: cimg/android:2022.06.1
|
||||||
working_directory: ~/ultrasonic
|
working_directory: ~/ultrasonic
|
||||||
|
environment:
|
||||||
|
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||||
|
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||||
|
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
|
@ -95,22 +106,22 @@ jobs:
|
||||||
command: |
|
command: |
|
||||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||||
mkdir -p /tmp/ultrasonic-release
|
mkdir -p /tmp/ultrasonic-release
|
||||||
${ANDROID_HOME}/build-tools/30.0.0/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
|
${ANDROID_HOME}/build-tools/32.0.0/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
|
||||||
${ANDROID_HOME}/build-tools/30.0.0/apksigner sign --verbose --ks ~/ultrasonic/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
|
${ANDROID_HOME}/build-tools/32.0.0/apksigner sign --verbose --ks ~/ultrasonic/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
|
||||||
${ANDROID_HOME}/build-tools/30.0.0/apksigner verify --verbose /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
|
${ANDROID_HOME}/build-tools/32.0.0/apksigner verify --verbose /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: /tmp/ultrasonic-release
|
root: /tmp/ultrasonic-release
|
||||||
paths:
|
paths:
|
||||||
- ultrasonic-*.apk*
|
- ultrasonic-*.apk*
|
||||||
publish_github_signed_apk:
|
publish_github_signed_apk:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang
|
- image: cimg/go:1.18
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: /tmp/ultrasonic-release
|
at: /tmp/ultrasonic-release
|
||||||
- run:
|
- run:
|
||||||
name: install ghr
|
name: install ghr
|
||||||
command: go get -v github.com/tcnksm/ghr
|
command: go install -v github.com/tcnksm/ghr@latest
|
||||||
- run:
|
- run:
|
||||||
name: publish release on github tag
|
name: publish release on github tag
|
||||||
command: ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} ${CIRCLE_TAG} /tmp/ultrasonic-release
|
command: ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} ${CIRCLE_TAG} /tmp/ultrasonic-release
|
||||||
|
@ -129,7 +140,7 @@ workflows:
|
||||||
- generate_signed_apk:
|
- generate_signed_apk:
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /^[0-9]+(\.[0-9]+)*/
|
only: /^[0-9]+(\.[0-9]+)*(-beta\.[0-9]+)?/
|
||||||
branches:
|
branches:
|
||||||
ignore: /.*/
|
ignore: /.*/
|
||||||
- publish_github_signed_apk:
|
- publish_github_signed_apk:
|
||||||
|
@ -137,7 +148,7 @@ workflows:
|
||||||
- generate_signed_apk
|
- generate_signed_apk
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /^[0-9]+(\.[0-9]+)*/
|
only: /^[0-9]+(\.[0-9]+)*(-beta\.[0-9]+)?/
|
||||||
branches:
|
branches:
|
||||||
ignore: /.*/
|
ignore: /.*/
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ captures/
|
||||||
*.iml
|
*.iml
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
|
||||||
# Keystore files
|
# Keystore files
|
||||||
*.jks
|
*.jks
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<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" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="11" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="CopyrightManager">
|
||||||
|
<copyright>
|
||||||
|
<option name="notice" value="&#36;file.fileName Copyright (C) 2009-&#36;today.year Ultrasonic developers Distributed under terms of the GNU GPLv3 license." />
|
||||||
|
<option name="myName" value="Default" />
|
||||||
|
</copyright>
|
||||||
|
</component>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<component name="CopyrightManager">
|
||||||
|
<settings default="Default">
|
||||||
|
<LanguageOptions name="Kotlin">
|
||||||
|
<option name="fileTypeOverride" value="3" />
|
||||||
|
</LanguageOptions>
|
||||||
|
</settings>
|
||||||
|
</component>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Reformat" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="processChangedFilesOnly" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK" />
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -18,18 +18,46 @@ By default Pull Request should be opened against **develop** branch, PR against
|
||||||
### Here are a few guidelines you should follow before submitting:
|
### Here are a few guidelines you should follow before submitting:
|
||||||
|
|
||||||
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
|
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
|
||||||
Use `git commit --signoff` to acknowledge this.
|
Use `git commit --signoff` to acknowledge this.
|
||||||
2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests
|
2. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
|
||||||
should be written in this programming language.
|
3. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
|
||||||
3. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
|
|
||||||
4. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
|
|
||||||
Refactoring existing messes is great, but watch out for breakage.
|
Refactoring existing messes is great, but watch out for breakage.
|
||||||
5. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
|
4. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
|
||||||
and test.
|
and test.
|
||||||
|
|
||||||
### Pull Request Process
|
### Pull Request Process
|
||||||
|
On each Pull Request Github runs a number of checks to make sure there are no problems.
|
||||||
|
|
||||||
|
#### Signed commits
|
||||||
|
Commits must be signed. [See here how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
|
||||||
|
|
||||||
|
#### KtLint
|
||||||
|
This programm checks if the source code is formatted correctly.
|
||||||
|
You can run it yourself locally with
|
||||||
|
|
||||||
|
`./gradlew -Pqc ktlintFormat`
|
||||||
|
|
||||||
|
Running this command will fix common problems and will notify you of problems it couldn't fix automatically.
|
||||||
|
|
||||||
|
#### Detekt
|
||||||
|
|
||||||
|
Detekt is a static analyser. It helps to find potential bugs in our code.
|
||||||
|
|
||||||
|
You can run it yourself locally with
|
||||||
|
|
||||||
|
`./gradlew -Pqc detekt`
|
||||||
|
|
||||||
|
There is a "baseline" file, in which errors which have been in the code base before are noted.
|
||||||
|
Sometimes it is necessary to regenerate this file by running:
|
||||||
|
|
||||||
|
`./gradlew -Pqc detektBaseline`
|
||||||
|
|
||||||
|
#### Lint
|
||||||
|
Lint looks for general problems in the code or unused resources etc.
|
||||||
|
You can run it with
|
||||||
|
|
||||||
|
`./gradlew -Pqc lintRelease`
|
||||||
|
|
||||||
|
If there is a need to regenerate the baseline, remove `ultrasonic/lint-baseline.xml` and rerun the command.
|
||||||
|
|
||||||
|
|
||||||
1. Ensure [all commits are signed-off](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification).
|
|
||||||
2. Check tests for the new code are added.
|
|
||||||
3. Check code style is passing.
|
|
||||||
4. Check code static analysis is passing.
|
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
## Problem description
|
## Problem description
|
||||||
|
|
||||||
Describe your problem here. Describe what you want to happen, and what happens
|
Describe your problem here. Describe what you want to happen, and what
|
||||||
if you try to do it. If you have a stack trace or any logs, please format them using
|
happens if you try to do it. If you have a stack trace or any logs, please
|
||||||
github triple backquote notation
|
format them using GitHub triple backquote notation.
|
||||||
|
|
||||||
### Steps to reproduce
|
### Steps to reproduce
|
||||||
|
|
||||||
Describe how somebody else could observe the same behavior you do. Don't share here any logins and
|
Describe how somebody else could observe the same behavior you do. Don't
|
||||||
passwords!
|
share here any logins and passwords!
|
||||||
|
|
||||||
## System information
|
## System information
|
||||||
|
|
||||||
|
### Ultrasonic client
|
||||||
|
|
||||||
* **Ultrasonic version**: *version of the app*
|
* **Ultrasonic version**: *version of the app*
|
||||||
* **Android version**: *Version of Android OS on the device*
|
* **Android version**: *Version of Android OS on the device*
|
||||||
* **Device info**: *Device manufacturer, model*
|
* **Device info**: *Device manufacturer, model*
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
* **Server name**: *Airsonic, Ampache, Supysonic...*
|
||||||
|
* **Server version**: *version of server software*
|
||||||
|
* **Protocol used**: *http or https (self certificate, letsencrypt...)*
|
||||||
|
|
||||||
## Additional notes
|
## Additional notes
|
||||||
|
|
||||||
Include any extra notes here. Otherwise you may remove this section.
|
Include any extra notes here. Otherwise you may remove this section.
|
||||||
|
|
64
README.md
64
README.md
|
@ -1,14 +1,25 @@
|
||||||
# Ultrasonic
|
# WE HAVE MOVED
|
||||||
[![Build Status](https://circleci.com/gh/ultrasonic/ultrasonic/tree/develop.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/ultrasonic)
|
|
||||||
[![Codecov branch](https://img.shields.io/codecov/c/github/ultrasonic/ultrasonic/develop.svg)]()
|
|
||||||
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
|
|
||||||
|
|
||||||
Ultrasonic is free and open-source music streaming Android client for [Subsonic](http://www.subsonic.org/) [API](http://www.subsonic.org/pages/api.jsp) (version 1.7.0 or higher) compatible servers.
|
Ultrasonic code is now hosted in [GitLab][ultrasonic].
|
||||||
|
|
||||||
|
- New Web: https://ultrasonic.gitlab.io
|
||||||
|
- New Git: https://gitlab.com/ultrasonic/ultrasonic
|
||||||
|
- New bugtracker: https://gitlab.com/ultrasonic/ultrasonic/-/issues
|
||||||
|
- New releases: https://gitlab.com/ultrasonic/ultrasonic/-/packages
|
||||||
|
|
||||||
|
[ultrasonic]: https://gitlab.com/ultrasonic/ultrasonic
|
||||||
|
|
||||||
|
# Ultrasonic
|
||||||
|
|
||||||
|
Ultrasonic is free and open-source music streaming Android client for
|
||||||
|
[Subsonic][subsonic] [API][subapi] (version 1.7.0 or higher) compatible
|
||||||
|
servers.
|
||||||
|
|
||||||
## Help wanted
|
## Help wanted
|
||||||
|
|
||||||
We currently don't have that much time to spend developing Subsonic, so any
|
We currently don't have that much time to spend developing Subsonic, so any
|
||||||
contributions or active developers are always welcomed.
|
contributions or active developers are always welcomed.
|
||||||
|
Have a look at [CONTRIBUTING](CONTRIBUTING.md) to get started.
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
|
@ -16,24 +27,26 @@ App is available to download at following stores:
|
||||||
|
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="70">](https://play.google.com/store/apps/details?id=org.moire.ultrasonic)
|
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="70">](https://play.google.com/store/apps/details?id=org.moire.ultrasonic)
|
||||||
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="70">](https://f-droid.org/packages/org.moire.ultrasonic/)
|
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="70">](https://f-droid.org/packages/org.moire.ultrasonic/)
|
||||||
[<img src="https://ultrasonic.github.io/assets/img/get-it-on-github.png" alt="Get it on GitHub" height="70">](https://github.com/ultrasonic/ultrasonic/releases)
|
[<img src="https://ultrasonic.gitlab.io/assets/img/get-it-on-gitlab.png" alt="Get it on GitLab" height="70">](https://gitlab.com/ultrasonic/ultrasonic/-/releases)
|
||||||
|
|
||||||
**Warning**: All three versions (Google Play, F-Droid and the APKs) are not
|
**Warning**: All three versions (Google Play, F-Droid and the APKs) are not
|
||||||
compatible (not signed by the same key)! You must uninstall one to install
|
compatible (not signed by the same key)! You must uninstall one to install
|
||||||
the other, which will delete all your data.
|
the other, which will delete all your data.
|
||||||
|
|
||||||
If you want to use the version downloaded from F-Droid or form Github with **Android Auto**, you must enable Unknown Sources as it is described in [this wiki page](https://github.com/ultrasonic/ultrasonic/wiki/Using-Ultrasonic-with-Android-Auto).
|
If you want to use the version downloaded from F-Droid or from GitLab with
|
||||||
|
**Android Auto**, you must enable Unknown Sources as it is described in
|
||||||
|
[this wiki page][wikiaa].
|
||||||
|
|
||||||
## Bugs and issues
|
## Bugs and issues
|
||||||
|
|
||||||
First, see if your issue haven’t been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
|
First, see if your issue haven’t been yet reported [here][issues], otherwise
|
||||||
otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
|
open [a new issue][newissue].
|
||||||
|
|
||||||
### Known (not our) bugs
|
### Known (not our) bugs
|
||||||
|
|
||||||
If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not
|
If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not
|
||||||
work. This is caused by bad implementation of Subsonic API by Madsonic. For
|
work. This is caused by bad implementation of Subsonic API by Madsonic. For
|
||||||
more info about this you can read [this bug](https://github.com/ultrasonic/ultrasonic/issues/129).
|
more info about this you can read [this bug][madbug].
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -41,16 +54,29 @@ See [CONTRIBUTING](CONTRIBUTING.md).
|
||||||
|
|
||||||
## Supported (tested) Subsonic API implementations
|
## Supported (tested) Subsonic API implementations
|
||||||
|
|
||||||
- [Subsonic](http://www.subsonic.org/pages/index.jsp)
|
- [Subsonic][subsonic]
|
||||||
- [Airsonic](https://github.com/airsonic/airsonic)
|
- [Airsonic-Advanced][airsonic]
|
||||||
- [Supysonic](https://github.com/spl0k/supysonic)
|
- [Supysonic][supysonic]
|
||||||
- [Ampache](https://ampache.org/)
|
- [Ampache][ampache]
|
||||||
|
|
||||||
Other *Subsonic API* implementations should work as well as long as they follow API
|
Other *Subsonic API* implementations should work as well as long as they
|
||||||
[documentation](http://www.subsonic.org/pages/api.jsp).
|
follow API [documentation][subapi].
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This software is licensed under the terms of the GNU General Public License version 3 (GPLv3).
|
This software is licensed under the terms of the GNU General Public License
|
||||||
|
version 3 (GPLv3).
|
||||||
|
|
||||||
Full text of the license is available in the [LICENSE](LICENSE) file and [online](https://opensource.org/licenses/gpl-3.0.html).
|
Full text of the license is available in the [LICENSE](LICENSE) file and
|
||||||
|
[online][gpl3].
|
||||||
|
|
||||||
|
[wikiaa]: https://gitlab.com/ultrasonic/ultrasonic/-/wikis/Using-Ultrasonic-with-Android-Auto
|
||||||
|
[issues]: https://gitlab.com/ultrasonic/ultrasonic/-/issues
|
||||||
|
[newissue]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/new
|
||||||
|
[madbug]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/129
|
||||||
|
[subsonic]: http://www.subsonic.org/
|
||||||
|
[subapi]: http://www.subsonic.org/pages/api.jsp
|
||||||
|
[airsonic]: https://github.com/airsonic-advanced/airsonic-advanced
|
||||||
|
[supysonic]: https://github.com/spl0k/supysonic
|
||||||
|
[ampache]: https://ampache.org/
|
||||||
|
[gpl3]: https://opensource.org/licenses/gpl-3.0.html
|
||||||
|
|
|
@ -17,7 +17,6 @@ buildscript {
|
||||||
classpath libs.kotlin
|
classpath libs.kotlin
|
||||||
classpath libs.ktlintGradle
|
classpath libs.ktlintGradle
|
||||||
classpath libs.detekt
|
classpath libs.detekt
|
||||||
classpath libs.jacoco
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +43,6 @@ allprojects {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: 'gradle_scripts/jacoco.gradle'
|
|
||||||
|
|
||||||
wrapper {
|
wrapper {
|
||||||
gradleVersion(libs.versions.gradle.get())
|
gradleVersion(libs.versions.gradle.get())
|
||||||
distributionType("all")
|
distributionType("all")
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
apply from: bootstrap.androidModule
|
apply from: bootstrap.androidModule
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
ext {
|
|
||||||
jacocoExclude = [
|
|
||||||
'**/domain/**'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.roomRuntime
|
implementation libs.roomRuntime
|
||||||
implementation libs.roomKtx
|
implementation libs.roomKtx
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Album.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Entity(tableName = "albums", primaryKeys = ["id", "serverId"])
|
||||||
|
data class Album(
|
||||||
|
override var id: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
override var serverId: Int = -1,
|
||||||
|
override var parent: String? = null,
|
||||||
|
override var album: String? = null,
|
||||||
|
override var title: String? = null,
|
||||||
|
override val name: String? = null,
|
||||||
|
override var discNumber: Int? = 0,
|
||||||
|
override var coverArt: String? = null,
|
||||||
|
override var songCount: Long? = null,
|
||||||
|
override var created: Date? = null,
|
||||||
|
override var artist: String? = null,
|
||||||
|
override var artistId: String? = null,
|
||||||
|
override var duration: Int? = 0,
|
||||||
|
override var year: Int? = 0,
|
||||||
|
override var genre: String? = null,
|
||||||
|
override var starred: Boolean = false,
|
||||||
|
override var path: String? = null,
|
||||||
|
override var closeness: Int = 0,
|
||||||
|
) : MusicDirectory.Child() {
|
||||||
|
override var isDirectory = true
|
||||||
|
override var isVideo = false
|
||||||
|
}
|
|
@ -1,14 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Artist.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity(tableName = "artists")
|
@Entity(tableName = "artists", primaryKeys = ["id", "serverId"])
|
||||||
data class Artist(
|
data class Artist(
|
||||||
@PrimaryKey override var id: String,
|
override var id: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
override var serverId: Int = -1,
|
||||||
override var name: String? = null,
|
override var name: String? = null,
|
||||||
override var index: String? = null,
|
override var index: String? = null,
|
||||||
override var coverArt: String? = null,
|
override var coverArt: String? = null,
|
||||||
override var albumCount: Long? = null,
|
override var albumCount: Long? = null,
|
||||||
override var closeness: Int = 0
|
override var closeness: Int = 0
|
||||||
) : ArtistOrIndex(id)
|
) : ArtistOrIndex(id, serverId)
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
|
/*
|
||||||
|
* ArtistOrIndex.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import androidx.room.Ignore
|
import androidx.room.Ignore
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
abstract class ArtistOrIndex(
|
abstract class ArtistOrIndex(
|
||||||
@Ignore
|
@Ignore
|
||||||
override var id: String,
|
override var id: String,
|
||||||
@Ignore
|
@Ignore
|
||||||
|
open var serverId: Int,
|
||||||
|
@Ignore
|
||||||
override var name: String? = null,
|
override var name: String? = null,
|
||||||
@Ignore
|
@Ignore
|
||||||
open var index: String? = null,
|
open var index: String? = null,
|
||||||
|
@ -18,15 +28,15 @@ abstract class ArtistOrIndex(
|
||||||
) : GenericEntry() {
|
) : GenericEntry() {
|
||||||
|
|
||||||
fun compareTo(other: ArtistOrIndex): Int {
|
fun compareTo(other: ArtistOrIndex): Int {
|
||||||
when {
|
return when {
|
||||||
this.closeness == other.closeness -> {
|
this.closeness == other.closeness -> {
|
||||||
return 0
|
0
|
||||||
}
|
}
|
||||||
this.closeness > other.closeness -> {
|
this.closeness > other.closeness -> {
|
||||||
return -1
|
-1
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
return 1
|
1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
|
||||||
|
|
||||||
data class Bookmark(
|
data class Bookmark(
|
||||||
val position: Int = 0,
|
val position: Int = 0,
|
||||||
|
@ -10,7 +9,7 @@ data class Bookmark(
|
||||||
val comment: String,
|
val comment: String,
|
||||||
val created: Date? = null,
|
val created: Date? = null,
|
||||||
val changed: Date? = null,
|
val changed: Date? = null,
|
||||||
val entry: Entry
|
val track: Track
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
companion object {
|
companion object {
|
||||||
private const val serialVersionUID = 8988990025189807803L
|
private const val serialVersionUID = 8988990025189807803L
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Index.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity(tableName = "indexes")
|
@Entity(tableName = "indexes", primaryKeys = ["id", "serverId"])
|
||||||
data class Index(
|
data class Index(
|
||||||
@PrimaryKey override var id: String,
|
override var id: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
override var serverId: Int = -1,
|
||||||
override var name: String? = null,
|
override var name: String? = null,
|
||||||
override var index: String? = null,
|
override var index: String? = null,
|
||||||
override var coverArt: String? = null,
|
override var coverArt: String? = null,
|
||||||
override var albumCount: Long? = null,
|
override var albumCount: Long? = null,
|
||||||
override var closeness: Int = 0,
|
override var closeness: Int = 0,
|
||||||
var musicFolderId: String? = null
|
var musicFolderId: String? = null
|
||||||
) : ArtistOrIndex(id)
|
) : ArtistOrIndex(id, serverId)
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
/*
|
||||||
|
* MusicDirectory.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import java.io.Serializable
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
||||||
|
@ -20,9 +24,9 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
||||||
return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
|
return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTracks(): List<Entry> {
|
fun getTracks(): List<Track> {
|
||||||
return mapNotNull {
|
return mapNotNull {
|
||||||
it as? Entry
|
it as? Track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +38,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
||||||
|
|
||||||
abstract class Child : GenericEntry() {
|
abstract class Child : GenericEntry() {
|
||||||
abstract override var id: String
|
abstract override var id: String
|
||||||
|
abstract var serverId: Int
|
||||||
abstract var parent: String?
|
abstract var parent: String?
|
||||||
abstract var isDirectory: Boolean
|
abstract var isDirectory: Boolean
|
||||||
abstract var album: String?
|
abstract var album: String?
|
||||||
|
@ -53,87 +58,4 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
||||||
abstract var closeness: Int
|
abstract var closeness: Int
|
||||||
abstract var isVideo: Boolean
|
abstract var isVideo: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rename to Track
|
|
||||||
@Entity
|
|
||||||
data class Entry(
|
|
||||||
@PrimaryKey override var id: String,
|
|
||||||
override var parent: String? = null,
|
|
||||||
override var isDirectory: Boolean = false,
|
|
||||||
override var title: String? = null,
|
|
||||||
override var album: String? = null,
|
|
||||||
var albumId: String? = null,
|
|
||||||
override var artist: String? = null,
|
|
||||||
override var artistId: String? = null,
|
|
||||||
var track: Int? = null,
|
|
||||||
override var year: Int? = null,
|
|
||||||
override var genre: String? = null,
|
|
||||||
var contentType: String? = null,
|
|
||||||
var suffix: String? = null,
|
|
||||||
var transcodedContentType: String? = null,
|
|
||||||
var transcodedSuffix: String? = null,
|
|
||||||
override var coverArt: String? = null,
|
|
||||||
var size: Long? = null,
|
|
||||||
override var songCount: Long? = null,
|
|
||||||
override var duration: Int? = null,
|
|
||||||
var bitRate: Int? = null,
|
|
||||||
override var path: String? = null,
|
|
||||||
override var isVideo: Boolean = false,
|
|
||||||
override var starred: Boolean = false,
|
|
||||||
override var discNumber: Int? = null,
|
|
||||||
var type: String? = null,
|
|
||||||
override var created: Date? = null,
|
|
||||||
override var closeness: Int = 0,
|
|
||||||
var bookmarkPosition: Int = 0,
|
|
||||||
var userRating: Int? = null,
|
|
||||||
var averageRating: Float? = null,
|
|
||||||
override var name: String? = null
|
|
||||||
) : Serializable, Child() {
|
|
||||||
fun setDuration(duration: Long) {
|
|
||||||
this.duration = duration.toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val serialVersionUID = -3339106650010798108L
|
|
||||||
}
|
|
||||||
|
|
||||||
fun compareTo(other: Entry): Int {
|
|
||||||
when {
|
|
||||||
this.closeness == other.closeness -> {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
this.closeness > other.closeness -> {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun compareTo(other: Identifiable) = compareTo(other as Entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Album(
|
|
||||||
@PrimaryKey override var id: String,
|
|
||||||
override var parent: String? = null,
|
|
||||||
override var album: String? = null,
|
|
||||||
override var title: String? = null,
|
|
||||||
override val name: String? = null,
|
|
||||||
override var discNumber: Int? = 0,
|
|
||||||
override var coverArt: String? = null,
|
|
||||||
override var songCount: Long? = null,
|
|
||||||
override var created: Date? = null,
|
|
||||||
override var artist: String? = null,
|
|
||||||
override var artistId: String? = null,
|
|
||||||
override var duration: Int? = 0,
|
|
||||||
override var year: Int? = 0,
|
|
||||||
override var genre: String? = null,
|
|
||||||
override var starred: Boolean = false,
|
|
||||||
override var path: String? = null,
|
|
||||||
override var closeness: Int = 0,
|
|
||||||
) : Child() {
|
|
||||||
override var isDirectory = true
|
|
||||||
override var isVideo = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
|
/*
|
||||||
|
* MusicFolder.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a top level directory in which music or other media is stored.
|
* Represents a top level directory in which music or other media is stored.
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "music_folders")
|
@Entity(tableName = "music_folders", primaryKeys = ["id", "serverId"])
|
||||||
data class MusicFolder(
|
data class MusicFolder(
|
||||||
@PrimaryKey override val id: String,
|
override val id: String,
|
||||||
override val name: String
|
override val name: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
var serverId: Int
|
||||||
) : GenericEntry()
|
) : GenericEntry()
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
package org.moire.ultrasonic.domain
|
|
||||||
|
|
||||||
enum class PlayerState {
|
|
||||||
IDLE,
|
|
||||||
DOWNLOADING,
|
|
||||||
PREPARING,
|
|
||||||
PREPARED,
|
|
||||||
STARTED,
|
|
||||||
STOPPED,
|
|
||||||
PAUSED,
|
|
||||||
COMPLETED
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package org.moire.ultrasonic.domain
|
|
||||||
|
|
||||||
enum class RepeatMode {
|
|
||||||
OFF {
|
|
||||||
override operator fun next(): RepeatMode = ALL
|
|
||||||
},
|
|
||||||
ALL {
|
|
||||||
override operator fun next(): RepeatMode = SINGLE
|
|
||||||
},
|
|
||||||
SINGLE {
|
|
||||||
override operator fun next(): RepeatMode = OFF
|
|
||||||
};
|
|
||||||
|
|
||||||
abstract operator fun next(): RepeatMode
|
|
||||||
}
|
|
|
@ -1,13 +1,10 @@
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory.Album
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The result of a search. Contains matching artists, albums and songs.
|
* The result of a search. Contains matching artists, albums and songs.
|
||||||
*/
|
*/
|
||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
val artists: List<ArtistOrIndex> = listOf(),
|
val artists: List<ArtistOrIndex> = listOf(),
|
||||||
val albums: List<Album> = listOf(),
|
val albums: List<Album> = listOf(),
|
||||||
val songs: List<Entry> = listOf()
|
val songs: List<Track> = listOf()
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
|
||||||
|
|
||||||
data class Share(
|
data class Share(
|
||||||
override var id: String,
|
override var id: String,
|
||||||
|
@ -12,7 +11,7 @@ data class Share(
|
||||||
var lastVisited: String? = null,
|
var lastVisited: String? = null,
|
||||||
var expires: String? = null,
|
var expires: String? = null,
|
||||||
var visitCount: Long? = null,
|
var visitCount: Long? = null,
|
||||||
private val entries: MutableList<Entry> = mutableListOf()
|
private val tracks: MutableList<Track> = mutableListOf()
|
||||||
) : Serializable, GenericEntry() {
|
) : Serializable, GenericEntry() {
|
||||||
override val name: String?
|
override val name: String?
|
||||||
get() {
|
get() {
|
||||||
|
@ -22,12 +21,12 @@ data class Share(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntries(): List<Entry> {
|
fun getEntries(): List<Track> {
|
||||||
return entries.toList()
|
return tracks.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEntry(entry: Entry) {
|
fun addEntry(track: Track) {
|
||||||
entries.add(entry)
|
tracks.add(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Track.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Entity(tableName = "tracks", primaryKeys = ["id", "serverId"])
|
||||||
|
data class Track(
|
||||||
|
override var id: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
override var serverId: Int = -1,
|
||||||
|
override var parent: String? = null,
|
||||||
|
override var isDirectory: Boolean = false,
|
||||||
|
override var title: String? = null,
|
||||||
|
override var album: String? = null,
|
||||||
|
var albumId: String? = null,
|
||||||
|
override var artist: String? = null,
|
||||||
|
override var artistId: String? = null,
|
||||||
|
var track: Int? = null,
|
||||||
|
override var year: Int? = null,
|
||||||
|
override var genre: String? = null,
|
||||||
|
var contentType: String? = null,
|
||||||
|
var suffix: String? = null,
|
||||||
|
var transcodedContentType: String? = null,
|
||||||
|
var transcodedSuffix: String? = null,
|
||||||
|
override var coverArt: String? = null,
|
||||||
|
var size: Long? = null,
|
||||||
|
override var songCount: Long? = null,
|
||||||
|
override var duration: Int? = null,
|
||||||
|
var bitRate: Int? = null,
|
||||||
|
override var path: String? = null,
|
||||||
|
override var isVideo: Boolean = false,
|
||||||
|
override var starred: Boolean = false,
|
||||||
|
override var discNumber: Int? = null,
|
||||||
|
var type: String? = null,
|
||||||
|
override var created: Date? = null,
|
||||||
|
override var closeness: Int = 0,
|
||||||
|
var bookmarkPosition: Int = 0,
|
||||||
|
var userRating: Int? = null,
|
||||||
|
var averageRating: Float? = null,
|
||||||
|
override var name: String? = null
|
||||||
|
) : Serializable, MusicDirectory.Child() {
|
||||||
|
fun setDuration(duration: Long) {
|
||||||
|
this.duration = duration.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val serialVersionUID = -3339106650010798108L
|
||||||
|
}
|
||||||
|
|
||||||
|
fun compareTo(other: Track): Int {
|
||||||
|
when {
|
||||||
|
this.closeness == other.closeness -> {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
this.closeness > other.closeness -> {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: Identifiable) = compareTo(other as Track)
|
||||||
|
}
|
|
@ -20,11 +20,3 @@ dependencies {
|
||||||
testImplementation libs.mockWebServer
|
testImplementation libs.mockWebServer
|
||||||
testImplementation libs.apacheCodecs
|
testImplementation libs.apacheCodecs
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
|
||||||
// Excluding data classes
|
|
||||||
jacocoExclude = [
|
|
||||||
'**/models/**',
|
|
||||||
'**/di/**'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import okhttp3.mockwebserver.MockResponse
|
import okhttp3.mockwebserver.MockResponse
|
||||||
import okhttp3.mockwebserver.MockWebServer
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
import okio.Okio
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
import org.amshove.kluent.`should be`
|
import org.amshove.kluent.`should be`
|
||||||
import org.amshove.kluent.`should contain`
|
import org.amshove.kluent.`should contain`
|
||||||
import org.amshove.kluent.`should not be`
|
import org.amshove.kluent.`should not be`
|
||||||
|
@ -40,12 +41,12 @@ fun MockWebServer.enqueueResponse(resourceName: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Any.loadJsonResponse(name: String): String {
|
fun Any.loadJsonResponse(name: String): String {
|
||||||
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
|
val source = javaClass.classLoader.getResourceAsStream(name)!!.source().buffer()
|
||||||
return source.readString(Charset.forName("UTF-8"))
|
return source.readString(Charset.forName("UTF-8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Any.loadResourceStream(name: String): InputStream {
|
fun Any.loadResourceStream(name: String): InputStream {
|
||||||
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
|
val source = javaClass.classLoader.getResourceAsStream(name)!!.source().buffer()
|
||||||
return source.inputStream()
|
return source.inputStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
|
||||||
val contentType = responseBody?.contentType()
|
val contentType = responseBody?.contentType()
|
||||||
if (
|
if (
|
||||||
contentType != null &&
|
contentType != null &&
|
||||||
contentType.type().equals("application", true) &&
|
contentType.type.equals("application", true) &&
|
||||||
contentType.subtype().equals("json", true)
|
contentType.subtype.equals("json", true)
|
||||||
) {
|
) {
|
||||||
val error = SubsonicAPIClient.jacksonMapper.readValue<SubsonicResponse>(
|
val error = SubsonicAPIClient.jacksonMapper.readValue<SubsonicResponse>(
|
||||||
responseBody.byteStream()
|
responseBody.byteStream()
|
||||||
|
@ -40,11 +40,11 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
|
||||||
* It creates Exceptions from the results returned by the Subsonic API
|
* It creates Exceptions from the results returned by the Subsonic API
|
||||||
*/
|
*/
|
||||||
@Suppress("ThrowsCount")
|
@Suppress("ThrowsCount")
|
||||||
fun <T : SubsonicResponse> Response<out T>.throwOnFailure(): Response<out T> {
|
fun <T : SubsonicResponse> Response<T>.throwOnFailure(): Response<T> {
|
||||||
val response = this
|
val response = this
|
||||||
|
|
||||||
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
|
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
|
||||||
return this as Response<T>
|
return this
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw IOException("Server error, code: " + response.code())
|
throw IOException("Server error, code: " + response.code())
|
||||||
|
|
|
@ -8,6 +8,7 @@ import java.security.cert.X509Certificate
|
||||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
import okhttp3.Credentials
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
@ -68,12 +69,24 @@ class SubsonicAPIClient(
|
||||||
.addInterceptor { chain ->
|
.addInterceptor { chain ->
|
||||||
// Adds default request params
|
// Adds default request params
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
val newUrl = originalRequest.url().newBuilder()
|
val newUrl = originalRequest.url.newBuilder()
|
||||||
.addQueryParameter("u", config.username)
|
.addQueryParameter("u", config.username)
|
||||||
.addQueryParameter("c", config.clientID)
|
.addQueryParameter("c", config.clientID)
|
||||||
.addQueryParameter("f", "json")
|
.addQueryParameter("f", "json")
|
||||||
.build()
|
.build()
|
||||||
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
|
val newRequestBuilder = originalRequest.newBuilder().url(newUrl)
|
||||||
|
if (originalRequest.url.username.isNotEmpty() &&
|
||||||
|
originalRequest.url.password.isNotEmpty()
|
||||||
|
) {
|
||||||
|
newRequestBuilder.addHeader(
|
||||||
|
"Authorization",
|
||||||
|
Credentials.basic(
|
||||||
|
originalRequest.url.username,
|
||||||
|
originalRequest.url.password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
chain.proceed(newRequestBuilder.build())
|
||||||
}
|
}
|
||||||
.addInterceptor(versionInterceptor)
|
.addInterceptor(versionInterceptor)
|
||||||
.addInterceptor(proxyPasswordInterceptor)
|
.addInterceptor(proxyPasswordInterceptor)
|
||||||
|
@ -109,7 +122,7 @@ class SubsonicAPIClient(
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.addLogging() {
|
private fun OkHttpClient.Builder.addLogging() {
|
||||||
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
|
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
|
||||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
loggingInterceptor.level = HttpLoggingInterceptor.Level.HEADERS
|
||||||
this.addInterceptor(loggingInterceptor)
|
this.addInterceptor(loggingInterceptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class PasswordHexInterceptor(private val password: String) : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Chain): Response {
|
override fun intercept(chain: Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
val updatedUrl = originalRequest.url().newBuilder()
|
val updatedUrl = originalRequest.url.newBuilder()
|
||||||
.addEncodedQueryParameter("p", passwordHex).build()
|
.addEncodedQueryParameter("p", passwordHex).build()
|
||||||
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build())
|
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build())
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ class PasswordMD5Interceptor(private val password: String) : Interceptor {
|
||||||
override fun intercept(chain: Chain): Response {
|
override fun intercept(chain: Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
val salt = getSalt()
|
val salt = getSalt()
|
||||||
val updatedUrl = originalRequest.url().newBuilder()
|
val updatedUrl = originalRequest.url.newBuilder()
|
||||||
.addQueryParameter("t", getPasswordMD5Hash(salt))
|
.addQueryParameter("t", getPasswordMD5Hash(salt))
|
||||||
.addQueryParameter("s", salt)
|
.addQueryParameter("s", salt)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -19,7 +19,7 @@ internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
||||||
internal class RangeHeaderInterceptor : Interceptor {
|
internal class RangeHeaderInterceptor : Interceptor {
|
||||||
override fun intercept(chain: Chain): Response {
|
override fun intercept(chain: Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
val headers = originalRequest.headers()
|
val headers = originalRequest.headers
|
||||||
return if (headers.names().contains("Range")) {
|
return if (headers.names().contains("Range")) {
|
||||||
val offsetValue = headers["Range"] ?: "0"
|
val offsetValue = headers["Range"] ?: "0"
|
||||||
val offset = "bytes=$offsetValue-"
|
val offset = "bytes=$offsetValue-"
|
||||||
|
|
|
@ -18,7 +18,7 @@ internal class VersionInterceptor(
|
||||||
val newRequest = originalRequest.newBuilder()
|
val newRequest = originalRequest.newBuilder()
|
||||||
.url(
|
.url(
|
||||||
originalRequest
|
originalRequest
|
||||||
.url()
|
.url
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addQueryParameter("v", protocolVersion.restApiVersion)
|
.addQueryParameter("v", protocolVersion.restApiVersion)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -1,68 +1,26 @@
|
||||||
<?xml version="1.0" ?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<SmellBaseline>
|
<SmellBaseline>
|
||||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
<ManuallySuppressedIssues/>
|
||||||
<CurrentIssues>
|
<CurrentIssues>
|
||||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background</ID>
|
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
|
||||||
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
|
|
||||||
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
|
||||||
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
|
|
||||||
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
|
||||||
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
|
|
||||||
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
|
||||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
|
|
||||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
|
|
||||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song)</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>
|
||||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
|
|
||||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
|
|
||||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
|
|
||||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
|
|
||||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix)</ID>
|
|
||||||
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
|
||||||
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
|
||||||
<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:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</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:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
|
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
|
||||||
<ID>LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: 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>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
|
||||||
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
|
||||||
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateDisplay(refresh: Boolean)</ID>
|
|
||||||
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
|
||||||
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: 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:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000</ID>
|
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID>
|
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID>
|
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
|
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
|
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID>
|
|
||||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
|
|
||||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
|
|
||||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||||
<ID>MagicNumber:SongView.kt$SongView$3</ID>
|
|
||||||
<ID>MagicNumber:SongView.kt$SongView$4</ID>
|
|
||||||
<ID>MagicNumber:SongView.kt$SongView$60</ID>
|
|
||||||
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</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>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>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
|
||||||
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
|
||||||
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
|
||||||
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
|
|
||||||
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
||||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception</ID>
|
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable</ID>
|
||||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable</ID>
|
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
|
||||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
<ID>TooGenericExceptionThrown:Downloader.kt$Downloader.DownloadTask$throw RuntimeException( String.format( Locale.ROOT, "Download of '%s' was cancelled", downloadFile.track ) )</ID>
|
||||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
|
||||||
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
|
||||||
<ID>TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song))</ID>
|
|
||||||
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
|
||||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||||
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
|
||||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||||
</CurrentIssues>
|
</CurrentIssues>
|
||||||
</SmellBaseline>
|
</SmellBaseline>
|
||||||
|
|
|
@ -64,13 +64,10 @@ style:
|
||||||
WildcardImport:
|
WildcardImport:
|
||||||
active: true
|
active: true
|
||||||
MaxLineLength:
|
MaxLineLength:
|
||||||
active: true
|
active: false
|
||||||
maxLineLength: 120
|
|
||||||
excludePackageStatements: false
|
|
||||||
excludeImportStatements: false
|
|
||||||
MagicNumber:
|
MagicNumber:
|
||||||
# 100 common in percentage, 1000 in milliseconds
|
# 100 common in percentage, 1000 in milliseconds
|
||||||
ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024']
|
ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024', '4096']
|
||||||
ignoreEnums: true
|
ignoreEnums: true
|
||||||
ignorePropertyDeclaration: true
|
ignorePropertyDeclaration: true
|
||||||
UnnecessaryAbstractClass:
|
UnnecessaryAbstractClass:
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
Others
|
Others
|
||||||
|
|
||||||
- #671: Bump versions.mockito from 4.1.0 to 4.3.1.
|
- #671: Bump versions.mockito from 4.1.0 to 4.3.1.
|
||||||
- Update translations.
|
- Update translations.
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
Enhancements
|
Enhancements
|
||||||
|
|
||||||
- #683: Rewrite the about and remove the webview.
|
- #683: Rewrite the about and remove the webview.
|
||||||
- #685: Server coloring feature.
|
- #685: Server coloring feature.
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Bug fixes
|
||||||
|
- #688: Connection failure.
|
|
@ -0,0 +1,14 @@
|
||||||
|
Bug fixes
|
||||||
|
- #673: Disabling "Browse Using ID3 Tags" causes artist search result content to mismatch.
|
||||||
|
- #679: Keyboard should be hidden when navigating away from Search.
|
||||||
|
- #686: In landscape mode, the scroll bar is missing in the about text.
|
||||||
|
- #691: TrackCollectionFragment: fix play all button.
|
||||||
|
- #698: Track based context menus do not function correctly in most fragments.
|
||||||
|
|
||||||
|
Features
|
||||||
|
- #669: Option to change language.
|
||||||
|
|
||||||
|
Enhancements
|
||||||
|
- #654: Update OkHttp.
|
||||||
|
- #694: Reword alert for better help.
|
||||||
|
- #702: Show Downloads on Play.
|
|
@ -1 +0,0 @@
|
||||||
Refactor and redesign artist list
|
|
|
@ -1 +0,0 @@
|
||||||
Fall backs to path when comparing tracks and fixes #369
|
|
|
@ -1 +0,0 @@
|
||||||
Refactored the application main menu
|
|
|
@ -1 +0,0 @@
|
||||||
Refactored the application main menu
|
|
|
@ -9,7 +9,5 @@ Enhancements
|
||||||
- #568: Rework Downloader.
|
- #568: Rework Downloader.
|
||||||
- #567: Use semantically correct API endpoint when streaming/downloading.
|
- #567: Use semantically correct API endpoint when streaming/downloading.
|
||||||
- #572: Moved drag handle to the left in the Now Playing list.
|
- #572: Moved drag handle to the left in the Now Playing list.
|
||||||
- #585: Added setting to disable Now Playing List sending for incompatible
|
- #585: Added setting to disable Now Playing List sending for incompatible bluetooth devices.
|
||||||
bluetooth devices.
|
- #596: Added option whether to create a share on the server when sharing songs.
|
||||||
- #596: Added option whether to create a share on the server when sharing
|
|
||||||
songs.
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
Otros
|
Otros
|
||||||
|
|
||||||
- #671: Actualizado versions.mockito de 4.1.0 a 4.3.1.
|
- #671: Actualizado versions.mockito de 4.1.0 a 4.3.1.
|
||||||
- Traducciones actualizadas.
|
- Traducciones actualizadas.
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
Mejoras
|
Mejoras
|
||||||
|
|
||||||
- #683: Reescribir el acerca de y eliminar el webview.
|
- #683: Reescribir el acerca de y eliminar el webview.
|
||||||
- #685: Posibilidad de seleccionar el color del servidor.
|
- #685: Posibilidad de seleccionar el color del servidor.
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Corrección de errores
|
||||||
|
- #688: Fallo de conexión.
|
|
@ -0,0 +1,14 @@
|
||||||
|
Corrección de errores
|
||||||
|
- #673: La desactivación de la opción "Examinar mediante etiquetas ID3" hace que el contenido de los resultados de la búsqueda de artistas no coincida.
|
||||||
|
- #679: El teclado debería estar oculto cuando se navega fuera de la Búsqueda.
|
||||||
|
- #686: En modo apaisado, falta la barra de desplazamiento en el texto de acerca de.
|
||||||
|
- #691: TrackCollectionFragment: arreglar el botón de reproducir todo.
|
||||||
|
- #698: Los menús contextuales basados en pistas no funcionan correctamente en la mayoría de los fragmentos.
|
||||||
|
|
||||||
|
Características
|
||||||
|
- #669: Opción de cambio de idioma.
|
||||||
|
|
||||||
|
Mejoras
|
||||||
|
- #654: Actualización de OkHttp.
|
||||||
|
- #694: Reformular la alerta para mejorar la ayuda.
|
||||||
|
- #702: Mostrar descargas al reproducir.
|
|
@ -1 +0,0 @@
|
||||||
Refactorizada y rediseñada la lista de artistas
|
|
|
@ -1 +0,0 @@
|
||||||
Cuando un fichero no tiene etiquetas de número de pista lo ordena por orden alfabético. Además hemos arreglado el bug #369
|
|
|
@ -1 +0,0 @@
|
||||||
Refactorizado el menú principal de la aplicación
|
|
|
@ -1 +0,0 @@
|
||||||
Refactorizado el menú principal de la aplicación
|
|
|
@ -1,6 +1,5 @@
|
||||||
Correción de errores
|
Correción de errores
|
||||||
- #594: Agregado un intent de PlaybackComplete cuando se completa la
|
- #594: Agregado un intent de PlaybackComplete cuando se completa la reproducción de una canción.
|
||||||
reproducción de una canción.
|
|
||||||
- #593: Corregidas las listas de álbumes.
|
- #593: Corregidas las listas de álbumes.
|
||||||
- #602: NPE corregido.
|
- #602: NPE corregido.
|
||||||
|
|
||||||
|
@ -8,11 +7,7 @@ Mejoras
|
||||||
- #558: La llamada a video puede ser estática.
|
- #558: La llamada a video puede ser estática.
|
||||||
- #559: Agregado un mejor soporte sin conexión.
|
- #559: Agregado un mejor soporte sin conexión.
|
||||||
- #568: Se ha reescrito el downloader.
|
- #568: Se ha reescrito el downloader.
|
||||||
- #567: Se utiliza el endpoint semánticamente correcto al realizar streaming
|
- #567: Se utiliza el endpoint semánticamente correcto al realizar streaming o descargar.
|
||||||
o descargar.
|
- #572: Se ha movido el botón de arrastre de canción hacia la izquierda en la lista de reproducción.
|
||||||
- #572: Se ha movido el botón de arrastre de canción hacia la izquierda en
|
- #585: Agregada una configuración para deshabilitar el envío de la Lista de reproducción en curso para dispositivos Bluetooth incompatibles.
|
||||||
la lista de reproducción.
|
- #596: Se agregó la opción de crear un recurso compartido en el servidor al compartir canciones.
|
||||||
- #585: Agregada una configuración para deshabilitar el envío de la Lista de
|
|
||||||
reproducción en curso para dispositivos Bluetooth incompatibles.
|
|
||||||
- #596: Se agregó la opción de crear un recurso compartido en el servidor al
|
|
||||||
compartir canciones.
|
|
||||||
|
|
|
@ -2,16 +2,13 @@ Corrección de errores
|
||||||
- #609: Comportamiento extraño de scrobbling (offset).
|
- #609: Comportamiento extraño de scrobbling (offset).
|
||||||
|
|
||||||
Mejoras
|
Mejoras
|
||||||
- #599: Se ha movido el selector de servidor y la configuración al menú de
|
- #599: Se ha movido el selector de servidor y la configuración al menú de navegación.
|
||||||
navegación.
|
- #600: Migración de la utilidad de permisos a Kotlin, aumento del SDK mínimo a 17.
|
||||||
- #600: Migración de la utilidad de permisos a Kotlin, aumento del SDK
|
|
||||||
mínimo a 17.
|
|
||||||
- #604: Implementar una vista de Descarga.
|
- #604: Implementar una vista de Descarga.
|
||||||
- #613: targetSdkVersion debe ser 30 o superior.
|
- #613: targetSdkVersion debe ser 30 o superior.
|
||||||
- #622: Refactorización de eventos.
|
- #622: Refactorización de eventos.
|
||||||
- #641: Eliminar el almacenamiento de funciones.
|
- #641: Eliminar el almacenamiento de funciones.
|
||||||
- #642: Eliminar MergeAdapter y SackOfViewsAdapter.
|
- #642: Eliminar MergeAdapter y SackOfViewsAdapter.
|
||||||
- #649: Unificar el manejo del diálogo de error.
|
- #649: Unificar el manejo del diálogo de error.
|
||||||
- #652: Manejo de ubicación de caché personalizado actualizado para eliminar
|
- #652: Manejo de ubicación de caché personalizado actualizado para eliminar isUri.
|
||||||
isUri.
|
|
||||||
- #662: Mejorar las migraciones de bases de datos.
|
- #662: Mejorar las migraciones de bases de datos.
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
[versions]
|
[versions]
|
||||||
# You need to run ./gradlew wrapper after updating the version
|
# You need to run ./gradlew wrapper after updating the version
|
||||||
gradle = "7.3.2"
|
gradle = "7.3.3"
|
||||||
|
|
||||||
navigation = "2.3.5"
|
navigation = "2.3.5"
|
||||||
gradlePlugin = "7.0.4"
|
gradlePlugin = "7.2.1"
|
||||||
androidxcore = "1.6.0"
|
androidxcore = "1.6.0"
|
||||||
ktlint = "0.43.2"
|
ktlint = "0.43.2"
|
||||||
ktlintGradle = "10.2.0"
|
ktlintGradle = "10.2.0"
|
||||||
detekt = "1.19.0"
|
detekt = "1.19.0"
|
||||||
jacoco = "0.8.7"
|
|
||||||
preferences = "1.1.1"
|
preferences = "1.1.1"
|
||||||
media = "1.3.1"
|
media = "1.3.1"
|
||||||
|
media3 = "1.0.0-beta01"
|
||||||
|
|
||||||
androidSupport = "28.0.0"
|
androidSupport = "1.4.0"
|
||||||
androidLegacySupport = "1.0.0"
|
androidLegacySupport = "1.0.0"
|
||||||
androidSupportDesign = "1.4.0"
|
androidSupportDesign = "1.6.1"
|
||||||
constraintLayout = "2.1.1"
|
constraintLayout = "2.1.1"
|
||||||
multidex = "2.0.1"
|
multidex = "2.0.1"
|
||||||
room = "2.4.0"
|
room = "2.4.2"
|
||||||
kotlin = "1.6.10"
|
kotlin = "1.6.10"
|
||||||
kotlinxCoroutines = "1.6.0-native-mt"
|
kotlinxCoroutines = "1.6.0-native-mt"
|
||||||
viewModelKtx = "2.3.0"
|
kotlinxGuava = "1.6.0"
|
||||||
|
viewModelKtx = "2.4.1"
|
||||||
|
|
||||||
retrofit = "2.6.4"
|
retrofit = "2.9.0"
|
||||||
jackson = "2.9.5"
|
jackson = "2.10.1"
|
||||||
okhttp = "3.12.13"
|
okhttp = "4.9.1"
|
||||||
koin = "3.0.2"
|
koin = "3.0.2"
|
||||||
picasso = "2.71828"
|
picasso = "2.71828"
|
||||||
|
|
||||||
|
@ -47,12 +48,11 @@ gradle = { module = "com.android.tools.build:gradle", version.r
|
||||||
kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
|
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
|
||||||
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
|
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
|
||||||
jacoco = { module = "org.jacoco:org.jacoco.core", version.ref = "jacoco" }
|
|
||||||
|
|
||||||
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
|
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
|
||||||
support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" }
|
support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" }
|
||||||
design = { module = "com.google.android.material:material", version.ref = "androidSupportDesign" }
|
design = { module = "com.google.android.material:material", version.ref = "androidSupportDesign" }
|
||||||
annotations = { module = "com.android.support:support-annotations", version.ref = "androidSupport" }
|
annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
|
||||||
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
|
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
|
||||||
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" }
|
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" }
|
||||||
room = { module = "androidx.room:room-compiler", version.ref = "room" }
|
room = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
|
@ -66,10 +66,14 @@ navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", ve
|
||||||
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
|
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
|
||||||
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
|
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
|
||||||
media = { module = "androidx.media:media", version.ref = "media" }
|
media = { module = "androidx.media:media", version.ref = "media" }
|
||||||
|
media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||||
|
media3okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
|
||||||
|
media3session = { module = "androidx.media3:media3-session", version.ref = "media3" }
|
||||||
|
|
||||||
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||||
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||||
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
|
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
|
||||||
|
kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxGuava"}
|
||||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
|
gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
|
||||||
jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" }
|
jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" }
|
||||||
|
@ -97,4 +101,3 @@ kluentAndroid = { module = "org.amshove.kluent:kluent-android", versio
|
||||||
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
||||||
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
||||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
ext.versions = [
|
ext.versions = [
|
||||||
minSdk : 21,
|
minSdk : 21,
|
||||||
targetSdk : 30,
|
targetSdk : 33,
|
||||||
compileSdk : 31,
|
compileSdk : 31,
|
||||||
]
|
]
|
|
@ -1,5 +1,6 @@
|
||||||
|
#Fri Jun 17 23:13:49 CEST 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'jacoco'
|
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -48,10 +47,6 @@ android {
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test) {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
jacoco {
|
|
||||||
includeNoLocationClasses = true
|
|
||||||
excludes += jacocoExclude
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -61,11 +56,4 @@ dependencies {
|
||||||
testRuntimeOnly libs.junitVintage
|
testRuntimeOnly libs.junitVintage
|
||||||
}
|
}
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
ext {
|
|
||||||
jacocoExclude = ['jdk.internal.*']
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
apply plugin: 'jacoco'
|
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
def mergedJacocoExec = file("${project.buildDir}/jacoco/jacocoMerged.exec")
|
|
||||||
|
|
||||||
def merge = tasks.register('jacocoMergeReports', JacocoMerge) {
|
|
||||||
group = "Reporting"
|
|
||||||
description = "Merge all jacoco reports from projects into one."
|
|
||||||
|
|
||||||
ListProperty<File> jacocoFiles = project.objects.listProperty(File.class)
|
|
||||||
project.subprojects { subproject ->
|
|
||||||
subproject.plugins.withId("jacoco") {
|
|
||||||
project.logger.info("${subproject.name} has Jacoco plugin applied")
|
|
||||||
subproject.tasks.withType(Test) { task ->
|
|
||||||
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
|
|
||||||
if (destFile.exists() && !task.name.contains("Release")) {
|
|
||||||
jacocoFiles.add(destFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executionData(jacocoFiles)
|
|
||||||
destinationFile(mergedJacocoExec)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('jacocoFullReport', JacocoReport) {
|
|
||||||
dependsOn merge
|
|
||||||
group = "Reporting"
|
|
||||||
description = "Generate full Jacoco coverage report including all modules."
|
|
||||||
|
|
||||||
getClassDirectories().setFrom(files())
|
|
||||||
getSourceDirectories().setFrom(files())
|
|
||||||
getExecutionData().setFrom(files())
|
|
||||||
|
|
||||||
reports {
|
|
||||||
xml.enabled = true
|
|
||||||
html.enabled = true
|
|
||||||
csv.enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always run merging, as all input calculation is done in doFirst {}
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
// Task will run anyway even if initial inputs are empty
|
|
||||||
onlyIf = { true }
|
|
||||||
|
|
||||||
project.subprojects { subproject ->
|
|
||||||
subproject.plugins.withId("jacoco") {
|
|
||||||
project.logger.info("${subproject.name} has Jacoco plugin applied")
|
|
||||||
subproject.plugins.withId("kotlin-android") {
|
|
||||||
project.logger.info("${subproject.name} is android project")
|
|
||||||
def mainSources = subproject.extensions.findByName("android").sourceSets['main']
|
|
||||||
project.logger.info("Android sources: ${mainSources.java.srcDirs}")
|
|
||||||
mainSources.java.srcDirs.forEach {
|
|
||||||
additionalSourceDirs(it)
|
|
||||||
}
|
|
||||||
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
|
|
||||||
additionalClassDirs(fileTree(
|
|
||||||
dir: "${subproject.buildDir}/tmp/kotlin-classes/debug",
|
|
||||||
excludes: subproject.jacocoExclude
|
|
||||||
))
|
|
||||||
}
|
|
||||||
subproject.plugins.withId("kotlin") { plugin ->
|
|
||||||
project.logger.info("${subproject.name} is common kotlin project")
|
|
||||||
SourceDirectorySet mainSources = subproject.extensions.getByName("kotlin")
|
|
||||||
.sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
|
|
||||||
.kotlin
|
|
||||||
mainSources.srcDirs.forEach {
|
|
||||||
project.logger.debug("Adding sources: $it")
|
|
||||||
additionalSourceDirs(it)
|
|
||||||
}
|
|
||||||
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
|
|
||||||
additionalClassDirs(fileTree(
|
|
||||||
dir: "${subproject.buildDir}/classes/kotlin/main",
|
|
||||||
excludes: subproject.jacocoExclude
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
subproject.tasks.withType(Test) { task ->
|
|
||||||
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
|
|
||||||
if (destFile.exists() && !task.name.contains("Release")) {
|
|
||||||
project.logger.info("Adding execution data: $destFile")
|
|
||||||
executionData(destFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'jacoco'
|
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@ -21,36 +20,8 @@ dependencies {
|
||||||
testRuntimeOnly libs.junitVintage
|
testRuntimeOnly libs.junitVintage
|
||||||
}
|
}
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
ext {
|
|
||||||
// override it in the module
|
|
||||||
jacocoExclude = ['jdk.internal.*']
|
|
||||||
}
|
|
||||||
|
|
||||||
jacocoTestReport {
|
|
||||||
reports {
|
|
||||||
html.required = true
|
|
||||||
xml.required = false
|
|
||||||
csv.required = false
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEvaluate {
|
|
||||||
getClassDirectories().setFrom(files(classDirectories.files.collect {
|
|
||||||
fileTree(dir: it, excludes: jacocoExclude)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("test").configure {
|
tasks.named("test").configure {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
jacoco {
|
|
||||||
excludes += jacocoExclude
|
|
||||||
includeNoLocationClasses = true
|
|
||||||
}
|
|
||||||
finalizedBy jacocoTestReport
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("ciTest") {
|
tasks.register("ciTest") {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'jacoco'
|
|
||||||
apply from: "../gradle_scripts/code_quality.gradle"
|
apply from: "../gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -9,15 +8,16 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.moire.ultrasonic"
|
applicationId "org.moire.ultrasonic"
|
||||||
versionCode 101
|
versionCode 103
|
||||||
versionName "3.1.0"
|
versionName "3.2.0"
|
||||||
|
|
||||||
minSdkVersion versions.minSdk
|
minSdkVersion versions.minSdk
|
||||||
targetSdkVersion versions.targetSdk
|
targetSdkVersion versions.targetSdk
|
||||||
|
resConfigs 'cs', 'de', 'en', 'es', 'fr', 'hu', 'it', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW'
|
||||||
resConfigs "cs", "de", "en", "es", "fr", "hu", "it", "nl", "pl", "pt", "pt-rBR", "ru", "zh-rCN", "zh-rTW"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bundle.language.enableSplit = false
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
|
@ -40,20 +40,12 @@ android {
|
||||||
main.java.srcDirs += "${projectDir}/src/main/kotlin"
|
main.java.srcDirs += "${projectDir}/src/main/kotlin"
|
||||||
test.java.srcDirs += "${projectDir}/src/test/kotlin"
|
test.java.srcDirs += "${projectDir}/src/test/kotlin"
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude 'META-INF/LICENSE'
|
resources {
|
||||||
|
excludes += ['META-INF/LICENSE']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
baselineFile file("lint-baseline.xml")
|
|
||||||
ignore 'MissingTranslation'
|
|
||||||
ignore 'UnusedQuantity'
|
|
||||||
warning 'ImpliedQuantity'
|
|
||||||
disable 'IconMissingDensityFolder', "VectorPath"
|
|
||||||
abortOnError true
|
|
||||||
warningsAsErrors true
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
|
@ -71,9 +63,18 @@ android {
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
arg("room.schemaLocation", "$buildDir/schemas".toString())
|
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lint {
|
||||||
|
baseline = file("lint-baseline.xml")
|
||||||
|
abortOnError true
|
||||||
|
warningsAsErrors true
|
||||||
|
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||||
|
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||||
|
warning 'ImpliedQuantity'
|
||||||
|
disable 'ObsoleteLintCustomCheck'
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +100,9 @@ dependencies {
|
||||||
implementation libs.constraintLayout
|
implementation libs.constraintLayout
|
||||||
implementation libs.preferences
|
implementation libs.preferences
|
||||||
implementation libs.media
|
implementation libs.media
|
||||||
|
implementation libs.media3exoplayer
|
||||||
|
implementation libs.media3session
|
||||||
|
implementation libs.media3okhttp
|
||||||
|
|
||||||
implementation libs.navigationFragment
|
implementation libs.navigationFragment
|
||||||
implementation libs.navigationUi
|
implementation libs.navigationUi
|
||||||
|
@ -108,6 +112,7 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.kotlinStdlib
|
implementation libs.kotlinStdlib
|
||||||
implementation libs.kotlinxCoroutines
|
implementation libs.kotlinxCoroutines
|
||||||
|
implementation libs.kotlinxGuava
|
||||||
implementation libs.koinAndroid
|
implementation libs.koinAndroid
|
||||||
implementation libs.okhttpLogging
|
implementation libs.okhttpLogging
|
||||||
implementation libs.fastScroll
|
implementation libs.fastScroll
|
||||||
|
@ -130,36 +135,3 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.timber
|
implementation libs.timber
|
||||||
}
|
}
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Excluding all java classes and stuff that should not be covered
|
|
||||||
ext {
|
|
||||||
jacocoExclude = [
|
|
||||||
'**/activity/**',
|
|
||||||
'**/audiofx/**',
|
|
||||||
'**/fragment/**',
|
|
||||||
'**/provider/**',
|
|
||||||
'**/receiver/**',
|
|
||||||
'**/service/**',
|
|
||||||
'**/Test/**',
|
|
||||||
'**/util/**',
|
|
||||||
'**/view/**',
|
|
||||||
'**/R$*.class',
|
|
||||||
'**/R.class',
|
|
||||||
'**/BuildConfig.class',
|
|
||||||
'**/di/**',
|
|
||||||
'jdk.internal.*'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(Test) {
|
|
||||||
jacoco.includeNoLocationClasses = true
|
|
||||||
jacoco.excludes += jacocoExclude
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,37 +1,15 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<issues format="6" by="lint 7.0.4" type="baseline" client="gradle" name="AGP (7.0.4)" variant="all" version="7.0.4">
|
<issues format="6" by="lint 7.2.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.2.1)" variant="all" version="7.2.1">
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="InflateParams"
|
id="InflateParams"
|
||||||
message="Avoid passing `null` as the view root (needed to resolve layout parameters on the inflated layout's root element)"
|
message="Avoid passing `null` as the view root (needed to resolve layout parameters on the inflated layout's root element)"
|
||||||
errorLine1=" View view = inflater.inflate(R.layout.jukebox_volume, null);"
|
errorLine1=" val view = inflater.inflate(R.layout.jukebox_volume, null)"
|
||||||
errorLine2=" ~~~~">
|
errorLine2=" ~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java"
|
file="src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt"
|
||||||
line="477"
|
line="331"
|
||||||
column="58"/>
|
column="66"/>
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="Typos"
|
|
||||||
message=""lizensiert" is a common misspelling; did you mean "lizenziert" ?"
|
|
||||||
errorLine1=" <string name="settings.testing_unlicensed">Verbindung OK, Server nicht lizensiert.</string>"
|
|
||||||
errorLine2=" ^">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values-de/strings.xml"
|
|
||||||
line="289"
|
|
||||||
column="76"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="PluralsCandidate"
|
|
||||||
message="Formatting %d followed by words ("Artists"): This should probably be a plural rather than a string"
|
|
||||||
errorLine1=" <string name="parser.artist_count">Got %d Artists.</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="134"
|
|
||||||
column="5"/>
|
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
|
@ -41,7 +19,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="151"
|
line="154"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -56,17 +34,6 @@
|
||||||
column="73"/>
|
column="73"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="SetJavaScriptEnabled"
|
|
||||||
message="Using `setJavaScriptEnabled` can introduce XSS vulnerabilities into your application, review carefully"
|
|
||||||
errorLine1=" webView.getSettings().setJavaScriptEnabled(true);"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/fragment/AboutFragment.java"
|
|
||||||
line="51"
|
|
||||||
column="9"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="TrustAllX509TrustManager"
|
id="TrustAllX509TrustManager"
|
||||||
message="`checkClientTrusted` is empty, which could cause insecure network traffic due to trusting arbitrary TLS/SSL certificates presented by peers">
|
message="`checkClientTrusted` is empty, which could cause insecure network traffic due to trusting arbitrary TLS/SSL certificates presented by peers">
|
||||||
|
@ -88,197 +55,32 @@
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
line="146"
|
line="155"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="ExportedReceiver"
|
id="ExportedReceiver"
|
||||||
message="Exported receiver does not require permission"
|
message="Exported receiver does not require permission"
|
||||||
errorLine1=" <receiver android:name=".receiver.UltrasonicIntentReceiver">"
|
errorLine1=" <receiver android:name=".receiver.UltrasonicIntentReceiver""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
line="81"
|
line="79"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="IntentFilterExportedReceiver"
|
id="ExportedService"
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise. For launcher activities, this should be set to `true`."
|
message="Exported service does not require permission"
|
||||||
errorLine1=" <activity android:name=".activity.NavigationActivity""
|
errorLine1=" <service android:name=".playback.PlaybackService""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
line="41"
|
line="68"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="IntentFilterExportedReceiver"
|
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise."
|
|
||||||
errorLine1=" <receiver android:name=".receiver.MediaButtonIntentReceiver">"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="76"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="IntentFilterExportedReceiver"
|
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise."
|
|
||||||
errorLine1=" <receiver android:name=".receiver.UltrasonicIntentReceiver">"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="81"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="IntentFilterExportedReceiver"
|
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise."
|
|
||||||
errorLine1=" <receiver android:name=".receiver.BluetoothIntentReceiver">"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="93"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="IntentFilterExportedReceiver"
|
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise."
|
|
||||||
errorLine1=" <receiver"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="101"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="IntentFilterExportedReceiver"
|
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise."
|
|
||||||
errorLine1=" <receiver"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="112"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="IntentFilterExportedReceiver"
|
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise."
|
|
||||||
errorLine1=" <receiver"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="123"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="IntentFilterExportedReceiver"
|
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise."
|
|
||||||
errorLine1=" <receiver"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="134"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnspecifiedImmutableFlag"
|
|
||||||
message="Missing `PendingIntent` mutability flag"
|
|
||||||
errorLine1=" return PendingIntent.getActivity(this, 0, intent, flags)"
|
|
||||||
errorLine2=" ~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt"
|
|
||||||
line="708"
|
|
||||||
column="59"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnspecifiedImmutableFlag"
|
|
||||||
message="Missing `PendingIntent` mutability flag"
|
|
||||||
errorLine1=" PendingIntent.FLAG_CANCEL_CURRENT"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt"
|
|
||||||
line="323"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnspecifiedImmutableFlag"
|
|
||||||
message="Missing `PendingIntent` mutability flag"
|
|
||||||
errorLine1=" PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT);"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java"
|
|
||||||
line="198"
|
|
||||||
column="80"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnspecifiedImmutableFlag"
|
|
||||||
message="Missing `PendingIntent` mutability flag"
|
|
||||||
errorLine1=" pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0);"
|
|
||||||
errorLine2=" ~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java"
|
|
||||||
line="206"
|
|
||||||
column="67"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnspecifiedImmutableFlag"
|
|
||||||
message="Missing `PendingIntent` mutability flag"
|
|
||||||
errorLine1=" pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0);"
|
|
||||||
errorLine2=" ~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java"
|
|
||||||
line="212"
|
|
||||||
column="67"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnspecifiedImmutableFlag"
|
|
||||||
message="Missing `PendingIntent` mutability flag"
|
|
||||||
errorLine1=" pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0);"
|
|
||||||
errorLine2=" ~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java"
|
|
||||||
line="218"
|
|
||||||
column="67"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnspecifiedImmutableFlag"
|
|
||||||
message="Missing `PendingIntent` mutability flag"
|
|
||||||
errorLine1=" return PendingIntent.getBroadcast(context, requestCode, intent, flags)"
|
|
||||||
errorLine2=" ~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/kotlin/org/moire/ultrasonic/util/Util.kt"
|
|
||||||
line="891"
|
|
||||||
column="73"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="NotifyDataSetChanged"
|
|
||||||
message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
|
|
||||||
errorLine1=" viewAdapter.notifyDataSetChanged()"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt"
|
|
||||||
line="908"
|
|
||||||
column="21"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="ObsoleteLayoutParam"
|
id="ObsoleteLayoutParam"
|
||||||
message="Invalid layout param in a `LinearLayout`: `layout_above`"
|
message="Invalid layout param in a `LinearLayout`: `layout_above`"
|
||||||
|
@ -345,6 +147,17 @@
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="UnusedResources"
|
||||||
|
message="The resource `R.drawable.ic_baseline_close` appears to be unused"
|
||||||
|
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||||
|
errorLine2="^">
|
||||||
|
<location
|
||||||
|
file="src/main/res/drawable/ic_baseline_close.xml"
|
||||||
|
line="1"
|
||||||
|
column="1"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnusedResources"
|
id="UnusedResources"
|
||||||
message="The resource `R.drawable.ic_menu_arrow` appears to be unused"
|
message="The resource `R.drawable.ic_menu_arrow` appears to be unused"
|
||||||
|
@ -358,189 +171,57 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnusedResources"
|
id="UnusedResources"
|
||||||
message="The resource `R.drawable.menu_arrow` appears to be unused"
|
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
|
||||||
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
errorLine1="<vector android:height="48dp""
|
||||||
errorLine2="^">
|
errorLine2="^">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/drawable/menu_arrow.xml"
|
file="src/main/res/drawable/media3_notification_pause.xml"
|
||||||
line="1"
|
line="1"
|
||||||
column="1"/>
|
column="1"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnusedResources"
|
id="UnusedResources"
|
||||||
message="The resource `R.string.main_shuffle` appears to be unused"
|
message="The resource `R.drawable.media3_notification_play` appears to be unused"
|
||||||
errorLine1=" <string name="main.shuffle">Shuffle Play</string>"
|
errorLine1="<vector android:height="48dp""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2="^">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/drawable/media3_notification_play.xml"
|
||||||
line="114"
|
line="1"
|
||||||
column="13"/>
|
column="1"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnusedResources"
|
id="UnusedResources"
|
||||||
message="The resource `R.string.menu_navigation` appears to be unused"
|
message="The resource `R.drawable.media3_notification_seek_to_next` appears to be unused"
|
||||||
errorLine1=" <string name="menu.navigation">Navigation</string>"
|
errorLine1="<vector android:height="32dp""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2="^">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/drawable/media3_notification_seek_to_next.xml"
|
||||||
line="128"
|
line="1"
|
||||||
column="13"/>
|
column="1"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnusedResources"
|
id="UnusedResources"
|
||||||
message="The resource `R.string.music_service_retry` appears to be unused"
|
message="The resource `R.drawable.media3_notification_seek_to_previous` appears to be unused"
|
||||||
errorLine1=" <string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string>"
|
errorLine1="<vector android:height="32dp""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2="^">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/drawable/media3_notification_seek_to_previous.xml"
|
||||||
line="133"
|
line="1"
|
||||||
column="13"/>
|
column="1"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnusedResources"
|
id="UnusedResources"
|
||||||
message="The resource `R.string.parser_artist_count` appears to be unused"
|
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
|
||||||
errorLine1=" <string name="parser.artist_count">Got %d Artists.</string>"
|
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2="^">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/drawable/media3_notification_small_icon.xml"
|
||||||
line="134"
|
line="1"
|
||||||
column="13"/>
|
column="1"/>
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.parser_reading` appears to be unused"
|
|
||||||
errorLine1=" <string name="parser.reading">Reading from server.</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="135"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.parser_reading_done` appears to be unused"
|
|
||||||
errorLine1=" <string name="parser.reading_done">Reading from server. Done!</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="136"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.progress_wait` appears to be unused"
|
|
||||||
errorLine1=" <string name="progress.wait">Please wait&#8230;</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="141"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.search_search` appears to be unused"
|
|
||||||
errorLine1=" <string name="search.search">Click to search</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="147"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.service_connecting` appears to be unused"
|
|
||||||
errorLine1=" <string name="service.connecting">Contacting server, please wait.</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="159"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.settings_allow_self_signed_certificate` appears to be unused"
|
|
||||||
errorLine1=" <string name="settings.allow_self_signed_certificate" translatable="false">allowSelfSignedCertificate</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="160"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.settings_enable_ldap_user_support` appears to be unused"
|
|
||||||
errorLine1=" <string name="settings.enable_ldap_user_support" translatable="false">enableLdapUserSupport</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="161"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.settings_invalid_username` appears to be unused"
|
|
||||||
errorLine1=" <string name="settings.invalid_username">Please specify a valid username (no trailing spaces).</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="230"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.settings_server_remove_server` appears to be unused"
|
|
||||||
errorLine1=" <string name="settings.server_remove_server">Remove Server</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="299"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.settings_server_unused` appears to be unused"
|
|
||||||
errorLine1=" <string name="settings.server_unused">Unused</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="302"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.string.settings_server_address_unset` appears to be unused"
|
|
||||||
errorLine1=" <string name="settings.server_address_unset" translatable="false">http://example.com</string>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="387"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnusedResources"
|
|
||||||
message="The resource `R.plurals.select_album_donate_dialog_n_trial_days_left` appears to be unused"
|
|
||||||
errorLine1=" <plurals name="select_album_donate_dialog_n_trial_days_left">"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/values/strings.xml"
|
|
||||||
line="447"
|
|
||||||
column="14"/>
|
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
|
@ -838,39 +519,6 @@
|
||||||
column="6"/>
|
column="6"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1=" <ImageView a:id="@+id/help_back""
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/help.xml"
|
|
||||||
line="12"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1=" <ImageView a:id="@+id/help_stop""
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/help.xml"
|
|
||||||
line="18"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1=" <ImageView a:id="@+id/help_forward""
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/help.xml"
|
|
||||||
line="24"
|
|
||||||
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`"
|
||||||
|
@ -1025,17 +673,6 @@
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="RelativeOverlap"
|
|
||||||
message="`LinearLayout-3` can overlap `LinearLayout-1` if LinearLayout-1, LinearLayout-3 grow due to localized text expansion"
|
|
||||||
errorLine1=" <LinearLayout"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/player_media_info.xml"
|
|
||||||
line="52"
|
|
||||||
column="6"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="RelativeOverlap"
|
id="RelativeOverlap"
|
||||||
message="`@id/current_playing_duration` can overlap `@id/current_playing_position` if @string/util.no_time, @string/util.no_time grow due to localized text expansion"
|
message="`@id/current_playing_duration` can overlap `@id/current_playing_position` if @string/util.no_time, @string/util.no_time grow due to localized text expansion"
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "4cea788a99b9bc28500948b1cd92e537",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "ServerSetting",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "color",
|
||||||
|
"columnName": "color",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userName",
|
||||||
|
"columnName": "userName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "jukeboxByDefault",
|
||||||
|
"columnName": "jukeboxByDefault",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "allowSelfSignedCertificate",
|
||||||
|
"columnName": "allowSelfSignedCertificate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ldapSupport",
|
||||||
|
"columnName": "ldapSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "musicFolderId",
|
||||||
|
"columnName": "musicFolderId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "minimumApiVersion",
|
||||||
|
"columnName": "minimumApiVersion",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chatSupport",
|
||||||
|
"columnName": "chatSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkSupport",
|
||||||
|
"columnName": "bookmarkSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shareSupport",
|
||||||
|
"columnName": "shareSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "podcastSupport",
|
||||||
|
"columnName": "podcastSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4cea788a99b9bc28500948b1cd92e537')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "0580217b1e87b02d2edaf9b008891cbc",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "artists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "indexes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "musicFolderId",
|
||||||
|
"columnName": "musicFolderId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "music_folders",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0580217b1e87b02d2edaf9b008891cbc')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,474 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "b6ac795e7857eac4fed2dbbd01f80fb8",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "artists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "albums",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `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`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parent",
|
||||||
|
"columnName": "parent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "album",
|
||||||
|
"columnName": "album",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "discNumber",
|
||||||
|
"columnName": "discNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songCount",
|
||||||
|
"columnName": "songCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "created",
|
||||||
|
"columnName": "created",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "artist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "genre",
|
||||||
|
"columnName": "genre",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isDirectory",
|
||||||
|
"columnName": "isDirectory",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVideo",
|
||||||
|
"columnName": "isVideo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tracks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `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`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parent",
|
||||||
|
"columnName": "parent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isDirectory",
|
||||||
|
"columnName": "isDirectory",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "album",
|
||||||
|
"columnName": "album",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumId",
|
||||||
|
"columnName": "albumId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "artist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "track",
|
||||||
|
"columnName": "track",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "genre",
|
||||||
|
"columnName": "genre",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentType",
|
||||||
|
"columnName": "contentType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "suffix",
|
||||||
|
"columnName": "suffix",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "transcodedContentType",
|
||||||
|
"columnName": "transcodedContentType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "transcodedSuffix",
|
||||||
|
"columnName": "transcodedSuffix",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songCount",
|
||||||
|
"columnName": "songCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bitRate",
|
||||||
|
"columnName": "bitRate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVideo",
|
||||||
|
"columnName": "isVideo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "discNumber",
|
||||||
|
"columnName": "discNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "created",
|
||||||
|
"columnName": "created",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkPosition",
|
||||||
|
"columnName": "bookmarkPosition",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userRating",
|
||||||
|
"columnName": "userRating",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "averageRating",
|
||||||
|
"columnName": "averageRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "indexes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "musicFolderId",
|
||||||
|
"columnName": "musicFolderId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "music_folders",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b6ac795e7857eac4fed2dbbd01f80fb8')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,514 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "95e83d6663a862c03ac46f9567453ded",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "artists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "albums",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parent",
|
||||||
|
"columnName": "parent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "album",
|
||||||
|
"columnName": "album",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "discNumber",
|
||||||
|
"columnName": "discNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songCount",
|
||||||
|
"columnName": "songCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "created",
|
||||||
|
"columnName": "created",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "artist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "genre",
|
||||||
|
"columnName": "genre",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isDirectory",
|
||||||
|
"columnName": "isDirectory",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVideo",
|
||||||
|
"columnName": "isVideo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tracks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parent",
|
||||||
|
"columnName": "parent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isDirectory",
|
||||||
|
"columnName": "isDirectory",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "album",
|
||||||
|
"columnName": "album",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumId",
|
||||||
|
"columnName": "albumId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "artist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "track",
|
||||||
|
"columnName": "track",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "genre",
|
||||||
|
"columnName": "genre",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentType",
|
||||||
|
"columnName": "contentType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "suffix",
|
||||||
|
"columnName": "suffix",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "transcodedContentType",
|
||||||
|
"columnName": "transcodedContentType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "transcodedSuffix",
|
||||||
|
"columnName": "transcodedSuffix",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songCount",
|
||||||
|
"columnName": "songCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bitRate",
|
||||||
|
"columnName": "bitRate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVideo",
|
||||||
|
"columnName": "isVideo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "discNumber",
|
||||||
|
"columnName": "discNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "created",
|
||||||
|
"columnName": "created",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkPosition",
|
||||||
|
"columnName": "bookmarkPosition",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userRating",
|
||||||
|
"columnName": "userRating",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "averageRating",
|
||||||
|
"columnName": "averageRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "indexes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "musicFolderId",
|
||||||
|
"columnName": "musicFolderId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "music_folders",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT NOT NULL, PRIMARY KEY(`id`, `serverId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '95e83d6663a862c03ac46f9567453ded')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/NoActionBar"
|
android:theme="@style/NoActionBar"
|
||||||
|
@ -40,8 +42,8 @@
|
||||||
|
|
||||||
<activity android:name=".activity.NavigationActivity"
|
<activity android:name=".activity.NavigationActivity"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
android:label="@string/common.appname"
|
android:launchMode="singleTask"
|
||||||
android:launchMode="singleTask">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.SEARCH"/>
|
<action android:name="android.intent.action.SEARCH"/>
|
||||||
|
@ -57,28 +59,25 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.MediaPlayerService"
|
android:name=".service.DownloadService"
|
||||||
android:label="Ultrasonic Media Player Service"
|
android:label="Ultrasonic Media Player Service"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
||||||
tools:ignore="ExportedService"
|
<service android:name=".playback.PlaybackService"
|
||||||
android:name=".service.AutoMediaBrowserService"
|
|
||||||
android:label="@string/common.appname"
|
android:label="@string/common.appname"
|
||||||
|
android:foregroundServiceType="mediaPlayback"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
<action android:name="androidx.media3.session.MediaLibraryService" />
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:name=".receiver.MediaButtonIntentReceiver">
|
<receiver android:name=".receiver.UltrasonicIntentReceiver"
|
||||||
<intent-filter android:priority="2147483647">
|
android:exported="true">
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver android:name=".receiver.UltrasonicIntentReceiver">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/>
|
<action android:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/>
|
||||||
<action android:name="org.moire.ultrasonic.CMD_PLAY"/>
|
<action android:name="org.moire.ultrasonic.CMD_PLAY"/>
|
||||||
|
@ -90,7 +89,8 @@
|
||||||
<action android:name="org.moire.ultrasonic.CMD_PROCESS_KEYCODE"/>
|
<action android:name="org.moire.ultrasonic.CMD_PROCESS_KEYCODE"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name=".receiver.BluetoothIntentReceiver">
|
<receiver android:name=".receiver.BluetoothIntentReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.bluetooth.device.action.ACL_CONNECTED"/>
|
<action android:name="android.bluetooth.device.action.ACL_CONNECTED"/>
|
||||||
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
|
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
|
||||||
|
@ -100,7 +100,8 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".provider.UltrasonicAppWidgetProvider4X1"
|
android:name=".provider.UltrasonicAppWidgetProvider4X1"
|
||||||
android:label="Ultrasonic (4x1)">
|
android:label="Ultrasonic (4x1)"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -111,7 +112,8 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".provider.UltrasonicAppWidgetProvider4X2"
|
android:name=".provider.UltrasonicAppWidgetProvider4X2"
|
||||||
android:label="Ultrasonic (4x2)">
|
android:label="Ultrasonic (4x2)"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -122,7 +124,8 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".provider.UltrasonicAppWidgetProvider4X3"
|
android:name=".provider.UltrasonicAppWidgetProvider4X3"
|
||||||
android:label="Ultrasonic (4x3)">
|
android:label="Ultrasonic (4x3)"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -133,7 +136,8 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".provider.UltrasonicAppWidgetProvider4X4"
|
android:name=".provider.UltrasonicAppWidgetProvider4X4"
|
||||||
android:label="Ultrasonic (4x4)">
|
android:label="Ultrasonic (4x4)"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -142,18 +146,17 @@
|
||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/appwidget_info_4x4"/>
|
android:resource="@xml/appwidget_info_4x4"/>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver android:name=".receiver.MediaButtonIntentReceiver"
|
||||||
<provider
|
android:exported="true">
|
||||||
android:name=".provider.SearchSuggestionProvider"
|
<intent-filter android:priority="2147483647">
|
||||||
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
|
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".receiver.A2dpIntentReceiver"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.android.music.playstatusrequest"/>
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<provider
|
||||||
|
android:name=".provider.SearchSuggestionProvider"
|
||||||
|
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -1,221 +0,0 @@
|
||||||
package org.moire.ultrasonic.provider;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.appwidget.AppWidgetManager;
|
|
||||||
import android.appwidget.AppWidgetProvider;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.widget.RemoteViews;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
|
||||||
import org.moire.ultrasonic.activity.NavigationActivity;
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
||||||
import org.moire.ultrasonic.imageloader.BitmapUtils;
|
|
||||||
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
|
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController;
|
|
||||||
import org.moire.ultrasonic.util.Constants;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Widget Provider for the Ultrasonic Widgets
|
|
||||||
*/
|
|
||||||
public class UltrasonicAppWidgetProvider extends AppWidgetProvider
|
|
||||||
{
|
|
||||||
protected int layoutId;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
|
|
||||||
{
|
|
||||||
defaultAppWidget(context, appWidgetIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize given widgets to default state, where we launch Ultrasonic on default click
|
|
||||||
* and hide actions if service not running.
|
|
||||||
*/
|
|
||||||
private void defaultAppWidget(Context context, int[] appWidgetIds)
|
|
||||||
{
|
|
||||||
final Resources res = context.getResources();
|
|
||||||
final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId);
|
|
||||||
|
|
||||||
views.setTextViewText(R.id.title, null);
|
|
||||||
views.setTextViewText(R.id.album, null);
|
|
||||||
views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text));
|
|
||||||
|
|
||||||
linkButtons(context, views, false);
|
|
||||||
pushUpdate(context, appWidgetIds, views);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views)
|
|
||||||
{
|
|
||||||
// Update specific list of appWidgetIds if given, otherwise default to all
|
|
||||||
final AppWidgetManager manager = AppWidgetManager.getInstance(context);
|
|
||||||
|
|
||||||
if (manager != null)
|
|
||||||
{
|
|
||||||
if (appWidgetIds != null)
|
|
||||||
{
|
|
||||||
manager.updateAppWidget(appWidgetIds, views);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
manager.updateAppWidget(new ComponentName(context, this.getClass()), views);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a change notification coming over from {@link MediaPlayerController}
|
|
||||||
*/
|
|
||||||
public void notifyChange(Context context, MusicDirectory.Entry currentSong, boolean playing, boolean setAlbum)
|
|
||||||
{
|
|
||||||
if (hasInstances(context))
|
|
||||||
{
|
|
||||||
performUpdate(context, currentSong, playing, setAlbum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check against {@link AppWidgetManager} if there are any instances of this widget.
|
|
||||||
*/
|
|
||||||
private boolean hasInstances(Context context)
|
|
||||||
{
|
|
||||||
AppWidgetManager manager = AppWidgetManager.getInstance(context);
|
|
||||||
|
|
||||||
if (manager != null)
|
|
||||||
{
|
|
||||||
int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass()));
|
|
||||||
return (appWidgetIds.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update all active widget instances by pushing changes
|
|
||||||
*/
|
|
||||||
private void performUpdate(Context context, MusicDirectory.Entry currentSong, boolean playing, boolean setAlbum)
|
|
||||||
{
|
|
||||||
final Resources res = context.getResources();
|
|
||||||
final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId);
|
|
||||||
|
|
||||||
String title = currentSong == null ? null : currentSong.getTitle();
|
|
||||||
String artist = currentSong == null ? null : currentSong.getArtist();
|
|
||||||
String album = currentSong == null ? null : currentSong.getAlbum();
|
|
||||||
CharSequence errorState = null;
|
|
||||||
|
|
||||||
// Show error message?
|
|
||||||
String status = Environment.getExternalStorageState();
|
|
||||||
if (status.equals(Environment.MEDIA_SHARED) || status.equals(Environment.MEDIA_UNMOUNTED))
|
|
||||||
{
|
|
||||||
errorState = res.getText(R.string.widget_sdcard_busy);
|
|
||||||
}
|
|
||||||
else if (status.equals(Environment.MEDIA_REMOVED))
|
|
||||||
{
|
|
||||||
errorState = res.getText(R.string.widget_sdcard_missing);
|
|
||||||
}
|
|
||||||
else if (currentSong == null)
|
|
||||||
{
|
|
||||||
errorState = res.getText(R.string.widget_initial_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorState != null)
|
|
||||||
{
|
|
||||||
// Show error state to user
|
|
||||||
views.setTextViewText(R.id.title, null);
|
|
||||||
views.setTextViewText(R.id.artist, errorState);
|
|
||||||
if (setAlbum)
|
|
||||||
{
|
|
||||||
views.setTextViewText(R.id.album, null);
|
|
||||||
}
|
|
||||||
views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No error, so show normal titles
|
|
||||||
views.setTextViewText(R.id.title, title);
|
|
||||||
views.setTextViewText(R.id.artist, artist);
|
|
||||||
if (setAlbum)
|
|
||||||
{
|
|
||||||
views.setTextViewText(R.id.album, album);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set correct drawable for pause state
|
|
||||||
if (playing)
|
|
||||||
{
|
|
||||||
views.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
views.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the cover art
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Bitmap bitmap = currentSong == null ? null : BitmapUtils.Companion.getAlbumArtBitmapFromDisk(currentSong, 240);
|
|
||||||
|
|
||||||
if (bitmap == null)
|
|
||||||
{
|
|
||||||
// Set default cover art
|
|
||||||
views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
views.setImageViewBitmap(R.id.appwidget_coverart, bitmap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception x)
|
|
||||||
{
|
|
||||||
Timber.e(x, "Failed to load cover art");
|
|
||||||
views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Link actions buttons to intents
|
|
||||||
linkButtons(context, views, currentSong != null);
|
|
||||||
|
|
||||||
pushUpdate(context, null, views);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Link up various button actions using {@link PendingIntent}.
|
|
||||||
*/
|
|
||||||
private static void linkButtons(Context context, RemoteViews views, boolean playerActive)
|
|
||||||
{
|
|
||||||
Intent intent = new Intent(context, NavigationActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
||||||
if (playerActive)
|
|
||||||
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true);
|
|
||||||
|
|
||||||
intent.setAction("android.intent.action.MAIN");
|
|
||||||
intent.addCategory("android.intent.category.LAUNCHER");
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent);
|
|
||||||
views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
|
|
||||||
|
|
||||||
// Emulate media button clicks.
|
|
||||||
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
|
|
||||||
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
|
|
||||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
|
|
||||||
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0);
|
|
||||||
views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
|
|
||||||
|
|
||||||
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
|
|
||||||
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
|
|
||||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
|
|
||||||
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0);
|
|
||||||
views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
|
|
||||||
|
|
||||||
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
|
|
||||||
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
|
|
||||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
|
|
||||||
pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0);
|
|
||||||
views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
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.provider;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
|
||||||
|
|
||||||
public class UltrasonicAppWidgetProvider4X1 extends UltrasonicAppWidgetProvider
|
|
||||||
{
|
|
||||||
|
|
||||||
public UltrasonicAppWidgetProvider4X1()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.layoutId = R.layout.appwidget4x1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UltrasonicAppWidgetProvider4X1 instance;
|
|
||||||
|
|
||||||
public static synchronized UltrasonicAppWidgetProvider4X1 getInstance()
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new UltrasonicAppWidgetProvider4X1();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
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.provider;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
|
||||||
|
|
||||||
public class UltrasonicAppWidgetProvider4X2 extends UltrasonicAppWidgetProvider
|
|
||||||
{
|
|
||||||
|
|
||||||
public UltrasonicAppWidgetProvider4X2()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.layoutId = R.layout.appwidget4x2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UltrasonicAppWidgetProvider4X2 instance;
|
|
||||||
|
|
||||||
public static synchronized UltrasonicAppWidgetProvider4X2 getInstance()
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new UltrasonicAppWidgetProvider4X2();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
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.provider;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
|
||||||
|
|
||||||
public class UltrasonicAppWidgetProvider4X3 extends UltrasonicAppWidgetProvider
|
|
||||||
{
|
|
||||||
|
|
||||||
public UltrasonicAppWidgetProvider4X3()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.layoutId = R.layout.appwidget4x3;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UltrasonicAppWidgetProvider4X3 instance;
|
|
||||||
|
|
||||||
public static synchronized UltrasonicAppWidgetProvider4X3 getInstance()
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new UltrasonicAppWidgetProvider4X3();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
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.provider;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
|
||||||
|
|
||||||
public class UltrasonicAppWidgetProvider4X4 extends UltrasonicAppWidgetProvider
|
|
||||||
{
|
|
||||||
|
|
||||||
public UltrasonicAppWidgetProvider4X4()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.layoutId = R.layout.appwidget4x4;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UltrasonicAppWidgetProvider4X4 instance;
|
|
||||||
|
|
||||||
public static synchronized UltrasonicAppWidgetProvider4X4 getInstance()
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new UltrasonicAppWidgetProvider4X4();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package org.moire.ultrasonic.receiver;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
|
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController;
|
|
||||||
|
|
||||||
import kotlin.Lazy;
|
|
||||||
|
|
||||||
import static org.koin.java.KoinJavaComponent.inject;
|
|
||||||
|
|
||||||
public class A2dpIntentReceiver extends BroadcastReceiver
|
|
||||||
{
|
|
||||||
private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse";
|
|
||||||
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent)
|
|
||||||
{
|
|
||||||
if (mediaPlayerControllerLazy.getValue().getCurrentPlaying() == null) return;
|
|
||||||
|
|
||||||
Entry song = mediaPlayerControllerLazy.getValue().getCurrentPlaying().getSong();
|
|
||||||
if (song == null) return;
|
|
||||||
|
|
||||||
Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE);
|
|
||||||
|
|
||||||
Integer duration = song.getDuration();
|
|
||||||
int playerPosition = mediaPlayerControllerLazy.getValue().getPlayerPosition();
|
|
||||||
int listSize = mediaPlayerControllerLazy.getValue().getPlaylistSize();
|
|
||||||
|
|
||||||
if (duration != null)
|
|
||||||
{
|
|
||||||
avrcpIntent.putExtra("duration", (long) duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
avrcpIntent.putExtra("position", (long) playerPosition);
|
|
||||||
avrcpIntent.putExtra("ListSize", (long) listSize);
|
|
||||||
|
|
||||||
switch (mediaPlayerControllerLazy.getValue().getPlayerState())
|
|
||||||
{
|
|
||||||
case STARTED:
|
|
||||||
avrcpIntent.putExtra("playing", true);
|
|
||||||
break;
|
|
||||||
case STOPPED:
|
|
||||||
case PAUSED:
|
|
||||||
case COMPLETED:
|
|
||||||
avrcpIntent.putExtra("playing", false);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.sendBroadcast(avrcpIntent);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
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.receiver;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
|
|
||||||
import org.moire.ultrasonic.util.Constants;
|
|
||||||
import org.moire.ultrasonic.util.Settings;
|
|
||||||
import org.moire.ultrasonic.util.Util;
|
|
||||||
|
|
||||||
import kotlin.Lazy;
|
|
||||||
|
|
||||||
import static org.koin.java.KoinJavaComponent.inject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Sindre Mehus
|
|
||||||
*/
|
|
||||||
public class MediaButtonIntentReceiver extends BroadcastReceiver
|
|
||||||
{
|
|
||||||
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent)
|
|
||||||
{
|
|
||||||
String intentAction = intent.getAction();
|
|
||||||
|
|
||||||
// If media button are turned off and we received a media button, exit
|
|
||||||
if (!Settings.getMediaButtonsEnabled() && Intent.ACTION_MEDIA_BUTTON.equals(intentAction))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Only process media buttons and CMD_PROCESS_KEYCODE, which is received from the widgets
|
|
||||||
if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction) &&
|
|
||||||
!Constants.CMD_PROCESS_KEYCODE.equals(intentAction)) return;
|
|
||||||
|
|
||||||
Bundle extras = intent.getExtras();
|
|
||||||
|
|
||||||
if (extras == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT);
|
|
||||||
Timber.i("Got MEDIA_BUTTON key event: %s", event);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Intent serviceIntent = new Intent(Constants.CMD_PROCESS_KEYCODE);
|
|
||||||
serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
|
|
||||||
lifecycleSupport.getValue().receiveIntent(serviceIntent);
|
|
||||||
|
|
||||||
if (isOrderedBroadcast())
|
|
||||||
{
|
|
||||||
abortBroadcast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception x)
|
|
||||||
{
|
|
||||||
// Ignored.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,489 +0,0 @@
|
||||||
/*
|
|
||||||
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.service;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Handler;
|
|
||||||
import timber.log.Timber;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.moire.ultrasonic.R;
|
|
||||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
|
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException;
|
|
||||||
import org.moire.ultrasonic.app.UApp;
|
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
|
||||||
import org.moire.ultrasonic.domain.JukeboxStatus;
|
|
||||||
import org.moire.ultrasonic.domain.PlayerState;
|
|
||||||
import org.moire.ultrasonic.util.Util;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
|
|
||||||
import kotlin.Lazy;
|
|
||||||
|
|
||||||
import static org.koin.java.KoinJavaComponent.inject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an asynchronous interface to the remote jukebox on the Subsonic server.
|
|
||||||
*
|
|
||||||
* @author Sindre Mehus
|
|
||||||
* @version $Id$
|
|
||||||
*/
|
|
||||||
public class JukeboxMediaPlayer
|
|
||||||
{
|
|
||||||
private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L;
|
|
||||||
|
|
||||||
private final TaskQueue tasks = new TaskQueue();
|
|
||||||
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
private ScheduledFuture<?> statusUpdateFuture;
|
|
||||||
private final AtomicLong timeOfLastUpdate = new AtomicLong();
|
|
||||||
private JukeboxStatus jukeboxStatus;
|
|
||||||
private float gain = 0.5f;
|
|
||||||
private VolumeToast volumeToast;
|
|
||||||
private final AtomicBoolean running = new AtomicBoolean();
|
|
||||||
private Thread serviceThread;
|
|
||||||
private boolean enabled = false;
|
|
||||||
|
|
||||||
// TODO: These create circular references, try to refactor
|
|
||||||
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
|
|
||||||
private final Downloader downloader;
|
|
||||||
|
|
||||||
// TODO: Report warning if queue fills up.
|
|
||||||
// TODO: Create shutdown method?
|
|
||||||
// TODO: Disable repeat.
|
|
||||||
// TODO: Persist RC state?
|
|
||||||
// TODO: Minimize status updates.
|
|
||||||
|
|
||||||
public JukeboxMediaPlayer(Downloader downloader)
|
|
||||||
{
|
|
||||||
this.downloader = downloader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startJukeboxService()
|
|
||||||
{
|
|
||||||
if (running.get())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
running.set(true);
|
|
||||||
startProcessTasks();
|
|
||||||
Timber.d("Started Jukebox Service");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopJukeboxService()
|
|
||||||
{
|
|
||||||
running.set(false);
|
|
||||||
Util.sleepQuietly(1000);
|
|
||||||
|
|
||||||
if (serviceThread != null)
|
|
||||||
{
|
|
||||||
serviceThread.interrupt();
|
|
||||||
}
|
|
||||||
Timber.d("Stopped Jukebox Service");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startProcessTasks()
|
|
||||||
{
|
|
||||||
serviceThread = new Thread()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
processTasks();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
serviceThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void startStatusUpdate()
|
|
||||||
{
|
|
||||||
stopStatusUpdate();
|
|
||||||
|
|
||||||
Runnable updateTask = new Runnable()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
tasks.remove(GetStatus.class);
|
|
||||||
tasks.add(new GetStatus());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
statusUpdateFuture = executorService.scheduleWithFixedDelay(updateTask, STATUS_UPDATE_INTERVAL_SECONDS, STATUS_UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void stopStatusUpdate()
|
|
||||||
{
|
|
||||||
if (statusUpdateFuture != null)
|
|
||||||
{
|
|
||||||
statusUpdateFuture.cancel(false);
|
|
||||||
statusUpdateFuture = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processTasks()
|
|
||||||
{
|
|
||||||
while (running.get())
|
|
||||||
{
|
|
||||||
JukeboxTask task = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!ActiveServerProvider.Companion.isOffline())
|
|
||||||
{
|
|
||||||
task = tasks.take();
|
|
||||||
JukeboxStatus status = task.execute();
|
|
||||||
onStatusUpdate(status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (InterruptedException ignored)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Throwable x)
|
|
||||||
{
|
|
||||||
onError(task, x);
|
|
||||||
}
|
|
||||||
|
|
||||||
Util.sleepQuietly(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onStatusUpdate(JukeboxStatus jukeboxStatus)
|
|
||||||
{
|
|
||||||
timeOfLastUpdate.set(System.currentTimeMillis());
|
|
||||||
this.jukeboxStatus = jukeboxStatus;
|
|
||||||
|
|
||||||
// Track change?
|
|
||||||
Integer index = jukeboxStatus.getCurrentPlayingIndex();
|
|
||||||
|
|
||||||
if (index != null && index != -1 && index != downloader.getCurrentPlayingIndex())
|
|
||||||
{
|
|
||||||
mediaPlayerControllerLazy.getValue().setCurrentPlaying(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onError(JukeboxTask task, Throwable x)
|
|
||||||
{
|
|
||||||
if (x instanceof ApiNotSupportedException && !(task instanceof Stop))
|
|
||||||
{
|
|
||||||
disableJukeboxOnError(x, R.string.download_jukebox_server_too_old);
|
|
||||||
}
|
|
||||||
else if (x instanceof OfflineException && !(task instanceof Stop))
|
|
||||||
{
|
|
||||||
disableJukeboxOnError(x, R.string.download_jukebox_offline);
|
|
||||||
}
|
|
||||||
else if (x instanceof SubsonicRESTException && ((SubsonicRESTException) x).getCode() == 50 && !(task instanceof Stop))
|
|
||||||
{
|
|
||||||
disableJukeboxOnError(x, R.string.download_jukebox_not_authorized);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Timber.e(x, "Failed to process jukebox task");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disableJukeboxOnError(Throwable x, final int resourceId)
|
|
||||||
{
|
|
||||||
Timber.w(x.toString());
|
|
||||||
Context context = UApp.Companion.applicationContext();
|
|
||||||
new Handler().post(() -> Util.toast(context, resourceId, false));
|
|
||||||
|
|
||||||
mediaPlayerControllerLazy.getValue().setJukeboxEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updatePlaylist()
|
|
||||||
{
|
|
||||||
if (!enabled) return;
|
|
||||||
|
|
||||||
tasks.remove(Skip.class);
|
|
||||||
tasks.remove(Stop.class);
|
|
||||||
tasks.remove(Start.class);
|
|
||||||
|
|
||||||
List<String> ids = new ArrayList<>();
|
|
||||||
for (DownloadFile file : downloader.getAll())
|
|
||||||
{
|
|
||||||
ids.add(file.getSong().getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.add(new SetPlaylist(ids));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void skip(final int index, final int offsetSeconds)
|
|
||||||
{
|
|
||||||
tasks.remove(Skip.class);
|
|
||||||
tasks.remove(Stop.class);
|
|
||||||
tasks.remove(Start.class);
|
|
||||||
|
|
||||||
startStatusUpdate();
|
|
||||||
|
|
||||||
if (jukeboxStatus != null)
|
|
||||||
{
|
|
||||||
jukeboxStatus.setPositionSeconds(offsetSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.add(new Skip(index, offsetSeconds));
|
|
||||||
mediaPlayerControllerLazy.getValue().setPlayerState(PlayerState.STARTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop()
|
|
||||||
{
|
|
||||||
tasks.remove(Stop.class);
|
|
||||||
tasks.remove(Start.class);
|
|
||||||
|
|
||||||
stopStatusUpdate();
|
|
||||||
|
|
||||||
tasks.add(new Stop());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start()
|
|
||||||
{
|
|
||||||
tasks.remove(Stop.class);
|
|
||||||
tasks.remove(Start.class);
|
|
||||||
|
|
||||||
startStatusUpdate();
|
|
||||||
tasks.add(new Start());
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void adjustVolume(boolean up)
|
|
||||||
{
|
|
||||||
float delta = up ? 0.05f : -0.05f;
|
|
||||||
gain += delta;
|
|
||||||
gain = Math.max(gain, 0.0f);
|
|
||||||
gain = Math.min(gain, 1.0f);
|
|
||||||
|
|
||||||
tasks.remove(SetGain.class);
|
|
||||||
tasks.add(new SetGain(gain));
|
|
||||||
|
|
||||||
Context context = UApp.Companion.applicationContext();
|
|
||||||
if (volumeToast == null) volumeToast = new VolumeToast(context);
|
|
||||||
|
|
||||||
volumeToast.setVolume(gain);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MusicService getMusicService()
|
|
||||||
{
|
|
||||||
return MusicServiceFactory.getMusicService();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPositionSeconds()
|
|
||||||
{
|
|
||||||
if (jukeboxStatus == null || jukeboxStatus.getPositionSeconds() == null || timeOfLastUpdate.get() == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jukeboxStatus.isPlaying())
|
|
||||||
{
|
|
||||||
int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - timeOfLastUpdate.get()) / 1000L);
|
|
||||||
return jukeboxStatus.getPositionSeconds() + secondsSinceLastUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
return jukeboxStatus.getPositionSeconds();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnabled(boolean enabled)
|
|
||||||
{
|
|
||||||
Timber.d("Jukebox Service setting enabled to %b", enabled);
|
|
||||||
this.enabled = enabled;
|
|
||||||
|
|
||||||
tasks.clear();
|
|
||||||
if (enabled)
|
|
||||||
{
|
|
||||||
updatePlaylist();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled()
|
|
||||||
{
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TaskQueue
|
|
||||||
{
|
|
||||||
private final LinkedBlockingQueue<JukeboxTask> queue = new LinkedBlockingQueue<>();
|
|
||||||
|
|
||||||
void add(JukeboxTask jukeboxTask)
|
|
||||||
{
|
|
||||||
queue.add(jukeboxTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
JukeboxTask take() throws InterruptedException
|
|
||||||
{
|
|
||||||
return queue.take();
|
|
||||||
}
|
|
||||||
|
|
||||||
void remove(Class<? extends JukeboxTask> taskClass)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Iterator<JukeboxTask> iterator = queue.iterator();
|
|
||||||
|
|
||||||
while (iterator.hasNext())
|
|
||||||
{
|
|
||||||
JukeboxTask task = iterator.next();
|
|
||||||
|
|
||||||
if (taskClass.equals(task.getClass()))
|
|
||||||
{
|
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Throwable x)
|
|
||||||
{
|
|
||||||
Timber.w(x, "Failed to clean-up task queue.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear()
|
|
||||||
{
|
|
||||||
queue.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract static class JukeboxTask
|
|
||||||
{
|
|
||||||
abstract JukeboxStatus execute() throws Exception;
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
return getClass().getSimpleName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class GetStatus extends JukeboxTask
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
JukeboxStatus execute() throws Exception
|
|
||||||
{
|
|
||||||
return getMusicService().getJukeboxStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SetPlaylist extends JukeboxTask
|
|
||||||
{
|
|
||||||
private final List<String> ids;
|
|
||||||
|
|
||||||
SetPlaylist(List<String> ids)
|
|
||||||
{
|
|
||||||
this.ids = ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
JukeboxStatus execute() throws Exception
|
|
||||||
{
|
|
||||||
return getMusicService().updateJukeboxPlaylist(ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Skip extends JukeboxTask
|
|
||||||
{
|
|
||||||
private final int index;
|
|
||||||
private final int offsetSeconds;
|
|
||||||
|
|
||||||
Skip(int index, int offsetSeconds)
|
|
||||||
{
|
|
||||||
this.index = index;
|
|
||||||
this.offsetSeconds = offsetSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
JukeboxStatus execute() throws Exception
|
|
||||||
{
|
|
||||||
return getMusicService().skipJukebox(index, offsetSeconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Stop extends JukeboxTask
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
JukeboxStatus execute() throws Exception
|
|
||||||
{
|
|
||||||
return getMusicService().stopJukebox();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Start extends JukeboxTask
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
JukeboxStatus execute() throws Exception
|
|
||||||
{
|
|
||||||
return getMusicService().startJukebox();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SetGain extends JukeboxTask
|
|
||||||
{
|
|
||||||
|
|
||||||
private final float gain;
|
|
||||||
|
|
||||||
private SetGain(float gain)
|
|
||||||
{
|
|
||||||
this.gain = gain;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
JukeboxStatus execute() throws Exception
|
|
||||||
{
|
|
||||||
return getMusicService().setJukeboxGain(gain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class VolumeToast extends Toast
|
|
||||||
{
|
|
||||||
|
|
||||||
private final ProgressBar progressBar;
|
|
||||||
|
|
||||||
public VolumeToast(Context context)
|
|
||||||
{
|
|
||||||
super(context);
|
|
||||||
setDuration(Toast.LENGTH_SHORT);
|
|
||||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
View view = inflater.inflate(R.layout.jukebox_volume, null);
|
|
||||||
progressBar = (ProgressBar) view.findViewById(R.id.jukebox_volume_progress_bar);
|
|
||||||
setView(view);
|
|
||||||
setGravity(Gravity.TOP, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVolume(float volume)
|
|
||||||
{
|
|
||||||
progressBar.setProgress(Math.round(100 * volume));
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ public class Scrobbler
|
||||||
{
|
{
|
||||||
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return;
|
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return;
|
||||||
|
|
||||||
final String id = song.getSong().getId();
|
final String id = song.getTrack().getId();
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
|
|
||||||
// Avoid duplicate registrations.
|
// Avoid duplicate registrations.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package org.moire.ultrasonic.service;
|
package org.moire.ultrasonic.service;
|
||||||
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
import org.moire.ultrasonic.domain.Track;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -13,7 +13,7 @@ public class State implements Serializable
|
||||||
{
|
{
|
||||||
public static final long serialVersionUID = -6346438781062572270L;
|
public static final long serialVersionUID = -6346438781062572270L;
|
||||||
|
|
||||||
public List<MusicDirectory.Entry> songs = new ArrayList<>();
|
public List<Track> songs = new ArrayList<>();
|
||||||
public int currentPlayingIndex;
|
public int currentPlayingIndex;
|
||||||
public int currentPlayingPosition;
|
public int currentPlayingPosition;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package org.moire.ultrasonic.util;
|
package org.moire.ultrasonic.util;
|
||||||
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
import org.moire.ultrasonic.domain.Track;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -12,5 +12,5 @@ public class ShareDetails
|
||||||
public String Description;
|
public String Description;
|
||||||
public boolean ShareOnServer;
|
public boolean ShareOnServer;
|
||||||
public long Expiration;
|
public long Expiration;
|
||||||
public List<MusicDirectory.Entry> Entries;
|
public List<Track> Entries;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
/*
|
|
||||||
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 org.moire.ultrasonic.data.ActiveServerProvider;
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
||||||
import org.moire.ultrasonic.service.MusicService;
|
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Sindre Mehus
|
|
||||||
* @version $Id$
|
|
||||||
*/
|
|
||||||
public class ShufflePlayBuffer
|
|
||||||
{
|
|
||||||
private static final int CAPACITY = 50;
|
|
||||||
private static final int REFILL_THRESHOLD = 40;
|
|
||||||
|
|
||||||
private final List<MusicDirectory.Entry> buffer = new ArrayList<>();
|
|
||||||
private ScheduledExecutorService executorService;
|
|
||||||
private int currentServer;
|
|
||||||
|
|
||||||
public boolean isEnabled = false;
|
|
||||||
|
|
||||||
public ShufflePlayBuffer()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCreate()
|
|
||||||
{
|
|
||||||
executorService = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
Runnable runnable = this::refill;
|
|
||||||
executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
|
|
||||||
Timber.i("ShufflePlayBuffer created");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onDestroy()
|
|
||||||
{
|
|
||||||
executorService.shutdown();
|
|
||||||
Timber.i("ShufflePlayBuffer destroyed");
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MusicDirectory.Entry> get(int size)
|
|
||||||
{
|
|
||||||
clearBufferIfNecessary();
|
|
||||||
|
|
||||||
List<MusicDirectory.Entry> result = new ArrayList<>(size);
|
|
||||||
synchronized (buffer)
|
|
||||||
{
|
|
||||||
while (!buffer.isEmpty() && result.size() < size)
|
|
||||||
{
|
|
||||||
result.add(buffer.remove(buffer.size() - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.i("Taking %d songs from shuffle play buffer. %d remaining.", result.size(), buffer.size());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refill()
|
|
||||||
{
|
|
||||||
if (!isEnabled) return;
|
|
||||||
|
|
||||||
// Check if active server has changed.
|
|
||||||
clearBufferIfNecessary();
|
|
||||||
|
|
||||||
if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected() && !ActiveServerProvider.Companion.isOffline()))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
MusicService service = MusicServiceFactory.getMusicService();
|
|
||||||
int n = CAPACITY - buffer.size();
|
|
||||||
MusicDirectory songs = service.getRandomSongs(n);
|
|
||||||
|
|
||||||
synchronized (buffer)
|
|
||||||
{
|
|
||||||
buffer.addAll(songs.getTracks());
|
|
||||||
Timber.i("Refilled shuffle play buffer with %d songs.", songs.getTracks().size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception x)
|
|
||||||
{
|
|
||||||
Timber.w(x, "Failed to refill shuffle play buffer.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearBufferIfNecessary()
|
|
||||||
{
|
|
||||||
synchronized (buffer)
|
|
||||||
{
|
|
||||||
if (currentServer != ActiveServerProvider.Companion.getActiveServerId())
|
|
||||||
{
|
|
||||||
currentServer = ActiveServerProvider.Companion.getActiveServerId();
|
|
||||||
buffer.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,290 +0,0 @@
|
||||||
package org.moire.ultrasonic.util;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
||||||
import org.moire.ultrasonic.service.DownloadFile;
|
|
||||||
import org.moire.ultrasonic.service.Supplier;
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.net.URLDecoder;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.StringTokenizer;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class StreamProxy implements Runnable
|
|
||||||
{
|
|
||||||
private Thread thread;
|
|
||||||
private boolean isRunning;
|
|
||||||
private ServerSocket socket;
|
|
||||||
private int port;
|
|
||||||
private Supplier<DownloadFile> currentPlaying;
|
|
||||||
|
|
||||||
public StreamProxy(Supplier<DownloadFile> currentPlaying)
|
|
||||||
{
|
|
||||||
|
|
||||||
// Create listening socket
|
|
||||||
try
|
|
||||||
{
|
|
||||||
socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
|
|
||||||
socket.setSoTimeout(5000);
|
|
||||||
port = socket.getLocalPort();
|
|
||||||
this.currentPlaying = currentPlaying;
|
|
||||||
}
|
|
||||||
catch (UnknownHostException e)
|
|
||||||
{ // impossible
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
Timber.e(e, "IOException initializing server");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPort()
|
|
||||||
{
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start()
|
|
||||||
{
|
|
||||||
thread = new Thread(this);
|
|
||||||
thread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop()
|
|
||||||
{
|
|
||||||
isRunning = false;
|
|
||||||
thread.interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
isRunning = true;
|
|
||||||
while (isRunning)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Socket client = socket.accept();
|
|
||||||
if (client == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Timber.i("Client connected");
|
|
||||||
|
|
||||||
StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client);
|
|
||||||
if (task.processRequest())
|
|
||||||
{
|
|
||||||
new Thread(task).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (SocketTimeoutException e)
|
|
||||||
{
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
Timber.e(e, "Error connecting to client");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.i("Proxy interrupted. Shutting down.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private class StreamToMediaPlayerTask implements Runnable {
|
|
||||||
String localPath;
|
|
||||||
Socket client;
|
|
||||||
int cbSkip;
|
|
||||||
|
|
||||||
StreamToMediaPlayerTask(Socket client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String readRequest() {
|
|
||||||
InputStream is;
|
|
||||||
String firstLine;
|
|
||||||
try {
|
|
||||||
is = client.getInputStream();
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
|
|
||||||
firstLine = reader.readLine();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "Error parsing request");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstLine == null) {
|
|
||||||
Timber.i("Proxy client closed connection without a request.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringTokenizer st = new StringTokenizer(firstLine);
|
|
||||||
st.nextToken(); // method
|
|
||||||
String uri = st.nextToken();
|
|
||||||
String realUri = uri.substring(1);
|
|
||||||
Timber.i(realUri);
|
|
||||||
|
|
||||||
return realUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean processRequest() {
|
|
||||||
final String uri = readRequest();
|
|
||||||
if (uri == null || uri.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read HTTP headers
|
|
||||||
Timber.i("Processing request: %s", uri);
|
|
||||||
|
|
||||||
try {
|
|
||||||
localPath = URLDecoder.decode(uri, Constants.UTF_8);
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
Timber.e(e, "Unsupported encoding");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.i("Processing request for file %s", localPath);
|
|
||||||
if (Storage.INSTANCE.isPathExists(localPath)) return true;
|
|
||||||
|
|
||||||
// Usually the .partial file will be requested here, but sometimes it has already
|
|
||||||
// been renamed, so check if it is completed since
|
|
||||||
String saveFileName = FileUtil.INSTANCE.getSaveFile(localPath);
|
|
||||||
String completeFileName = FileUtil.INSTANCE.getCompleteFile(saveFileName);
|
|
||||||
|
|
||||||
if (Storage.INSTANCE.isPathExists(saveFileName)) {
|
|
||||||
localPath = saveFileName;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Storage.INSTANCE.isPathExists(completeFileName)) {
|
|
||||||
localPath = completeFileName;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.e("File %s does not exist", localPath);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
Timber.i("Streaming song in background");
|
|
||||||
DownloadFile downloadFile = currentPlaying == null? null : currentPlaying.get();
|
|
||||||
MusicDirectory.Entry song = downloadFile.getSong();
|
|
||||||
long fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8;
|
|
||||||
Timber.i("Streaming fileSize: %d", fileSize);
|
|
||||||
|
|
||||||
// Create HTTP header
|
|
||||||
String headers = "HTTP/1.0 200 OK\r\n";
|
|
||||||
headers += "Content-Type: application/octet-stream\r\n";
|
|
||||||
headers += "Connection: close\r\n";
|
|
||||||
headers += "\r\n";
|
|
||||||
|
|
||||||
long cbToSend = fileSize - cbSkip;
|
|
||||||
OutputStream output = null;
|
|
||||||
byte[] buff = new byte[64 * 1024];
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
output = new BufferedOutputStream(client.getOutputStream(), 32 * 1024);
|
|
||||||
output.write(headers.getBytes());
|
|
||||||
|
|
||||||
if (!downloadFile.isWorkDone())
|
|
||||||
{
|
|
||||||
// Loop as long as there's stuff to send
|
|
||||||
while (isRunning && !client.isClosed())
|
|
||||||
{
|
|
||||||
// See if there's more to send
|
|
||||||
String file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
|
|
||||||
int cbSentThisBatch = 0;
|
|
||||||
|
|
||||||
AbstractFile storageFile = Storage.INSTANCE.getFromPath(file);
|
|
||||||
if (storageFile != null)
|
|
||||||
{
|
|
||||||
InputStream input = storageFile.getFileInputStream();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
long skip = input.skip(cbSkip);
|
|
||||||
int cbToSendThisBatch = input.available();
|
|
||||||
|
|
||||||
while (cbToSendThisBatch > 0)
|
|
||||||
{
|
|
||||||
int cbToRead = Math.min(cbToSendThisBatch, buff.length);
|
|
||||||
int cbRead = input.read(buff, 0, cbToRead);
|
|
||||||
|
|
||||||
if (cbRead == -1)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
cbToSendThisBatch -= cbRead;
|
|
||||||
cbToSend -= cbRead;
|
|
||||||
output.write(buff, 0, cbRead);
|
|
||||||
output.flush();
|
|
||||||
cbSkip += cbRead;
|
|
||||||
cbSentThisBatch += cbRead;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Done regardless of whether or not it thinks it is
|
|
||||||
if (downloadFile.isWorkDone() && cbSkip >= file.length())
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we did nothing this batch, block for a second
|
|
||||||
if (cbSentThisBatch == 0)
|
|
||||||
{
|
|
||||||
Timber.d("Blocking until more data appears (%d)", cbToSend);
|
|
||||||
Util.sleepQuietly(1000L);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Timber.w("Requesting data for completely downloaded file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (SocketException socketException)
|
|
||||||
{
|
|
||||||
Timber.e("SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Timber.e("Exception thrown from streaming task:");
|
|
||||||
Timber.e("%s : %s", e.getClass().getName(), e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (output != null)
|
|
||||||
{
|
|
||||||
output.close();
|
|
||||||
}
|
|
||||||
client.close();
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
Timber.e("IOException while cleaning up streaming task:");
|
|
||||||
Timber.e("%s : %s", e.getClass().getName(), e.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,6 +18,8 @@
|
||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.view;
|
package org.moire.ultrasonic.view;
|
||||||
|
|
||||||
|
import static org.koin.java.KoinJavaComponent.inject;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
@ -29,14 +31,11 @@ import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
|
|
||||||
import org.moire.ultrasonic.audiofx.VisualizerController;
|
import org.moire.ultrasonic.audiofx.VisualizerController;
|
||||||
import org.moire.ultrasonic.domain.PlayerState;
|
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController;
|
import org.moire.ultrasonic.service.MediaPlayerController;
|
||||||
|
|
||||||
import kotlin.Lazy;
|
import kotlin.Lazy;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static org.koin.java.KoinJavaComponent.inject;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple class that draws waveform data received from a
|
* A simple class that draws waveform data received from a
|
||||||
* {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture}
|
* {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture}
|
||||||
|
@ -130,7 +129,7 @@ public class VisualizerView extends View
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaPlayerControllerLazy.getValue().getPlayerState() != PlayerState.STARTED)
|
if (!mediaPlayerControllerLazy.getValue().isPlaying())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
/*
|
/*
|
||||||
* NavigationActivity.kt
|
* NavigationActivity.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.activity
|
package org.moire.ultrasonic.activity
|
||||||
|
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
@ -25,6 +27,9 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Player.STATE_BUFFERING
|
||||||
|
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
|
||||||
|
@ -36,23 +41,24 @@ import androidx.navigation.ui.setupWithNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.data.ServerSettingDao
|
import org.moire.ultrasonic.data.ServerSettingDao
|
||||||
import org.moire.ultrasonic.domain.PlayerState
|
|
||||||
import org.moire.ultrasonic.fragment.OnBackPressedHandler
|
import org.moire.ultrasonic.fragment.OnBackPressedHandler
|
||||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||||
import org.moire.ultrasonic.service.DownloadFile
|
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
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.ServerColor
|
import org.moire.ultrasonic.util.ServerColor
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Storage
|
import org.moire.ultrasonic.util.Storage
|
||||||
|
@ -61,7 +67,7 @@ import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main Activity of Ultrasonic which loads all other screens as Fragments
|
* The main (and only) Activity of Ultrasonic which loads all other screens as Fragments
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class NavigationActivity : AppCompatActivity() {
|
class NavigationActivity : AppCompatActivity() {
|
||||||
|
@ -78,8 +84,8 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
private var headerBackgroundImage: ImageView? = null
|
private var headerBackgroundImage: ImageView? = null
|
||||||
|
|
||||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||||
private var themeChangedEventSubscription: Disposable? = null
|
|
||||||
private var playerStateSubscription: Disposable? = null
|
private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
private val serverSettingsModel: ServerSettingsModel by viewModel()
|
private val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||||
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
|
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
|
||||||
|
@ -93,6 +99,16 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
private var cachedServerCount: Int = 0
|
private var cachedServerCount: Int = 0
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Timber.d("onCreate called")
|
||||||
|
|
||||||
|
// First check if Koin has been started
|
||||||
|
if (UApp.instance != null && !UApp.instance!!.initiated) {
|
||||||
|
Timber.d("Starting Koin")
|
||||||
|
UApp.instance!!.startKoin()
|
||||||
|
} else {
|
||||||
|
Timber.d("No need to start Koin")
|
||||||
|
}
|
||||||
|
|
||||||
setUncaughtExceptionHandler()
|
setUncaughtExceptionHandler()
|
||||||
Util.applyTheme(this)
|
Util.applyTheme(this)
|
||||||
|
|
||||||
|
@ -154,9 +170,8 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
setMenuForServerCapabilities()
|
setMenuForServerCapabilities()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine first run and migrate server settings to DB as early as possible
|
// Determine if this is a first run
|
||||||
var showWelcomeScreen = Util.isFirstRun()
|
val showWelcomeScreen = Util.isFirstRun()
|
||||||
val areServersMigrated: Boolean = serverSettingsModel.migrateFromPreferences()
|
|
||||||
|
|
||||||
// Migrate Feature storage if needed
|
// Migrate Feature storage if needed
|
||||||
// TODO: Remove in December 2022
|
// TODO: Remove in December 2022
|
||||||
|
@ -164,9 +179,6 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
Settings.migrateFeatureStorage()
|
Settings.migrateFeatureStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are any servers in the DB, do not show the welcome screen
|
|
||||||
showWelcomeScreen = showWelcomeScreen and !areServersMigrated
|
|
||||||
|
|
||||||
loadSettings()
|
loadSettings()
|
||||||
|
|
||||||
// This is a first run with only the demo entry inside the database
|
// This is a first run with only the demo entry inside the database
|
||||||
|
@ -180,25 +192,25 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
hideNowPlaying()
|
hideNowPlaying()
|
||||||
}
|
}
|
||||||
|
|
||||||
playerStateSubscription = RxBus.playerStateObservable.subscribe {
|
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
||||||
if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED)
|
if (it.state == STATE_READY)
|
||||||
showNowPlaying()
|
showNowPlaying()
|
||||||
else
|
else
|
||||||
hideNowPlaying()
|
hideNowPlaying()
|
||||||
}
|
}
|
||||||
|
|
||||||
themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe {
|
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
||||||
recreate()
|
recreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
serverRepository.liveServerCount().observe(
|
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
|
||||||
this,
|
updateNavigationHeaderForServer()
|
||||||
{ count ->
|
}
|
||||||
cachedServerCount = count ?: 0
|
|
||||||
updateNavigationHeaderForServer()
|
serverRepository.liveServerCount().observe(this) { count ->
|
||||||
}
|
cachedServerCount = count ?: 0
|
||||||
)
|
updateNavigationHeaderForServer()
|
||||||
ActiveServerProvider.liveActiveServerId.observe(this, { updateNavigationHeaderForServer() })
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNavigationHeaderForServer() {
|
private fun updateNavigationHeaderForServer() {
|
||||||
|
@ -224,6 +236,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
Timber.d("onResume called")
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
Storage.reset()
|
Storage.reset()
|
||||||
|
@ -237,10 +250,11 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
Timber.d("onDestroy called")
|
||||||
themeChangedEventSubscription?.dispose()
|
rxBusSubscription.dispose()
|
||||||
playerStateSubscription?.dispose()
|
|
||||||
imageLoaderProvider.clearImageLoader()
|
imageLoaderProvider.clearImageLoader()
|
||||||
|
UApp.instance!!.shutdownKoin()
|
||||||
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
@ -351,19 +365,35 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the customized language settings if needed
|
||||||
|
*/
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
val locale = Settings.overrideLanguage
|
||||||
|
val localeUpdatedContext: ContextWrapper = LocaleHelper.wrap(newBase, locale)
|
||||||
|
super.attachBaseContext(localeUpdatedContext)
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
|
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exit() {
|
private fun exit() {
|
||||||
|
Timber.d("User choose to exit the app")
|
||||||
|
|
||||||
|
// Broadcast that the service is being shutdown
|
||||||
|
RxBus.stopCommandPublisher.onNext(Unit)
|
||||||
|
|
||||||
lifecycleSupport.onDestroy()
|
lifecycleSupport.onDestroy()
|
||||||
finish()
|
finishAndRemoveTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showWelcomeDialog() {
|
private fun showWelcomeDialog() {
|
||||||
if (!infoDialogDisplayed) {
|
if (!infoDialogDisplayed) {
|
||||||
infoDialogDisplayed = true
|
infoDialogDisplayed = true
|
||||||
|
|
||||||
|
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
||||||
|
|
||||||
InfoDialog.Builder(this)
|
InfoDialog.Builder(this)
|
||||||
.setTitle(R.string.main_welcome_title)
|
.setTitle(R.string.main_welcome_title)
|
||||||
.setMessage(R.string.main_welcome_text_demo)
|
.setMessage(R.string.main_welcome_text_demo)
|
||||||
|
@ -406,10 +436,10 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nowPlayingView != null) {
|
if (nowPlayingView != null) {
|
||||||
val playerState: PlayerState = mediaPlayerController.playerState
|
val playerState: Int = mediaPlayerController.playbackState
|
||||||
if (playerState == PlayerState.PAUSED || playerState == PlayerState.STARTED) {
|
if (playerState == STATE_BUFFERING || playerState == STATE_READY) {
|
||||||
val file: DownloadFile? = mediaPlayerController.currentPlaying
|
val item: MediaItem? = mediaPlayerController.currentMediaItem
|
||||||
if (file != null) {
|
if (item != null) {
|
||||||
nowPlayingView?.visibility = View.VISIBLE
|
nowPlayingView?.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.drakeet.multitype.ItemViewBinder
|
import com.drakeet.multitype.ItemViewBinder
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.Album
|
||||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
|
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
|
||||||
|
@ -31,11 +31,11 @@ import timber.log.Timber
|
||||||
* Creates a Row in a RecyclerView which contains the details of an Album
|
* Creates a Row in a RecyclerView which contains the details of an Album
|
||||||
*/
|
*/
|
||||||
class AlbumRowBinder(
|
class AlbumRowBinder(
|
||||||
val onItemClick: (MusicDirectory.Album) -> Unit,
|
val onItemClick: (Album) -> Unit,
|
||||||
val onContextMenuClick: (MenuItem, MusicDirectory.Album) -> Boolean,
|
val onContextMenuClick: (MenuItem, Album) -> Boolean,
|
||||||
private val imageLoader: ImageLoader,
|
private val imageLoader: ImageLoader,
|
||||||
context: Context
|
context: Context
|
||||||
) : ItemViewBinder<MusicDirectory.Album, AlbumRowBinder.ViewHolder>(), KoinComponent {
|
) : ItemViewBinder<Album, AlbumRowBinder.ViewHolder>(), KoinComponent {
|
||||||
|
|
||||||
private val starDrawable: Drawable =
|
private val starDrawable: Drawable =
|
||||||
Util.getDrawableFromAttribute(context, R.attr.star_full)
|
Util.getDrawableFromAttribute(context, R.attr.star_full)
|
||||||
|
@ -46,7 +46,7 @@ class AlbumRowBinder(
|
||||||
val layout = R.layout.list_item_album
|
val layout = R.layout.list_item_album
|
||||||
val contextMenuLayout = R.menu.context_menu_artist
|
val contextMenuLayout = R.menu.context_menu_artist
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Album) {
|
override fun onBindViewHolder(holder: ViewHolder, item: Album) {
|
||||||
holder.album.text = item.title
|
holder.album.text = item.title
|
||||||
holder.artist.text = item.artist
|
holder.artist.text = item.artist
|
||||||
holder.details.setOnClickListener { onItemClick(item) }
|
holder.details.setOnClickListener { onItemClick(item) }
|
||||||
|
@ -86,7 +86,7 @@ class AlbumRowBinder(
|
||||||
/**
|
/**
|
||||||
* Handles the star / unstar action for an album
|
* Handles the star / unstar action for an album
|
||||||
*/
|
*/
|
||||||
private fun onStarClick(entry: MusicDirectory.Album, star: ImageView) {
|
private fun onStarClick(entry: Album, star: ImageView) {
|
||||||
entry.starred = !entry.starred
|
entry.starred = !entry.starred
|
||||||
star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
|
star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
|
||||||
val musicService = getMusicService()
|
val musicService = getMusicService()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* ArtistRowAdapter.kt
|
* ArtistRowBinder.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.drakeet.multitype.ItemViewBinder
|
import com.drakeet.multitype.ItemViewBinder
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
|
@ -57,7 +58,7 @@ class ArtistRowBinder(
|
||||||
|
|
||||||
holder.coverArtId = item.coverArt
|
holder.coverArtId = item.coverArt
|
||||||
|
|
||||||
if (Settings.shouldShowArtistPicture) {
|
if (showArtistPicture()) {
|
||||||
holder.coverArt.visibility = View.VISIBLE
|
holder.coverArt.visibility = View.VISIBLE
|
||||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||||
imageLoader.loadImage(
|
imageLoader.loadImage(
|
||||||
|
@ -102,11 +103,16 @@ class ArtistRowBinder(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSectionFromName(name: String): String {
|
private fun getSectionFromName(name: String): String {
|
||||||
var section = name.first().uppercaseChar()
|
if (name.isEmpty()) return SECTION_KEY_DEFAULT
|
||||||
if (!section.isLetter()) section = '#'
|
val section = name.first().uppercaseChar()
|
||||||
|
if (!section.isLetter()) return SECTION_KEY_DEFAULT
|
||||||
return section.toString()
|
return section.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showArtistPicture(): Boolean {
|
||||||
|
return ActiveServerProvider.isID3Enabled() && Settings.shouldShowArtistPicture
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of our ViewHolder class
|
* Creates an instance of our ViewHolder class
|
||||||
*/
|
*/
|
||||||
|
@ -123,4 +129,8 @@ class ArtistRowBinder(
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SECTION_KEY_DEFAULT = "#"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
|
||||||
throw IllegalAccessException("You must use submitList() to add data to the Adapter")
|
throw IllegalAccessException("You must use submitList() to add data to the Adapter")
|
||||||
}
|
}
|
||||||
|
|
||||||
var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
private var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
||||||
AdapterListUpdateCallback(this),
|
AdapterListUpdateCallback(this),
|
||||||
AsyncDifferConfig.Builder(diffCallback).build()
|
AsyncDifferConfig.Builder(diffCallback).build()
|
||||||
)
|
)
|
||||||
|
@ -182,12 +182,11 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
|
||||||
|
|
||||||
// Select them all
|
// Select them all
|
||||||
getCurrentList().mapNotNullTo(
|
getCurrentList().mapNotNullTo(
|
||||||
selectedSet,
|
selectedSet
|
||||||
{ entry ->
|
) { entry ->
|
||||||
// Exclude any -1 ids, eg. headers and other UI elements
|
// Exclude any -1 ids, eg. headers and other UI elements
|
||||||
entry.longId.takeIf { it != -1L }
|
entry.longId.takeIf { it != -1L }
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
return selectedSet.count()
|
return selectedSet.count()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
@ -30,7 +30,7 @@ import org.moire.ultrasonic.util.Util
|
||||||
*/
|
*/
|
||||||
internal class ServerRowAdapter(
|
internal class ServerRowAdapter(
|
||||||
private var context: Context,
|
private var context: Context,
|
||||||
private var data: Array<ServerSetting>,
|
passedData: Array<ServerSetting>,
|
||||||
private val model: ServerSettingsModel,
|
private val model: ServerSettingsModel,
|
||||||
private val activeServerProvider: ActiveServerProvider,
|
private val activeServerProvider: ActiveServerProvider,
|
||||||
private val manageMode: Boolean,
|
private val manageMode: Boolean,
|
||||||
|
@ -38,6 +38,12 @@ internal class ServerRowAdapter(
|
||||||
private val serverEditRequestedCallback: ((Int) -> Unit)
|
private val serverEditRequestedCallback: ((Int) -> Unit)
|
||||||
) : BaseAdapter() {
|
) : BaseAdapter() {
|
||||||
|
|
||||||
|
private var data: MutableList<ServerSetting> = mutableListOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
setData(passedData)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MENU_ID_EDIT = 1
|
private const val MENU_ID_EDIT = 1
|
||||||
private const val MENU_ID_DELETE = 2
|
private const val MENU_ID_DELETE = 2
|
||||||
|
@ -49,12 +55,19 @@ internal class ServerRowAdapter(
|
||||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
|
||||||
fun setData(data: Array<ServerSetting>) {
|
fun setData(data: Array<ServerSetting>) {
|
||||||
this.data = data
|
this.data.clear()
|
||||||
|
|
||||||
|
// In read mode show the offline server as well
|
||||||
|
if (!manageMode) {
|
||||||
|
this.data.add(ActiveServerProvider.OFFLINE_DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data.addAll(data)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCount(): Int {
|
override fun getCount(): Int {
|
||||||
return if (manageMode) data.size else data.size + 1
|
return data.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItem(position: Int): Any {
|
override fun getItem(position: Int): Any {
|
||||||
|
@ -69,11 +82,11 @@ internal class ServerRowAdapter(
|
||||||
* Creates the Row representation of a Server Setting
|
* Creates the Row representation of a Server Setting
|
||||||
*/
|
*/
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
|
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
||||||
var index = position
|
var position = pos
|
||||||
|
|
||||||
// Skip "Offline" in manage mode
|
// Skip "Offline" in manage mode
|
||||||
if (manageMode) index++
|
if (manageMode) position++
|
||||||
|
|
||||||
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)
|
||||||
|
@ -83,22 +96,17 @@ internal class ServerRowAdapter(
|
||||||
val layout = vi?.findViewById<ConstraintLayout>(R.id.server_layout)
|
val layout = vi?.findViewById<ConstraintLayout>(R.id.server_layout)
|
||||||
val image = vi?.findViewById<ImageView>(R.id.server_image)
|
val image = vi?.findViewById<ImageView>(R.id.server_image)
|
||||||
val serverMenu = vi?.findViewById<ImageButton>(R.id.server_menu)
|
val serverMenu = vi?.findViewById<ImageButton>(R.id.server_menu)
|
||||||
val setting = data.singleOrNull { t -> t.index == index }
|
val setting = data.singleOrNull { t -> t.index == position }
|
||||||
|
|
||||||
if (index == 0) {
|
text?.text = setting?.name ?: ""
|
||||||
text?.text = context.getString(R.string.main_offline)
|
description?.text = setting?.url ?: ""
|
||||||
description?.text = ""
|
if (setting == null) serverMenu?.visibility = View.INVISIBLE
|
||||||
} else {
|
|
||||||
text?.text = setting?.name ?: ""
|
|
||||||
description?.text = setting?.url ?: ""
|
|
||||||
if (setting == null) serverMenu?.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
val icon: Drawable?
|
val icon: Drawable?
|
||||||
val background: Drawable?
|
val background: Drawable?
|
||||||
|
|
||||||
// Configure icons for the row
|
// Configure icons for the row
|
||||||
if (index == 0) {
|
if (setting?.id == ActiveServerProvider.OFFLINE_DB_ID) {
|
||||||
serverMenu?.visibility = View.INVISIBLE
|
serverMenu?.visibility = View.INVISIBLE
|
||||||
icon = Util.getDrawableFromAttribute(context, R.attr.screen_on_off)
|
icon = Util.getDrawableFromAttribute(context, R.attr.screen_on_off)
|
||||||
background = ContextCompat.getDrawable(context, R.drawable.circle)
|
background = ContextCompat.getDrawable(context, R.drawable.circle)
|
||||||
|
@ -116,7 +124,7 @@ internal class ServerRowAdapter(
|
||||||
image?.background = background
|
image?.background = background
|
||||||
|
|
||||||
// Highlight the Active Server's row by changing its background
|
// Highlight the Active Server's row by changing its background
|
||||||
if (index == activeServerProvider.getActiveServer().index) {
|
if (position == activeServerProvider.getActiveServer().index) {
|
||||||
layout?.background = ContextCompat.getDrawable(context, R.drawable.select_ripple)
|
layout?.background = ContextCompat.getDrawable(context, R.drawable.select_ripple)
|
||||||
} else {
|
} else {
|
||||||
layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple)
|
layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple)
|
||||||
|
@ -128,7 +136,7 @@ internal class ServerRowAdapter(
|
||||||
R.drawable.select_ripple_circle
|
R.drawable.select_ripple_circle
|
||||||
)
|
)
|
||||||
|
|
||||||
serverMenu?.setOnClickListener { view -> serverMenuClick(view, index) }
|
serverMenu?.setOnClickListener { view -> serverMenuClick(view, position) }
|
||||||
|
|
||||||
return vi
|
return vi
|
||||||
}
|
}
|
||||||
|
@ -192,7 +200,8 @@ internal class ServerRowAdapter(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
MENU_ID_DELETE -> {
|
MENU_ID_DELETE -> {
|
||||||
serverDeletedCallback.invoke(position)
|
val server = getItem(position) as ServerSetting
|
||||||
|
serverDeletedCallback.invoke(server.id)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
MENU_ID_UP -> {
|
MENU_ID_UP -> {
|
||||||
|
|
|
@ -12,12 +12,12 @@ 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
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.DownloadFile
|
import org.moire.ultrasonic.service.DownloadFile
|
||||||
import org.moire.ultrasonic.service.Downloader
|
import org.moire.ultrasonic.service.Downloader
|
||||||
|
|
||||||
class TrackViewBinder(
|
class TrackViewBinder(
|
||||||
val onItemClick: (DownloadFile) -> Unit,
|
val onItemClick: (DownloadFile, Int) -> Unit,
|
||||||
val onContextMenuClick: ((MenuItem, DownloadFile) -> Boolean)? = null,
|
val onContextMenuClick: ((MenuItem, DownloadFile) -> Boolean)? = null,
|
||||||
val checkable: Boolean,
|
val checkable: Boolean,
|
||||||
val draggable: Boolean,
|
val draggable: Boolean,
|
||||||
|
@ -29,7 +29,7 @@ class TrackViewBinder(
|
||||||
|
|
||||||
// Set our layout files
|
// Set our layout files
|
||||||
val layout = R.layout.list_item_track
|
val layout = R.layout.list_item_track
|
||||||
val contextMenuLayout = R.menu.context_menu_track
|
private val contextMenuLayout = R.menu.context_menu_track
|
||||||
|
|
||||||
private val downloader: Downloader by inject()
|
private val downloader: Downloader by inject()
|
||||||
private val imageHelper: Utils.ImageHelper = Utils.ImageHelper(context)
|
private val imageHelper: Utils.ImageHelper = Utils.ImageHelper(context)
|
||||||
|
@ -41,15 +41,14 @@ class TrackViewBinder(
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
|
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
|
||||||
val downloadFile: DownloadFile?
|
|
||||||
val diffAdapter = adapter as BaseAdapter<*>
|
val diffAdapter = adapter as BaseAdapter<*>
|
||||||
|
|
||||||
when (item) {
|
val downloadFile: DownloadFile = when (item) {
|
||||||
is MusicDirectory.Entry -> {
|
is Track -> {
|
||||||
downloadFile = downloader.getDownloadFileForSong(item)
|
downloader.getDownloadFileForSong(item)
|
||||||
}
|
}
|
||||||
is DownloadFile -> {
|
is DownloadFile -> {
|
||||||
downloadFile = item
|
item
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
return
|
return
|
||||||
|
@ -77,7 +76,7 @@ class TrackViewBinder(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Minimize or maximize the Text view (if song title is very long)
|
// Minimize or maximize the Text view (if song title is very long)
|
||||||
if (!downloadFile.song.isDirectory) {
|
if (!downloadFile.track.isDirectory) {
|
||||||
holder.maximizeOrMinimize()
|
holder.maximizeOrMinimize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,11 +85,11 @@ class TrackViewBinder(
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemView.setOnClickListener {
|
holder.itemView.setOnClickListener {
|
||||||
if (checkable && !downloadFile.song.isVideo) {
|
if (checkable && !downloadFile.track.isVideo) {
|
||||||
val nowChecked = !holder.check.isChecked
|
val nowChecked = !holder.check.isChecked
|
||||||
holder.isChecked = nowChecked
|
holder.isChecked = nowChecked
|
||||||
} else {
|
} else {
|
||||||
onItemClick(downloadFile)
|
onItemClick(downloadFile, holder.bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,41 +102,37 @@ class TrackViewBinder(
|
||||||
|
|
||||||
// Notify the adapter of selection changes
|
// Notify the adapter of selection changes
|
||||||
holder.observableChecked.observe(
|
holder.observableChecked.observe(
|
||||||
lifecycleOwner,
|
lifecycleOwner
|
||||||
{ isCheckedNow ->
|
) { isCheckedNow ->
|
||||||
if (isCheckedNow) {
|
if (isCheckedNow) {
|
||||||
diffAdapter.notifySelected(holder.entry!!.longId)
|
diffAdapter.notifySelected(holder.entry!!.longId)
|
||||||
} else {
|
} else {
|
||||||
diffAdapter.notifyUnselected(holder.entry!!.longId)
|
diffAdapter.notifyUnselected(holder.entry!!.longId)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
// Listen to changes in selection status and update ourselves
|
// Listen to changes in selection status and update ourselves
|
||||||
diffAdapter.selectionRevision.observe(
|
diffAdapter.selectionRevision.observe(
|
||||||
lifecycleOwner,
|
lifecycleOwner
|
||||||
{
|
) {
|
||||||
val newStatus = diffAdapter.isSelected(item.longId)
|
val newStatus = diffAdapter.isSelected(item.longId)
|
||||||
|
|
||||||
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
|
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// Observe download status
|
// Observe download status
|
||||||
downloadFile.status.observe(
|
downloadFile.status.observe(
|
||||||
lifecycleOwner,
|
lifecycleOwner
|
||||||
{
|
) {
|
||||||
holder.updateStatus(it)
|
holder.updateStatus(it)
|
||||||
diffAdapter.notifyChanged()
|
diffAdapter.notifyChanged()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
downloadFile.progress.observe(
|
downloadFile.progress.observe(
|
||||||
lifecycleOwner,
|
lifecycleOwner
|
||||||
{
|
) {
|
||||||
holder.updateProgress(it)
|
holder.updateProgress(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewRecycled(holder: TrackViewHolder) {
|
override fun onViewRecycled(holder: TrackViewHolder) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.DownloadFile
|
import org.moire.ultrasonic.service.DownloadFile
|
||||||
import org.moire.ultrasonic.service.DownloadStatus
|
import org.moire.ultrasonic.service.DownloadStatus
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
|
@ -44,7 +44,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
var duration: TextView = view.findViewById(R.id.song_duration)
|
var duration: TextView = view.findViewById(R.id.song_duration)
|
||||||
var progress: TextView = view.findViewById(R.id.song_status)
|
var progress: TextView = view.findViewById(R.id.song_status)
|
||||||
|
|
||||||
var entry: MusicDirectory.Entry? = null
|
var entry: Track? = null
|
||||||
private set
|
private set
|
||||||
var downloadFile: DownloadFile? = null
|
var downloadFile: DownloadFile? = null
|
||||||
private set
|
private set
|
||||||
|
@ -67,7 +67,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
isSelected: Boolean = false
|
isSelected: Boolean = false
|
||||||
) {
|
) {
|
||||||
val useFiveStarRating = Settings.useFiveStarRating
|
val useFiveStarRating = Settings.useFiveStarRating
|
||||||
val song = file.song
|
val song = file.track
|
||||||
downloadFile = file
|
downloadFile = file
|
||||||
entry = song
|
entry = song
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
}
|
}
|
||||||
|
|
||||||
rxSubscription = RxBus.playerStateObservable.subscribe {
|
rxSubscription = RxBus.playerStateObservable.subscribe {
|
||||||
setPlayIcon(it.track == downloadFile)
|
setPlayIcon(it.index == bindingAdapterPosition && it.track == downloadFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupStarButtons(song: MusicDirectory.Entry, useFiveStarRating: Boolean) {
|
private fun setupStarButtons(song: Track, useFiveStarRating: Boolean) {
|
||||||
if (useFiveStarRating) {
|
if (useFiveStarRating) {
|
||||||
// Hide single star
|
// Hide single star
|
||||||
star.isVisible = false
|
star.isVisible = false
|
||||||
|
@ -153,6 +153,8 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
star.setImageDrawable(imageHelper.starHollowDrawable)
|
star.setImageDrawable(imageHelper.starHollowDrawable)
|
||||||
song.starred = false
|
song.starred = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should this be done here ?
|
||||||
Thread {
|
Thread {
|
||||||
val musicService = MusicServiceFactory.getMusicService()
|
val musicService = MusicServiceFactory.getMusicService()
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -2,8 +2,12 @@ package org.moire.ultrasonic.app
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
import org.koin.core.logger.Level
|
import org.koin.core.logger.Level
|
||||||
import org.moire.ultrasonic.BuildConfig
|
import org.moire.ultrasonic.BuildConfig
|
||||||
import org.moire.ultrasonic.di.appPermanentStorage
|
import org.moire.ultrasonic.di.appPermanentStorage
|
||||||
|
@ -23,22 +27,39 @@ import timber.log.Timber.DebugTree
|
||||||
|
|
||||||
class UApp : MultiDexApplication() {
|
class UApp : MultiDexApplication() {
|
||||||
|
|
||||||
|
private var ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
instance = this
|
instance = this
|
||||||
|
// if (BuildConfig.DEBUG)
|
||||||
|
// StrictMode.enableDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var initiated = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
initiated = true
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Timber.plant(DebugTree())
|
Timber.plant(DebugTree())
|
||||||
}
|
}
|
||||||
if (Settings.debugLogToFile) {
|
|
||||||
FileLoggerTree.plantToTimberForest()
|
Timber.d("onCreate called")
|
||||||
|
|
||||||
|
// In general we should not access the settings from the main thread to avoid blocking...
|
||||||
|
ioScope.launch {
|
||||||
|
if (Settings.debugLogToFile) {
|
||||||
|
FileLoggerTree.plantToTimberForest()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startKoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun startKoin() {
|
||||||
startKoin {
|
startKoin {
|
||||||
// TODO Currently there is a bug in Koin which makes necessary to set the loglevel to ERROR
|
// TODO Currently there is a bug in Koin which makes necessary to set the log level to ERROR
|
||||||
logger(TimberKoinLogger(Level.ERROR))
|
logger(TimberKoinLogger(Level.ERROR))
|
||||||
// logger(TimberKoinLogger(Level.INFO))
|
// logger(TimberKoinLogger(Level.INFO))
|
||||||
|
|
||||||
|
@ -55,8 +76,13 @@ class UApp : MultiDexApplication() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun shutdownKoin() {
|
||||||
|
stopKoin()
|
||||||
|
initiated = false
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: UApp? = null
|
var instance: UApp? = null
|
||||||
|
|
||||||
fun applicationContext(): Context {
|
fun applicationContext(): Context {
|
||||||
return instance!!.applicationContext
|
return instance!!.applicationContext
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
/*
|
||||||
|
* ActiveServerProvider.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -11,6 +17,7 @@ import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.di.DB_FILENAME
|
import org.moire.ultrasonic.di.DB_FILENAME
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
||||||
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
@ -19,8 +26,6 @@ import timber.log.Timber
|
||||||
/**
|
/**
|
||||||
* This class can be used to retrieve the properties of the Active Server
|
* This class can be used to retrieve the properties of the Active Server
|
||||||
* It caches the settings read up from the DB to improve performance.
|
* It caches the settings read up from the DB to improve performance.
|
||||||
*
|
|
||||||
* TODO: There seems to be some confusion whether offline id is 0 or -1. Clean this up (carefully!)
|
|
||||||
*/
|
*/
|
||||||
class ActiveServerProvider(
|
class ActiveServerProvider(
|
||||||
private val repository: ServerSettingDao
|
private val repository: ServerSettingDao
|
||||||
|
@ -35,7 +40,7 @@ class ActiveServerProvider(
|
||||||
*/
|
*/
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun getActiveServer(serverId: Int = getActiveServerId()): ServerSetting {
|
fun getActiveServer(serverId: Int = getActiveServerId()): ServerSetting {
|
||||||
if (serverId > 0) {
|
if (serverId > OFFLINE_DB_ID) {
|
||||||
if (cachedServer != null && cachedServer!!.id == serverId) return cachedServer!!
|
if (cachedServer != null && cachedServer!!.id == serverId) return cachedServer!!
|
||||||
|
|
||||||
// Ideally this is the only call where we block the thread while using the repository
|
// Ideally this is the only call where we block the thread while using the repository
|
||||||
|
@ -53,22 +58,31 @@ class ActiveServerProvider(
|
||||||
return cachedServer!!
|
return cachedServer!!
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveServerId(0)
|
// Fallback to Offline
|
||||||
|
setActiveServerById(OFFLINE_DB_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ServerSetting(
|
return OFFLINE_DB
|
||||||
id = -1,
|
}
|
||||||
index = 0,
|
|
||||||
name = UApp.applicationContext().getString(R.string.main_offline),
|
/**
|
||||||
url = "http://localhost",
|
* Resolves the index (sort order) of a server to its id (unique)
|
||||||
userName = "",
|
* @param index: The index of the server in the server selector
|
||||||
password = "",
|
* @return id: The unique id of the server
|
||||||
jukeboxByDefault = false,
|
*/
|
||||||
allowSelfSignedCertificate = false,
|
fun getServerIdFromIndex(index: Int): Int {
|
||||||
ldapSupport = false,
|
if (index <= OFFLINE_DB_INDEX) {
|
||||||
musicFolderId = "",
|
// Offline mode is selected
|
||||||
minimumApiVersion = null
|
return OFFLINE_DB_ID
|
||||||
)
|
}
|
||||||
|
|
||||||
|
var id: Int
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
id = repository.findByIndex(index)?.id ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,15 +91,15 @@ class ActiveServerProvider(
|
||||||
*/
|
*/
|
||||||
fun setActiveServerByIndex(index: Int) {
|
fun setActiveServerByIndex(index: Int) {
|
||||||
Timber.d("setActiveServerByIndex $index")
|
Timber.d("setActiveServerByIndex $index")
|
||||||
if (index < 1) {
|
if (index <= OFFLINE_DB_INDEX) {
|
||||||
// Offline mode is selected
|
// Offline mode is selected
|
||||||
setActiveServerId(0)
|
setActiveServerById(OFFLINE_DB_ID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
val serverId = repository.findByIndex(index)?.id ?: 0
|
val serverId = repository.findByIndex(index)?.id ?: 0
|
||||||
setActiveServerId(serverId)
|
setActiveServerById(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,21 +117,22 @@ class ActiveServerProvider(
|
||||||
|
|
||||||
Timber.i("Switching to new database, id:$activeServer")
|
Timber.i("Switching to new database, id:$activeServer")
|
||||||
cachedServerId = activeServer
|
cachedServerId = activeServer
|
||||||
return Room.databaseBuilder(
|
cachedDatabase = initDatabase(activeServer)
|
||||||
UApp.applicationContext(),
|
|
||||||
MetaDatabase::class.java,
|
return cachedDatabase!!
|
||||||
METADATA_DB + cachedServerId
|
|
||||||
)
|
|
||||||
.fallbackToDestructiveMigrationOnDowngrade()
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val offlineMetaDatabase: MetaDatabase by lazy {
|
val offlineMetaDatabase: MetaDatabase by lazy {
|
||||||
Room.databaseBuilder(
|
initDatabase(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDatabase(serverId: Int): MetaDatabase {
|
||||||
|
return Room.databaseBuilder(
|
||||||
UApp.applicationContext(),
|
UApp.applicationContext(),
|
||||||
MetaDatabase::class.java,
|
MetaDatabase::class.java,
|
||||||
METADATA_DB + 0
|
METADATA_DB + serverId
|
||||||
)
|
)
|
||||||
|
.addMigrations(META_MIGRATION_2_3)
|
||||||
.fallbackToDestructiveMigrationOnDowngrade()
|
.fallbackToDestructiveMigrationOnDowngrade()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -177,16 +192,30 @@ class ActiveServerProvider(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val METADATA_DB = "$DB_FILENAME-meta-"
|
const val METADATA_DB = "$DB_FILENAME-meta-"
|
||||||
val liveActiveServerId: MutableLiveData<Int> = MutableLiveData(getActiveServerId())
|
const val OFFLINE_DB_ID = -1
|
||||||
|
const val OFFLINE_DB_INDEX = 0
|
||||||
|
|
||||||
|
val OFFLINE_DB = ServerSetting(
|
||||||
|
id = OFFLINE_DB_ID,
|
||||||
|
index = OFFLINE_DB_INDEX,
|
||||||
|
name = UApp.applicationContext().getString(R.string.main_offline),
|
||||||
|
url = "http://localhost",
|
||||||
|
userName = "",
|
||||||
|
password = "",
|
||||||
|
jukeboxByDefault = false,
|
||||||
|
allowSelfSignedCertificate = false,
|
||||||
|
ldapSupport = false,
|
||||||
|
musicFolderId = "",
|
||||||
|
minimumApiVersion = null
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries if the Active Server is the "Offline" mode of Ultrasonic
|
* Queries if the Active Server is the "Offline" mode of Ultrasonic
|
||||||
* @return True, if the "Offline" mode is selected
|
* @return True, if the "Offline" mode is selected
|
||||||
*/
|
*/
|
||||||
fun isOffline(): Boolean {
|
fun isOffline(): Boolean {
|
||||||
return getActiveServerId() < 1
|
return getActiveServerId() == OFFLINE_DB_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -197,13 +226,16 @@ class ActiveServerProvider(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the Id of the Active Server
|
* Sets the Active Server by its unique id
|
||||||
|
* @param serverId: The id of the desired server
|
||||||
*/
|
*/
|
||||||
fun setActiveServerId(serverId: Int) {
|
fun setActiveServerById(serverId: Int) {
|
||||||
resetMusicService()
|
resetMusicService()
|
||||||
|
|
||||||
Settings.activeServer = serverId
|
Settings.activeServer = serverId
|
||||||
liveActiveServerId.postValue(serverId)
|
|
||||||
|
Timber.i("setActiveServerById done, new id: %s", serverId)
|
||||||
|
RxBus.activeServerChangePublisher.onNext(serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -217,6 +249,13 @@ class ActiveServerProvider(
|
||||||
return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false)
|
return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries if ID3 tags should be used
|
||||||
|
*/
|
||||||
|
fun isID3Enabled(): Boolean {
|
||||||
|
return Settings.shouldUseId3Tags && (!isOffline() || Settings.useId3TagsOffline)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries if Server Scaling is enabled
|
* Queries if Server Scaling is enabled
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import org.moire.ultrasonic.domain.Album
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface AlbumDao : GenericDao<Album> {
|
||||||
|
/**
|
||||||
|
* Clear the whole database
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM albums")
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all albums
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM albums")
|
||||||
|
fun get(): List<Album>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all albums in a specific range
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM albums LIMIT :offset,:size")
|
||||||
|
fun get(size: Int, offset: Int = 0): List<Album>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get album by id
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM albums where id LIKE :albumId LIMIT 1")
|
||||||
|
fun get(albumId: String): Album
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get albums by artist
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM albums WHERE artistId LIKE :id")
|
||||||
|
fun byArtist(id: String): List<Album>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear albums by artist
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM albums WHERE artistId LIKE :id")
|
||||||
|
fun clearByArtist(id: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Make generic
|
||||||
|
* Upserts (insert or update) an object to the database
|
||||||
|
*
|
||||||
|
* @param obj the object to upsert
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
@JvmSuppressWildcards
|
||||||
|
fun upsert(obj: Album) {
|
||||||
|
val id = insertIgnoring(obj)
|
||||||
|
if (id == -1L) {
|
||||||
|
update(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upserts (insert or update) a list of objects
|
||||||
|
*
|
||||||
|
* @param objList the object to be upserted
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
@JvmSuppressWildcards
|
||||||
|
fun upsert(objList: List<Album>) {
|
||||||
|
val insertResult = insertIgnoring(objList)
|
||||||
|
val updateList: MutableList<Album> = ArrayList()
|
||||||
|
for (i in insertResult.indices) {
|
||||||
|
if (insertResult[i] == -1L) {
|
||||||
|
updateList.add(objList[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateList.isNotEmpty()) {
|
||||||
|
update(updateList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
* Room Database to be used to store global data for the whole app.
|
* Room Database to be used to store global data for the whole app.
|
||||||
* This could be settings or data that are not specific to any remote music database
|
* This could be settings or data that are not specific to any remote music database
|
||||||
*/
|
*/
|
||||||
@Database(entities = [ServerSetting::class], version = 4)
|
@Database(
|
||||||
|
entities = [ServerSetting::class],
|
||||||
|
version = 5,
|
||||||
|
exportSchema = true
|
||||||
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -175,3 +179,89 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO `_new_ServerSetting` (
|
||||||
|
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
||||||
|
`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,
|
||||||
|
`podcastSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`
|
||||||
|
)
|
||||||
|
SELECT `ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,
|
||||||
|
`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,
|
||||||
|
`bookmarkSupport`,`name`,`podcastSupport`,`id`,`allowSelfSignedCertificate`,
|
||||||
|
`chatSupport`
|
||||||
|
FROM `ServerSetting`
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL("DROP TABLE `ServerSetting`")
|
||||||
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
||||||
|
`id` INTEGER PRIMARY KEY 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
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO `_new_ServerSetting` (
|
||||||
|
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
||||||
|
`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,
|
||||||
|
`podcastSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`
|
||||||
|
)
|
||||||
|
SELECT `ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,
|
||||||
|
`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,
|
||||||
|
`bookmarkSupport`,`name`,`podcastSupport`,`id`,`allowSelfSignedCertificate`,
|
||||||
|
`chatSupport`
|
||||||
|
FROM `ServerSetting`
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
database.execSQL("DROP TABLE `ServerSetting`")
|
||||||
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import androidx.room.Query
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ArtistsDao {
|
interface ArtistDao {
|
||||||
/**
|
/**
|
||||||
* Insert a list in the database. If the item already exists, replace it.
|
* Insert a list in the database. If the item already exists, replace it.
|
||||||
*
|
*
|
||||||
|
@ -43,5 +43,5 @@ interface ArtistsDao {
|
||||||
* Get artist by id
|
* Get artist by id
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM artists WHERE id LIKE :id")
|
@Query("SELECT * FROM artists WHERE id LIKE :id")
|
||||||
fun get(id: String): Artist
|
fun get(id: String): Artist?
|
||||||
}
|
}
|
|
@ -53,6 +53,7 @@ interface IndexDao : GenericDao<Index> {
|
||||||
fun get(musicFolderId: String): List<Index>
|
fun get(musicFolderId: String): List<Index>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO: Make generic
|
||||||
* Upserts (insert or update) an object to the database
|
* Upserts (insert or update) an object to the database
|
||||||
*
|
*
|
||||||
* @param obj the object to upsert
|
* @param obj the object to upsert
|
||||||
|
|
|
@ -1,23 +1,85 @@
|
||||||
|
/*
|
||||||
|
* MetaDatabase.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import java.util.Date
|
||||||
|
import org.moire.ultrasonic.domain.Album
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.Index
|
import org.moire.ultrasonic.domain.Index
|
||||||
import org.moire.ultrasonic.domain.MusicFolder
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
|
import org.moire.ultrasonic.domain.Track
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This database is used to store and cache the ID3 metadata
|
* This database is used to store and cache the ID3 metadata
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Artist::class, Index::class, MusicFolder::class],
|
entities = [
|
||||||
version = 1
|
Artist::class,
|
||||||
|
Album::class,
|
||||||
|
Track::class,
|
||||||
|
Index::class,
|
||||||
|
MusicFolder::class
|
||||||
|
],
|
||||||
|
autoMigrations = [
|
||||||
|
AutoMigration(
|
||||||
|
from = 1,
|
||||||
|
to = 2
|
||||||
|
),
|
||||||
|
],
|
||||||
|
exportSchema = true,
|
||||||
|
version = 3
|
||||||
)
|
)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
abstract class MetaDatabase : RoomDatabase() {
|
abstract class MetaDatabase : RoomDatabase() {
|
||||||
abstract fun artistsDao(): ArtistsDao
|
abstract fun artistDao(): ArtistDao
|
||||||
|
|
||||||
|
abstract fun albumDao(): AlbumDao
|
||||||
|
|
||||||
|
abstract fun trackDao(): TrackDao
|
||||||
|
|
||||||
abstract fun musicFoldersDao(): MusicFoldersDao
|
abstract fun musicFoldersDao(): MusicFoldersDao
|
||||||
|
|
||||||
abstract fun indexDao(): IndexDao
|
abstract fun indexDao(): IndexDao
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
@TypeConverter
|
||||||
|
fun fromTimestamp(value: Long?): Date? {
|
||||||
|
return value?.let { Date(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun dateToTimestamp(date: Date?): Long? {
|
||||||
|
return date?.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ktlint-disable max-line-length */
|
||||||
|
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("DROP TABLE `albums`")
|
||||||
|
database.execSQL("DROP TABLE `indexes`")
|
||||||
|
database.execSQL("DROP TABLE `artists`")
|
||||||
|
database.execSQL("DROP TABLE `tracks`")
|
||||||
|
database.execSQL("DROP TABLE `music_folders`")
|
||||||
|
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`))")
|
||||||
|
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`))")
|
||||||
|
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`))")
|
||||||
|
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`))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ktlint-enable max-line-length */
|
||||||
|
|
|
@ -19,7 +19,8 @@ import androidx.room.PrimaryKey
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
data class ServerSetting(
|
data class ServerSetting(
|
||||||
@PrimaryKey var id: Int,
|
// Default ID is 0, which will trigger SQLite to generate a unique ID.
|
||||||
|
@PrimaryKey(autoGenerate = true) var id: Int = 0,
|
||||||
@ColumnInfo(name = "index") var index: Int,
|
@ColumnInfo(name = "index") var index: Int,
|
||||||
@ColumnInfo(name = "name") var name: String,
|
@ColumnInfo(name = "name") var name: String,
|
||||||
@ColumnInfo(name = "url") var url: String,
|
@ColumnInfo(name = "url") var url: String,
|
||||||
|
@ -37,9 +38,6 @@ data class ServerSetting(
|
||||||
@ColumnInfo(name = "podcastSupport") var podcastSupport: Boolean? = null
|
@ColumnInfo(name = "podcastSupport") var podcastSupport: Boolean? = null
|
||||||
) {
|
) {
|
||||||
constructor() : this (
|
constructor() : this (
|
||||||
-1, 0, "", "", null, "", "", false, false, false, null, null
|
0, 0, "", "", null, "", "", false, false, false, null, null
|
||||||
)
|
|
||||||
constructor(name: String, url: String) : this(
|
|
||||||
-1, 0, name, url, null, "", "", false, false, false, null, null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,12 +69,6 @@ interface ServerSettingDao {
|
||||||
@Query("SELECT COUNT(*) FROM serverSetting")
|
@Query("SELECT COUNT(*) FROM serverSetting")
|
||||||
fun liveServerCount(): LiveData<Int?>
|
fun liveServerCount(): LiveData<Int?>
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the greatest value of the Id column in the table
|
|
||||||
*/
|
|
||||||
@Query("SELECT MAX([id]) FROM serverSetting")
|
|
||||||
suspend fun getMaxId(): Int?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the greatest value of the Index column in the table
|
* Retrieves the greatest value of the Index column in the table
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Query
|
||||||
|
import org.moire.ultrasonic.domain.Track
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
@Entity(tableName = "tracks")
|
||||||
|
interface TrackDao : GenericDao<Track> {
|
||||||
|
/**
|
||||||
|
* Clear the whole database
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM tracks")
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all albums
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM tracks")
|
||||||
|
fun get(): List<Track>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get albums by artist
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM tracks WHERE albumId LIKE :id")
|
||||||
|
fun byAlbum(id: String): List<Track>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get albums by artist
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM tracks WHERE artistId LIKE :id")
|
||||||
|
fun byArtist(id: String): List<Track>
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue