Compare commits
260 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 | |
Óscar García Amor | e2ddb35ce3 | |
Óscar García Amor | 9581a17bd9 | |
Óscar García Amor | 3fbf47dedf | |
Óscar García Amor | 87aea4847c | |
Nite | f210a6e363 | |
Nite | a9ee09bc2f | |
Nite | bf96f36cb4 | |
Nite | da69fb9f1f | |
Nite | 5962cc2add | |
Nite | 0d4b400105 | |
Nite | ca9ba68a5e | |
Nite | d00a30940e | |
Nite | 586bc51a9c | |
Óscar García Amor | a53d5378bf | |
Óscar García Amor | 4778d18fb9 | |
Óscar García Amor | 2ff944d77d | |
Nite | 462719ccd1 | |
Nite | e39f232732 | |
dependabot[bot] | ca89d4b55e | |
dependabot[bot] | c66ea1c437 | |
tzugen | 987fbbe02a | |
tzugen | b958dcabe4 | |
tzugen | 1eca5a756e | |
tzugen | dd92d00be5 | |
tzugen | 7531a4d4aa | |
tzugen | 82f45bd5dd | |
tzugen | c0ef964a3e | |
tzugen | 65347a20fa | |
tzugen | 7f42ed6a37 | |
dependabot[bot] | 95f37ba2e2 | |
tzugen | fa434342d9 |
|
@ -1,23 +1,33 @@
|
|||
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:
|
||||
keys:
|
||||
- v1-ultrasonic-{{ .Branch }}-{{ checksum "dependencies.gradle" }}
|
||||
- v1-ultrasonic-{{ .Branch }}
|
||||
- v1-ultrasonic
|
||||
- v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- v2-ultrasonic-{{ .Branch }}
|
||||
- v2-ultrasonic
|
||||
- run:
|
||||
name: configure gradle.properties for CI building
|
||||
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 "dependencies.gradle" }}
|
||||
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,15 +82,19 @@ 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:
|
||||
keys:
|
||||
- v1-ultrasonic-{{ .Branch }}-{{ checksum "dependencies.gradle" }}
|
||||
- v1-ultrasonic-{{ .Branch }}
|
||||
- v1-ultrasonic
|
||||
- v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- v2-ultrasonic-{{ .Branch }}
|
||||
- v2-ultrasonic
|
||||
- run:
|
||||
name: decrypt ultrasonic-keystore
|
||||
command: openssl aes-256-cbc -K ${ULTRASONIC_KEYSTORE_KEY} -iv ${ULTRASONIC_KEYSTORE_IV} -in ultrasonic-keystore.enc -out ultrasonic-keystore -d
|
||||
|
@ -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>
|
|
@ -19,17 +19,45 @@ By default Pull Request should be opened against **develop** branch, PR against
|
|||
|
||||
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
|
||||
Use `git commit --signoff` to acknowledge this.
|
||||
2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests
|
||||
should be written in this programming language.
|
||||
3. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
|
||||
4. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
|
||||
2. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
|
||||
3. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
|
||||
Refactoring existing messes is great, but watch out for breakage.
|
||||
5. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
|
||||
4. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
|
||||
and test.
|
||||
|
||||
### Pull Request Process
|
||||
On each Pull Request Github runs a number of checks to make sure there are no problems.
|
||||
|
||||
#### Signed commits
|
||||
Commits must be signed. [See here how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
|
||||
|
||||
#### KtLint
|
||||
This programm checks if the source code is formatted correctly.
|
||||
You can run it yourself locally with
|
||||
|
||||
`./gradlew -Pqc ktlintFormat`
|
||||
|
||||
Running this command will fix common problems and will notify you of problems it couldn't fix automatically.
|
||||
|
||||
#### Detekt
|
||||
|
||||
Detekt is a static analyser. It helps to find potential bugs in our code.
|
||||
|
||||
You can run it yourself locally with
|
||||
|
||||
`./gradlew -Pqc detekt`
|
||||
|
||||
There is a "baseline" file, in which errors which have been in the code base before are noted.
|
||||
Sometimes it is necessary to regenerate this file by running:
|
||||
|
||||
`./gradlew -Pqc detektBaseline`
|
||||
|
||||
#### Lint
|
||||
Lint looks for general problems in the code or unused resources etc.
|
||||
You can run it with
|
||||
|
||||
`./gradlew -Pqc lintRelease`
|
||||
|
||||
If there is a need to regenerate the baseline, remove `ultrasonic/lint-baseline.xml` and rerun the command.
|
||||
|
||||
|
||||
1. Ensure [all commits are signed-off](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification).
|
||||
2. Check tests for the new code are added.
|
||||
3. Check code style is passing.
|
||||
4. Check code static analysis is passing.
|
||||
|
|
|
@ -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.
|
||||
|
|
62
README.md
62
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.
|
||||
|
||||
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
|
||||
|
|
15
build.gradle
15
build.gradle
|
@ -1,6 +1,6 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
apply from: 'dependencies.gradle'
|
||||
apply from: 'gradle/versions.gradle'
|
||||
|
||||
ext.bootstrap = [
|
||||
kotlinModule : "${project.rootDir}/gradle_scripts/kotlin-module-bootstrap.gradle",
|
||||
|
@ -13,11 +13,10 @@ buildscript {
|
|||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
}
|
||||
dependencies {
|
||||
classpath gradlePlugins.gradle
|
||||
classpath gradlePlugins.kotlin
|
||||
classpath gradlePlugins.ktlintGradle
|
||||
classpath gradlePlugins.detekt
|
||||
classpath gradlePlugins.jacoco
|
||||
classpath libs.gradle
|
||||
classpath libs.kotlin
|
||||
classpath libs.ktlintGradle
|
||||
classpath libs.detekt
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,9 +43,7 @@ allprojects {
|
|||
}
|
||||
}
|
||||
|
||||
apply from: 'gradle_scripts/jacoco.gradle'
|
||||
|
||||
wrapper {
|
||||
gradleVersion(versions.gradle)
|
||||
gradleVersion(libs.versions.gradle.get())
|
||||
distributionType("all")
|
||||
}
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
apply from: bootstrap.androidModule
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
ext {
|
||||
jacocoExclude = [
|
||||
'**/domain/**'
|
||||
]
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation androidSupport.roomRuntime
|
||||
implementation androidSupport.roomKtx
|
||||
kapt androidSupport.room
|
||||
implementation libs.roomRuntime
|
||||
implementation libs.roomKtx
|
||||
kapt libs.room
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -1,30 +1,22 @@
|
|||
apply from: bootstrap.kotlinModule
|
||||
|
||||
dependencies {
|
||||
api other.retrofit
|
||||
api other.jacksonConverter
|
||||
api other.koinCore
|
||||
api libs.retrofit
|
||||
api libs.jacksonConverter
|
||||
api libs.koinCore
|
||||
|
||||
implementation(other.jacksonKotlin) {
|
||||
implementation(libs.jacksonKotlin) {
|
||||
exclude module: 'kotlin-reflect'
|
||||
}
|
||||
implementation other.kotlinReflect // for jackson kotlin, but to use the same version
|
||||
implementation other.okhttpLogging
|
||||
implementation other.timber
|
||||
implementation libs.kotlinReflect // for jackson kotlin, but to use the same version
|
||||
implementation libs.okhttpLogging
|
||||
implementation libs.timber
|
||||
|
||||
testImplementation testing.kotlinJunit
|
||||
testImplementation testing.mockito
|
||||
testImplementation testing.mockitoInline
|
||||
testImplementation testing.mockitoKotlin
|
||||
testImplementation testing.kluent
|
||||
testImplementation testing.mockWebServer
|
||||
testImplementation testing.apacheCodecs
|
||||
}
|
||||
|
||||
ext {
|
||||
// Excluding data classes
|
||||
jacocoExclude = [
|
||||
'**/models/**',
|
||||
'**/di/**'
|
||||
]
|
||||
testImplementation libs.kotlinJunit
|
||||
testImplementation libs.mockito
|
||||
testImplementation libs.mockitoInline
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.kluent
|
||||
testImplementation libs.mockWebServer
|
||||
testImplementation libs.apacheCodecs
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.api.subsonic.models.MusicDirectory
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
@ -83,7 +96,7 @@ class SubsonicAPIClient(
|
|||
|
||||
// Create the Retrofit instance, and register a special converter factory
|
||||
// It will update our protocol version to the correct version, once we made a successful call
|
||||
val retrofit: Retrofit = Retrofit.Builder()
|
||||
private val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl("${config.baseUrl}/rest/")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(
|
||||
|
@ -109,17 +122,20 @@ class SubsonicAPIClient(
|
|||
|
||||
private fun OkHttpClient.Builder.addLogging() {
|
||||
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
|
||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.HEADERS
|
||||
this.addInterceptor(loggingInterceptor)
|
||||
}
|
||||
|
||||
@SuppressWarnings("TrustAllX509TrustManager", "EmptyFunctionBlock")
|
||||
private fun OkHttpClient.Builder.allowSelfSignedCertificates() {
|
||||
val trustManager = object : X509TrustManager {
|
||||
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
|
||||
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
val trustManager =
|
||||
@Suppress("CustomX509TrustManager")
|
||||
object : X509TrustManager {
|
||||
@Suppress("TrustAllX509TrustManager")
|
||||
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
|
||||
@Suppress("TrustAllX509TrustManager")
|
||||
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
|
||||
val sslContext = SSLContext.getInstance("SSL")
|
||||
sslContext.init(null, arrayOf(trustManager), SecureRandom())
|
||||
|
|
|
@ -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,110 +0,0 @@
|
|||
ext.versions = [
|
||||
minSdk : 21,
|
||||
targetSdk : 30,
|
||||
compileSdk : 30,
|
||||
// You need to run ./gradlew wrapper after updating the version
|
||||
gradle : '7.2',
|
||||
|
||||
navigation : "2.3.5",
|
||||
gradlePlugin : "4.2.2",
|
||||
androidxcore : "1.6.0",
|
||||
ktlint : "0.37.1",
|
||||
ktlintGradle : "10.2.0",
|
||||
detekt : "1.19.0",
|
||||
jacoco : "0.8.7",
|
||||
preferences : "1.1.1",
|
||||
media : "1.3.1",
|
||||
|
||||
androidSupport : "28.0.0",
|
||||
androidLegacySupport : "1.0.0",
|
||||
androidSupportDesign : "1.4.0",
|
||||
constraintLayout : "2.1.1",
|
||||
multidex : "2.0.1",
|
||||
room : "2.3.0",
|
||||
kotlin : "1.5.31",
|
||||
kotlinxCoroutines : "1.5.2-native-mt",
|
||||
viewModelKtx : "2.3.0",
|
||||
|
||||
retrofit : "2.6.4",
|
||||
jackson : "2.9.5",
|
||||
okhttp : "3.12.13",
|
||||
koin : "3.0.2",
|
||||
picasso : "2.71828",
|
||||
|
||||
junit4 : "4.13.2",
|
||||
junit5 : "5.8.1",
|
||||
mockito : "4.1.0",
|
||||
mockitoKotlin : "4.0.0",
|
||||
kluent : "1.68",
|
||||
apacheCodecs : "1.15",
|
||||
robolectric : "4.6.1",
|
||||
timber : "4.7.1",
|
||||
fastScroll : "2.0.1",
|
||||
colorPicker : "2.2.3",
|
||||
rxJava : "3.1.2",
|
||||
rxAndroid : "3.0.0",
|
||||
multiType : "4.3.0",
|
||||
]
|
||||
|
||||
ext.gradlePlugins = [
|
||||
gradle : "com.android.tools.build:gradle:$versions.gradlePlugin",
|
||||
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
|
||||
ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
|
||||
detekt : "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
|
||||
jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco",
|
||||
]
|
||||
|
||||
ext.androidSupport = [
|
||||
core : "androidx.core:core-ktx:$versions.androidxcore",
|
||||
support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport",
|
||||
design : "com.google.android.material:material:$versions.androidSupportDesign",
|
||||
annotations : "com.android.support:support-annotations:$versions.androidSupport",
|
||||
multidex : "androidx.multidex:multidex:$versions.multidex",
|
||||
constraintLayout : "androidx.constraintlayout:constraintlayout:$versions.constraintLayout",
|
||||
room : "androidx.room:room-compiler:$versions.room",
|
||||
roomRuntime : "androidx.room:room-runtime:$versions.room",
|
||||
roomKtx : "androidx.room:room-ktx:$versions.room",
|
||||
viewModelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.viewModelKtx",
|
||||
navigationFragment : "androidx.navigation:navigation-fragment:$versions.navigation",
|
||||
navigationUi : "androidx.navigation:navigation-ui:$versions.navigation",
|
||||
navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:$versions.navigation",
|
||||
navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$versions.navigation",
|
||||
navigationFeature : "androidx.navigation:navigation-dynamic-features-fragment:$versions.navigation",
|
||||
preferences : "androidx.preference:preference:$versions.preferences",
|
||||
media : "androidx.media:media:$versions.media",
|
||||
]
|
||||
|
||||
ext.other = [
|
||||
kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin",
|
||||
kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin",
|
||||
kotlinxCoroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.kotlinxCoroutines",
|
||||
retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
|
||||
gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
|
||||
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",
|
||||
jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson",
|
||||
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
|
||||
koinCore : "io.insert-koin:koin-core:$versions.koin",
|
||||
koinAndroid : "io.insert-koin:koin-android:$versions.koin",
|
||||
koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin",
|
||||
picasso : "com.squareup.picasso:picasso:$versions.picasso",
|
||||
timber : "com.jakewharton.timber:timber:$versions.timber",
|
||||
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
|
||||
colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker",
|
||||
rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava",
|
||||
rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid",
|
||||
multiType : "com.drakeet.multitype:multitype:$versions.multiType",
|
||||
]
|
||||
|
||||
ext.testing = [
|
||||
junit : "junit:junit:$versions.junit4",
|
||||
junitVintage : "org.junit.vintage:junit-vintage-engine:$versions.junit5",
|
||||
kotlinJunit : "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin",
|
||||
mockitoKotlin : "org.mockito.kotlin:mockito-kotlin:$versions.mockitoKotlin",
|
||||
mockito : "org.mockito:mockito-core:$versions.mockito",
|
||||
mockitoInline : "org.mockito:mockito-inline:$versions.mockito",
|
||||
kluent : "org.amshove.kluent:kluent:$versions.kluent",
|
||||
kluentAndroid : "org.amshove.kluent:kluent-android:$versions.kluent",
|
||||
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp",
|
||||
apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs",
|
||||
robolectric : "org.robolectric:robolectric:$versions.robolectric"
|
||||
]
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Others
|
||||
- #671: Bump versions.mockito from 4.1.0 to 4.3.1.
|
||||
- Update translations.
|
|
@ -0,0 +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.
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Otros
|
||||
- #671: Actualizado versions.mockito de 4.1.0 a 4.3.1.
|
||||
- Traducciones actualizadas.
|
|
@ -0,0 +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.
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
[versions]
|
||||
# You need to run ./gradlew wrapper after updating the version
|
||||
gradle = "7.3.3"
|
||||
|
||||
navigation = "2.3.5"
|
||||
gradlePlugin = "7.2.1"
|
||||
androidxcore = "1.6.0"
|
||||
ktlint = "0.43.2"
|
||||
ktlintGradle = "10.2.0"
|
||||
detekt = "1.19.0"
|
||||
preferences = "1.1.1"
|
||||
media = "1.3.1"
|
||||
media3 = "1.0.0-beta01"
|
||||
|
||||
androidSupport = "1.4.0"
|
||||
androidLegacySupport = "1.0.0"
|
||||
androidSupportDesign = "1.6.1"
|
||||
constraintLayout = "2.1.1"
|
||||
multidex = "2.0.1"
|
||||
room = "2.4.2"
|
||||
kotlin = "1.6.10"
|
||||
kotlinxCoroutines = "1.6.0-native-mt"
|
||||
kotlinxGuava = "1.6.0"
|
||||
viewModelKtx = "2.4.1"
|
||||
|
||||
retrofit = "2.9.0"
|
||||
jackson = "2.10.1"
|
||||
okhttp = "4.9.1"
|
||||
koin = "3.0.2"
|
||||
picasso = "2.71828"
|
||||
|
||||
junit4 = "4.13.2"
|
||||
junit5 = "5.8.1"
|
||||
mockito = "4.3.1"
|
||||
mockitoKotlin = "4.0.0"
|
||||
kluent = "1.68"
|
||||
apacheCodecs = "1.15"
|
||||
robolectric = "4.6.1"
|
||||
timber = "4.7.1"
|
||||
fastScroll = "2.0.1"
|
||||
colorPicker = "2.2.3"
|
||||
rxJava = "3.1.2"
|
||||
rxAndroid = "3.0.0"
|
||||
multiType = "4.3.0"
|
||||
|
||||
[libraries]
|
||||
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradlePlugin" }
|
||||
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" }
|
||||
|
||||
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 = "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" }
|
||||
roomRuntime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
roomKtx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
viewModelKtx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "viewModelKtx" }
|
||||
navigationFragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigation" }
|
||||
navigationUi = { module = "androidx.navigation:navigation-ui", version.ref = "navigation" }
|
||||
navigationFragmentKtx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
||||
navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
||||
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
|
||||
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" }
|
||||
jacksonKotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
||||
okhttpLogging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||
koinCore = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||
koinAndroid = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
||||
koinViewModel = { module = "io.insert-koin:koin-android-viewmodel", version.ref = "koin" }
|
||||
picasso = { module = "com.squareup.picasso:picasso", version.ref = "picasso" }
|
||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||
fastScroll = { module = "com.simplecityapps:recyclerview-fastscroll", version.ref = "fastScroll" }
|
||||
colorPickerView = { module = "com.github.skydoves:colorpickerview", version.ref = "colorPicker" }
|
||||
rxJava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxJava" }
|
||||
rxAndroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxAndroid" }
|
||||
multiType = { module = "com.drakeet.multitype:multitype", version.ref = "multiType" }
|
||||
|
||||
junit = { module = "junit:junit", version.ref = "junit4" }
|
||||
junitVintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" }
|
||||
kotlinJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||
mockitoKotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
|
||||
mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
|
||||
mockitoInline = { module = "org.mockito:mockito-inline", version.ref = "mockito" }
|
||||
kluent = { module = "org.amshove.kluent:kluent", version.ref = "kluent" }
|
||||
kluentAndroid = { module = "org.amshove.kluent:kluent-android", version.ref = "kluent" }
|
||||
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" }
|
|
@ -0,0 +1,5 @@
|
|||
ext.versions = [
|
||||
minSdk : 21,
|
||||
targetSdk : 33,
|
||||
compileSdk : 31,
|
||||
]
|
Binary file not shown.
|
@ -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.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,24 +47,13 @@ android {
|
|||
|
||||
tasks.withType(Test) {
|
||||
useJUnitPlatform()
|
||||
jacoco {
|
||||
includeNoLocationClasses = true
|
||||
excludes += jacocoExclude
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api other.kotlinStdlib
|
||||
api libs.kotlinStdlib
|
||||
|
||||
testImplementation testing.junit
|
||||
testRuntimeOnly testing.junitVintage
|
||||
testImplementation libs.junit
|
||||
testRuntimeOnly libs.junitVintage
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion(versions.jacoco)
|
||||
}
|
||||
|
||||
ext {
|
||||
jacocoExclude = ['jdk.internal.*']
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ if (isCodeQualityEnabled) {
|
|||
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
||||
|
||||
ktlint {
|
||||
version = versions.ktlint
|
||||
version = libs.versions.ktlint.get()
|
||||
outputToConsole = true
|
||||
android = true
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ if (isCodeQualityEnabled) {
|
|||
|
||||
detekt {
|
||||
buildUponDefaultConfig = true
|
||||
toolVersion = versions.detekt
|
||||
toolVersion = libs.versions.detekt.get()
|
||||
// Builds the AST in parallel. Rules are always executed in parallel.
|
||||
// Can lead to speedups in larger projects.
|
||||
parallel = true
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
apply plugin: 'jacoco'
|
||||
|
||||
jacoco {
|
||||
toolVersion(versions.jacoco)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -15,42 +14,14 @@ sourceSets {
|
|||
|
||||
|
||||
dependencies {
|
||||
api other.kotlinStdlib
|
||||
api libs.kotlinStdlib
|
||||
|
||||
testImplementation testing.junit
|
||||
testRuntimeOnly testing.junitVintage
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion(versions.jacoco)
|
||||
}
|
||||
|
||||
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)
|
||||
}))
|
||||
}
|
||||
testImplementation libs.junit
|
||||
testRuntimeOnly libs.junitVintage
|
||||
}
|
||||
|
||||
tasks.named("test").configure {
|
||||
useJUnitPlatform()
|
||||
jacoco {
|
||||
excludes += jacocoExclude
|
||||
includeNoLocationClasses = true
|
||||
}
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
|
||||
tasks.register("ciTest") {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -17,67 +17,101 @@
|
|||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
|
@ -106,80 +140,95 @@ location of your Java installation."
|
|||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
enableFeaturePreview("VERSION_CATALOGS")
|
||||
|
||||
include ':core:domain'
|
||||
include ':core:subsonic-api'
|
||||
include ':ultrasonic'
|
||||
|
|
|
@ -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 99
|
||||
versionName "3.0.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'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -85,81 +86,52 @@ dependencies {
|
|||
implementation project(':core:domain')
|
||||
implementation project(':core:subsonic-api')
|
||||
|
||||
api(other.picasso) {
|
||||
api(libs.picasso) {
|
||||
exclude group: "com.android.support"
|
||||
}
|
||||
|
||||
implementation androidSupport.core
|
||||
implementation androidSupport.support
|
||||
implementation androidSupport.design
|
||||
implementation androidSupport.multidex
|
||||
implementation androidSupport.roomRuntime
|
||||
implementation androidSupport.roomKtx
|
||||
implementation androidSupport.viewModelKtx
|
||||
implementation androidSupport.constraintLayout
|
||||
implementation androidSupport.preferences
|
||||
implementation androidSupport.media
|
||||
implementation libs.core
|
||||
implementation libs.support
|
||||
implementation libs.design
|
||||
implementation libs.multidex
|
||||
implementation libs.roomRuntime
|
||||
implementation libs.roomKtx
|
||||
implementation libs.viewModelKtx
|
||||
implementation libs.constraintLayout
|
||||
implementation libs.preferences
|
||||
implementation libs.media
|
||||
implementation libs.media3exoplayer
|
||||
implementation libs.media3session
|
||||
implementation libs.media3okhttp
|
||||
|
||||
implementation androidSupport.navigationFragment
|
||||
implementation androidSupport.navigationUi
|
||||
implementation androidSupport.navigationFragmentKtx
|
||||
implementation androidSupport.navigationUiKtx
|
||||
implementation androidSupport.navigationFeature
|
||||
implementation libs.navigationFragment
|
||||
implementation libs.navigationUi
|
||||
implementation libs.navigationFragmentKtx
|
||||
implementation libs.navigationUiKtx
|
||||
implementation libs.navigationFeature
|
||||
|
||||
implementation other.kotlinStdlib
|
||||
implementation other.kotlinxCoroutines
|
||||
implementation other.koinAndroid
|
||||
implementation other.okhttpLogging
|
||||
implementation other.fastScroll
|
||||
implementation other.colorPickerView
|
||||
implementation other.rxJava
|
||||
implementation other.rxAndroid
|
||||
implementation other.multiType
|
||||
implementation libs.kotlinStdlib
|
||||
implementation libs.kotlinxCoroutines
|
||||
implementation libs.kotlinxGuava
|
||||
implementation libs.koinAndroid
|
||||
implementation libs.okhttpLogging
|
||||
implementation libs.fastScroll
|
||||
implementation libs.colorPickerView
|
||||
implementation libs.rxJava
|
||||
implementation libs.rxAndroid
|
||||
implementation libs.multiType
|
||||
|
||||
kapt androidSupport.room
|
||||
kapt libs.room
|
||||
|
||||
testImplementation other.kotlinReflect
|
||||
testImplementation testing.junit
|
||||
testRuntimeOnly testing.junitVintage
|
||||
testImplementation testing.kotlinJunit
|
||||
testImplementation testing.kluent
|
||||
testImplementation testing.mockito
|
||||
testImplementation testing.mockitoInline
|
||||
testImplementation testing.mockitoKotlin
|
||||
testImplementation testing.robolectric
|
||||
testImplementation libs.kotlinReflect
|
||||
testImplementation libs.junit
|
||||
testRuntimeOnly libs.junitVintage
|
||||
testImplementation libs.kotlinJunit
|
||||
testImplementation libs.kluent
|
||||
testImplementation libs.mockito
|
||||
testImplementation libs.mockitoInline
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.robolectric
|
||||
|
||||
implementation other.timber
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion(versions.jacoco)
|
||||
}
|
||||
|
||||
// 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(versions.jacoco)
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
jacoco.includeNoLocationClasses = true
|
||||
jacoco.excludes += jacocoExclude
|
||||
implementation libs.timber
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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,48 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>About Ultrasonic</title>
|
||||
<link rel="stylesheet" href="../style.css" type="text/css">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h3><img src="../img/ultrasonic.png" alt="">Ultrasonic</h3>
|
||||
|
||||
<p>
|
||||
Mit <b>Ultrasonic</b> können Sie mit dem Subsonic Media Streamer ganz einfach Musik von Ihrem Heimcomputer auf Ihr Android-Handy streamen oder
|
||||
herunterladen. Die Subsonic-Server-Software erfordert eine zusätzliche, von Ultrasonic getrennte Konfiguration. Für weitere Informationen oder
|
||||
zur Installation der Subsonic-Server-Software auf Ihrem Computer besuchen Sie bitte <a href="http://subsonic.org">subsonic.org</a>. Die Basisversion
|
||||
von Subsonic ist kostenlos. Wenn Sie Subsonic zum ersten Mal installieren, sind die Premium-Funktionen 30 Tage lang verfügbar, so dass Sie sie
|
||||
ausprobieren können, bevor Sie sich für ein Upgrade entscheiden. Klicken Sie <a href="http://www.subsonic.org/pages/premium.jsp">hier</a>, um
|
||||
ein Upgrade auf Subsonic Premium durchzuführen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Standardmäßig ist Ultrasonic nicht konfiguriert. Wenn Sie Ihren eigenen Server eingerichtet haben, gehen Sie bitte zu <b>Einstellungen</b> und
|
||||
ändern Sie die Konfiguration so, dass er mit Ihrem eigenen Computer verbunden wird.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Wenn Sie mit Ultrasonic zufrieden sind, klicken Sie bitte auf "Spenden", um die weitere Entwicklung zu unterstützen. Diese Spende ist von der
|
||||
Subsonic-Server-Software getrennt und gewährt Ihnen keinen Zugang zu den Premium-Funktionen von Subsonic.
|
||||
</p>
|
||||
|
||||
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_s-xclick">
|
||||
<input type="hidden" name="hosted_button_id" value="DQXEZRDRAGCA8">
|
||||
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" name="submit" alt="PayPal - The safer, easier way to pay online!">
|
||||
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
||||
</form>
|
||||
|
||||
<p>
|
||||
Um Feature-Anfragen oder Fehlerberichte einzureichen, besuchen Sie bitte das Ultrasonic für Android <a href="http://forum.subsonic.org/forum/viewforum.php?f=17">Forum</a>.
|
||||
Der Quellcode von Ultrasonic ist unter <a href="https://github.com/ogarcia/ultrasonic">github.com</a> verfügbar und unter den Bedingungen der GNU General Public License Version 3 (GPLv3) lizenziert.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,52 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>About Ultrasonic</title>
|
||||
<link rel="stylesheet" href="../style.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3><img src="../img/ultrasonic.png" alt="">Ultrasonic</h3>
|
||||
|
||||
<p>
|
||||
With <b>Ultrasonic</b> you can easily stream or download music from your
|
||||
home computer to your Android phone using the Subsonic media streamer.
|
||||
The Subsonic server software requires additional configuration separate
|
||||
from Ultrasonic. For more information or to install the Subsonic server
|
||||
software on your computer, please visit
|
||||
<a href="http://subsonic.org">subsonic.org</a>. The basic version of
|
||||
Subsonic is free. When you first install Subsonic, the premium features
|
||||
are available for 30 days so you can try them out before deciding to
|
||||
upgrade. Click
|
||||
<a href="http://www.subsonic.org/pages/premium.jsp">here</a> to upgrade
|
||||
to Subsonic Premium.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
By default, Ultrasonic is not configured. Once you've set up your own
|
||||
server, please go to <b>Settings</b> and change the configuration so
|
||||
that it connects to your own computer.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you are pleased with Ultrasonic, please click "Donate" to help
|
||||
further development. This donation is separate from the Subsonic server
|
||||
software and does not grant you access to the premium features of
|
||||
Subsonic.
|
||||
</p>
|
||||
|
||||
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_s-xclick">
|
||||
<input type="hidden" name="hosted_button_id" value="DQXEZRDRAGCA8">
|
||||
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" name="submit" alt="PayPal - The safer, easier way to pay online!">
|
||||
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
||||
</form>
|
||||
|
||||
<p>
|
||||
To submit feature requests or file bug reports, please visit the
|
||||
Ultrasonic for Android
|
||||
<a href="http://forum.subsonic.org/forum/viewforum.php?f=17">forum</a>.
|
||||
Source code for Ultrasonic is available at
|
||||
<a href="https://github.com/ogarcia/ultrasonic">github.com</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
|
@ -1,61 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Aide de Ultrasonic</title>
|
||||
<link rel="stylesheet" href="../style.css" type="text/css">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h3><img src="../img/ultrasonic.png" alt=""> Bienvenue dans Ultrasonic</h3>
|
||||
|
||||
<p>
|
||||
Avec <b>Ultrasonic</b>, vous pouvez facilement écouter ou télécharger de la musique à partir de votre ordinateur personnel sur votre appareil Android
|
||||
(et bien d'autres choses sont possibles).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pour installer le serveur Subsonic sur votre ordinateur, visitez <a href="http://subsonic.org">subsonic.org</a>.
|
||||
Celui-ci est disponible pour Windows, Mac, Linux et Unix.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Par défaut, cette application n'est pas configurée. Après avoir configuré votre
|
||||
serveur personnel, veuillez accéder aux <b>Paramètres</b> et modifier la configuration afin de vous connecter à votre propre ordinateur ou vos appareils mobiles.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Vous pouvez utiliser cette application gratuitement pendant 30 jours.
|
||||
Ensuite, vous devrez effectuer un don au projet Subsonic.
|
||||
En tant que donateur, vous obtiendrez les bénéfices suivants:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Écoute et téléchargement illimités vers autant de iPhones et d'appareils Android que souhaité.</li>
|
||||
<li>Lecture de vidéos.</li>
|
||||
<li>Une adresse web personnalisée pour votre serveur Subsonic (<em>votrenom</em>.subsonic.org).</li>
|
||||
<li>Aucunes publicités dans l'interface web de Subsonic.</li>
|
||||
<li>Accès gratuit aux nouvelles fonctionnalités avancées.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Le montant suggéré pour le don est de <b>20€</b>, mais n'importe quel montant est traité et accepté.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Cliquez sur le bouton suivants pour accéder à PayPal, d'où vous pourrez payer soit par carte de crédit, soit en utilisant votre compte PayPal.
|
||||
Une fois le don reçu et traité, vous recevrez votre clé d'activation par e-mail.
|
||||
</p>
|
||||
|
||||
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_s-xclick">
|
||||
<input type="hidden" name="hosted_button_id" value="DQXEZRDRAGCA8">
|
||||
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" name="submit" alt="PayPal - The safer, easier way to pay online!">
|
||||
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
||||
</form>
|
||||
|
||||
<p>
|
||||
Pour plus d'information, veuiller visitez <a href="http://subsonic.org/">subsonic.org</a>. Le code source de Ultrasonic est disponible à l'adresse suivante : <a href="https://github.com/ogarcia/ultrasonic">github.com</a>.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Taken from http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css
|
||||
*/
|
||||
body {
|
||||
font: 13px / 1.231 arial, helvetica, clean, sans-serif;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size:inherit;
|
||||
font:100%;
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
|
||||
/**
|
||||
* Displays online help and about information in a WebView
|
||||
*/
|
||||
public class AboutFragment extends Fragment {
|
||||
|
||||
private WebView webView;
|
||||
private ImageView backButton;
|
||||
private ImageView forwardButton;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.help, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
|
||||
swipeRefresh = view.findViewById(R.id.help_refresh);
|
||||
swipeRefresh.setEnabled(false);
|
||||
|
||||
webView = view.findViewById(R.id.help_contents);
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.setWebViewClient(new HelpClient());
|
||||
|
||||
if (savedInstanceState != null)
|
||||
{
|
||||
webView.restoreState(savedInstanceState);
|
||||
}
|
||||
else
|
||||
{
|
||||
webView.loadUrl(getResources().getString(R.string.help_url));
|
||||
}
|
||||
|
||||
backButton = view.findViewById(R.id.help_back);
|
||||
backButton.setOnClickListener(new Button.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View view)
|
||||
{
|
||||
webView.goBack();
|
||||
}
|
||||
});
|
||||
|
||||
ImageView stopButton = view.findViewById(R.id.help_stop);
|
||||
stopButton.setOnClickListener(new Button.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View view)
|
||||
{
|
||||
webView.stopLoading();
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
});
|
||||
|
||||
forwardButton = view.findViewById(R.id.help_forward);
|
||||
forwardButton.setOnClickListener(new Button.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View view)
|
||||
{
|
||||
webView.goForward();
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Nicer Back key handling?
|
||||
webView.setFocusableInTouchMode(true);
|
||||
webView.requestFocus();
|
||||
webView.setOnKeyListener( new View.OnKeyListener()
|
||||
{
|
||||
@Override
|
||||
public boolean onKey( View v, int keyCode, KeyEvent event )
|
||||
{
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK)
|
||||
{
|
||||
if (webView.canGoBack())
|
||||
{
|
||||
webView.goBack();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NotNull Bundle state)
|
||||
{
|
||||
webView.saveState(state);
|
||||
super.onSaveInstanceState(state);
|
||||
}
|
||||
|
||||
private final class HelpClient extends WebViewClient
|
||||
{
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
swipeRefresh.setRefreshing(true);
|
||||
super.onPageStarted(view, url, favicon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url)
|
||||
{
|
||||
swipeRefresh.setRefreshing(false);
|
||||
String versionName = Util.getVersionName(getContext());
|
||||
String title = String.format("%s (%s)", view.getTitle(), versionName);
|
||||
|
||||
FragmentTitle.Companion.setTitle(AboutFragment.this, title);
|
||||
|
||||
backButton.setEnabled(view.canGoBack());
|
||||
forwardButton.setEnabled(view.canGoForward());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
|
||||
{
|
||||
Util.toast(getContext(), description);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -18,22 +18,24 @@
|
|||
*/
|
||||
package org.moire.ultrasonic.receiver;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import timber.log.Timber;
|
||||
|
||||
import org.moire.ultrasonic.util.Constants;
|
||||
import org.moire.ultrasonic.util.Settings;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Resume or pause playback on Bluetooth A2DP connect/disconnect.
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
public class BluetoothIntentReceiver extends BroadcastReceiver
|
||||
{
|
||||
@Override
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
*/
|
||||
package org.moire.ultrasonic.service.ssl;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
|
@ -34,6 +36,7 @@ import javax.net.ssl.X509TrustManager;
|
|||
/**
|
||||
* @since 4.1
|
||||
*/
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
class TrustManagerDecorator implements X509TrustManager
|
||||
{
|
||||
|
||||
|
|
|
@ -1,117 +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 java.lang.ref.SoftReference;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class LRUCache<K, V>
|
||||
{
|
||||
|
||||
private final int capacity;
|
||||
private final Map<K, TimestampedValue> map;
|
||||
|
||||
public LRUCache(int capacity)
|
||||
{
|
||||
map = new HashMap<K, TimestampedValue>(capacity);
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
public synchronized V get(K key)
|
||||
{
|
||||
TimestampedValue value = map.get(key);
|
||||
|
||||
V result = null;
|
||||
if (value != null)
|
||||
{
|
||||
value.updateTimestamp();
|
||||
result = value.getValue();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public synchronized void put(K key, V value)
|
||||
{
|
||||
if (map.size() >= capacity)
|
||||
{
|
||||
removeOldest();
|
||||
}
|
||||
map.put(key, new TimestampedValue(value));
|
||||
}
|
||||
|
||||
public void clear()
|
||||
{
|
||||
map.clear();
|
||||
}
|
||||
|
||||
private void removeOldest()
|
||||
{
|
||||
K oldestKey = null;
|
||||
long oldestTimestamp = Long.MAX_VALUE;
|
||||
|
||||
for (Map.Entry<K, TimestampedValue> entry : map.entrySet())
|
||||
{
|
||||
K key = entry.getKey();
|
||||
TimestampedValue value = entry.getValue();
|
||||
if (value.getTimestamp() < oldestTimestamp)
|
||||
{
|
||||
oldestTimestamp = value.getTimestamp();
|
||||
oldestKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestKey != null)
|
||||
{
|
||||
map.remove(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
private final class TimestampedValue
|
||||
{
|
||||
|
||||
private final SoftReference<V> value;
|
||||
private long timestamp;
|
||||
|
||||
public TimestampedValue(V value)
|
||||
{
|
||||
this.value = new SoftReference<V>(value);
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public V getValue()
|
||||
{
|
||||
return value.get();
|
||||
}
|
||||
|
||||
public long getTimestamp()
|
||||
{
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void updateTimestamp()
|
||||
{
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue