Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • bugfix/46-adjust-now-playing-top-margin
  • deploy-in-docker
  • develop
  • enhancement/speed-up-pipelines
  • fix-ci-for-tags
  • gradle-v8
  • housekeeping/mavenCentral
  • housekeeping/remove-warnings
  • renovate/io.mockk-mockk-1.x
  • technical/skip-metadata-file-for-branches
  • technical/update-compile-sdk-version-docker
  • technical/update-jvmTarget-version
  • technical/upgrade-appcompat-1.5.x
  • technical/upgrade-exoplayer
  • 0.0.1
  • 0.1
  • 0.1.1
  • 0.1.2
  • 0.1.3
  • 0.1.4
  • 0.1.5
  • 0.1.5-1
  • 0.2.0
  • 0.2.1
  • 0.2.1-1
  • 0.3.0
  • fdroid-dummy-1
  • fdroid-dummy-2
  • fdroid-dummy-3
  • fdroid-dummy-4
  • fdroid-dummy-5
31 results

Target

Select target project
  • funkwhale/funkwhale-android
  • creak/funkwhale-android
  • Keunes/funkwhale-android
  • Mouath/funkwhale-android
4 results
Select Git revision
  • bugfix/46-adjust-now-playing-top-margin
  • bugfix/90-error-playing-user-radio
  • deploy-in-docker
  • develop
  • enhancement/89-update-appstore-metadata
  • enhancement/speed-up-pipelines
  • housekeeping/remove-warnings
  • renovate/configure
  • 0.0.1
  • 0.1
  • 0.1.1
