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:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/android:api-30
|
||||
- image: cimg/android:2022.06.1
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: -Xmx3200m
|
||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -18,6 +27,7 @@ jobs:
|
|||
command: |
|
||||
sed -i '/^org.gradle.jvmargs/d' gradle.properties
|
||||
sed -i 's/^org.gradle.daemon=true/org.gradle.daemon=false/g' gradle.properties
|
||||
cat gradle.properties
|
||||
- run:
|
||||
name: checkstyle
|
||||
command: ./gradlew -Pqc ktlintCheck
|
||||
|
@ -31,7 +41,6 @@ jobs:
|
|||
name: unit-tests
|
||||
command: |
|
||||
./gradlew ciTest testDebugUnitTest
|
||||
./gradlew jacocoFullReport
|
||||
- run:
|
||||
name: lint
|
||||
command: ./gradlew :ultrasonic:lintRelease
|
||||
|
@ -44,18 +53,16 @@ jobs:
|
|||
- save_cache:
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: v1-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
key: v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- store_artifacts:
|
||||
path: ultrasonic/build/reports
|
||||
destination: reports
|
||||
- store_artifacts:
|
||||
path: subsonic-api/build/reports
|
||||
destination: reports
|
||||
- store_artifacts:
|
||||
path: build/reports/jacoco/jacocoFullReport/
|
||||
push_translations:
|
||||
docker:
|
||||
- image: circleci/python:3.6
|
||||
- image: cimg/python:3.6
|
||||
working_directory: ~/ultrasonic
|
||||
steps:
|
||||
- checkout
|
||||
|
@ -75,8 +82,12 @@ jobs:
|
|||
tx push -s
|
||||
generate_signed_apk:
|
||||
docker:
|
||||
- image: circleci/android:api-30
|
||||
- image: cimg/android:2022.06.1
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -95,22 +106,22 @@ jobs:
|
|||
command: |
|
||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
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/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/30.0.0/apksigner verify --verbose /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/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/32.0.0/apksigner verify --verbose /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
|
||||
- persist_to_workspace:
|
||||
root: /tmp/ultrasonic-release
|
||||
paths:
|
||||
- ultrasonic-*.apk*
|
||||
publish_github_signed_apk:
|
||||
docker:
|
||||
- image: circleci/golang
|
||||
- image: cimg/go:1.18
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /tmp/ultrasonic-release
|
||||
- run:
|
||||
name: install ghr
|
||||
command: go get -v github.com/tcnksm/ghr
|
||||
command: go install -v github.com/tcnksm/ghr@latest
|
||||
- run:
|
||||
name: publish release on github tag
|
||||
command: ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} ${CIRCLE_TAG} /tmp/ultrasonic-release
|
||||
|
@ -129,7 +140,7 @@ workflows:
|
|||
- generate_signed_apk:
|
||||
filters:
|
||||
tags:
|
||||
only: /^[0-9]+(\.[0-9]+)*/
|
||||
only: /^[0-9]+(\.[0-9]+)*(-beta\.[0-9]+)?/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- publish_github_signed_apk:
|
||||
|
@ -137,7 +148,7 @@ workflows:
|
|||
- generate_signed_apk
|
||||
filters:
|
||||
tags:
|
||||
only: /^[0-9]+(\.[0-9]+)*/
|
||||
only: /^[0-9]+(\.[0-9]+)*(-beta\.[0-9]+)?/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ captures/
|
|||
*.iml
|
||||
.idea/
|
||||
|
||||
|
||||
# Keystore files
|
||||
*.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:
|
||||
|
||||
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
|
||||
Use `git commit --signoff` to acknowledge this.
|
||||
2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests
|
||||
should be written in this programming language.
|
||||
3. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
|
||||
4. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
|
||||
Use `git commit --signoff` to acknowledge this.
|
||||
2. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
|
||||
3. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
|
||||
Refactoring existing messes is great, but watch out for breakage.
|
||||
5. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
|
||||
4. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
|
||||
and test.
|
||||
|
||||
### Pull Request Process
|
||||
On each Pull Request Github runs a number of checks to make sure there are no problems.
|
||||
|
||||
#### Signed commits
|
||||
Commits must be signed. [See here how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
|
||||
|
||||
#### KtLint
|
||||
This programm checks if the source code is formatted correctly.
|
||||
You can run it yourself locally with
|
||||
|
||||
`./gradlew -Pqc ktlintFormat`
|
||||
|
||||
Running this command will fix common problems and will notify you of problems it couldn't fix automatically.
|
||||
|
||||
#### Detekt
|
||||
|
||||
Detekt is a static analyser. It helps to find potential bugs in our code.
|
||||
|
||||
You can run it yourself locally with
|
||||
|
||||
`./gradlew -Pqc detekt`
|
||||
|
||||
There is a "baseline" file, in which errors which have been in the code base before are noted.
|
||||
Sometimes it is necessary to regenerate this file by running:
|
||||
|
||||
`./gradlew -Pqc detektBaseline`
|
||||
|
||||
#### Lint
|
||||
Lint looks for general problems in the code or unused resources etc.
|
||||
You can run it with
|
||||
|
||||
`./gradlew -Pqc lintRelease`
|
||||
|
||||
If there is a need to regenerate the baseline, remove `ultrasonic/lint-baseline.xml` and rerun the command.
|
||||
|
||||
|
||||
1. Ensure [all commits are signed-off](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification).
|
||||
2. Check tests for the new code are added.
|
||||
3. Check code style is passing.
|
||||
4. Check code static analysis is passing.
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
## Problem description
|
||||
|
||||
Describe your problem here. Describe what you want to happen, and what happens
|
||||
if you try to do it. If you have a stack trace or any logs, please format them using
|
||||
github triple backquote notation
|
||||
Describe your problem here. Describe what you want to happen, and what
|
||||
happens if you try to do it. If you have a stack trace or any logs, please
|
||||
format them using GitHub triple backquote notation.
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
Describe how somebody else could observe the same behavior you do. Don't share here any logins and
|
||||
passwords!
|
||||
Describe how somebody else could observe the same behavior you do. Don't
|
||||
share here any logins and passwords!
|
||||
|
||||
## System information
|
||||
|
||||
### Ultrasonic client
|
||||
|
||||
* **Ultrasonic version**: *version of the app*
|
||||
* **Android version**: *Version of Android OS on the device*
|
||||
* **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
|
||||
|
||||
Include any extra notes here. Otherwise you may remove this section.
|
||||
|
|
64
README.md
64
README.md
|
@ -1,14 +1,25 @@
|
|||
# Ultrasonic
|
||||
[![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/)
|
||||
# WE HAVE MOVED
|
||||
|
||||
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
|
||||
|
||||
We currently don't have that much time to spend developing Subsonic, so any
|
||||
contributions or active developers are always welcomed.
|
||||
Have a look at [CONTRIBUTING](CONTRIBUTING.md) to get started.
|
||||
|
||||
## 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://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
|
||||
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
|
||||
|
||||
First, see if your issue haven’t been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
|
||||
otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
|
||||
First, see if your issue haven’t been yet reported [here][issues], otherwise
|
||||
open [a new issue][newissue].
|
||||
|
||||
### Known (not our) bugs
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -41,16 +54,29 @@ See [CONTRIBUTING](CONTRIBUTING.md).
|
|||
|
||||
## Supported (tested) Subsonic API implementations
|
||||
|
||||
- [Subsonic](http://www.subsonic.org/pages/index.jsp)
|
||||
- [Airsonic](https://github.com/airsonic/airsonic)
|
||||
- [Supysonic](https://github.com/spl0k/supysonic)
|
||||
- [Ampache](https://ampache.org/)
|
||||
- [Subsonic][subsonic]
|
||||
- [Airsonic-Advanced][airsonic]
|
||||
- [Supysonic][supysonic]
|
||||
- [Ampache][ampache]
|
||||
|
||||
Other *Subsonic API* implementations should work as well as long as they follow API
|
||||
[documentation](http://www.subsonic.org/pages/api.jsp).
|
||||
Other *Subsonic API* implementations should work as well as long as they
|
||||
follow API [documentation][subapi].
|
||||
|
||||
## 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.ktlintGradle
|
||||
classpath libs.detekt
|
||||
classpath libs.jacoco
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,8 +43,6 @@ allprojects {
|
|||
}
|
||||
}
|
||||
|
||||
apply from: 'gradle_scripts/jacoco.gradle'
|
||||
|
||||
wrapper {
|
||||
gradleVersion(libs.versions.gradle.get())
|
||||
distributionType("all")
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
apply from: bootstrap.androidModule
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
ext {
|
||||
jacocoExclude = [
|
||||
'**/domain/**'
|
||||
]
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.roomRuntime
|
||||
implementation libs.roomKtx
|
||||
|
|
|
@ -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
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "artists")
|
||||
@Entity(tableName = "artists", primaryKeys = ["id", "serverId"])
|
||||
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 index: String? = null,
|
||||
override var coverArt: String? = null,
|
||||
override var albumCount: Long? = null,
|
||||
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
|
||||
|
||||
import androidx.room.Ignore
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
abstract class ArtistOrIndex(
|
||||
@Ignore
|
||||
override var id: String,
|
||||
@Ignore
|
||||
open var serverId: Int,
|
||||
@Ignore
|
||||
override var name: String? = null,
|
||||
@Ignore
|
||||
open var index: String? = null,
|
||||
|
@ -18,15 +28,15 @@ abstract class ArtistOrIndex(
|
|||
) : GenericEntry() {
|
||||
|
||||
fun compareTo(other: ArtistOrIndex): Int {
|
||||
when {
|
||||
return when {
|
||||
this.closeness == other.closeness -> {
|
||||
return 0
|
||||
0
|
||||
}
|
||||
this.closeness > other.closeness -> {
|
||||
return -1
|
||||
-1
|
||||
}
|
||||
else -> {
|
||||
return 1
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.moire.ultrasonic.domain
|
|||
|
||||
import java.io.Serializable
|
||||
import java.util.Date
|
||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
||||
|
||||
data class Bookmark(
|
||||
val position: Int = 0,
|
||||
|
@ -10,7 +9,7 @@ data class Bookmark(
|
|||
val comment: String,
|
||||
val created: Date? = null,
|
||||
val changed: Date? = null,
|
||||
val entry: Entry
|
||||
val track: Track
|
||||
) : Serializable {
|
||||
companion object {
|
||||
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
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "indexes")
|
||||
@Entity(tableName = "indexes", primaryKeys = ["id", "serverId"])
|
||||
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 index: String? = null,
|
||||
override var coverArt: String? = null,
|
||||
override var albumCount: Long? = null,
|
||||
override var closeness: Int = 0,
|
||||
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
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
import java.util.Date
|
||||
|
||||
class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
||||
|
@ -20,9 +24,9 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||
return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
|
||||
}
|
||||
|
||||
fun getTracks(): List<Entry> {
|
||||
fun getTracks(): List<Track> {
|
||||
return mapNotNull {
|
||||
it as? Entry
|
||||
it as? Track
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,6 +38,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||
|
||||
abstract class Child : GenericEntry() {
|
||||
abstract override var id: String
|
||||
abstract var serverId: Int
|
||||
abstract var parent: String?
|
||||
abstract var isDirectory: Boolean
|
||||
abstract var album: String?
|
||||
|
@ -53,87 +58,4 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||
abstract var closeness: Int
|
||||
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
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* 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(
|
||||
@PrimaryKey override val id: String,
|
||||
override val name: String
|
||||
override val id: String,
|
||||
override val name: String,
|
||||
@ColumnInfo(defaultValue = "-1")
|
||||
var serverId: Int
|
||||
) : 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
|
||||
|
||||
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.
|
||||
*/
|
||||
data class SearchResult(
|
||||
val artists: List<ArtistOrIndex> = 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
|
||||
|
||||
import java.io.Serializable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
||||
|
||||
data class Share(
|
||||
override var id: String,
|
||||
|
@ -12,7 +11,7 @@ data class Share(
|
|||
var lastVisited: String? = null,
|
||||
var expires: String? = null,
|
||||
var visitCount: Long? = null,
|
||||
private val entries: MutableList<Entry> = mutableListOf()
|
||||
private val tracks: MutableList<Track> = mutableListOf()
|
||||
) : Serializable, GenericEntry() {
|
||||
override val name: String?
|
||||
get() {
|
||||
|
@ -22,12 +21,12 @@ data class Share(
|
|||
return null
|
||||
}
|
||||
|
||||
fun getEntries(): List<Entry> {
|
||||
return entries.toList()
|
||||
fun getEntries(): List<Track> {
|
||||
return tracks.toList()
|
||||
}
|
||||
|
||||
fun addEntry(entry: Entry) {
|
||||
entries.add(entry)
|
||||
fun addEntry(track: Track) {
|
||||
tracks.add(track)
|
||||
}
|
||||
|
||||
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.apacheCodecs
|
||||
}
|
||||
|
||||
ext {
|
||||
// Excluding data classes
|
||||
jacocoExclude = [
|
||||
'**/models/**',
|
||||
'**/di/**'
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import java.util.Locale
|
|||
import java.util.TimeZone
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okio.Okio
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should contain`
|
||||
import org.amshove.kluent.`should not be`
|
||||
|
@ -40,12 +41,12 @@ fun MockWebServer.enqueueResponse(resourceName: 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"))
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
|
|||
val contentType = responseBody?.contentType()
|
||||
if (
|
||||
contentType != null &&
|
||||
contentType.type().equals("application", true) &&
|
||||
contentType.subtype().equals("json", true)
|
||||
contentType.type.equals("application", true) &&
|
||||
contentType.subtype.equals("json", true)
|
||||
) {
|
||||
val error = SubsonicAPIClient.jacksonMapper.readValue<SubsonicResponse>(
|
||||
responseBody.byteStream()
|
||||
|
@ -40,11 +40,11 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
|
|||
* It creates Exceptions from the results returned by the Subsonic API
|
||||
*/
|
||||
@Suppress("ThrowsCount")
|
||||
fun <T : SubsonicResponse> Response<out T>.throwOnFailure(): Response<out T> {
|
||||
fun <T : SubsonicResponse> Response<T>.throwOnFailure(): Response<T> {
|
||||
val response = this
|
||||
|
||||
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
|
||||
return this as Response<T>
|
||||
return this
|
||||
}
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Server error, code: " + response.code())
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.security.cert.X509Certificate
|
|||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
@ -68,12 +69,24 @@ class SubsonicAPIClient(
|
|||
.addInterceptor { chain ->
|
||||
// Adds default request params
|
||||
val originalRequest = chain.request()
|
||||
val newUrl = originalRequest.url().newBuilder()
|
||||
val newUrl = originalRequest.url.newBuilder()
|
||||
.addQueryParameter("u", config.username)
|
||||
.addQueryParameter("c", config.clientID)
|
||||
.addQueryParameter("f", "json")
|
||||
.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(proxyPasswordInterceptor)
|
||||
|
@ -109,7 +122,7 @@ class SubsonicAPIClient(
|
|||
|
||||
private fun OkHttpClient.Builder.addLogging() {
|
||||
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
|
||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.HEADERS
|
||||
this.addInterceptor(loggingInterceptor)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class PasswordHexInterceptor(private val password: String) : Interceptor {
|
|||
|
||||
override fun intercept(chain: Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val updatedUrl = originalRequest.url().newBuilder()
|
||||
val updatedUrl = originalRequest.url.newBuilder()
|
||||
.addEncodedQueryParameter("p", passwordHex).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 {
|
||||
val originalRequest = chain.request()
|
||||
val salt = getSalt()
|
||||
val updatedUrl = originalRequest.url().newBuilder()
|
||||
val updatedUrl = originalRequest.url.newBuilder()
|
||||
.addQueryParameter("t", getPasswordMD5Hash(salt))
|
||||
.addQueryParameter("s", salt)
|
||||
.build()
|
||||
|
|
|
@ -19,7 +19,7 @@ internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
|||
internal class RangeHeaderInterceptor : Interceptor {
|
||||
override fun intercept(chain: Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val headers = originalRequest.headers()
|
||||
val headers = originalRequest.headers
|
||||
return if (headers.names().contains("Range")) {
|
||||
val offsetValue = headers["Range"] ?: "0"
|
||||
val offset = "bytes=$offsetValue-"
|
||||
|
|
|
@ -18,7 +18,7 @@ internal class VersionInterceptor(
|
|||
val newRequest = originalRequest.newBuilder()
|
||||
.url(
|
||||
originalRequest
|
||||
.url()
|
||||
.url
|
||||
.newBuilder()
|
||||
.addQueryParameter("v", protocolVersion.restApiVersion)
|
||||
.build()
|
||||
|
|
|
@ -1,68 +1,26 @@
|
|||
<?xml version="1.0" ?>
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background</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: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("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: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: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: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: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>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>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID>
|
||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</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:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
|
||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</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: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:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</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>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionThrown:Downloader.kt$Downloader.DownloadTask$throw RuntimeException( String.format( Locale.ROOT, "Download of '%s' was cancelled", downloadFile.track ) )</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
@ -64,13 +64,10 @@ style:
|
|||
WildcardImport:
|
||||
active: true
|
||||
MaxLineLength:
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: false
|
||||
excludeImportStatements: false
|
||||
active: false
|
||||
MagicNumber:
|
||||
# 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
|
||||
ignorePropertyDeclaration: true
|
||||
UnnecessaryAbstractClass:
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
Others
|
||||
|
||||
- #671: Bump versions.mockito from 4.1.0 to 4.3.1.
|
||||
- Update translations.
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
Enhancements
|
||||
|
||||
- #683: Rewrite the about and remove the webview.
|
||||
- #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.
|
||||
- #567: Use semantically correct API endpoint when streaming/downloading.
|
||||
- #572: Moved drag handle to the left in the Now Playing list.
|
||||
- #585: Added setting to disable Now Playing List sending for incompatible
|
||||
bluetooth devices.
|
||||
- #596: Added option whether to create a share on the server when sharing
|
||||
songs.
|
||||
- #585: Added setting to disable Now Playing List sending for incompatible bluetooth devices.
|
||||
- #596: Added option whether to create a share on the server when sharing songs.
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
Otros
|
||||
|
||||
- #671: Actualizado versions.mockito de 4.1.0 a 4.3.1.
|
||||
- Traducciones actualizadas.
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
Mejoras
|
||||
|
||||
- #683: Reescribir el acerca de y eliminar el webview.
|
||||
- #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
|
||||
- #594: Agregado un intent de PlaybackComplete cuando se completa la
|
||||
reproducción de una canción.
|
||||
- #594: Agregado un intent de PlaybackComplete cuando se completa la reproducción de una canción.
|
||||
- #593: Corregidas las listas de álbumes.
|
||||
- #602: NPE corregido.
|
||||
|
||||
|
@ -8,11 +7,7 @@ Mejoras
|
|||
- #558: La llamada a video puede ser estática.
|
||||
- #559: Agregado un mejor soporte sin conexión.
|
||||
- #568: Se ha reescrito el downloader.
|
||||
- #567: Se utiliza el endpoint semánticamente correcto al realizar streaming
|
||||
o descargar.
|
||||
- #572: Se ha movido el botón de arrastre de canción hacia la izquierda en
|
||||
la lista de reproducción.
|
||||
- #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.
|
||||
- #567: Se utiliza el endpoint semánticamente correcto al realizar streaming o descargar.
|
||||
- #572: Se ha movido el botón de arrastre de canción hacia la izquierda en la lista de reproducción.
|
||||
- #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).
|
||||
|
||||
Mejoras
|
||||
- #599: Se ha movido el selector de servidor y la configuración al menú de
|
||||
navegación.
|
||||
- #600: Migración de la utilidad de permisos a Kotlin, aumento del SDK
|
||||
mínimo a 17.
|
||||
- #599: Se ha movido el selector de servidor y la configuración al menú de navegación.
|
||||
- #600: Migración de la utilidad de permisos a Kotlin, aumento del SDK mínimo a 17.
|
||||
- #604: Implementar una vista de Descarga.
|
||||
- #613: targetSdkVersion debe ser 30 o superior.
|
||||
- #622: Refactorización de eventos.
|
||||
- #641: Eliminar el almacenamiento de funciones.
|
||||
- #642: Eliminar MergeAdapter y SackOfViewsAdapter.
|
||||
- #649: Unificar el manejo del diálogo de error.
|
||||
- #652: Manejo de ubicación de caché personalizado actualizado para eliminar
|
||||
isUri.
|
||||
- #652: Manejo de ubicación de caché personalizado actualizado para eliminar isUri.
|
||||
- #662: Mejorar las migraciones de bases de datos.
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
[versions]
|
||||
# You need to run ./gradlew wrapper after updating the version
|
||||
gradle = "7.3.2"
|
||||
gradle = "7.3.3"
|
||||
|
||||
navigation = "2.3.5"
|
||||
gradlePlugin = "7.0.4"
|
||||
gradlePlugin = "7.2.1"
|
||||
androidxcore = "1.6.0"
|
||||
ktlint = "0.43.2"
|
||||
ktlintGradle = "10.2.0"
|
||||
detekt = "1.19.0"
|
||||
jacoco = "0.8.7"
|
||||
preferences = "1.1.1"
|
||||
media = "1.3.1"
|
||||
media3 = "1.0.0-beta01"
|
||||
|
||||
androidSupport = "28.0.0"
|
||||
androidSupport = "1.4.0"
|
||||
androidLegacySupport = "1.0.0"
|
||||
androidSupportDesign = "1.4.0"
|
||||
androidSupportDesign = "1.6.1"
|
||||
constraintLayout = "2.1.1"
|
||||
multidex = "2.0.1"
|
||||
room = "2.4.0"
|
||||
room = "2.4.2"
|
||||
kotlin = "1.6.10"
|
||||
kotlinxCoroutines = "1.6.0-native-mt"
|
||||
viewModelKtx = "2.3.0"
|
||||
kotlinxGuava = "1.6.0"
|
||||
viewModelKtx = "2.4.1"
|
||||
|
||||
retrofit = "2.6.4"
|
||||
jackson = "2.9.5"
|
||||
okhttp = "3.12.13"
|
||||
retrofit = "2.9.0"
|
||||
jackson = "2.10.1"
|
||||
okhttp = "4.9.1"
|
||||
koin = "3.0.2"
|
||||
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" }
|
||||
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
|
||||
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
|
||||
jacoco = { module = "org.jacoco:org.jacoco.core", version.ref = "jacoco" }
|
||||
|
||||
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
|
||||
support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" }
|
||||
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" }
|
||||
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" }
|
||||
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" }
|
||||
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
|
||||
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" }
|
||||
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||
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" }
|
||||
gsonConverter = { module = "com.squareup.retrofit2:converter-gson", 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" }
|
||||
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
ext.versions = [
|
||||
minSdk : 21,
|
||||
targetSdk : 30,
|
||||
targetSdk : 33,
|
||||
compileSdk : 31,
|
||||
]
|
|
@ -1,5 +1,6 @@
|
|||
#Fri Jun 17 23:13:49 CEST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'jacoco'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
|
||||
android {
|
||||
|
@ -48,10 +47,6 @@ android {
|
|||
|
||||
tasks.withType(Test) {
|
||||
useJUnitPlatform()
|
||||
jacoco {
|
||||
includeNoLocationClasses = true
|
||||
excludes += jacocoExclude
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -61,11 +56,4 @@ dependencies {
|
|||
testRuntimeOnly libs.junitVintage
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion(libs.versions.jacoco.get())
|
||||
}
|
||||
|
||||
ext {
|
||||
jacocoExclude = ['jdk.internal.*']
|
||||
}
|
||||
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
apply plugin: 'jacoco'
|
||||
|
||||
jacoco {
|
||||
toolVersion(libs.versions.jacoco.get())
|
||||
}
|
||||
|
||||
def mergedJacocoExec = file("${project.buildDir}/jacoco/jacocoMerged.exec")
|
||||
|
||||
def merge = tasks.register('jacocoMergeReports', JacocoMerge) {
|
||||
group = "Reporting"
|
||||
description = "Merge all jacoco reports from projects into one."
|
||||
|
||||
ListProperty<File> jacocoFiles = project.objects.listProperty(File.class)
|
||||
project.subprojects { subproject ->
|
||||
subproject.plugins.withId("jacoco") {
|
||||
project.logger.info("${subproject.name} has Jacoco plugin applied")
|
||||
subproject.tasks.withType(Test) { task ->
|
||||
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
|
||||
if (destFile.exists() && !task.name.contains("Release")) {
|
||||
jacocoFiles.add(destFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executionData(jacocoFiles)
|
||||
destinationFile(mergedJacocoExec)
|
||||
}
|
||||
|
||||
tasks.register('jacocoFullReport', JacocoReport) {
|
||||
dependsOn merge
|
||||
group = "Reporting"
|
||||
description = "Generate full Jacoco coverage report including all modules."
|
||||
|
||||
getClassDirectories().setFrom(files())
|
||||
getSourceDirectories().setFrom(files())
|
||||
getExecutionData().setFrom(files())
|
||||
|
||||
reports {
|
||||
xml.enabled = true
|
||||
html.enabled = true
|
||||
csv.enabled = false
|
||||
}
|
||||
|
||||
// Always run merging, as all input calculation is done in doFirst {}
|
||||
outputs.upToDateWhen { false }
|
||||
// Task will run anyway even if initial inputs are empty
|
||||
onlyIf = { true }
|
||||
|
||||
project.subprojects { subproject ->
|
||||
subproject.plugins.withId("jacoco") {
|
||||
project.logger.info("${subproject.name} has Jacoco plugin applied")
|
||||
subproject.plugins.withId("kotlin-android") {
|
||||
project.logger.info("${subproject.name} is android project")
|
||||
def mainSources = subproject.extensions.findByName("android").sourceSets['main']
|
||||
project.logger.info("Android sources: ${mainSources.java.srcDirs}")
|
||||
mainSources.java.srcDirs.forEach {
|
||||
additionalSourceDirs(it)
|
||||
}
|
||||
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
|
||||
additionalClassDirs(fileTree(
|
||||
dir: "${subproject.buildDir}/tmp/kotlin-classes/debug",
|
||||
excludes: subproject.jacocoExclude
|
||||
))
|
||||
}
|
||||
subproject.plugins.withId("kotlin") { plugin ->
|
||||
project.logger.info("${subproject.name} is common kotlin project")
|
||||
SourceDirectorySet mainSources = subproject.extensions.getByName("kotlin")
|
||||
.sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
|
||||
.kotlin
|
||||
mainSources.srcDirs.forEach {
|
||||
project.logger.debug("Adding sources: $it")
|
||||
additionalSourceDirs(it)
|
||||
}
|
||||
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
|
||||
additionalClassDirs(fileTree(
|
||||
dir: "${subproject.buildDir}/classes/kotlin/main",
|
||||
excludes: subproject.jacocoExclude
|
||||
))
|
||||
}
|
||||
|
||||
subproject.tasks.withType(Test) { task ->
|
||||
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
|
||||
if (destFile.exists() && !task.name.contains("Release")) {
|
||||
project.logger.info("Adding execution data: $destFile")
|
||||
executionData(destFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'jacoco'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
|
||||
sourceSets {
|
||||
|
@ -21,36 +20,8 @@ dependencies {
|
|||
testRuntimeOnly libs.junitVintage
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion(libs.versions.jacoco.get())
|
||||
}
|
||||
|
||||
ext {
|
||||
// override it in the module
|
||||
jacocoExclude = ['jdk.internal.*']
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
reports {
|
||||
html.required = true
|
||||
xml.required = false
|
||||
csv.required = false
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
getClassDirectories().setFrom(files(classDirectories.files.collect {
|
||||
fileTree(dir: it, excludes: jacocoExclude)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("test").configure {
|
||||
useJUnitPlatform()
|
||||
jacoco {
|
||||
excludes += jacocoExclude
|
||||
includeNoLocationClasses = true
|
||||
}
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
|
||||
tasks.register("ciTest") {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'jacoco'
|
||||
apply from: "../gradle_scripts/code_quality.gradle"
|
||||
|
||||
android {
|
||||
|
@ -9,15 +8,16 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "org.moire.ultrasonic"
|
||||
versionCode 101
|
||||
versionName "3.1.0"
|
||||
versionCode 103
|
||||
versionName "3.2.0"
|
||||
|
||||
minSdkVersion versions.minSdk
|
||||
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 {
|
||||
release {
|
||||
minifyEnabled true
|
||||
|
@ -40,20 +40,12 @@ android {
|
|||
main.java.srcDirs += "${projectDir}/src/main/kotlin"
|
||||
test.java.srcDirs += "${projectDir}/src/test/kotlin"
|
||||
}
|
||||
|
||||
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 {
|
||||
jvmTarget = "1.8"
|
||||
|
@ -71,9 +63,18 @@ android {
|
|||
|
||||
kapt {
|
||||
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.preferences
|
||||
implementation libs.media
|
||||
implementation libs.media3exoplayer
|
||||
implementation libs.media3session
|
||||
implementation libs.media3okhttp
|
||||
|
||||
implementation libs.navigationFragment
|
||||
implementation libs.navigationUi
|
||||
|
@ -108,6 +112,7 @@ dependencies {
|
|||
|
||||
implementation libs.kotlinStdlib
|
||||
implementation libs.kotlinxCoroutines
|
||||
implementation libs.kotlinxGuava
|
||||
implementation libs.koinAndroid
|
||||
implementation libs.okhttpLogging
|
||||
implementation libs.fastScroll
|
||||
|
@ -130,36 +135,3 @@ dependencies {
|
|||
|
||||
implementation libs.timber
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion(libs.versions.jacoco.get())
|
||||
}
|
||||
|
||||
// Excluding all java classes and stuff that should not be covered
|
||||
ext {
|
||||
jacocoExclude = [
|
||||
'**/activity/**',
|
||||
'**/audiofx/**',
|
||||
'**/fragment/**',
|
||||
'**/provider/**',
|
||||
'**/receiver/**',
|
||||
'**/service/**',
|
||||
'**/Test/**',
|
||||
'**/util/**',
|
||||
'**/view/**',
|
||||
'**/R$*.class',
|
||||
'**/R.class',
|
||||
'**/BuildConfig.class',
|
||||
'**/di/**',
|
||||
'jdk.internal.*'
|
||||
]
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion(libs.versions.jacoco.get())
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
jacoco.includeNoLocationClasses = true
|
||||
jacoco.excludes += jacocoExclude
|
||||
}
|
||||
|
|
|
@ -1,37 +1,15 @@
|
|||
<?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
|
||||
id="InflateParams"
|
||||
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);"
|
||||
errorLine2=" ~~~~">
|
||||
errorLine1=" val view = inflater.inflate(R.layout.jukebox_volume, null)"
|
||||
errorLine2=" ~~~~">
|
||||
<location
|
||||
file="src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java"
|
||||
line="477"
|
||||
column="58"/>
|
||||
</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"/>
|
||||
file="src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt"
|
||||
line="331"
|
||||
column="66"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
|
@ -41,7 +19,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="151"
|
||||
line="154"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -56,17 +34,6 @@
|
|||
column="73"/>
|
||||
</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
|
||||
id="TrustAllX509TrustManager"
|
||||
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=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="146"
|
||||
line="155"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedReceiver"
|
||||
message="Exported receiver does not require permission"
|
||||
errorLine1=" <receiver android:name=".receiver.UltrasonicIntentReceiver">"
|
||||
errorLine1=" <receiver android:name=".receiver.UltrasonicIntentReceiver""
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="81"
|
||||
line="79"
|
||||
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. For launcher activities, this should be set to `true`."
|
||||
errorLine1=" <activity android:name=".activity.NavigationActivity""
|
||||
errorLine2=" ~~~~~~~~">
|
||||
id="ExportedService"
|
||||
message="Exported service does not require permission"
|
||||
errorLine1=" <service android:name=".playback.PlaybackService""
|
||||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="41"
|
||||
line="68"
|
||||
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.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
|
||||
id="ObsoleteLayoutParam"
|
||||
message="Invalid layout param in a `LinearLayout`: `layout_above`"
|
||||
|
@ -345,6 +147,17 @@
|
|||
column="5"/>
|
||||
</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
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.ic_menu_arrow` appears to be unused"
|
||||
|
@ -358,189 +171,57 @@
|
|||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.menu_arrow` appears to be unused"
|
||||
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
|
||||
errorLine1="<vector android:height="48dp""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/drawable/menu_arrow.xml"
|
||||
file="src/main/res/drawable/media3_notification_pause.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.string.main_shuffle` appears to be unused"
|
||||
errorLine1=" <string name="main.shuffle">Shuffle Play</string>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
message="The resource `R.drawable.media3_notification_play` appears to be unused"
|
||||
errorLine1="<vector android:height="48dp""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="114"
|
||||
column="13"/>
|
||||
file="src/main/res/drawable/media3_notification_play.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.string.menu_navigation` appears to be unused"
|
||||
errorLine1=" <string name="menu.navigation">Navigation</string>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
message="The resource `R.drawable.media3_notification_seek_to_next` appears to be unused"
|
||||
errorLine1="<vector android:height="32dp""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="128"
|
||||
column="13"/>
|
||||
file="src/main/res/drawable/media3_notification_seek_to_next.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.string.music_service_retry` appears to be unused"
|
||||
errorLine1=" <string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
message="The resource `R.drawable.media3_notification_seek_to_previous` appears to be unused"
|
||||
errorLine1="<vector android:height="32dp""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="133"
|
||||
column="13"/>
|
||||
file="src/main/res/drawable/media3_notification_seek_to_previous.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.string.parser_artist_count` appears to be unused"
|
||||
errorLine1=" <string name="parser.artist_count">Got %d Artists.</string>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
|
||||
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="134"
|
||||
column="13"/>
|
||||
</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"/>
|
||||
file="src/main/res/drawable/media3_notification_small_icon.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
|
@ -838,39 +519,6 @@
|
|||
column="6"/>
|
||||
</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
|
||||
id="LabelFor"
|
||||
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"/>
|
||||
</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
|
||||
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"
|
||||
|
|
|
@ -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
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/NoActionBar"
|
||||
|
@ -40,8 +42,8 @@
|
|||
|
||||
<activity android:name=".activity.NavigationActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:label="@string/common.appname"
|
||||
android:launchMode="singleTask">
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.SEARCH"/>
|
||||
|
@ -57,28 +59,25 @@
|
|||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.MediaPlayerService"
|
||||
android:name=".service.DownloadService"
|
||||
android:label="Ultrasonic Media Player Service"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<service
|
||||
tools:ignore="ExportedService"
|
||||
android:name=".service.AutoMediaBrowserService"
|
||||
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
||||
<service android:name=".playback.PlaybackService"
|
||||
android:label="@string/common.appname"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaLibraryService" />
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receiver.MediaButtonIntentReceiver">
|
||||
<intent-filter android:priority="2147483647">
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receiver.UltrasonicIntentReceiver">
|
||||
<receiver android:name=".receiver.UltrasonicIntentReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/>
|
||||
<action android:name="org.moire.ultrasonic.CMD_PLAY"/>
|
||||
|
@ -90,7 +89,8 @@
|
|||
<action android:name="org.moire.ultrasonic.CMD_PROCESS_KEYCODE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receiver.BluetoothIntentReceiver">
|
||||
<receiver android:name=".receiver.BluetoothIntentReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.bluetooth.device.action.ACL_CONNECTED"/>
|
||||
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
|
||||
|
@ -100,7 +100,8 @@
|
|||
</receiver>
|
||||
<receiver
|
||||
android:name=".provider.UltrasonicAppWidgetProvider4X1"
|
||||
android:label="Ultrasonic (4x1)">
|
||||
android:label="Ultrasonic (4x1)"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
@ -111,7 +112,8 @@
|
|||
</receiver>
|
||||
<receiver
|
||||
android:name=".provider.UltrasonicAppWidgetProvider4X2"
|
||||
android:label="Ultrasonic (4x2)">
|
||||
android:label="Ultrasonic (4x2)"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
@ -122,7 +124,8 @@
|
|||
</receiver>
|
||||
<receiver
|
||||
android:name=".provider.UltrasonicAppWidgetProvider4X3"
|
||||
android:label="Ultrasonic (4x3)">
|
||||
android:label="Ultrasonic (4x3)"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
@ -133,7 +136,8 @@
|
|||
</receiver>
|
||||
<receiver
|
||||
android:name=".provider.UltrasonicAppWidgetProvider4X4"
|
||||
android:label="Ultrasonic (4x4)">
|
||||
android:label="Ultrasonic (4x4)"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
@ -142,18 +146,17 @@
|
|||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/appwidget_info_4x4"/>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name=".provider.SearchSuggestionProvider"
|
||||
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.A2dpIntentReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.android.music.playstatusrequest"/>
|
||||
<receiver android:name=".receiver.MediaButtonIntentReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter android:priority="2147483647">
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<provider
|
||||
android:name=".provider.SearchSuggestionProvider"
|
||||
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"
|
||||
android:exported="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</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;
|
||||
|
||||
final String id = song.getSong().getId();
|
||||
final String id = song.getTrack().getId();
|
||||
if (id == null) return;
|
||||
|
||||
// Avoid duplicate registrations.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package org.moire.ultrasonic.service;
|
||||
|
||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
||||
import org.moire.ultrasonic.domain.Track;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
|
@ -13,7 +13,7 @@ public class State implements Serializable
|
|||
{
|
||||
public static final long serialVersionUID = -6346438781062572270L;
|
||||
|
||||
public List<MusicDirectory.Entry> songs = new ArrayList<>();
|
||||
public List<Track> songs = new ArrayList<>();
|
||||
public int currentPlayingIndex;
|
||||
public int currentPlayingPosition;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package org.moire.ultrasonic.util;
|
||||
|
||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
||||
import org.moire.ultrasonic.domain.Track;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -12,5 +12,5 @@ public class ShareDetails
|
|||
public String Description;
|
||||
public boolean ShareOnServer;
|
||||
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;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
|
@ -29,14 +31,11 @@ import androidx.lifecycle.LifecycleOwner;
|
|||
import androidx.lifecycle.Observer;
|
||||
|
||||
import org.moire.ultrasonic.audiofx.VisualizerController;
|
||||
import org.moire.ultrasonic.domain.PlayerState;
|
||||
import org.moire.ultrasonic.service.MediaPlayerController;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
/**
|
||||
* A simple class that draws waveform data received from a
|
||||
* {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture}
|
||||
|
@ -130,7 +129,7 @@ public class VisualizerView extends View
|
|||
return;
|
||||
}
|
||||
|
||||
if (mediaPlayerControllerLazy.getValue().getPlayerState() != PlayerState.STARTED)
|
||||
if (!mediaPlayerControllerLazy.getValue().isPlaying())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
/*
|
||||
* NavigationActivity.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
package org.moire.ultrasonic.activity
|
||||
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
|
@ -25,6 +27,9 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.view.GravityCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
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.findNavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
|
@ -36,23 +41,24 @@ import androidx.navigation.ui.setupWithNavController
|
|||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.button.MaterialButton
|
||||
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.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.ServerSettingDao
|
||||
import org.moire.ultrasonic.domain.PlayerState
|
||||
import org.moire.ultrasonic.fragment.OnBackPressedHandler
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||
import org.moire.ultrasonic.service.DownloadFile
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.InfoDialog
|
||||
import org.moire.ultrasonic.util.LocaleHelper
|
||||
import org.moire.ultrasonic.util.ServerColor
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Storage
|
||||
|
@ -61,7 +67,7 @@ import org.moire.ultrasonic.util.Util
|
|||
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")
|
||||
class NavigationActivity : AppCompatActivity() {
|
||||
|
@ -78,8 +84,8 @@ class NavigationActivity : AppCompatActivity() {
|
|||
private var headerBackgroundImage: ImageView? = null
|
||||
|
||||
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 lifecycleSupport: MediaPlayerLifecycleSupport by inject()
|
||||
|
@ -93,6 +99,16 @@ class NavigationActivity : AppCompatActivity() {
|
|||
private var cachedServerCount: Int = 0
|
||||
|
||||
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()
|
||||
Util.applyTheme(this)
|
||||
|
||||
|
@ -154,9 +170,8 @@ class NavigationActivity : AppCompatActivity() {
|
|||
setMenuForServerCapabilities()
|
||||
}
|
||||
|
||||
// Determine first run and migrate server settings to DB as early as possible
|
||||
var showWelcomeScreen = Util.isFirstRun()
|
||||
val areServersMigrated: Boolean = serverSettingsModel.migrateFromPreferences()
|
||||
// Determine if this is a first run
|
||||
val showWelcomeScreen = Util.isFirstRun()
|
||||
|
||||
// Migrate Feature storage if needed
|
||||
// TODO: Remove in December 2022
|
||||
|
@ -164,9 +179,6 @@ class NavigationActivity : AppCompatActivity() {
|
|||
Settings.migrateFeatureStorage()
|
||||
}
|
||||
|
||||
// If there are any servers in the DB, do not show the welcome screen
|
||||
showWelcomeScreen = showWelcomeScreen and !areServersMigrated
|
||||
|
||||
loadSettings()
|
||||
|
||||
// This is a first run with only the demo entry inside the database
|
||||
|
@ -180,25 +192,25 @@ class NavigationActivity : AppCompatActivity() {
|
|||
hideNowPlaying()
|
||||
}
|
||||
|
||||
playerStateSubscription = RxBus.playerStateObservable.subscribe {
|
||||
if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED)
|
||||
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
||||
if (it.state == STATE_READY)
|
||||
showNowPlaying()
|
||||
else
|
||||
hideNowPlaying()
|
||||
}
|
||||
|
||||
themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe {
|
||||
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
||||
recreate()
|
||||
}
|
||||
|
||||
serverRepository.liveServerCount().observe(
|
||||
this,
|
||||
{ count ->
|
||||
cachedServerCount = count ?: 0
|
||||
updateNavigationHeaderForServer()
|
||||
}
|
||||
)
|
||||
ActiveServerProvider.liveActiveServerId.observe(this, { updateNavigationHeaderForServer() })
|
||||
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
|
||||
updateNavigationHeaderForServer()
|
||||
}
|
||||
|
||||
serverRepository.liveServerCount().observe(this) { count ->
|
||||
cachedServerCount = count ?: 0
|
||||
updateNavigationHeaderForServer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNavigationHeaderForServer() {
|
||||
|
@ -224,6 +236,7 @@ class NavigationActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onResume() {
|
||||
Timber.d("onResume called")
|
||||
super.onResume()
|
||||
|
||||
Storage.reset()
|
||||
|
@ -237,10 +250,11 @@ class NavigationActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
themeChangedEventSubscription?.dispose()
|
||||
playerStateSubscription?.dispose()
|
||||
Timber.d("onDestroy called")
|
||||
rxBusSubscription.dispose()
|
||||
imageLoaderProvider.clearImageLoader()
|
||||
UApp.instance!!.shutdownKoin()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
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() {
|
||||
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
|
||||
}
|
||||
|
||||
private fun exit() {
|
||||
Timber.d("User choose to exit the app")
|
||||
|
||||
// Broadcast that the service is being shutdown
|
||||
RxBus.stopCommandPublisher.onNext(Unit)
|
||||
|
||||
lifecycleSupport.onDestroy()
|
||||
finish()
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
|
||||
private fun showWelcomeDialog() {
|
||||
if (!infoDialogDisplayed) {
|
||||
infoDialogDisplayed = true
|
||||
|
||||
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
||||
|
||||
InfoDialog.Builder(this)
|
||||
.setTitle(R.string.main_welcome_title)
|
||||
.setMessage(R.string.main_welcome_text_demo)
|
||||
|
@ -406,10 +436,10 @@ class NavigationActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
if (nowPlayingView != null) {
|
||||
val playerState: PlayerState = mediaPlayerController.playerState
|
||||
if (playerState == PlayerState.PAUSED || playerState == PlayerState.STARTED) {
|
||||
val file: DownloadFile? = mediaPlayerController.currentPlaying
|
||||
if (file != null) {
|
||||
val playerState: Int = mediaPlayerController.playbackState
|
||||
if (playerState == STATE_BUFFERING || playerState == STATE_READY) {
|
||||
val item: MediaItem? = mediaPlayerController.currentMediaItem
|
||||
if (item != null) {
|
||||
nowPlayingView?.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -20,7 +20,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.drakeet.multitype.ItemViewBinder
|
||||
import org.koin.core.component.KoinComponent
|
||||
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.service.MusicServiceFactory.getMusicService
|
||||
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
|
||||
*/
|
||||
class AlbumRowBinder(
|
||||
val onItemClick: (MusicDirectory.Album) -> Unit,
|
||||
val onContextMenuClick: (MenuItem, MusicDirectory.Album) -> Boolean,
|
||||
val onItemClick: (Album) -> Unit,
|
||||
val onContextMenuClick: (MenuItem, Album) -> Boolean,
|
||||
private val imageLoader: ImageLoader,
|
||||
context: Context
|
||||
) : ItemViewBinder<MusicDirectory.Album, AlbumRowBinder.ViewHolder>(), KoinComponent {
|
||||
) : ItemViewBinder<Album, AlbumRowBinder.ViewHolder>(), KoinComponent {
|
||||
|
||||
private val starDrawable: Drawable =
|
||||
Util.getDrawableFromAttribute(context, R.attr.star_full)
|
||||
|
@ -46,7 +46,7 @@ class AlbumRowBinder(
|
|||
val layout = R.layout.list_item_album
|
||||
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.artist.text = item.artist
|
||||
holder.details.setOnClickListener { onItemClick(item) }
|
||||
|
@ -86,7 +86,7 @@ class AlbumRowBinder(
|
|||
/**
|
||||
* 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
|
||||
star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
|
||||
val musicService = getMusicService()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* ArtistRowAdapter.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
* ArtistRowBinder.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.drakeet.multitype.ItemViewBinder
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
|
@ -57,7 +58,7 @@ class ArtistRowBinder(
|
|||
|
||||
holder.coverArtId = item.coverArt
|
||||
|
||||
if (Settings.shouldShowArtistPicture) {
|
||||
if (showArtistPicture()) {
|
||||
holder.coverArt.visibility = View.VISIBLE
|
||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||
imageLoader.loadImage(
|
||||
|
@ -102,11 +103,16 @@ class ArtistRowBinder(
|
|||
}
|
||||
|
||||
private fun getSectionFromName(name: String): String {
|
||||
var section = name.first().uppercaseChar()
|
||||
if (!section.isLetter()) section = '#'
|
||||
if (name.isEmpty()) return SECTION_KEY_DEFAULT
|
||||
val section = name.first().uppercaseChar()
|
||||
if (!section.isLetter()) return SECTION_KEY_DEFAULT
|
||||
return section.toString()
|
||||
}
|
||||
|
||||
private fun showArtistPicture(): Boolean {
|
||||
return ActiveServerProvider.isID3Enabled() && Settings.shouldShowArtistPicture
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of our ViewHolder class
|
||||
*/
|
||||
|
@ -123,4 +129,8 @@ class ArtistRowBinder(
|
|||
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
||||
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")
|
||||
}
|
||||
|
||||
var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
||||
private var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
||||
AdapterListUpdateCallback(this),
|
||||
AsyncDifferConfig.Builder(diffCallback).build()
|
||||
)
|
||||
|
@ -182,12 +182,11 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
|
|||
|
||||
// Select them all
|
||||
getCurrentList().mapNotNullTo(
|
||||
selectedSet,
|
||||
{ entry ->
|
||||
// Exclude any -1 ids, eg. headers and other UI elements
|
||||
entry.longId.takeIf { it != -1L }
|
||||
}
|
||||
)
|
||||
selectedSet
|
||||
) { entry ->
|
||||
// Exclude any -1 ids, eg. headers and other UI elements
|
||||
entry.longId.takeIf { it != -1L }
|
||||
}
|
||||
|
||||
return selectedSet.count()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package org.moire.ultrasonic.fragment
|
||||
package org.moire.ultrasonic.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
|
@ -30,7 +30,7 @@ import org.moire.ultrasonic.util.Util
|
|||
*/
|
||||
internal class ServerRowAdapter(
|
||||
private var context: Context,
|
||||
private var data: Array<ServerSetting>,
|
||||
passedData: Array<ServerSetting>,
|
||||
private val model: ServerSettingsModel,
|
||||
private val activeServerProvider: ActiveServerProvider,
|
||||
private val manageMode: Boolean,
|
||||
|
@ -38,6 +38,12 @@ internal class ServerRowAdapter(
|
|||
private val serverEditRequestedCallback: ((Int) -> Unit)
|
||||
) : BaseAdapter() {
|
||||
|
||||
private var data: MutableList<ServerSetting> = mutableListOf()
|
||||
|
||||
init {
|
||||
setData(passedData)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MENU_ID_EDIT = 1
|
||||
private const val MENU_ID_DELETE = 2
|
||||
|
@ -49,12 +55,19 @@ internal class ServerRowAdapter(
|
|||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return if (manageMode) data.size else data.size + 1
|
||||
return data.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any {
|
||||
|
@ -69,11 +82,11 @@ internal class ServerRowAdapter(
|
|||
* Creates the Row representation of a Server Setting
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
|
||||
var index = position
|
||||
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
||||
var position = pos
|
||||
|
||||
// Skip "Offline" in manage mode
|
||||
if (manageMode) index++
|
||||
if (manageMode) position++
|
||||
|
||||
var vi: View? = convertView
|
||||
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 image = vi?.findViewById<ImageView>(R.id.server_image)
|
||||
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 = context.getString(R.string.main_offline)
|
||||
description?.text = ""
|
||||
} else {
|
||||
text?.text = setting?.name ?: ""
|
||||
description?.text = setting?.url ?: ""
|
||||
if (setting == null) serverMenu?.visibility = View.INVISIBLE
|
||||
}
|
||||
text?.text = setting?.name ?: ""
|
||||
description?.text = setting?.url ?: ""
|
||||
if (setting == null) serverMenu?.visibility = View.INVISIBLE
|
||||
|
||||
val icon: Drawable?
|
||||
val background: Drawable?
|
||||
|
||||
// Configure icons for the row
|
||||
if (index == 0) {
|
||||
if (setting?.id == ActiveServerProvider.OFFLINE_DB_ID) {
|
||||
serverMenu?.visibility = View.INVISIBLE
|
||||
icon = Util.getDrawableFromAttribute(context, R.attr.screen_on_off)
|
||||
background = ContextCompat.getDrawable(context, R.drawable.circle)
|
||||
|
@ -116,7 +124,7 @@ internal class ServerRowAdapter(
|
|||
image?.background = 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)
|
||||
} else {
|
||||
layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple)
|
||||
|
@ -128,7 +136,7 @@ internal class ServerRowAdapter(
|
|||
R.drawable.select_ripple_circle
|
||||
)
|
||||
|
||||
serverMenu?.setOnClickListener { view -> serverMenuClick(view, index) }
|
||||
serverMenu?.setOnClickListener { view -> serverMenuClick(view, position) }
|
||||
|
||||
return vi
|
||||
}
|
||||
|
@ -192,7 +200,8 @@ internal class ServerRowAdapter(
|
|||
return true
|
||||
}
|
||||
MENU_ID_DELETE -> {
|
||||
serverDeletedCallback.invoke(position)
|
||||
val server = getItem(position) as ServerSetting
|
||||
serverDeletedCallback.invoke(server.id)
|
||||
return true
|
||||
}
|
||||
MENU_ID_UP -> {
|
||||
|
|
|
@ -12,12 +12,12 @@ import org.koin.core.component.KoinComponent
|
|||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
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.Downloader
|
||||
|
||||
class TrackViewBinder(
|
||||
val onItemClick: (DownloadFile) -> Unit,
|
||||
val onItemClick: (DownloadFile, Int) -> Unit,
|
||||
val onContextMenuClick: ((MenuItem, DownloadFile) -> Boolean)? = null,
|
||||
val checkable: Boolean,
|
||||
val draggable: Boolean,
|
||||
|
@ -29,7 +29,7 @@ class TrackViewBinder(
|
|||
|
||||
// Set our layout files
|
||||
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 imageHelper: Utils.ImageHelper = Utils.ImageHelper(context)
|
||||
|
@ -41,15 +41,14 @@ class TrackViewBinder(
|
|||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Suppress("LongMethod")
|
||||
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
|
||||
val downloadFile: DownloadFile?
|
||||
val diffAdapter = adapter as BaseAdapter<*>
|
||||
|
||||
when (item) {
|
||||
is MusicDirectory.Entry -> {
|
||||
downloadFile = downloader.getDownloadFileForSong(item)
|
||||
val downloadFile: DownloadFile = when (item) {
|
||||
is Track -> {
|
||||
downloader.getDownloadFileForSong(item)
|
||||
}
|
||||
is DownloadFile -> {
|
||||
downloadFile = item
|
||||
item
|
||||
}
|
||||
else -> {
|
||||
return
|
||||
|
@ -77,7 +76,7 @@ class TrackViewBinder(
|
|||
}
|
||||
} else {
|
||||
// Minimize or maximize the Text view (if song title is very long)
|
||||
if (!downloadFile.song.isDirectory) {
|
||||
if (!downloadFile.track.isDirectory) {
|
||||
holder.maximizeOrMinimize()
|
||||
}
|
||||
}
|
||||
|
@ -86,11 +85,11 @@ class TrackViewBinder(
|
|||
}
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
if (checkable && !downloadFile.song.isVideo) {
|
||||
if (checkable && !downloadFile.track.isVideo) {
|
||||
val nowChecked = !holder.check.isChecked
|
||||
holder.isChecked = nowChecked
|
||||
} else {
|
||||
onItemClick(downloadFile)
|
||||
onItemClick(downloadFile, holder.bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,41 +102,37 @@ class TrackViewBinder(
|
|||
|
||||
// Notify the adapter of selection changes
|
||||
holder.observableChecked.observe(
|
||||
lifecycleOwner,
|
||||
{ isCheckedNow ->
|
||||
if (isCheckedNow) {
|
||||
diffAdapter.notifySelected(holder.entry!!.longId)
|
||||
} else {
|
||||
diffAdapter.notifyUnselected(holder.entry!!.longId)
|
||||
}
|
||||
lifecycleOwner
|
||||
) { isCheckedNow ->
|
||||
if (isCheckedNow) {
|
||||
diffAdapter.notifySelected(holder.entry!!.longId)
|
||||
} else {
|
||||
diffAdapter.notifyUnselected(holder.entry!!.longId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Listen to changes in selection status and update ourselves
|
||||
diffAdapter.selectionRevision.observe(
|
||||
lifecycleOwner,
|
||||
{
|
||||
val newStatus = diffAdapter.isSelected(item.longId)
|
||||
lifecycleOwner
|
||||
) {
|
||||
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
|
||||
downloadFile.status.observe(
|
||||
lifecycleOwner,
|
||||
{
|
||||
holder.updateStatus(it)
|
||||
diffAdapter.notifyChanged()
|
||||
}
|
||||
)
|
||||
lifecycleOwner
|
||||
) {
|
||||
holder.updateStatus(it)
|
||||
diffAdapter.notifyChanged()
|
||||
}
|
||||
|
||||
downloadFile.progress.observe(
|
||||
lifecycleOwner,
|
||||
{
|
||||
holder.updateProgress(it)
|
||||
}
|
||||
)
|
||||
lifecycleOwner
|
||||
) {
|
||||
holder.updateProgress(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: TrackViewHolder) {
|
||||
|
|
|
@ -15,7 +15,7 @@ import io.reactivex.rxjava3.disposables.Disposable
|
|||
import org.koin.core.component.KoinComponent
|
||||
import org.moire.ultrasonic.R
|
||||
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.DownloadStatus
|
||||
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 progress: TextView = view.findViewById(R.id.song_status)
|
||||
|
||||
var entry: MusicDirectory.Entry? = null
|
||||
var entry: Track? = null
|
||||
private set
|
||||
var downloadFile: DownloadFile? = null
|
||||
private set
|
||||
|
@ -67,7 +67,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
|||
isSelected: Boolean = false
|
||||
) {
|
||||
val useFiveStarRating = Settings.useFiveStarRating
|
||||
val song = file.song
|
||||
val song = file.track
|
||||
downloadFile = file
|
||||
entry = song
|
||||
|
||||
|
@ -109,7 +109,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
|||
}
|
||||
|
||||
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) {
|
||||
// Hide single star
|
||||
star.isVisible = false
|
||||
|
@ -153,6 +153,8 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
|||
star.setImageDrawable(imageHelper.starHollowDrawable)
|
||||
song.starred = false
|
||||
}
|
||||
|
||||
// Should this be done here ?
|
||||
Thread {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
try {
|
||||
|
|
|
@ -2,8 +2,12 @@ package org.moire.ultrasonic.app
|
|||
|
||||
import android.content.Context
|
||||
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.core.context.startKoin
|
||||
import org.koin.core.context.stopKoin
|
||||
import org.koin.core.logger.Level
|
||||
import org.moire.ultrasonic.BuildConfig
|
||||
import org.moire.ultrasonic.di.appPermanentStorage
|
||||
|
@ -23,22 +27,39 @@ import timber.log.Timber.DebugTree
|
|||
|
||||
class UApp : MultiDexApplication() {
|
||||
|
||||
private var ioScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
init {
|
||||
instance = this
|
||||
// if (BuildConfig.DEBUG)
|
||||
// StrictMode.enableDefaults()
|
||||
}
|
||||
|
||||
var initiated = false
|
||||
|
||||
override fun onCreate() {
|
||||
initiated = true
|
||||
super.onCreate()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
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 {
|
||||
// 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.INFO))
|
||||
|
||||
|
@ -55,8 +76,13 @@ class UApp : MultiDexApplication() {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun shutdownKoin() {
|
||||
stopKoin()
|
||||
initiated = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var instance: UApp? = null
|
||||
var instance: UApp? = null
|
||||
|
||||
fun applicationContext(): Context {
|
||||
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
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.Room
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -11,6 +17,7 @@ import org.moire.ultrasonic.R
|
|||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.di.DB_FILENAME
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
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
|
||||
* 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(
|
||||
private val repository: ServerSettingDao
|
||||
|
@ -35,7 +40,7 @@ class ActiveServerProvider(
|
|||
*/
|
||||
@JvmOverloads
|
||||
fun getActiveServer(serverId: Int = getActiveServerId()): ServerSetting {
|
||||
if (serverId > 0) {
|
||||
if (serverId > OFFLINE_DB_ID) {
|
||||
if (cachedServer != null && cachedServer!!.id == serverId) return cachedServer!!
|
||||
|
||||
// Ideally this is the only call where we block the thread while using the repository
|
||||
|
@ -53,22 +58,31 @@ class ActiveServerProvider(
|
|||
return cachedServer!!
|
||||
}
|
||||
|
||||
setActiveServerId(0)
|
||||
// Fallback to Offline
|
||||
setActiveServerById(OFFLINE_DB_ID)
|
||||
}
|
||||
|
||||
return ServerSetting(
|
||||
id = -1,
|
||||
index = 0,
|
||||
name = UApp.applicationContext().getString(R.string.main_offline),
|
||||
url = "http://localhost",
|
||||
userName = "",
|
||||
password = "",
|
||||
jukeboxByDefault = false,
|
||||
allowSelfSignedCertificate = false,
|
||||
ldapSupport = false,
|
||||
musicFolderId = "",
|
||||
minimumApiVersion = null
|
||||
)
|
||||
return OFFLINE_DB
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the index (sort order) of a server to its id (unique)
|
||||
* @param index: The index of the server in the server selector
|
||||
* @return id: The unique id of the server
|
||||
*/
|
||||
fun getServerIdFromIndex(index: Int): Int {
|
||||
if (index <= OFFLINE_DB_INDEX) {
|
||||
// Offline mode is selected
|
||||
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) {
|
||||
Timber.d("setActiveServerByIndex $index")
|
||||
if (index < 1) {
|
||||
if (index <= OFFLINE_DB_INDEX) {
|
||||
// Offline mode is selected
|
||||
setActiveServerId(0)
|
||||
setActiveServerById(OFFLINE_DB_ID)
|
||||
return
|
||||
}
|
||||
|
||||
launch {
|
||||
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")
|
||||
cachedServerId = activeServer
|
||||
return Room.databaseBuilder(
|
||||
UApp.applicationContext(),
|
||||
MetaDatabase::class.java,
|
||||
METADATA_DB + cachedServerId
|
||||
)
|
||||
.fallbackToDestructiveMigrationOnDowngrade()
|
||||
.build()
|
||||
cachedDatabase = initDatabase(activeServer)
|
||||
|
||||
return cachedDatabase!!
|
||||
}
|
||||
|
||||
val offlineMetaDatabase: MetaDatabase by lazy {
|
||||
Room.databaseBuilder(
|
||||
initDatabase(0)
|
||||
}
|
||||
|
||||
private fun initDatabase(serverId: Int): MetaDatabase {
|
||||
return Room.databaseBuilder(
|
||||
UApp.applicationContext(),
|
||||
MetaDatabase::class.java,
|
||||
METADATA_DB + 0
|
||||
METADATA_DB + serverId
|
||||
)
|
||||
.addMigrations(META_MIGRATION_2_3)
|
||||
.fallbackToDestructiveMigrationOnDowngrade()
|
||||
.build()
|
||||
}
|
||||
|
@ -177,16 +192,30 @@ class ActiveServerProvider(
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
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
|
||||
* @return True, if the "Offline" mode is selected
|
||||
*/
|
||||
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()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries if ID3 tags should be used
|
||||
*/
|
||||
fun isID3Enabled(): Boolean {
|
||||
return Settings.shouldUseId3Tags && (!isOffline() || Settings.useId3TagsOffline)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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() {
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
||||
@Dao
|
||||
interface ArtistsDao {
|
||||
interface ArtistDao {
|
||||
/**
|
||||
* Insert a list in the database. If the item already exists, replace it.
|
||||
*
|
||||
|
@ -43,5 +43,5 @@ interface ArtistsDao {
|
|||
* Get artist by 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>
|
||||
|
||||
/**
|
||||
* TODO: Make generic
|
||||
* Upserts (insert or update) an object to the database
|
||||
*
|
||||
* @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
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
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.Index
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
|
||||
/**
|
||||
* This database is used to store and cache the ID3 metadata
|
||||
*/
|
||||
|
||||
@Database(
|
||||
entities = [Artist::class, Index::class, MusicFolder::class],
|
||||
version = 1
|
||||
entities = [
|
||||
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 fun artistsDao(): ArtistsDao
|
||||
abstract fun artistDao(): ArtistDao
|
||||
|
||||
abstract fun albumDao(): AlbumDao
|
||||
|
||||
abstract fun trackDao(): TrackDao
|
||||
|
||||
abstract fun musicFoldersDao(): MusicFoldersDao
|
||||
|
||||
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
|
||||
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 = "name") var name: String,
|
||||
@ColumnInfo(name = "url") var url: String,
|
||||
|
@ -37,9 +38,6 @@ data class ServerSetting(
|
|||
@ColumnInfo(name = "podcastSupport") var podcastSupport: Boolean? = null
|
||||
) {
|
||||
constructor() : this (
|
||||
-1, 0, "", "", null, "", "", false, false, false, null, null
|
||||
)
|
||||
constructor(name: String, url: String) : this(
|
||||
-1, 0, name, url, null, "", "", false, false, false, null, null
|
||||
0, 0, "", "", null, "", "", false, false, false, null, null
|
||||
)
|
||||
}
|
||||
|
|
|
@ -69,12 +69,6 @@ interface ServerSettingDao {
|
|||
@Query("SELECT COUNT(*) FROM serverSetting")
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -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