Compare commits

..

No commits in common. "develop" and "3.0.0" have entirely different histories.

238 changed files with 9147 additions and 8143 deletions

View File

@ -1,33 +1,23 @@
version: 2.1 version: 3
parameters:
memory-config:
type: string
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g"
memory-config-debug:
type: string
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*"
jobs: jobs:
build: build:
docker: docker:
- image: cimg/android:2022.06.1 - image: circleci/android:api-30
working_directory: ~/ultrasonic working_directory: ~/ultrasonic
environment: environment:
JVM_OPTS: << pipeline.parameters.memory-config >> JVM_OPTS: -Xmx3200m
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
GRADLE_OPTS: << pipeline.parameters.memory-config >>
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }} - v1-ultrasonic-{{ .Branch }}-{{ checksum "dependencies.gradle" }}
- v2-ultrasonic-{{ .Branch }} - v1-ultrasonic-{{ .Branch }}
- v2-ultrasonic - v1-ultrasonic
- run: - run:
name: configure gradle.properties for CI building name: configure gradle.properties for CI building
command: | command: |
sed -i '/^org.gradle.jvmargs/d' gradle.properties sed -i '/^org.gradle.jvmargs/d' gradle.properties
sed -i 's/^org.gradle.daemon=true/org.gradle.daemon=false/g' gradle.properties sed -i 's/^org.gradle.daemon=true/org.gradle.daemon=false/g' gradle.properties
cat gradle.properties
- run: - run:
name: checkstyle name: checkstyle
command: ./gradlew -Pqc ktlintCheck command: ./gradlew -Pqc ktlintCheck
@ -41,6 +31,7 @@ jobs:
name: unit-tests name: unit-tests
command: | command: |
./gradlew ciTest testDebugUnitTest ./gradlew ciTest testDebugUnitTest
./gradlew jacocoFullReport
- run: - run:
name: lint name: lint
command: ./gradlew :ultrasonic:lintRelease command: ./gradlew :ultrasonic:lintRelease
@ -53,16 +44,18 @@ jobs:
- save_cache: - save_cache:
paths: paths:
- ~/.gradle - ~/.gradle
key: v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }} key: v1-ultrasonic-{{ .Branch }}-{{ checksum "dependencies.gradle" }}
- store_artifacts: - store_artifacts:
path: ultrasonic/build/reports path: ultrasonic/build/reports
destination: reports destination: reports
- store_artifacts: - store_artifacts:
path: subsonic-api/build/reports path: subsonic-api/build/reports
destination: reports destination: reports
- store_artifacts:
path: build/reports/jacoco/jacocoFullReport/
push_translations: push_translations:
docker: docker:
- image: cimg/python:3.6 - image: circleci/python:3.6
working_directory: ~/ultrasonic working_directory: ~/ultrasonic
steps: steps:
- checkout - checkout
@ -82,19 +75,15 @@ jobs:
tx push -s tx push -s
generate_signed_apk: generate_signed_apk:
docker: docker:
- image: cimg/android:2022.06.1 - image: circleci/android:api-30
working_directory: ~/ultrasonic working_directory: ~/ultrasonic
environment:
JVM_OPTS: << pipeline.parameters.memory-config >>
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
GRADLE_OPTS: << pipeline.parameters.memory-config >>
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }} - v1-ultrasonic-{{ .Branch }}-{{ checksum "dependencies.gradle" }}
- v2-ultrasonic-{{ .Branch }} - v1-ultrasonic-{{ .Branch }}
- v2-ultrasonic - v1-ultrasonic
- run: - run:
name: decrypt ultrasonic-keystore 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 command: openssl aes-256-cbc -K ${ULTRASONIC_KEYSTORE_KEY} -iv ${ULTRASONIC_KEYSTORE_IV} -in ultrasonic-keystore.enc -out ultrasonic-keystore -d
@ -106,22 +95,22 @@ jobs:
command: | command: |
export PATH="${JAVA_HOME}/bin:${PATH}" export PATH="${JAVA_HOME}/bin:${PATH}"
mkdir -p /tmp/ultrasonic-release mkdir -p /tmp/ultrasonic-release
${ANDROID_HOME}/build-tools/32.0.0/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk ${ANDROID_HOME}/build-tools/30.0.0/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/30.0.0/apksigner sign --verbose --ks ~/ultrasonic/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
${ANDROID_HOME}/build-tools/32.0.0/apksigner verify --verbose /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk ${ANDROID_HOME}/build-tools/30.0.0/apksigner verify --verbose /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
- persist_to_workspace: - persist_to_workspace:
root: /tmp/ultrasonic-release root: /tmp/ultrasonic-release
paths: paths:
- ultrasonic-*.apk* - ultrasonic-*.apk*
publish_github_signed_apk: publish_github_signed_apk:
docker: docker:
- image: cimg/go:1.18 - image: circleci/golang
steps: steps:
- attach_workspace: - attach_workspace:
at: /tmp/ultrasonic-release at: /tmp/ultrasonic-release
- run: - run:
name: install ghr name: install ghr
command: go install -v github.com/tcnksm/ghr@latest command: go get -v github.com/tcnksm/ghr
- run: - run:
name: publish release on github tag name: publish release on github tag
command: ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} ${CIRCLE_TAG} /tmp/ultrasonic-release command: ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} ${CIRCLE_TAG} /tmp/ultrasonic-release
@ -140,7 +129,7 @@ workflows:
- generate_signed_apk: - generate_signed_apk:
filters: filters:
tags: tags:
only: /^[0-9]+(\.[0-9]+)*(-beta\.[0-9]+)?/ only: /^[0-9]+(\.[0-9]+)*/
branches: branches:
ignore: /.*/ ignore: /.*/
- publish_github_signed_apk: - publish_github_signed_apk:
@ -148,7 +137,7 @@ workflows:
- generate_signed_apk - generate_signed_apk
filters: filters:
tags: tags:
only: /^[0-9]+(\.[0-9]+)*(-beta\.[0-9]+)?/ only: /^[0-9]+(\.[0-9]+)*/
branches: branches:
ignore: /.*/ ignore: /.*/

1
.gitignore vendored
View File

@ -39,7 +39,6 @@ captures/
*.iml *.iml
.idea/ .idea/
# Keystore files # Keystore files
*.jks *.jks

View File

@ -1,128 +0,0 @@
<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>

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="&amp;#36;file.fileName&#10;Copyright (C) 2009-&amp;#36;today.year Ultrasonic developers&#10;&#10;Distributed under terms of the GNU GPLv3 license." />
<option name="myName" value="Default" />
</copyright>
</component>

View File

@ -1,7 +0,0 @@
<component name="CopyrightManager">
<settings default="Default">
<LanguageOptions name="Kotlin">
<option name="fileTypeOverride" value="3" />
</LanguageOptions>
</settings>
</component>

View File

@ -1,8 +0,0 @@
<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>

View File