11 results
Show changes
Commits on Source (429)
Showing
with 548 additions and 682 deletions
*.iml
.gradle
**/.gradle
/local.properties
/.idea
.DS_Store
/build
**/build
/captures
.externalNativeBuild
*.keystore
image: jangrewe/gitlab-ci-android
# This image lives in https://dev.funkwhale.audio/funkwhale/ci
image: $CI_REGISTRY/funkwhale/ci/android:latest
variables:
COBERTURA_REPORT: '$CI_PROJECT_DIR/app/build/reports/cobertura.xml'
......@@ -6,11 +7,20 @@ variables:
JACOCO_XML_LOCATION: '$CI_PROJECT_DIR/app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml'
stages:
- build_ci_env
- test
- visualize
- build
- test-after-build
- deploy
cache: &global_cache
key: ${CI_PIPELINE_ID}
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull
.gradle-default:
before_script:
- export GRADLE_USER_HOME=$(pwd)/.gradle
......@@ -19,11 +29,6 @@ stages:
script:
- echo "Overwrite me"
cache:
key: ${CI_PROJECT_ID}
paths:
- .gradle/
.build:
stage: build
variables:
......@@ -34,7 +39,7 @@ stages:
before_script:
- git fetch --unshallow --tags
after_script:
- export versionCode=`$ANDROID_HOME/build-tools/30.0.2/aapt dump badging $apk_file | grep versionCode | awk '{print $3}' | sed s/versionCode=//g | sed s/\'//g`
- export versionCode=`$ANDROID_HOME/build-tools/30.0.3/aapt dump badging $apk_file | grep versionCode | awk '{print $3}' | sed s/versionCode=//g | sed s/\'//g`
- apt update && apt install gettext-base
- cat $metadata_template | envsubst > $metadata_file
extends: .gradle-default
......@@ -43,6 +48,9 @@ stages:
- $apk_file
- $metadata_file
- $output_metadata
cache:
# inherit all global cache settings
<<: *global_cache
test:
extends: .gradle-default
......@@ -50,17 +58,30 @@ test:
except:
- tags
script:
- ./gradlew test jacocoTestReport
- ./gradlew --no-daemon --stacktrace test jacocoTestReport
- awk -F"," '{ instructions += $4 + $5; covered += $5 } END { print covered, "/", instructions, " instructions covered"; print 100*covered/instructions, "% covered" }' $JACOCO_CSV_LOCATION
artifacts:
reports:
junit: app/build/test-results/test**/TEST-*.xml
paths:
- $JACOCO_XML_LOCATION
cache:
# inherit all global cache settings
<<: *global_cache
# override the policy
policy: pull-push
test_nonfree_code:
stage: test-after-build
image: registry.funkwhale.audio/funkwhale/ci/android-fdroidserver
script:
- fdroid scanner -v app/build/outputs/apk/*/app-*.apk |& tee output.txt
- cat output.txt
- (! grep "CRITICAL" output.txt)
coverage:
stage: visualize
image: gjrtimmer/jacoco2cobertura:1.0.8
image: haynes/jacoco2cobertura:1.0.9
script:
# convert report from jacoco to cobertura, use relative project path
- 'python /opt/cover2cover.py $JACOCO_XML_LOCATION $CI_PROJECT_DIR/app/src/main/java > app/build/reports/cobertura.xml'
......@@ -71,13 +92,15 @@ coverage:
- tags
artifacts:
reports:
cobertura: $COBERTURA_REPORT
coverage_report:
coverage_format: cobertura
path: $COBERTURA_REPORT
build-develop:
extends: .build
script:
- echo -n $PREVIEW_SIGNING_KEY_STORE | base64 -d > app/android.keystore
- ./gradlew assembleDebug -Psigning.store=android.keystore -Psigning.store_passphrase=$PREVIEW_SIGNING_KEY_PASS -Psigning.key_passphrase=$PREVIEW_SIGNING_KEY_PASS
- ./gradlew --stacktrace --no-daemon assembleDebug -x check -Psigning.store=android.keystore -Psigning.store_passphrase=$PREVIEW_SIGNING_KEY_PASS -Psigning.key_passphrase=$PREVIEW_SIGNING_KEY_PASS
only:
- develop
......@@ -90,45 +113,51 @@ build-release:
extends: .build
script:
- echo -n $SIGNING_KEY_STORE | base64 -d > app/android.keystore
- ./gradlew assembleRelease -Psigning.store=android.keystore -Psigning.store_passphrase=$SIGNING_KEY_PASS -Psigning.key_passphrase=$SIGNING_KEY_PASS
- ./gradlew --stacktrace --no-daemon assembleRelease -Psigning.store=android.keystore -Psigning.store_passphrase=$SIGNING_KEY_PASS -Psigning.key_passphrase=$SIGNING_KEY_PASS
only:
- tags
build-bleeding-edge:
extends: .build
script:
- ./gradlew assembleDebug
- ./gradlew --stacktrace --no-daemon -x check assembleDebug
except:
- develop
- tags
.deploy:
image: debian
before_script:
- apt update && apt -y install openssh-server
image: curlimages/curl:latest
script:
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file $FILE "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/$PACKAGE/$CI_COMMIT_SHORT_SHA/$PACKAGE-$CI_COMMIT_SHORT_SHA.apk"'
deploy-develop:
extends: .deploy
stage: deploy
only:
- develop
script:
- eval `ssh-agent -s`
- ssh-add <(echo "$SSH_PRIVATE_KEY")
- scp -o StrictHostKeyChecking=no app/build/outputs/apk/debug/app-debug.apk fdroid@apps.funkwhale.audio:/srv/fdroid/fdroid/develop/repo/audio.funkwhale.ffa.dev-$CI_COMMIT_SHORT_SHA.apk
- scp -o StrictHostKeyChecking=no app/build/outputs/apk/debug/output-metadata.json fdroid@apps.funkwhale.audio:/srv/fdroid/fdroid/develop/output-metadata.json
- scp -o StrictHostKeyChecking=no metadata/audio.funkwhale.android.dev.yml fdroid@apps.funkwhale.audio:/srv/fdroid/fdroid/develop/metadata/audio.funkwhale.ffa.dev.yml
- ssh -o StrictHostKeyChecking=no fdroid@apps.funkwhale.audio 'docker run --rm -u $(id -u):$(id -g) -v /srv/fdroid/fdroid/develop:/repo registry.gitlab.com/fdroid/docker-executable-fdroidserver:master update'
variables:
FILE: app/build/outputs/apk/debug/app-debug.apk
PACKAGE: audio.funkwhale.ffa.dev
deploy-release:
extends: .deploy
stage: deploy
only:
- tags
script:
- eval `ssh-agent -s`
- ssh-add <(echo "$SSH_PRIVATE_KEY")
- scp -o StrictHostKeyChecking=no app/build/outputs/apk/release/app-release.apk fdroid@apps.funkwhale.audio:/srv/fdroid/fdroid/develop/repo/audio.funkwhale.ffa-$CI_COMMIT_TAG.apk
- scp -o StrictHostKeyChecking=no app/build/outputs/apk/release/output-metadata.json fdroid@apps.funkwhale.audio:/srv/fdroid/fdroid/develop/output-metadata.json
- scp -o StrictHostKeyChecking=no metadata/audio.funkwhale.android.yml fdroid@apps.funkwhale.audio:/srv/fdroid/fdroid/develop/metadata/audio.funkwhale.ffa.yml
- ssh -o StrictHostKeyChecking=no fdroid@apps.funkwhale.audio 'docker run --rm -u $(id -u):$(id -g) -v /srv/fdroid/fdroid/develop:/repo registry.gitlab.com/fdroid/docker-executable-fdroidserver:master update'
variables:
FILE: app/build/outputs/apk/release/app-release.apk
PACKAGE: audio.funkwhale.ffa
trigger-fdroid-update-develop:
stage: .post
only:
- develop
image: curlimages/curl:7.88.1
script: curl "https://fdroid.funkwhale.audio/hooks/update-index?name=audio.funkwhale.ffa.dev&version=$CI_COMMIT_SHORT_SHA"
trigger-fdroid-update-release:
stage: .post
only:
- tags
image: curlimages/curl:7.88.1
script: curl "https://fdroid.funkwhale.audio/hooks/update-index?name=audio.funkwhale.ffa&version=$CI_COMMIT_SHORT_SHA"
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
......@@ -22,6 +13,10 @@ A clear and concise description of what the bug is.
A clear and concise description of what you expected to happen.
**Actual behavior**
A clear and consise description of what actually happened, instead.
**Screenshots**
If applicable, add screenshots to help explain your problem.
......@@ -36,3 +31,5 @@ If applicable, add screenshots to help explain your problem.
**Logs**
Add any related logs from ADB or from the "Copy logs" setting.
/label ~"Type: Bug"
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=11.0.11.hs-adpt
java=11.0.13-tem
java temurin-11.0.16+101
0.3.0 (2023-12-12)
Features:
- Add option to limit bandwidth usage by streaming transcoded music
- Improve player bottom sheet, in particular fling support
Enhancements:
- Refactor CoverArt.withContext().
Bugfixes:
- Fix buffering progress bar display
- Fix landscape view induced MainActivity leak.
- Fix Too Many Receivers exception
0.2.1 (2023-04-18)
Bugfixes:
- Removed navigation-dynamic-features-fragment, which has proprietary dependencies and isn't needed
0.2.0 (2023-04-05)
Features:
- Add filtering functionality to favorites view (thanks @PhieF)
- Allow backward skip after pause by configurable number of seconds (contributed by hdasch)
- Use the track cover in an album track list if one is available
Bugfixes:
- Make the mini player overlay stay on top (contributed by @christophehenry)
- Use Picasso stableKey for better caching against pre-signed URLs (thanks @rickosborne)
0.1.5 (2022-07-04)
Bugfixes:
- Fix App crashes when interacting with playlist (@Mouath)
- Fix leaked database cursor resource
- Fix playback order to respect preference setting on albums fragment
- Fix the removal of existing downloads
- Fix unresponsive bluetooth buttons with Oreo and later (thanks @hdasch)
- Fix warnings in log output due to leaked BufferedReader resource (thanks @hdasch)
- Fixes problem where users are logged out sporadically (thanks to @hdasch)
0.1.4 (2021-09-18)
Bugfixes:
- Fix application crash when opening playlists view (#99)
0.1.3 (2021-09-17)
Bugfixes:
- Disable landscape mode to avoid application crashes (#93)
- Fix handling of hostname 'https://' scheme prefix (#88)
- Remember scroll positions in list views (Artists/Albums/...) (#95)
- Remove trailing slash from hostname (#92)
- Use correct radio identifier for user radio (#90)
Other:
- Add hard coded version information for F-Droid deployment (#97)
- Automatically update the favorites list view (#28)
- Update Fastlane metadata for app store deployments (#89)
0.1.1 (2021-08-31)
Features:
......
# Funkwhale for Android
# Funkwhale for Android
This is the official Android music player for [Funkwhale](https://funkwhale.audio), native to both Android (developed in Kotlin) and to Funkwhale (uses its native API instead of Subsonic).
It is based on the amazing [Otter](https://github.com/apognu/otter) made by [apognu](https://github.com/apognu) and would not be possible without his groundwork!
......@@ -7,15 +7,14 @@ You can get help and discuss Funkwhale on Matrix on [#funkwhale-android:matrix.o
## Installation
Currently you can install a preview version of Funkwhale for Android through a selfhosted [F-Droid repository](https://fdroid.funkwhale.audio/develop/).
You'll have to add this repository to your F-Droid client, please visit the link above for further instructions. Once you added the repository, you can
use F-Droid as usual and search for "Funkwhale".
We have an official version available on F-Droid and the Google Play-Store, but you can also install a preview version of Funkwhale for Android™ through our selfhosted [F-Droid repository](https://fdroid.funkwhale.audio/develop/).
You'll have to add this repository to your F-Droid client, please visit the link above for further instructions. Once you added the repository, you can use F-Droid as usual and search for "Funkwhale".
## State
Funkwhale for Android is work in Progress. Please bear with us, there will be bugs, there will be crashes and there will be performance and UX issues.
Funkwhale for Android is work in Progress. Please bear with us, there will be bugs, there will be crashes and there will be performance and UX issues.
Here is the list of Funkwhale for Android's features:
Here is the list of Funkwhale for Android's features:
* Basic collection browsing (artists, albums and tracks)
* Playlists listing
......@@ -27,20 +26,16 @@ Here is the list of Funkwhale for Android's features:
* Radios playback
* Dark mode! 🎉
Funkwhale for Android will try to behave as you would expect a mobile music player to. That means it integrates with the OS's media controls (including headset controls) or pause on incoming calls. If there is anything you would like it to do, please [open an issue](https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/issues/new).
Funkwhale for Android will try to behave as you would expect a mobile music player to. That means it integrates with the OS's media controls (including headset controls) or pause on incoming calls. If there is anything you would like it to do, please [open an issue](https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/issues/new).
## Screenshots
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png" width="200" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png" width="200" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png" width="200" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png" width="200" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png" width="200" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/6.png" width="200" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/7.png" width="200" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png" width="33%" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png" width="33%" />
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png" width="33%" />
## Translation
Funkwhale for Android is being translated by the community through [Weblate](https://translate.funkwhale.audio/settings/funkwhale/ffa). If you would like to contribute to its localization or add a new language, you can help out there.
Funkwhale for Android is being translated by the community through [Weblate](https://translate.funkwhale.audio/settings/funkwhale/ffa). If you would like to contribute to its localization or add a new language, you can help out there.
[![Translation status](https://translate.funkwhale.audio/widgets/funkwhale/-/ffa/multi-auto.svg)](https://translate.funkwhale.audio/engage/funkwhale/)
......@@ -5,12 +5,16 @@ import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
id("androidx.navigation.safeargs.kotlin")
id("kotlin-parcelize")
id("kotlin-kapt")
id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
id("org.jlleitschuh.gradle.ktlint") version "11.2.0"
id("com.gladed.androidgitversion") version "0.4.14"
id("com.github.triplet.play") version "2.4.2"
id("com.github.triplet.play") version "3.8.1"
id("de.mobilej.unmock")
id("com.github.ben-manes.versions")
id("org.jetbrains.kotlin.android")
jacoco
}
......@@ -26,33 +30,41 @@ unMock {
}
androidGitVersion {
codeFormat = "MMNNPPBBB"
codeFormat = "MMNNPPBBB" // Keep in sync with version_code() in dist/create_release.sh
format = "%tag%%-count%%-commit%%-branch%"
}
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
namespace = "audio.funkwhale.ffa"
testCoverage {
version = Versions.jacoco
version = "0.8.7"
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildFeatures {
viewBinding = true
dataBinding = true
}
packagingOptions {
resources.excludes.add("META-INF/LICENSE.md")
resources.excludes.add("META-INF/LICENSE-notice.md")
}
lint {
disable += listOf("MissingTranslation", "ExtraTranslation")
}
compileSdk = 30
compileSdk = 33
defaultConfig {
......@@ -62,7 +74,7 @@ android {
versionName = androidGitVersion.name()
minSdk = 24
targetSdk = 30
targetSdk = 33
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
......@@ -141,64 +153,75 @@ ktlint {
}
play {
isEnabled = props.hasProperty("play.credentials")
enabled.set(props.hasProperty("play.credentials"))
if (isEnabled) {
serviceAccountCredentials = file(props.getProperty("play.credentials"))
defaultToAppBundles = true
track = "beta"
if (enabled.get()) {
serviceAccountCredentials.set(file(props.getProperty("play.credentials")))
defaultToAppBundles.set(true)
track.set("beta")
}
}
dependencies {
val navVersion: String by rootProject.extra
val lifecycleVersion: String by rootProject.extra
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("androidx.core:core-ktx:1.6.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha03")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("com.google.android.material:material:1.4.0")
implementation("com.android.support.constraint:constraint-layout:2.0.4")
implementation("com.google.android.material:material:1.9.0") {
exclude("androidx.constraintlayout")
}
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("com.google.android.exoplayer:exoplayer-core:${Versions.exoPlayer}")
implementation("com.google.android.exoplayer:exoplayer-ui:${Versions.exoPlayer}")
implementation("com.google.android.exoplayer:extension-mediasession:${Versions.exoPlayer}")
implementation("com.google.android.exoplayer:exoplayer-core:2.18.1")
implementation("com.google.android.exoplayer:exoplayer-ui:2.18.1")
implementation("com.google.android.exoplayer:extension-mediasession:2.18.1")
implementation("io.insert-koin:koin-core:${Versions.koin}")
implementation("io.insert-koin:koin-android:${Versions.koin}")
testImplementation("io.insert-koin:koin-test:${Versions.koin}")
implementation("io.insert-koin:koin-core:3.5.3")
implementation("io.insert-koin:koin-android:3.5.3")
testImplementation("io.insert-koin:koin-test:3.5.3")
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:${Versions.exoPlayerExtensions}") {
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:789a4f83169cff5c7a91655bb828fde2cfde671a") {
isTransitive = false
}
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:${Versions.exoPlayerExtensions}") {
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:789a4f83169cff5c7a91655bb828fde2cfde671a") {
isTransitive = false
}
implementation("com.aliassadi:power-preference-lib:${Versions.powerPreference}")
implementation("com.github.kittinunf.fuel:fuel:${Versions.fuel}")
implementation("com.github.kittinunf.fuel:fuel-coroutines:${Versions.fuel}")
implementation("com.github.kittinunf.fuel:fuel-android:${Versions.fuel}")
implementation("com.github.kittinunf.fuel:fuel-gson:${Versions.fuel}")
implementation("com.google.code.gson:gson:${Versions.gson}")
implementation("com.github.AliAsadi:PowerPreference:2.1.1")
implementation("com.github.kittinunf.fuel:fuel:2.3.1")
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.3.1")
implementation("com.github.kittinunf.fuel:fuel-android:2.3.1")
implementation("com.github.kittinunf.fuel:fuel-gson:2.3.1")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.picasso:picasso:2.71828")
implementation("jp.wasabeef:picasso-transformations:2.4.0")
implementation("net.openid:appauth:${Versions.openIdAppAuth}")
implementation("net.openid:appauth:0.11.1")
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.12.0")
testImplementation("androidx.test:core:1.4.0")
testImplementation("io.strikt:strikt-core:${Versions.strikt}")
testImplementation("org.robolectric:robolectric:${Versions.robolectric}")
testImplementation("io.mockk:mockk:1.13.4")
testImplementation("androidx.test:core:1.5.0")
testImplementation("io.strikt:strikt-core:0.34.1")
testImplementation("org.robolectric:robolectric:4.9.2")
debugImplementation("io.sentry:sentry-android:6.17.0")
androidTestImplementation("io.mockk:mockk-android:${Versions.mockk}")
androidTestImplementation("io.mockk:mockk-android:1.13.4")
androidTestImplementation("androidx.navigation:navigation-testing:$navVersion")
}
project.afterEvaluate {
......@@ -241,13 +264,15 @@ project.afterEvaluate {
sourceDirectories.setFrom(files(listOf(mainSrc)))
classDirectories.setFrom(files(listOf(debugTree)))
executionData.setFrom(fileTree(project.buildDir) {
executionData.setFrom(
fileTree(project.buildDir) {
setIncludes(
listOf(
"outputs/unit_test_code_coverage/debugUnitTest/*.exec",
"outputs/code_coverage/debugAndroidTest/connected/**/*.ec"
)
)
})
}
)
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="audio.funkwhale.ffa">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
......@@ -21,7 +22,8 @@
<activity
android:name=".activities.SplashActivity"
android:launchMode="singleInstance"
android:noHistory="true">
android:noHistory="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
......@@ -35,23 +37,33 @@
<activity
android:name=".activities.LoginActivity"
android:configChanges="screenSize|orientation"
android:launchMode="singleInstance" />
android:launchMode="singleInstance"
android:screenOrientation="portrait" />
<activity android:name=".activities.MainActivity" />
<activity
android:name=".activities.MainActivity" />
<activity
android:name=".activities.SearchActivity"
android:launchMode="singleTop" />
android:name=".activities.DownloadsActivity"
android:screenOrientation="portrait" />
<activity android:name=".activities.DownloadsActivity" />
<activity
android:name=".activities.SettingsActivity"
android:screenOrientation="portrait" />
<activity android:name=".activities.SettingsActivity" />
<activity
android:name=".activities.LicencesActivity"
android:screenOrientation="portrait" />
<activity android:name=".activities.LicencesActivity" />
<activity
android:name="net.openid.appauth.AuthorizationManagementActivity"
android:launchMode="@integer/launch_mode_for_app_auth"
tools:replace="android:launchMode" />
<service
android:name=".playback.PlayerService"
android:foregroundServiceType="mediaPlayback">
android:foregroundServiceType="mediaPlayback"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
......@@ -70,12 +82,14 @@
</service>
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<receiver android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<meta-data android:name="io.sentry.dsn" android:value="https://4e377f47d01242baae2d9d8bd689c3ef@am.funkwhale.audio/4" />
</application>
</manifest>
......@@ -5,13 +5,13 @@ import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import audio.funkwhale.ffa.koin.authModule
import audio.funkwhale.ffa.koin.exoplayerModule
import audio.funkwhale.ffa.utils.*
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.FFACache
import com.preference.PowerPreference
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import org.koin.core.context.startKoin
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
class FFA : Application() {
......@@ -23,11 +23,6 @@ class FFA : Application() {
var defaultExceptionHandler: Thread.UncaughtExceptionHandler? = null
val eventBus: BroadcastChannel<Event> = BroadcastChannel(10)
val commandBus: BroadcastChannel<Command> = BroadcastChannel(10)
val requestBus: BroadcastChannel<Request> = BroadcastChannel(10)
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
override fun onCreate() {
super.onCreate()
......@@ -78,7 +73,7 @@ class FFA : Application() {
builder.appendLine(e.toString())
FFACache.set(this@FFA, "crashdump", builder.toString().toByteArray())
FFACache.set(this@FFA, "crashdump", builder.toString())
}
}
......
......@@ -14,7 +14,6 @@ import com.google.android.exoplayer2.offline.DownloadManager
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.inject
......@@ -65,20 +64,19 @@ class DownloadsActivity : AppCompatActivity() {
private fun refresh() {
lifecycleScope.launch(Main) {
val cursor = exoDownloadManager.downloadIndex.getDownloads()
adapter.downloads.clear()
exoDownloadManager.downloadIndex.getDownloads()
.use { cursor ->
while (cursor.moveToNext()) {
val download = cursor.download
download.getMetadata()?.let { info ->
adapter.downloads.add(info.apply {
this.download = download
})
adapter.downloads.add(
info.apply { this.download = download }
)
}
}
}
adapter.notifyDataSetChanged()
}
}
......@@ -101,15 +99,17 @@ class DownloadsActivity : AppCompatActivity() {
}
private suspend fun refreshProgress() {
val cursor = exoDownloadManager.downloadIndex.getDownloads()
exoDownloadManager.downloadIndex.getDownloads()
.use { cursor ->
while (cursor.moveToNext()) {
val download = cursor.download
download.getMetadata()?.let { info ->
adapter.downloads.withIndex().associate { it.value to it.index }
.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
if (download.state == Download.STATE_DOWNLOADING && download.percentDownloaded != info.download?.percentDownloaded ?: 0) {
if (download.state == Download.STATE_DOWNLOADING &&
download.percentDownloaded != (info.download?.percentDownloaded ?: 0)
) {
withContext(Main) {
adapter.downloads[match.second] = info.apply {
this.download = download
......@@ -122,6 +122,7 @@ class DownloadsActivity : AppCompatActivity() {
}
}
}
}
inner class DownloadChangedListener : DownloadsAdapter.OnDownloadChangedListener {
override fun onItemRemoved(index: Int) {
......
......@@ -90,7 +90,8 @@ class LicencesActivity : AppCompatActivity() {
holder.licence.text = item.licence
}
inner class ViewHolder(binding: RowLicenceBinding) : RecyclerView.ViewHolder(binding.root),
inner class ViewHolder(binding: RowLicenceBinding) :
RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
val name = binding.name
val licence = binding.licence
......
package audio.funkwhale.ffa.activities
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.doOnLayout
import androidx.lifecycle.lifecycleScope
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivityLoginBinding
import audio.funkwhale.ffa.fragments.LoginDialog
import audio.funkwhale.ffa.utils.*
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.FuelResult
import audio.funkwhale.ffa.utils.OAuth
import audio.funkwhale.ffa.utils.Userinfo
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
......@@ -37,13 +43,10 @@ class LoginActivity : AppCompatActivity() {
limitContainerWidth()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
data?.let {
when (requestCode) {
0 -> {
oAuth.exchange(this, data) {
private var resultLauncher =
registerForActivityResult(StartActivityForResult()) { result ->
result.data?.let {
oAuth.exchange(this, it) {
PowerPreference
.getFileByName(AppContext.PREFS_CREDENTIALS)
.setBoolean("anonymous", false)
......@@ -59,14 +62,19 @@ class LoginActivity : AppCompatActivity() {
}
}
}
}
}
override fun onResume() {
super.onResume()
with(binding) {
val preferences = getPreferences(Context.MODE_PRIVATE)
val hn = preferences?.getString("hostname", "")
if (hn != null && !hn.isEmpty()) {
hostname.text = Editable.Factory.getInstance().newEditable(hn)
}
cleartext.setChecked(preferences?.getBoolean("cleartext", false) ?: false)
anonymous.setChecked(preferences?.getBoolean("anonymous", false) ?: false)
login.setOnClickListener {
var hostname = hostname.text.toString().trim()
var hostname = hostname.text.toString().trim().trim('/')
try {
validateHostname(hostname, cleartext.isChecked)?.let {
......@@ -97,6 +105,12 @@ class LoginActivity : AppCompatActivity() {
hostnameField.error = message
}
if (hostnameField.error == null) {
val preferences = getPreferences(Context.MODE_PRIVATE)
preferences?.edit()?.putString("hostname", hostname)?.commit()
preferences?.edit()?.putBoolean("cleartext", cleartext.isChecked)?.commit()
preferences?.edit()?.putBoolean("anonymous", anonymous.isChecked)?.commit()
}
}
}
}
......@@ -131,7 +145,7 @@ class LoginActivity : AppCompatActivity() {
oAuth.init(hostname)
return oAuth.register {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
oAuth.authorize(this)
resultLauncher.launch(oAuth.authorizeIntent(this))
}
}
......
package audio.funkwhale.ffa.activities
import android.content.*
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
......@@ -36,8 +40,6 @@ class SettingsActivity : AppCompatActivity() {
)
.commit()
}
fun getThemeResId(): Int = R.style.AppTheme
}
class SettingsFragment :
......@@ -47,7 +49,7 @@ class SettingsFragment :
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
......@@ -56,14 +58,14 @@ class SettingsFragment :
updateValues()
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) {
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
"oss_licences" -> startActivity(Intent(activity, LicencesActivity::class.java))
"crash" -> {
activity?.let { activity ->
(activity.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.also { clip ->
FFACache.get(activity, "crashdump")?.readLines()?.joinToString("\n").also {
FFACache.getLines(activity, "crashdump")?.joinToString("\n").also {
clip.setPrimaryClip(ClipData.newPlainText("Funkwhale logs", it))
Toast.makeText(
......@@ -112,6 +114,14 @@ class SettingsFragment :
}
}
preferenceManager.findPreference<ListPreference>("bandwidth_limitation")?.let {
it.summary = when (it.value) {
"unlimited" -> activity.getString(R.string.settings_bandwidth_limitation_summary_unlimited)
"limited" -> activity.getString(R.string.settings_bandwidth_limitation_summary_limited)
else -> activity.getString(R.string.settings_bandwidth_limitation_summary_unlimited)
}
}
preferenceManager.findPreference<ListPreference>("play_order")?.let {
it.summary = when (it.value) {
"shuffle" -> activity.getString(R.string.settings_play_order_shuffle_summary)
......@@ -146,7 +156,7 @@ class SettingsFragment :
}
preferenceManager.findPreference<SeekBarPreference>("media_cache_size")?.let {
it.summary = getString(R.string.settings_media_cache_size_summary, it.value)
it.summary = getString(R.string.settings_media_cache_size_summary, it.value as Int) // manual cast to address a bug in AGP
}
preferenceManager.findPreference<Preference>("version")?.let {
......
......@@ -6,7 +6,9 @@ import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.utils.*
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.OAuth
import audio.funkwhale.ffa.utils.Settings
import org.koin.java.KoinJavaComponent.inject
class SplashActivity : AppCompatActivity() {
......
......@@ -8,9 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
import audio.funkwhale.ffa.databinding.RowAlbumBinding
import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.model.Album
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso
import audio.funkwhale.ffa.utils.CoverArt
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
class AlbumsAdapter(
......@@ -19,6 +17,10 @@ class AlbumsAdapter(
private val listener: OnAlbumClickListener
) : FFAAdapter<Album, AlbumsAdapter.ViewHolder>() {
init {
this.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
}
interface OnAlbumClickListener {
fun onClick(view: View?, album: Album)
}
......@@ -41,8 +43,7 @@ class AlbumsAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val album = data[position]
Picasso.get()
.maybeLoad(maybeNormalizeUrl(album.cover()))
CoverArt.requestCreator(album.cover())
.fit()
.transform(RoundedCornersTransformation(8, 0))
.into(holder.art)
......
......@@ -8,9 +8,8 @@ import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.RowAlbumGridBinding
import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.model.Album
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.CoverArt
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
class AlbumsGridAdapter(
......@@ -40,10 +39,8 @@ class AlbumsGridAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val album = data[position]
Picasso.get()
.maybeLoad(maybeNormalizeUrl(album.cover()))
CoverArt.requestCreator(maybeNormalizeUrl(album.cover()))
.fit()
.placeholder(R.drawable.cover)
.transform(RoundedCornersTransformation(16, 0))
.into(holder.cover)
......
......@@ -9,9 +9,8 @@ import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.RowArtistBinding
import audio.funkwhale.ffa.fragments.FFAAdapter
import audio.funkwhale.ffa.model.Artist
import audio.funkwhale.ffa.utils.maybeLoad
import audio.funkwhale.ffa.utils.CoverArt
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
class ArtistsAdapter(
......@@ -42,6 +41,8 @@ class ArtistsAdapter(
super.onItemRangeInserted(positionStart, itemCount)
}
})
this.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
}
override fun getItemCount() = active.size
......@@ -60,15 +61,12 @@ class ArtistsAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val artist = active[position]
artist.albums?.let { albums ->
if (albums.isNotEmpty()) {
Picasso.get()
.maybeLoad(maybeNormalizeUrl(albums[0].cover?.urls?.original))
artist.cover()?.let { coverUrl ->
CoverArt.requestCreator(maybeNormalizeUrl(coverUrl))
.fit()
.transform(RoundedCornersTransformation(8, 0))
.into(holder.art)
}
}
holder.name.text = artist.name
......
package audio.funkwhale.ffa.adapters
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager2.adapter.FragmentStateAdapter
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.*
import audio.funkwhale.ffa.fragments.AlbumsGridFragment
import audio.funkwhale.ffa.fragments.ArtistsFragment
import audio.funkwhale.ffa.fragments.FavoritesFragment
import audio.funkwhale.ffa.fragments.PlaylistsFragment
import audio.funkwhale.ffa.fragments.RadiosFragment
class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) : FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
var tabs = mutableListOf<Fragment>()
class BrowseTabsAdapter(val context: Fragment) : FragmentStateAdapter(context) {
override fun getItemCount() = 5
override fun getCount() = 5
override fun getItem(position: Int): Fragment {
tabs.getOrNull(position)?.let {
return it
}
val fragment = when (position) {
override fun createFragment(position: Int): Fragment = when (position) {
0 -> ArtistsFragment()
1 -> AlbumsGridFragment()
2 -> PlaylistsFragment()
......@@ -25,12 +21,7 @@ class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) : Fragm
else -> ArtistsFragment()
}
tabs.add(position, fragment)
return fragment
}
override fun getPageTitle(position: Int): String {
fun tabText(position: Int): String {
return when (position) {
0 -> context.getString(R.string.artists)
1 -> context.getString(R.string.albums)
......