@ -1,4 +0,0 @@
<?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>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -18,46 +18,18 @@ By default Pull Request should be opened against **develop** branch, PR against
### Here are a few guidelines you should follow before submitting: ### Here are a few guidelines you should follow before submitting:
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted. 1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
Use `git commit --signoff` to acknowledge this. Use `git commit --signoff` to acknowledge this.
2. **No Breakage:** New features or changes to existing ones must not degrade the user experience. 2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests
3. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms. 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.
Refactoring existing messes is great, but watch out for breakage. Refactoring existing messes is great, but watch out for breakage.
4. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review 5. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
and test. and test.
### Pull Request Process ### Pull Request Process
On each Pull Request Github runs a number of checks to make sure there are no problems.
#### Signed commits
Commits must be signed. [See here how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
#### KtLint
This programm checks if the source code is formatted correctly.
You can run it yourself locally with
`./gradlew -Pqc ktlintFormat`
Running this command will fix common problems and will notify you of problems it couldn't fix automatically.
#### Detekt
Detekt is a static analyser. It helps to find potential bugs in our code.
You can run it yourself locally with
`./gradlew -Pqc detekt`
There is a "baseline" file, in which errors which have been in the code base before are noted.
Sometimes it is necessary to regenerate this file by running:
`./gradlew -Pqc detektBaseline`
#### Lint
Lint looks for general problems in the code or unused resources etc.
You can run it with
`./gradlew -Pqc lintRelease`
If there is a need to regenerate the baseline, remove `ultrasonic/lint-baseline.xml` and rerun the command.
1. Ensure [all commits are signed-off](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification).
2. Check tests for the new code are added.
3. Check code style is passing.
4. Check code static analysis is passing.

View File

@ -1,28 +1,20 @@
## Problem description ## Problem description
Describe your problem here. Describe what you want to happen, and what Describe your problem here. Describe what you want to happen, and what happens
happens if you try to do it. If you have a stack trace or any logs, please if you try to do it. If you have a stack trace or any logs, please format them using
format them using GitHub triple backquote notation. github triple backquote notation
### Steps to reproduce ### Steps to reproduce
Describe how somebody else could observe the same behavior you do. Don't Describe how somebody else could observe the same behavior you do. Don't share here any logins and
share here any logins and passwords! passwords!
## System information ## System information
### Ultrasonic client
* **Ultrasonic version**: *version of the app* * **Ultrasonic version**: *version of the app*
* **Android version**: *Version of Android OS on the device* * **Android version**: *Version of Android OS on the device*
* **Device info**: *Device manufacturer, model* * **Device info**: *Device manufacturer, model*
### Server
* **Server name**: *Airsonic, Ampache, Supysonic...*
* **Server version**: *version of server software*
* **Protocol used**: *http or https (self certificate, letsencrypt...)*
## Additional notes ## Additional notes
Include any extra notes here. Otherwise you may remove this section. Include any extra notes here. Otherwise you may remove this section.

View File

@ -1,25 +1,14 @@
# WE HAVE MOVED
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
[![Build Status](https://circleci.com/gh/ultrasonic/ultrasonic/tree/develop.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/ultrasonic)
[![Codecov branch](https://img.shields.io/codecov/c/github/ultrasonic/ultrasonic/develop.svg)]()
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
Ultrasonic is free and open-source music streaming Android client for 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.
[Subsonic][subsonic] [API][subapi] (version 1.7.0 or higher) compatible
servers.
## Help wanted ## Help wanted
We currently don't have that much time to spend developing Subsonic, so any We currently don't have that much time to spend developing Subsonic, so any
contributions or active developers are always welcomed. contributions or active developers are always welcomed.
Have a look at [CONTRIBUTING](CONTRIBUTING.md) to get started.
## Download ## Download
@ -27,26 +16,24 @@ App is available to download at following stores:
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="70">](https://play.google.com/store/apps/details?id=org.moire.ultrasonic) [<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="70">](https://play.google.com/store/apps/details?id=org.moire.ultrasonic)
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="70">](https://f-droid.org/packages/org.moire.ultrasonic/) [<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="70">](https://f-droid.org/packages/org.moire.ultrasonic/)
[<img src="https://ultrasonic.gitlab.io/assets/img/get-it-on-gitlab.png" alt="Get it on GitLab" height="70">](https://gitlab.com/ultrasonic/ultrasonic/-/releases) [<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)
**Warning**: All three versions (Google Play, F-Droid and the APKs) are not **Warning**: All three versions (Google Play, F-Droid and the APKs) are not
compatible (not signed by the same key)! You must uninstall one to install compatible (not signed by the same key)! You must uninstall one to install
the other, which will delete all your data. the other, which will delete all your data.
If you want to use the version downloaded from F-Droid or from GitLab with 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).
**Android Auto**, you must enable Unknown Sources as it is described in
[this wiki page][wikiaa].
## Bugs and issues ## Bugs and issues
First, see if your issue havent been yet reported [here][issues], otherwise First, see if your issue havent been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
open [a new issue][newissue]. otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
### Known (not our) bugs ### Known (not our) bugs
If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not
work. This is caused by bad implementation of Subsonic API by Madsonic. For work. This is caused by bad implementation of Subsonic API by Madsonic. For
more info about this you can read [this bug][madbug]. more info about this you can read [this bug](https://github.com/ultrasonic/ultrasonic/issues/129).
## Contributing ## Contributing
@ -54,29 +41,16 @@ See [CONTRIBUTING](CONTRIBUTING.md).
## Supported (tested) Subsonic API implementations ## Supported (tested) Subsonic API implementations
- [Subsonic][subsonic] - [Subsonic](http://www.subsonic.org/pages/index.jsp)
- [Airsonic-Advanced][airsonic] - [Airsonic](https://github.com/airsonic/airsonic)
- [Supysonic][supysonic] - [Supysonic](https://github.com/spl0k/supysonic)
- [Ampache][ampache] - [Ampache](https://ampache.org/)
Other *Subsonic API* implementations should work as well as long as they Other *Subsonic API* implementations should work as well as long as they follow API
follow API [documentation][subapi]. [documentation](http://www.subsonic.org/pages/api.jsp).
## License ## License
This software is licensed under the terms of the GNU General Public License This software is licensed under the terms of the GNU General Public License version 3 (GPLv3).
version 3 (GPLv3).
Full text of the license is available in the [LICENSE](LICENSE) file and Full text of the license is available in the [LICENSE](LICENSE) file and [online](https://opensource.org/licenses/gpl-3.0.html).
[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

View File

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
apply from: 'gradle/versions.gradle' apply from: 'dependencies.gradle'
ext.bootstrap = [ ext.bootstrap = [
kotlinModule : "${project.rootDir}/gradle_scripts/kotlin-module-bootstrap.gradle", kotlinModule : "${project.rootDir}/gradle_scripts/kotlin-module-bootstrap.gradle",
@ -13,10 +13,11 @@ buildscript {
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
} }
dependencies { dependencies {
classpath libs.gradle classpath gradlePlugins.gradle
classpath libs.kotlin classpath gradlePlugins.kotlin
classpath libs.ktlintGradle classpath gradlePlugins.ktlintGradle
classpath libs.detekt classpath gradlePlugins.detekt
classpath gradlePlugins.jacoco
} }
} }
@ -43,7 +44,9 @@ allprojects {
} }
} }
apply from: 'gradle_scripts/jacoco.gradle'
wrapper { wrapper {
gradleVersion(libs.versions.gradle.get()) gradleVersion(versions.gradle)
distributionType("all") distributionType("all")
} }

View File

@ -1,8 +1,14 @@
apply from: bootstrap.androidModule apply from: bootstrap.androidModule
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
dependencies { ext {
implementation libs.roomRuntime jacocoExclude = [
implementation libs.roomKtx '**/domain/**'
kapt libs.room ]
}
dependencies {
implementation androidSupport.roomRuntime
implementation androidSupport.roomKtx
kapt androidSupport.room
} }

View File

@ -1,38 +0,0 @@
/*
* 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
}

View File

@ -1,23 +1,14 @@
/*
* Artist.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "artists", primaryKeys = ["id", "serverId"]) @Entity(tableName = "artists")
data class Artist( data class Artist(
override var id: String, @PrimaryKey override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
override var name: String? = null, override var name: String? = null,
override var index: String? = null, override var index: String? = null,
override var coverArt: String? = null, override var coverArt: String? = null,
override var albumCount: Long? = null, override var albumCount: Long? = null,
override var closeness: Int = 0 override var closeness: Int = 0
) : ArtistOrIndex(id, serverId) ) : ArtistOrIndex(id)

View File

@ -1,21 +1,11 @@
/*
* ArtistOrIndex.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import androidx.room.Ignore import androidx.room.Ignore
@Suppress("LongParameterList")
abstract class ArtistOrIndex( abstract class ArtistOrIndex(
@Ignore @Ignore
override var id: String, override var id: String,
@Ignore @Ignore
open var serverId: Int,
@Ignore
override var name: String? = null, override var name: String? = null,
@Ignore @Ignore
open var index: String? = null, open var index: String? = null,
@ -28,15 +18,15 @@ abstract class ArtistOrIndex(
) : GenericEntry() { ) : GenericEntry() {
fun compareTo(other: ArtistOrIndex): Int { fun compareTo(other: ArtistOrIndex): Int {
return when { when {
this.closeness == other.closeness -> { this.closeness == other.closeness -> {
0 return 0
} }
this.closeness > other.closeness -> { this.closeness > other.closeness -> {
-1 return -1
} }
else -> { else -> {
1 return 1
} }
} }
} }

View File

@ -2,6 +2,7 @@ package org.moire.ultrasonic.domain
import java.io.Serializable import java.io.Serializable
import java.util.Date import java.util.Date
import org.moire.ultrasonic.domain.MusicDirectory.Entry
data class Bookmark( data class Bookmark(
val position: Int = 0, val position: Int = 0,
@ -9,7 +10,7 @@ data class Bookmark(
val comment: String, val comment: String,
val created: Date? = null, val created: Date? = null,
val changed: Date? = null, val changed: Date? = null,
val track: Track val entry: Entry
) : Serializable { ) : Serializable {
companion object { companion object {
private const val serialVersionUID = 8988990025189807803L private const val serialVersionUID = 8988990025189807803L

View File

@ -1,24 +1,15 @@
/*
* Index.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "indexes", primaryKeys = ["id", "serverId"]) @Entity(tableName = "indexes")
data class Index( data class Index(
override var id: String, @PrimaryKey override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
override var name: String? = null, override var name: String? = null,
override var index: String? = null, override var index: String? = null,
override var coverArt: String? = null, override var coverArt: String? = null,
override var albumCount: Long? = null, override var albumCount: Long? = null,
override var closeness: Int = 0, override var closeness: Int = 0,
var musicFolderId: String? = null var musicFolderId: String? = null
) : ArtistOrIndex(id, serverId) ) : ArtistOrIndex(id)

View File

@ -1,12 +1,8 @@
/*
* MusicDirectory.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
import java.util.Date import java.util.Date
class MusicDirectory : ArrayList<MusicDirectory.Child>() { class MusicDirectory : ArrayList<MusicDirectory.Child>() {
@ -24,9 +20,9 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles } return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
} }
fun getTracks(): List<Track> { fun getTracks(): List<Entry> {
return mapNotNull { return mapNotNull {
it as? Track it as? Entry
} }
} }
@ -38,7 +34,6 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
abstract class Child : GenericEntry() { abstract class Child : GenericEntry() {
abstract override var id: String abstract override var id: String
abstract var serverId: Int
abstract var parent: String? abstract var parent: String?
abstract var isDirectory: Boolean abstract var isDirectory: Boolean
abstract var album: String? abstract var album: String?
@ -58,4 +53,87 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
abstract var closeness: Int abstract var closeness: Int
abstract var isVideo: Boolean abstract var isVideo: Boolean
} }
// TODO: Rename to Track
@Entity
data class Entry(
@PrimaryKey override var id: String,
override var parent: String? = null,
override var isDirectory: Boolean = false,
override var title: String? = null,
override var album: String? = null,
var albumId: String? = null,
override var artist: String? = null,
override var artistId: String? = null,
var track: Int? = null,
override var year: Int? = null,
override var genre: String? = null,
var contentType: String? = null,
var suffix: String? = null,
var transcodedContentType: String? = null,
var transcodedSuffix: String? = null,
override var coverArt: String? = null,
var size: Long? = null,
override var songCount: Long? = null,
override var duration: Int? = null,
var bitRate: Int? = null,
override var path: String? = null,
override var isVideo: Boolean = false,
override var starred: Boolean = false,
override var discNumber: Int? = null,
var type: String? = null,
override var created: Date? = null,
override var closeness: Int = 0,
var bookmarkPosition: Int = 0,
var userRating: Int? = null,
var averageRating: Float? = null,
override var name: String? = null
) : Serializable, Child() {
fun setDuration(duration: Long) {
this.duration = duration.toInt()
}
companion object {
private const val serialVersionUID = -3339106650010798108L
}
fun compareTo(other: Entry): Int {
when {
this.closeness == other.closeness -> {
return 0
}
this.closeness > other.closeness -> {
return -1
}
else -> {
return 1
}
}
}
override fun compareTo(other: Identifiable) = compareTo(other as Entry)
}
data class Album(
@PrimaryKey override var id: String,
override var parent: String? = null,
override var album: String? = null,
override var title: String? = null,
override val name: String? = null,
override var discNumber: Int? = 0,
override var coverArt: String? = null,
override var songCount: Long? = null,
override var created: Date? = null,
override var artist: String? = null,
override var artistId: String? = null,
override var duration: Int? = 0,
override var year: Int? = 0,
override var genre: String? = null,
override var starred: Boolean = false,
override var path: String? = null,
override var closeness: Int = 0,
) : Child() {
override var isDirectory = true
override var isVideo = false
}
} }

View File

@ -1,22 +1,13 @@
/*
* MusicFolder.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey
/** /**
* Represents a top level directory in which music or other media is stored. * Represents a top level directory in which music or other media is stored.
*/ */
@Entity(tableName = "music_folders", primaryKeys = ["id", "serverId"]) @Entity(tableName = "music_folders")
data class MusicFolder( data class MusicFolder(
override val id: String, @PrimaryKey override val id: String,
override val name: String, override val name: String
@ColumnInfo(defaultValue = "-1")
var serverId: Int
) : GenericEntry() ) : GenericEntry()

View File

@ -0,0 +1,12 @@
package org.moire.ultrasonic.domain
enum class PlayerState {
IDLE,
DOWNLOADING,
PREPARING,
PREPARED,
STARTED,
STOPPED,
PAUSED,
COMPLETED
}

View File

@ -0,0 +1,15 @@
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
}

View File

@ -1,10 +1,13 @@
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import org.moire.ultrasonic.domain.MusicDirectory.Album
import org.moire.ultrasonic.domain.MusicDirectory.Entry
/** /**
* The result of a search. Contains matching artists, albums and songs. * The result of a search. Contains matching artists, albums and songs.
*/ */
data class SearchResult( data class SearchResult(
val artists: List<ArtistOrIndex> = listOf(), val artists: List<ArtistOrIndex> = listOf(),
val albums: List<Album> = listOf(), val albums: List<Album> = listOf(),
val songs: List<Track> = listOf() val songs: List<Entry> = listOf()
) )

View File

@ -1,6 +1,7 @@
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import java.io.Serializable import java.io.Serializable
import org.moire.ultrasonic.domain.MusicDirectory.Entry
data class Share( data class Share(
override var id: String, override var id: String,
@ -11,7 +12,7 @@ data class Share(
var lastVisited: String? = null, var lastVisited: String? = null,
var expires: String? = null, var expires: String? = null,
var visitCount: Long? = null, var visitCount: Long? = null,
private val tracks: MutableList<Track> = mutableListOf() private val entries: MutableList<Entry> = mutableListOf()
) : Serializable, GenericEntry() { ) : Serializable, GenericEntry() {
override val name: String? override val name: String?
get() { get() {
@ -21,12 +22,12 @@ data class Share(
return null return null
} }
fun getEntries(): List<Track> { fun getEntries(): List<Entry> {
return tracks.toList() return entries.toList()
} }
fun addEntry(track: Track) { fun addEntry(entry: Entry) {
tracks.add(track) entries.add(entry)
} }
companion object { companion object {

View File

@ -1,74 +0,0 @@
/*
* 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)
}

View File

@ -1,22 +1,30 @@
apply from: bootstrap.kotlinModule apply from: bootstrap.kotlinModule
dependencies { dependencies {
api libs.retrofit api other.retrofit
api libs.jacksonConverter api other.jacksonConverter
api libs.koinCore api other.koinCore
implementation(libs.jacksonKotlin) { implementation(other.jacksonKotlin) {
exclude module: 'kotlin-reflect' exclude module: 'kotlin-reflect'
} }
implementation libs.kotlinReflect // for jackson kotlin, but to use the same version implementation other.kotlinReflect // for jackson kotlin, but to use the same version
implementation libs.okhttpLogging implementation other.okhttpLogging
implementation libs.timber implementation other.timber
testImplementation libs.kotlinJunit testImplementation testing.kotlinJunit
testImplementation libs.mockito testImplementation testing.mockito
testImplementation libs.mockitoInline testImplementation testing.mockitoInline
testImplementation libs.mockitoKotlin testImplementation testing.mockitoKotlin
testImplementation libs.kluent testImplementation testing.kluent
testImplementation libs.mockWebServer testImplementation testing.mockWebServer
testImplementation libs.apacheCodecs testImplementation testing.apacheCodecs
}
ext {
// Excluding data classes
jacocoExclude = [
'**/models/**',
'**/di/**'
]
} }

View File

@ -8,8 +8,7 @@ import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import okio.buffer import okio.Okio
import okio.source
import org.amshove.kluent.`should be` import org.amshove.kluent.`should be`
import org.amshove.kluent.`should contain` import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not be`
@ -41,12 +40,12 @@ fun MockWebServer.enqueueResponse(resourceName: String) {
} }
fun Any.loadJsonResponse(name: String): String { fun Any.loadJsonResponse(name: String): String {
val source = javaClass.classLoader.getResourceAsStream(name)!!.source().buffer() val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
return source.readString(Charset.forName("UTF-8")) return source.readString(Charset.forName("UTF-8"))
} }
fun Any.loadResourceStream(name: String): InputStream { fun Any.loadResourceStream(name: String): InputStream {
val source = javaClass.classLoader.getResourceAsStream(name)!!.source().buffer() val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
return source.inputStream() return source.inputStream()
} }

View File

@ -1,8 +1,8 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not be`
import org.junit.Test import org.junit.Test

View File

@ -1,8 +1,8 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not be`
import org.junit.Test import org.junit.Test

View File

@ -1,7 +1,7 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not be`
import org.junit.Test import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectory import org.moire.ultrasonic.api.subsonic.models.MusicDirectory

View File

@ -1,8 +1,8 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not be`
import org.junit.Test import org.junit.Test

View File

@ -17,8 +17,8 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
val contentType = responseBody?.contentType() val contentType = responseBody?.contentType()
if ( if (
contentType != null && contentType != null &&
contentType.type.equals("application", true) && contentType.type().equals("application", true) &&
contentType.subtype.equals("json", true) contentType.subtype().equals("json", true)
) { ) {
val error = SubsonicAPIClient.jacksonMapper.readValue<SubsonicResponse>( val error = SubsonicAPIClient.jacksonMapper.readValue<SubsonicResponse>(
responseBody.byteStream() responseBody.byteStream()
@ -40,11 +40,11 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
* It creates Exceptions from the results returned by the Subsonic API * It creates Exceptions from the results returned by the Subsonic API
*/ */
@Suppress("ThrowsCount") @Suppress("ThrowsCount")
fun <T : SubsonicResponse> Response<T>.throwOnFailure(): Response<T> { fun <T : SubsonicResponse> Response<out T>.throwOnFailure(): Response<out T> {
val response = this val response = this
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) { if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
return this return this as Response<T>
} }
if (!response.isSuccessful) { if (!response.isSuccessful) {
throw IOException("Server error, code: " + response.code()) throw IOException("Server error, code: " + response.code())

View File

@ -8,7 +8,6 @@ import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
import okhttp3.Credentials
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
@ -69,24 +68,12 @@ class SubsonicAPIClient(
.addInterceptor { chain -> .addInterceptor { chain ->
// Adds default request params // Adds default request params
val originalRequest = chain.request() val originalRequest = chain.request()
val newUrl = originalRequest.url.newBuilder() val newUrl = originalRequest.url().newBuilder()
.addQueryParameter("u", config.username) .addQueryParameter("u", config.username)
.addQueryParameter("c", config.clientID) .addQueryParameter("c", config.clientID)
.addQueryParameter("f", "json") .addQueryParameter("f", "json")
.build() .build()
val newRequestBuilder = originalRequest.newBuilder().url(newUrl) chain.proceed(originalRequest.newBuilder().url(newUrl).build())
if (originalRequest.url.username.isNotEmpty() &&
originalRequest.url.password.isNotEmpty()
) {
newRequestBuilder.addHeader(
"Authorization",
Credentials.basic(
originalRequest.url.username,
originalRequest.url.password
)
)
}
chain.proceed(newRequestBuilder.build())
} }
.addInterceptor(versionInterceptor) .addInterceptor(versionInterceptor)
.addInterceptor(proxyPasswordInterceptor) .addInterceptor(proxyPasswordInterceptor)
@ -96,7 +83,7 @@ class SubsonicAPIClient(
// Create the Retrofit instance, and register a special converter factory // 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 // It will update our protocol version to the correct version, once we made a successful call
private val retrofit: Retrofit = Retrofit.Builder() val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("${config.baseUrl}/rest/") .baseUrl("${config.baseUrl}/rest/")
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory( .addConverterFactory(
@ -122,20 +109,17 @@ class SubsonicAPIClient(
private fun OkHttpClient.Builder.addLogging() { private fun OkHttpClient.Builder.addLogging() {
val loggingInterceptor = HttpLoggingInterceptor(okLogger) val loggingInterceptor = HttpLoggingInterceptor(okLogger)
loggingInterceptor.level = HttpLoggingInterceptor.Level.HEADERS loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
this.addInterceptor(loggingInterceptor) this.addInterceptor(loggingInterceptor)
} }
@SuppressWarnings("TrustAllX509TrustManager", "EmptyFunctionBlock")
private fun OkHttpClient.Builder.allowSelfSignedCertificates() { private fun OkHttpClient.Builder.allowSelfSignedCertificates() {
val trustManager = val trustManager = object : X509TrustManager {
@Suppress("CustomX509TrustManager") override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
object : X509TrustManager { override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
@Suppress("TrustAllX509TrustManager") override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
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") val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf(trustManager), SecureRandom()) sslContext.init(null, arrayOf(trustManager), SecureRandom())

View File

@ -18,7 +18,7 @@ class PasswordHexInterceptor(private val password: String) : Interceptor {
override fun intercept(chain: Chain): Response { override fun intercept(chain: Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val updatedUrl = originalRequest.url.newBuilder() val updatedUrl = originalRequest.url().newBuilder()
.addEncodedQueryParameter("p", passwordHex).build() .addEncodedQueryParameter("p", passwordHex).build()
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build()) return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build())
} }

View File

@ -21,7 +21,7 @@ class PasswordMD5Interceptor(private val password: String) : Interceptor {
override fun intercept(chain: Chain): Response { override fun intercept(chain: Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val salt = getSalt() val salt = getSalt()
val updatedUrl = originalRequest.url.newBuilder() val updatedUrl = originalRequest.url().newBuilder()
.addQueryParameter("t", getPasswordMD5Hash(salt)) .addQueryParameter("t", getPasswordMD5Hash(salt))
.addQueryParameter("s", salt) .addQueryParameter("s", salt)
.build() .build()

View File

@ -19,7 +19,7 @@ internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
internal class RangeHeaderInterceptor : Interceptor { internal class RangeHeaderInterceptor : Interceptor {
override fun intercept(chain: Chain): Response { override fun intercept(chain: Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val headers = originalRequest.headers val headers = originalRequest.headers()
return if (headers.names().contains("Range")) { return if (headers.names().contains("Range")) {
val offsetValue = headers["Range"] ?: "0" val offsetValue = headers["Range"] ?: "0"
val offset = "bytes=$offsetValue-" val offset = "bytes=$offsetValue-"

View File

@ -18,7 +18,7 @@ internal class VersionInterceptor(
val newRequest = originalRequest.newBuilder() val newRequest = originalRequest.newBuilder()
.url( .url(
originalRequest originalRequest
.url .url()
.newBuilder() .newBuilder()
.addQueryParameter("v", protocolVersion.restApiVersion) .addQueryParameter("v", protocolVersion.restApiVersion)
.build() .build()

110
dependencies.gradle Normal file
View File

@ -0,0 +1,110 @@
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"
]

View File

@ -1,26 +1,68 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version="1.0" ?>
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues/> <ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues> <CurrentIssues>
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.&lt;no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID> <ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !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.&lt;no name provided&gt;$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID> <ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID> <ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID> <ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID> <ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID>
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID> <ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID> <ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID> <ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array&lt;ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID> <ID>LongMethod: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&lt;ServerSetting&gt;, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -&gt; Unit), private val serverEditRequestedCallback: ((Int) -&gt; Unit) )</ID>
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID> <ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID> <ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$60000</ID>
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</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:RESTMusicService.kt$RESTMusicService$206</ID> <ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
<ID>MagicNumber:SongView.kt$SongView$3</ID>
<ID>MagicNumber:SongView.kt$SongView$4</ID>
<ID>MagicNumber:SongView.kt$SongView$60</ID>
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID> <ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID> <ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable</ID> <ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception</ID>
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID> <ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable</ID>
<ID>TooGenericExceptionThrown:Downloader.kt$Downloader.DownloadTask$throw RuntimeException( String.format( Locale.ROOT, "Download of '%s' was cancelled", downloadFile.track ) )</ID> <ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$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>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID> <ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID> <ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
</CurrentIssues> </CurrentIssues>
</SmellBaseline> </SmellBaseline>

View File

@ -64,10 +64,13 @@ style:
WildcardImport: WildcardImport:
active: true active: true
MaxLineLength: MaxLineLength:
active: false active: true
maxLineLength: 120
excludePackageStatements: false
excludeImportStatements: false
MagicNumber: MagicNumber:
# 100 common in percentage, 1000 in milliseconds # 100 common in percentage, 1000 in milliseconds
ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024', '4096'] ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024']
ignoreEnums: true ignoreEnums: true
ignorePropertyDeclaration: true ignorePropertyDeclaration: true
UnnecessaryAbstractClass: UnnecessaryAbstractClass:

View File

@ -1,3 +0,0 @@
Others
- #671: Bump versions.mockito from 4.1.0 to 4.3.1.
- Update translations.

View File

@ -1,3 +0,0 @@
Enhancements
- #683: Rewrite the about and remove the webview.
- #685: Server coloring feature.

View File

@ -1,2 +0,0 @@
Bug fixes
- #688: Connection failure.

View File

@ -1,14 +0,0 @@
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.

View File

@ -0,0 +1 @@
Refactor and redesign artist list

View File

@ -0,0 +1 @@
Fall backs to path when comparing tracks and fixes #369

View File

@ -0,0 +1 @@
Refactored the application main menu

View File

@ -0,0 +1 @@
Refactored the application main menu

View File

@ -9,5 +9,7 @@ Enhancements
- #568: Rework Downloader. - #568: Rework Downloader.
- #567: Use semantically correct API endpoint when streaming/downloading. - #567: Use semantically correct API endpoint when streaming/downloading.
- #572: Moved drag handle to the left in the Now Playing list. - #572: Moved drag handle to the left in the Now Playing list.
- #585: Added setting to disable Now Playing List sending for incompatible bluetooth devices. - #585: Added setting to disable Now Playing List sending for incompatible
- #596: Added option whether to create a share on the server when sharing songs. bluetooth devices.
- #596: Added option whether to create a share on the server when sharing
songs.

View File

@ -1,3 +0,0 @@
Otros
- #671: Actualizado versions.mockito de 4.1.0 a 4.3.1.
- Traducciones actualizadas.

View File

@ -1,3 +0,0 @@
Mejoras
- #683: Reescribir el acerca de y eliminar el webview.
- #685: Posibilidad de seleccionar el color del servidor.

View File

@ -1,2 +0,0 @@
Corrección de errores
- #688: Fallo de conexión.

View File

@ -1,14 +0,0 @@
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.

View File

@ -0,0 +1 @@
Refactorizada y rediseñada la lista de artistas

View File

@ -0,0 +1 @@
Cuando un fichero no tiene etiquetas de número de pista lo ordena por orden alfabético. Además hemos arreglado el bug #369

View File

@ -0,0 +1 @@
Refactorizado el menú principal de la aplicación

View File

@ -0,0 +1 @@
Refactorizado el menú principal de la aplicación

View File

@ -1,5 +1,6 @@
Correción de errores 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. - #593: Corregidas las listas de álbumes.
- #602: NPE corregido. - #602: NPE corregido.
@ -7,7 +8,11 @@ Mejoras
- #558: La llamada a video puede ser estática. - #558: La llamada a video puede ser estática.
- #559: Agregado un mejor soporte sin conexión. - #559: Agregado un mejor soporte sin conexión.
- #568: Se ha reescrito el downloader. - #568: Se ha reescrito el downloader.
- #567: Se utiliza el endpoint semánticamente correcto al realizar streaming o descargar. - #567: Se utiliza el endpoint semánticamente correcto al realizar streaming
- #572: Se ha movido el botón de arrastre de canción hacia la izquierda en la lista de reproducción. o descargar.
- #585: Agregada una configuración para deshabilitar el envío de la Lista de reproducción en curso para dispositivos Bluetooth incompatibles. - #572: Se ha movido el botón de arrastre de canción hacia la izquierda en
- #596: Se agregó la opción de crear un recurso compartido en el servidor al compartir canciones. 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.

View File

@ -2,13 +2,16 @@ Corrección de errores
- #609: Comportamiento extraño de scrobbling (offset). - #609: Comportamiento extraño de scrobbling (offset).
Mejoras Mejoras
- #599: Se ha movido el selector de servidor y la configuración al menú de navegación. - #599: Se ha movido el selector de servidor y la configuración al menú de
- #600: Migración de la utilidad de permisos a Kotlin, aumento del SDK mínimo a 17. 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. - #604: Implementar una vista de Descarga.
- #613: targetSdkVersion debe ser 30 o superior. - #613: targetSdkVersion debe ser 30 o superior.
- #622: Refactorización de eventos. - #622: Refactorización de eventos.
- #641: Eliminar el almacenamiento de funciones. - #641: Eliminar el almacenamiento de funciones.
- #642: Eliminar MergeAdapter y SackOfViewsAdapter. - #642: Eliminar MergeAdapter y SackOfViewsAdapter.
- #649: Unificar el manejo del diálogo de error. - #649: Unificar el manejo del diálogo de error.
- #652: Manejo de ubicación de caché personalizado actualizado para eliminar isUri. - #652: Manejo de ubicación de caché personalizado actualizado para eliminar
isUri.
- #662: Mejorar las migraciones de bases de datos. - #662: Mejorar las migraciones de bases de datos.

View File

@ -1,103 +0,0 @@
[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" }

View File

@ -1,5 +0,0 @@
ext.versions = [
minSdk : 21,
targetSdk : 33,
compileSdk : 31,
]

Binary file not shown.

View File

@ -1,6 +1,5 @@
#Fri Jun 17 23:13:49 CEST 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -3,6 +3,7 @@
*/ */
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'jacoco'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
android { android {
@ -47,13 +48,24 @@ android {
tasks.withType(Test) { tasks.withType(Test) {
useJUnitPlatform() useJUnitPlatform()
jacoco {
includeNoLocationClasses = true
excludes += jacocoExclude
}
} }
dependencies { dependencies {
api libs.kotlinStdlib api other.kotlinStdlib
testImplementation libs.junit testImplementation testing.junit
testRuntimeOnly libs.junitVintage testRuntimeOnly testing.junitVintage
} }
jacoco {
toolVersion(versions.jacoco)
}
ext {
jacocoExclude = ['jdk.internal.*']
}

View File

@ -6,7 +6,7 @@ if (isCodeQualityEnabled) {
apply plugin: "org.jlleitschuh.gradle.ktlint" apply plugin: "org.jlleitschuh.gradle.ktlint"
ktlint { ktlint {
version = libs.versions.ktlint.get() version = versions.ktlint
outputToConsole = true outputToConsole = true
android = true android = true
} }
@ -21,7 +21,7 @@ if (isCodeQualityEnabled) {
detekt { detekt {
buildUponDefaultConfig = true buildUponDefaultConfig = true
toolVersion = libs.versions.detekt.get() toolVersion = versions.detekt
// Builds the AST in parallel. Rules are always executed in parallel. // Builds the AST in parallel. Rules are always executed in parallel.
// Can lead to speedups in larger projects. // Can lead to speedups in larger projects.
parallel = true parallel = true

View File

@ -0,0 +1,92 @@
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)
}
}
}
}
}

View File

@ -3,6 +3,7 @@
*/ */
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
sourceSets { sourceSets {
@ -14,14 +15,42 @@ sourceSets {
dependencies { dependencies {
api libs.kotlinStdlib api other.kotlinStdlib
testImplementation libs.junit testImplementation testing.junit
testRuntimeOnly libs.junitVintage 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)
}))
}
} }
tasks.named("test").configure { tasks.named("test").configure {
useJUnitPlatform() useJUnitPlatform()
jacoco {
excludes += jacocoExclude
includeNoLocationClasses = true
}
finalizedBy jacocoTestReport
} }
tasks.register("ciTest") { tasks.register("ciTest") {

257
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/bin/sh #!/usr/bin/env sh
# #
# Copyright © 2015-2021 the original authors. # Copyright 2015 the original author or authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -17,101 +17,67 @@
# #
############################################################################## ##############################################################################
# ##
# Gradle start up script for POSIX generated by Gradle. ## Gradle start up script for UN*X
# ##
# 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 # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
app_path=$0 PRG="$0"
# Need this for relative symlinks.
# Need this for daisy-chained symlinks. while [ -h "$PRG" ] ; do
while ls=`ls -ld "$PRG"`
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path link=`expr "$ls" : '.*-> \(.*\)$'`
[ -h "$app_path" ] if expr "$link" : '/.*' > /dev/null; then
do PRG="$link"
ls=$( ls -ld "$app_path" ) else
link=${ls#*' -> '} PRG=`dirname "$PRG"`"/$link"
case $link in #( fi
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD="maximum"
warn () { warn () {
echo "$*" echo "$*"
} >&2 }
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} >&2 }
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "$( uname )" in #( case "`uname`" in
CYGWIN* ) cygwin=true ;; #( CYGWIN* )
Darwin* ) darwin=true ;; #( cygwin=true
MSYS* | MINGW* ) msys=true ;; #( ;;
NONSTOP* ) nonstop=true ;; Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -121,9 +87,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java JAVACMD="$JAVA_HOME/jre/sh/java"
else else
JAVACMD=$JAVA_HOME/bin/java JAVACMD="$JAVA_HOME/bin/java"
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -132,7 +98,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else 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. 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 Please set the JAVA_HOME variable in your environment to match the
@ -140,95 +106,80 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
case $MAX_FD in #( MAX_FD_LIMIT=`ulimit -H -n`
max*) if [ $? -eq 0 ] ; then
MAX_FD=$( ulimit -H -n ) || if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
warn "Could not query maximum file descriptor limit" MAX_FD="$MAX_FD_LIMIT"
esac fi
case $MAX_FD in #( ulimit -n $MAX_FD
'' | soft) :;; #( if [ $? -ne 0 ] ; then
*) warn "Could not set maximum file descriptor limit: $MAX_FD"
ulimit -n "$MAX_FD" || fi
warn "Could not set maximum file descriptor limit to $MAX_FD" else
esac warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi fi
# Collect all arguments for the java command, stacking in reverse order: # For Darwin, add options to specify how the application appears in the dock
# * args from the command line if $darwin; then
# * the main class name GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
# * -classpath fi
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=`cygpath --unix "$JAVACMD"`
# Now convert the arguments - kludge to limit ourselves to /bin/sh # We build the pattern for arguments to be converted via cygpath
for arg do ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
if SEP=""
case $arg in #( for dir in $ROOTDIRSRAW ; do
-*) false ;; # don't mess with options #( ROOTDIRS="$ROOTDIRS$SEP$dir"
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath SEP="|"
[ -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 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" ;;
esac
fi fi
# Collect all arguments for the java command; # Escape application args
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of save () {
# shell script including quotes and variable substitutions, so put them in for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
# double quotes to make sure that they get re-expanded; and echo " "
# * put everything else in single quotes, so that it's not re-expanded. }
APP_ARGS=`save "$@"`
set -- \ # Collect all arguments for the java command, following the shell quoting and substitution rules
"-Dorg.gradle.appname=$APP_BASE_NAME" \ eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-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" "$@" exec "$JAVACMD" "$@"

View File

@ -1,5 +1,3 @@
enableFeaturePreview("VERSION_CATALOGS")
include ':core:domain' include ':core:domain'
include ':core:subsonic-api' include ':core:subsonic-api'
include ':ultrasonic' include ':ultrasonic'

View File

@ -1,6 +1,7 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply from: "../gradle_scripts/code_quality.gradle" apply from: "../gradle_scripts/code_quality.gradle"
android { android {
@ -8,15 +9,14 @@ android {
defaultConfig { defaultConfig {
applicationId "org.moire.ultrasonic" applicationId "org.moire.ultrasonic"
versionCode 103 versionCode 99
versionName "3.2.0" versionName "3.0.0"
minSdkVersion versions.minSdk minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
resConfigs 'cs', 'de', 'en', 'es', 'fr', 'hu', 'it', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW'
}
bundle.language.enableSplit = false resConfigs "cs", "de", "en", "es", "fr", "hu", "it", "nl", "pl", "pt", "pt-rBR", "ru", "zh-rCN", "zh-rTW"
}
buildTypes { buildTypes {
release { release {
@ -40,12 +40,20 @@ android {
main.java.srcDirs += "${projectDir}/src/main/kotlin" main.java.srcDirs += "${projectDir}/src/main/kotlin"
test.java.srcDirs += "${projectDir}/src/test/kotlin" test.java.srcDirs += "${projectDir}/src/test/kotlin"
} }
packagingOptions { packagingOptions {
resources { exclude 'META-INF/LICENSE'
excludes += ['META-INF/LICENSE']
}
} }
lintOptions {
baselineFile file("lint-baseline.xml")
ignore 'MissingTranslation'
ignore 'UnusedQuantity'
warning 'ImpliedQuantity'
disable 'IconMissingDensityFolder', "VectorPath"
abortOnError true
warningsAsErrors true
}
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
@ -63,18 +71,9 @@ android {
kapt { kapt {
arguments { arguments {
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString()) arg("room.schemaLocation", "$buildDir/schemas".toString())
} }
} }
lint {
baseline = file("lint-baseline.xml")
abortOnError true
warningsAsErrors true
disable 'IconMissingDensityFolder', 'VectorPath'
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
warning 'ImpliedQuantity'
disable 'ObsoleteLintCustomCheck'
}
} }
@ -86,52 +85,81 @@ dependencies {
implementation project(':core:domain') implementation project(':core:domain')
implementation project(':core:subsonic-api') implementation project(':core:subsonic-api')
api(libs.picasso) { api(other.picasso) {
exclude group: "com.android.support" exclude group: "com.android.support"
} }
implementation libs.core implementation androidSupport.core
implementation libs.support implementation androidSupport.support
implementation libs.design implementation androidSupport.design
implementation libs.multidex implementation androidSupport.multidex
implementation libs.roomRuntime implementation androidSupport.roomRuntime
implementation libs.roomKtx implementation androidSupport.roomKtx
implementation libs.viewModelKtx implementation androidSupport.viewModelKtx
implementation libs.constraintLayout implementation androidSupport.constraintLayout
implementation libs.preferences implementation androidSupport.preferences
implementation libs.media implementation androidSupport.media
implementation libs.media3exoplayer
implementation libs.media3session
implementation libs.media3okhttp
implementation libs.navigationFragment implementation androidSupport.navigationFragment
implementation libs.navigationUi implementation androidSupport.navigationUi
implementation libs.navigationFragmentKtx implementation androidSupport.navigationFragmentKtx
implementation libs.navigationUiKtx implementation androidSupport.navigationUiKtx
implementation libs.navigationFeature implementation androidSupport.navigationFeature
implementation libs.kotlinStdlib implementation other.kotlinStdlib
implementation libs.kotlinxCoroutines implementation other.kotlinxCoroutines
implementation libs.kotlinxGuava implementation other.koinAndroid
implementation libs.koinAndroid implementation other.okhttpLogging
implementation libs.okhttpLogging implementation other.fastScroll
implementation libs.fastScroll implementation other.colorPickerView
implementation libs.colorPickerView implementation other.rxJava
implementation libs.rxJava implementation other.rxAndroid
implementation libs.rxAndroid implementation other.multiType
implementation libs.multiType
kapt libs.room kapt androidSupport.room
testImplementation libs.kotlinReflect testImplementation other.kotlinReflect
testImplementation libs.junit testImplementation testing.junit
testRuntimeOnly libs.junitVintage testRuntimeOnly testing.junitVintage
testImplementation libs.kotlinJunit testImplementation testing.kotlinJunit
testImplementation libs.kluent testImplementation testing.kluent
testImplementation libs.mockito testImplementation testing.mockito
testImplementation libs.mockitoInline testImplementation testing.mockitoInline
testImplementation libs.mockitoKotlin testImplementation testing.mockitoKotlin
testImplementation libs.robolectric testImplementation testing.robolectric
implementation libs.timber 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
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,124 +0,0 @@
{
"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')"
]
}
}

View File

@ -1,146 +0,0 @@
{
"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')"
]
}
}

View File

@ -1,474 +0,0 @@
{
"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')"
]
}
}

View File

@ -1,514 +0,0 @@
{
"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')"
]
}
}

View File

@ -21,8 +21,6 @@
<application <application
android:allowBackup="false" android:allowBackup="false"
android:fullBackupContent="@xml/backup_descriptor"
android:dataExtractionRules="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/NoActionBar" android:theme="@style/NoActionBar"
@ -42,8 +40,8 @@
<activity android:name=".activity.NavigationActivity" <activity android:name=".activity.NavigationActivity"
android:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask" android:label="@string/common.appname"
android:exported="true"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SEARCH"/> <action android:name="android.intent.action.SEARCH"/>
@ -59,25 +57,28 @@
</activity> </activity>
<service <service
android:name=".service.DownloadService" android:name=".service.MediaPlayerService"
android:label="Ultrasonic Media Player Service" android:label="Ultrasonic Media Player Service"
android:exported="false"> android:exported="false">
</service> </service>
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md --> <service
<service android:name=".playback.PlaybackService" tools:ignore="ExportedService"
android:name=".service.AutoMediaBrowserService"
android:label="@string/common.appname" android:label="@string/common.appname"
android:foregroundServiceType="mediaPlayback"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService" />
<action android:name="android.media.browse.MediaBrowserService" /> <action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>
</service> </service>
<receiver android:name=".receiver.UltrasonicIntentReceiver" <receiver android:name=".receiver.MediaButtonIntentReceiver">
android:exported="true"> <intent-filter android:priority="2147483647">
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
<receiver android:name=".receiver.UltrasonicIntentReceiver">
<intent-filter> <intent-filter>
<action android:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/> <action android:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/>
<action android:name="org.moire.ultrasonic.CMD_PLAY"/> <action android:name="org.moire.ultrasonic.CMD_PLAY"/>
@ -89,8 +90,7 @@
<action android:name="org.moire.ultrasonic.CMD_PROCESS_KEYCODE"/> <action android:name="org.moire.ultrasonic.CMD_PROCESS_KEYCODE"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".receiver.BluetoothIntentReceiver" <receiver android:name=".receiver.BluetoothIntentReceiver">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.bluetooth.device.action.ACL_CONNECTED"/> <action android:name="android.bluetooth.device.action.ACL_CONNECTED"/>
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/> <action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
@ -100,8 +100,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".provider.UltrasonicAppWidgetProvider4X1" android:name=".provider.UltrasonicAppWidgetProvider4X1"
android:label="Ultrasonic (4x1)" android:label="Ultrasonic (4x1)">
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter> </intent-filter>
@ -112,8 +111,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".provider.UltrasonicAppWidgetProvider4X2" android:name=".provider.UltrasonicAppWidgetProvider4X2"
android:label="Ultrasonic (4x2)" android:label="Ultrasonic (4x2)">
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter> </intent-filter>
@ -124,8 +122,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".provider.UltrasonicAppWidgetProvider4X3" android:name=".provider.UltrasonicAppWidgetProvider4X3"
android:label="Ultrasonic (4x3)" android:label="Ultrasonic (4x3)">
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter> </intent-filter>
@ -136,8 +133,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".provider.UltrasonicAppWidgetProvider4X4" android:name=".provider.UltrasonicAppWidgetProvider4X4"
android:label="Ultrasonic (4x4)" android:label="Ultrasonic (4x4)">
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter> </intent-filter>
@ -146,17 +142,18 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info_4x4"/> android:resource="@xml/appwidget_info_4x4"/>
</receiver> </receiver>
<receiver android:name=".receiver.MediaButtonIntentReceiver"
android:exported="true">
<intent-filter android:priority="2147483647">
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
<provider <provider
android:name=".provider.SearchSuggestionProvider" android:name=".provider.SearchSuggestionProvider"
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider" android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
android:exported="true" />
<receiver
android:name=".receiver.A2dpIntentReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.android.music.playstatusrequest"/>
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,48 @@
<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>

View File

@ -0,0 +1,52 @@
<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>

View File

@ -0,0 +1,61 @@
<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 &eacute;couter ou t&eacute;l&eacute;charger de la musique &agrave; 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&eacute;faut, cette application n'est pas configur&eacutee. Apr&egrave;s avoir configur&eacute; votre
serveur personnel, veuillez acc&eacute;der aux <b>Param&egrave;tres</b> et modifier la configuration afin de vous connecter &agrave; 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&eacute;n&eacute;fices suivants:
</p>
<ul>
<li>&Eacute;coute et t&eacute;l&eacute;chargement illimit&eacute;s vers autant de iPhones et d'appareils Android que souhait&eacute;.</li>
<li>Lecture de vid&eacute;os.</li>
<li>Une adresse web personnalis&eacute;e pour votre serveur Subsonic (<em>votrenom</em>.subsonic.org).</li>
<li>Aucunes publicit&eacute;s dans l'interface web de Subsonic.</li>
<li>Acc&egrave;s gratuit aux nouvelles fonctionnalit&eacute;s avanc&eacute;es.</li>
</ul>
<p>
Le montant sugg&eacute;r&eacute; pour le don est de <b>20&euro;</b>, mais n'importe quel montant est trait&eacute; et accept&eacute;.
</p>
<p>
Cliquez sur le bouton suivants pour acc&eacute;der &agrave; PayPal, d'o&ugrave; vous pourrez payer soit par carte de cr&eacute;dit, soit en utilisant votre compte PayPal.
Une fois le don re&ccedil;u et trait&eacute;, vous recevrez votre cl&eacute; 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 &agrave l'adresse suivante : <a href="https://github.com/ogarcia/ultrasonic">github.com</a>.
</p>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,11 @@
/*
* 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%;
}

View File

@ -0,0 +1,149 @@
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);
}
}
}

View File

@ -0,0 +1,221 @@
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);
}
}

View File

@ -0,0 +1,42 @@
/*
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;
}
}

View File

@ -0,0 +1,42 @@
/*
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;
}
}

View File

@ -0,0 +1,42 @@
/*
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;
}
}

View File

@ -0,0 +1,42 @@
/*
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;
}
}

View File

@ -0,0 +1,57 @@
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);
}
}

View File

@ -18,24 +18,22 @@
*/ */
package org.moire.ultrasonic.receiver; package org.moire.ultrasonic.receiver;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import timber.log.Timber;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Settings; import org.moire.ultrasonic.util.Settings;
import org.moire.ultrasonic.util.Util;
import timber.log.Timber;
/** /**
* Resume or pause playback on Bluetooth A2DP connect/disconnect. * Resume or pause playback on Bluetooth A2DP connect/disconnect.
* *
* @author Sindre Mehus * @author Sindre Mehus
*/ */
@SuppressLint("MissingPermission")
public class BluetoothIntentReceiver extends BroadcastReceiver public class BluetoothIntentReceiver extends BroadcastReceiver
{ {
@Override @Override

View File

@ -0,0 +1,83 @@
/*
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.
}
}
}

View File

@ -0,0 +1,489 @@
/*
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();
}
}
}

View File

@ -18,7 +18,7 @@ public class Scrobbler
{ {
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return; if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return;
final String id = song.getTrack().getId(); final String id = song.getSong().getId();
if (id == null) return; if (id == null) return;
// Avoid duplicate registrations. // Avoid duplicate registrations.

View File

@ -1,6 +1,6 @@
package org.moire.ultrasonic.service; package org.moire.ultrasonic.service;
import org.moire.ultrasonic.domain.Track; import org.moire.ultrasonic.domain.MusicDirectory;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
@ -13,7 +13,7 @@ public class State implements Serializable
{ {
public static final long serialVersionUID = -6346438781062572270L; public static final long serialVersionUID = -6346438781062572270L;
public List<Track> songs = new ArrayList<>(); public List<MusicDirectory.Entry> songs = new ArrayList<>();
public int currentPlayingIndex; public int currentPlayingIndex;
public int currentPlayingPosition; public int currentPlayingPosition;
} }

View File

@ -26,8 +26,6 @@
*/ */
package org.moire.ultrasonic.service.ssl; package org.moire.ultrasonic.service.ssl;
import android.annotation.SuppressLint;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@ -36,7 +34,6 @@ import javax.net.ssl.X509TrustManager;
/** /**
* @since 4.1 * @since 4.1
*/ */
@SuppressLint("CustomX509TrustManager")
class TrustManagerDecorator implements X509TrustManager class TrustManagerDecorator implements X509TrustManager
{ {

View File

@ -0,0 +1,117 @@
/*
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();
}
}
}

View File

@ -1,6 +1,6 @@
package org.moire.ultrasonic.util; package org.moire.ultrasonic.util;
import org.moire.ultrasonic.domain.Track; import org.moire.ultrasonic.domain.MusicDirectory;
import java.util.List; import java.util.List;
@ -12,5 +12,5 @@ public class ShareDetails
public String Description; public String Description;
public boolean ShareOnServer; public boolean ShareOnServer;
public long Expiration; public long Expiration;
public List<Track> Entries; public List<MusicDirectory.Entry> Entries;
} }

View File

@ -0,0 +1,124 @@
/*
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