From 754762b24dab39452559f96cfd8319a2e329cabc Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 17 Apr 2024 19:49:44 -0600 Subject: [PATCH 001/550] ui: new app icon The first march towards material design 3.1. --- app/src/main/res/color/fill_icon_bg.xml | 14 ---- .../res/drawable/ic_launcher_background.xml | 28 ++++++- .../res/drawable/ic_launcher_foreground.xml | 2 +- app/src/main/res/drawable/ic_splash_anim.xml | 74 ++++++++++++++++-- .../res/mipmap-anydpi-v26/ic_launcher.xml | 4 +- app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 3856 -> 3068 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 2558 -> 2020 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 5290 -> 4332 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 7912 -> 6562 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 10526 -> 9148 bytes 10 files changed, 97 insertions(+), 25 deletions(-) delete mode 100644 app/src/main/res/color/fill_icon_bg.xml diff --git a/app/src/main/res/color/fill_icon_bg.xml b/app/src/main/res/color/fill_icon_bg.xml deleted file mode 100644 index 916db1e7d..000000000 --- a/app/src/main/res/color/fill_icon_bg.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index c16ace719..de625ab6e 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -6,8 +6,34 @@ android:viewportHeight="108"> + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 7a0184ca7..c9b0b6b07 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,7 +1,7 @@ - + + + + + + + + + + @@ -36,7 +67,7 @@ android:interpolator="@android:interpolator/fast_out_slow_in" android:propertyName="scaleX" android:startOffset="0" - android:valueFrom="4" + android:valueFrom="0" android:valueTo="1" android:valueType="floatType" /> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755bf..7ff316695 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,6 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index f22c5e0b4662a463ae20d5da1e702df194e747a5..6c8afca3750e2472b588a891fb008529d39fac33 100644 GIT binary patch literal 3068 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 SKJX)~|4P5<4YygLi;Drq#clHd diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 2e4171a8334bfc71f29d1ab66369a3717d0fd26b..3b14f713c7fcac02efa49be3aae438423e568775 100644 GIT binary patch literal 2020 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 UP_q4p*Z#umyS!`XQmokn0P-lu_5c6? diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 04aa61f9885b33bad6f903f1126cd801b0fd7422..bab17b31e9b3c2d9d1d8e300b30e546bc4b016b7 100644 GIT binary patch literal 4332 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 z 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 UtXfM|x6>#}Ewe^eE8n>S07i!DW&i*H literal 7912 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 S6TG#Qpff-*3^<_4j|BiHgbdyQ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 389bd1376c60a1a6f5469122db1744f8776a1dc7..99001d9a988924fa3dd3a9e86737ac3e558cd08d 100644 GIT binary patch literal 9148 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%0D=wh#&?4 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 G)<^(sYKCwC literal 10526 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 Date: Wed, 17 Apr 2024 22:07:38 -0600 Subject: [PATCH 002/550] build: update deps --- app/build.gradle | 6 +++--- build.gradle | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e43141a3c..522858562 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,9 +86,9 @@ dependencies { // --- SUPPORT --- // General - implementation "androidx.core:core-ktx:1.12.0" + implementation "androidx.core:core-ktx:1.13.0" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.activity:activity-ktx:1.8.2" + implementation "androidx.activity:activity-ktx:1.9.0" implementation "androidx.fragment:fragment-ktx:1.6.2" // Components @@ -137,7 +137,7 @@ dependencies { // Material // TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just // PR a fix. - implementation "com.google.android.material:material:1.10.0" + implementation "com.google.android.material:material:1.13.0-alpha01" // Dependency Injection implementation "com.google.dagger:dagger:$hilt_version" diff --git a/build.gradle b/build.gradle index 2b2c7a9c5..73de01b89 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.9.23' - navigation_version = "2.5.3" + navigation_version = "2.7.7" hilt_version = '2.51.1' } From 7995d3ac9881ee2ec9e84b525d7699d18a606948 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 17 Apr 2024 22:07:50 -0600 Subject: [PATCH 003/550] ui: material 3.1 (first draft) I'm mostly cowboying through patching things to look nice. I'll re-add round mode configs and actually try to migrate to standard spacing later. --- app/src/main/ic_launcher-playstore.png | Bin 0 -> 16098 bytes .../java/org/oxycblt/auxio/MainActivity.kt | 2 +- .../java/org/oxycblt/auxio/MainFragment.kt | 5 +-- .../detail/list/PlaylistDetailListAdapter.kt | 5 +-- .../java/org/oxycblt/auxio/image/CoverView.kt | 4 +- .../auxio/music/service/MediaItemBrowser.kt | 21 ++-------- .../auxio/playback/PlaybackBarFragment.kt | 4 ++ .../playback/PlaybackBottomSheetBehavior.kt | 5 +-- .../auxio/playback/queue/QueueAdapter.kt | 9 ++-- .../queue/QueueBottomSheetBehavior.kt | 5 +-- .../service/MediaSessionServiceFragment.kt | 2 +- .../playback/ui/AnimatedMaterialButton.kt | 4 ++ .../java/org/oxycblt/auxio/tasker/Tasker.kt | 19 ++++++++- app/src/main/res/drawable/ic_pause_48.xml | 11 +++++ app/src/main/res/drawable/ic_play_48.xml | 11 +++++ app/src/main/res/drawable/ic_skip_next_40.xml | 11 +++++ app/src/main/res/drawable/ic_skip_prev_40.xml | 11 +++++ app/src/main/res/drawable/ic_splash_anim.xml | 20 +-------- .../res/drawable/sel_playing_state_48.xml | 5 +++ .../layout-h480dp/fragment_playback_panel.xml | 17 +++++--- .../res/layout-w600dp-land/fragment_main.xml | 1 + app/src/main/res/layout/fragment_main.xml | 3 +- app/src/main/res/layout/view_seek_bar.xml | 5 ++- app/src/main/res/values/dimens.xml | 6 +-- app/src/main/res/values/styles_core.xml | 2 +- app/src/main/res/values/styles_ui.xml | 39 ++++++++++++------ 26 files changed, 142 insertions(+), 85 deletions(-) create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/res/drawable/ic_pause_48.xml create mode 100644 app/src/main/res/drawable/ic_play_48.xml create mode 100644 app/src/main/res/drawable/ic_skip_next_40.xml create mode 100644 app/src/main/res/drawable/ic_skip_prev_40.xml create mode 100644 app/src/main/res/drawable/sel_playing_state_48.xml diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..7d8c77dc1c73e6c715285862e01533af1aca189c GIT binary patch literal 16098 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 @@ -277,7 +276,7 @@ class MainFragment : // Reduce playback sheet elevation as it expands. This involves both updating the // shadow elevation for older versions, and fading out the background drawable // containing the elevation overlay. - binding.playbackSheet.translationZ = elevationNormal * outPlaybackRatio + binding.playbackSheet.elevation = elevationNormal * outPlaybackRatio playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt() // Fade out the playback bar as the panel expands. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index ca8a0657b..334f39703 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -30,7 +30,6 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.IntegerTable -import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemEditHeaderBinding import org.oxycblt.auxio.databinding.ItemEditableSongBinding import org.oxycblt.auxio.list.EditableListListener @@ -46,7 +45,6 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat -import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.logD @@ -232,8 +230,7 @@ private constructor(private val binding: ItemEditableSongBinding) : override val delete = binding.background override val background = MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { - fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface) - elevation = binding.context.getDimen(R.dimen.elevation_normal) + fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurfaceContainerHigh) alpha = 0 } diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index 792755dc7..75712201c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -461,7 +461,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr companion object { val SIZING_CORNER_RADII = arrayOf( - R.dimen.size_corners_small, R.dimen.size_corners_small, R.dimen.size_corners_medium) + R.dimen.size_corners_small, + R.dimen.size_corners_medium, + R.dimen.size_corners_mid_large) val SIZING_ICON_SIZE = arrayOf(R.dimen.size_icon_small, R.dimen.size_icon_medium, null) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt index 5c8c35ede..63b68925d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemBrowser.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.music.service import android.content.Context @@ -141,10 +141,8 @@ constructor( is MediaSessionUID.Category -> return uid.toMediaItem(context) is MediaSessionUID.Single -> musicRepository.find(uid.uid)?.let { musicRepository.find(it.uid) } - is MediaSessionUID.Joined -> musicRepository.find(uid.childUid)?.let { musicRepository.find(it.uid) } - null -> null } ?: return null @@ -179,40 +177,32 @@ constructor( when (mediaSessionUID) { MediaSessionUID.Category.ROOT -> MediaSessionUID.Category.IMPORTANT.map { it.toMediaItem(context) } - MediaSessionUID.Category.SONGS -> listSettings.songSort.songs(deviceLibrary.songs).map { it.toMediaItem(context, null) } - MediaSessionUID.Category.ALBUMS -> listSettings.albumSort.albums(deviceLibrary.albums).map { it.toMediaItem(context) } - MediaSessionUID.Category.ARTISTS -> listSettings.artistSort.artists(deviceLibrary.artists).map { it.toMediaItem(context) } - MediaSessionUID.Category.GENRES -> listSettings.genreSort.genres(deviceLibrary.genres).map { it.toMediaItem(context) } - MediaSessionUID.Category.PLAYLISTS -> userLibrary.playlists.map { it.toMediaItem(context) } } } - is MediaSessionUID.Single -> { getChildMediaItems(mediaSessionUID.uid) } - is MediaSessionUID.Joined -> { getChildMediaItems(mediaSessionUID.childUid) } - null -> { return null } @@ -225,24 +215,20 @@ constructor( val songs = listSettings.albumSongSort.songs(item.songs) songs.map { it.toMediaItem(context, item) } } - is Artist -> { val albums = ARTIST_ALBUMS_SORT.albums(item.explicitAlbums + item.implicitAlbums) val songs = listSettings.artistSongSort.songs(item.songs) albums.map { it.toMediaItem(context) } + songs.map { it.toMediaItem(context, item) } } - is Genre -> { val artists = GENRE_ARTISTS_SORT.artists(item.artists) val songs = listSettings.genreSongSort.songs(item.songs) artists.map { it.toMediaItem(context) } + - songs.map { it.toMediaItem(context, null) } + songs.map { it.toMediaItem(context, null) } } - is Playlist -> { item.songs.map { it.toMediaItem(context, item) } } - is Song, null -> return null } @@ -339,8 +325,7 @@ constructor( deviceLibrary.albums, deviceLibrary.artists, deviceLibrary.genres, - userLibrary.playlists - ) + userLibrary.playlists) val results = searchEngine.search(items, query) for (entry in searchSubscribers.entries) { if (entry.value == query) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index e21aebb63..7d0dfb1f9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -76,6 +76,10 @@ class PlaybackBarFragment : ViewBindingFragment() { binding.playbackProgressBar.trackColor = context.getColorCompat(R.color.sel_track).defaultColor + // binding.playbackProgressBar.wavelength = 48 + // binding.playbackProgressBar.speed = 20 + // binding.playbackProgressBar.amplitude = 5 + // -- VIEWMODEL SETUP --- collectImmediately(playbackModel.song, ::updateSong) collectImmediately(playbackModel.isPlaying, ::updatePlaying) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt index a2ed51882..37934c0e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt @@ -29,7 +29,6 @@ import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.BaseBottomSheetBehavior import org.oxycblt.auxio.util.getAttrColorCompat -import org.oxycblt.auxio.util.getDimen /** * The [BaseBottomSheetBehavior] for the playback bottom sheet. This bottom sheet @@ -40,8 +39,8 @@ class PlaybackBottomSheetBehavior(context: Context, attributeSet: Attr BaseBottomSheetBehavior(context, attributeSet) { val sheetBackgroundDrawable = MaterialShapeDrawable.createWithElevationOverlay(context).apply { - fillColor = context.getAttrColorCompat(MR.attr.colorSurface) - elevation = context.getDimen(R.dimen.elevation_normal) + fillColor = context.getAttrColorCompat(MR.attr.colorSurfaceContainerLow) + setCornerSize(context.resources.getDimension(R.dimen.size_corners_mid_large)) } init { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 350ec3daa..9bd554a59 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -26,7 +26,6 @@ import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable -import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemEditableSongBinding import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.list.adapter.FlexibleListAdapter @@ -37,7 +36,6 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat -import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.logD @@ -120,8 +118,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS override val delete = binding.background override val background = MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { - fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface) - elevation = binding.context.getDimen(R.dimen.elevation_normal) * 5 + fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurfaceContainerHighest) alpha = 0 } @@ -142,8 +139,8 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS LayerDrawable( arrayOf( MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply { - fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface) - elevation = binding.context.getDimen(R.dimen.elevation_normal) + fillColor = + binding.context.getAttrColorCompat(MR.attr.colorSurfaceContainerHigh) }, background)) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt index ddf70b00d..24f2cfd23 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt @@ -28,7 +28,6 @@ import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.BaseBottomSheetBehavior import org.oxycblt.auxio.util.getAttrColorCompat -import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat @@ -65,8 +64,8 @@ class QueueBottomSheetBehavior(context: Context, attributeSet: Attribu override fun createBackground(context: Context) = MaterialShapeDrawable.createWithElevationOverlay(context).apply { // The queue sheet's background is a static elevated background. - fillColor = context.getAttrColorCompat(MR.attr.colorSurface) - elevation = context.getDimen(R.dimen.elevation_normal) + fillColor = context.getAttrColorCompat(MR.attr.colorSurfaceContainerHigh) + setCornerSize(context.resources.getDimension(R.dimen.size_corners_mid_large)) } override fun applyWindowInsets(child: View, insets: WindowInsets): WindowInsets { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt index 65caa80af..dfb87061e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt @@ -255,7 +255,7 @@ constructor( mediaSession.setCustomLayout(layout) } - override fun invalidate(ids: Map){ + override fun invalidate(ids: Map) { for (id in ids) { mediaSession.notifyChildrenChanged(id.key, id.value, null) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index 14d4f78a9..87380055d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -21,6 +21,7 @@ package org.oxycblt.auxio.playback.ui import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet +import android.view.animation.AnimationUtils import com.google.android.material.button.MaterialButton import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.RippleFixMaterialButton @@ -64,6 +65,9 @@ class AnimatedMaterialButton : RippleFixMaterialButton { animator = ValueAnimator.ofFloat(currentCornerRadiusRatio, targetRadius).apply { duration = context.getInteger(R.integer.anim_fade_enter_duration).toLong() + interpolator = + AnimationUtils.loadInterpolator( + context, android.R.interpolator.fast_out_slow_in) addUpdateListener { updateCornerRadiusRatio(animatedValue as Float) } start() } diff --git a/app/src/main/java/org/oxycblt/auxio/tasker/Tasker.kt b/app/src/main/java/org/oxycblt/auxio/tasker/Tasker.kt index e823bb338..ec2d6ac99 100644 --- a/app/src/main/java/org/oxycblt/auxio/tasker/Tasker.kt +++ b/app/src/main/java/org/oxycblt/auxio/tasker/Tasker.kt @@ -1,2 +1,19 @@ +/* + * Copyright (c) 2024 Auxio Project + * Tasker.kt is part of Auxio. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + package org.oxycblt.auxio.tasker - diff --git a/app/src/main/res/drawable/ic_pause_48.xml b/app/src/main/res/drawable/ic_pause_48.xml new file mode 100644 index 000000000..76e9b9f6e --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_play_48.xml b/app/src/main/res/drawable/ic_play_48.xml new file mode 100644 index 000000000..d163cd960 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_skip_next_40.xml b/app/src/main/res/drawable/ic_skip_next_40.xml new file mode 100644 index 000000000..28ca513fc --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next_40.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_skip_prev_40.xml b/app/src/main/res/drawable/ic_skip_prev_40.xml new file mode 100644 index 000000000..d21330db7 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_prev_40.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_splash_anim.xml b/app/src/main/res/drawable/ic_splash_anim.xml index b2b705d9f..b96ab0ed5 100644 --- a/app/src/main/res/drawable/ic_splash_anim.xml +++ b/app/src/main/res/drawable/ic_splash_anim.xml @@ -8,9 +8,7 @@ android:viewportHeight="108"> + android:pivotY="56"> - - diff --git a/app/src/main/res/drawable/sel_playing_state_48.xml b/app/src/main/res/drawable/sel_playing_state_48.xml new file mode 100644 index 000000000..d9b283a03 --- /dev/null +++ b/app/src/main/res/drawable/sel_playing_state_48.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml index 4157231ac..a31cbcd62 100644 --- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml @@ -102,11 +102,14 @@ @@ -45,10 +44,10 @@ + android:layout_height="wrap_content" + xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 140539c9b..1ee68e73f 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -16,8 +16,8 @@ 192dp 256dp - 8dp - 16dp + 12dp + 14dp 24dp 48dp @@ -36,7 +36,7 @@ 2sp - 3dp + 6dp 78dp 64dp diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index f43098404..6cc3b58da 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -18,7 +18,7 @@ - + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 1ee68e73f..8c7863411 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -21,10 +21,11 @@ 24dp 48dp + 56dp 56dp 64dp 64dp - 72dp + 80dp 24dp 32dp @@ -35,6 +36,9 @@ 22sp 2sp + 10dp + 24dp + 6dp diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index f1ba27ed8..78ddd6fe2 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -22,6 +22,8 @@ gone false 0dp + @dimen/slider_track_height + @dimen/slider_thumb_height - + + + + + + + + + - - From f742aa75922991483fa726bfd5e81534c9088395 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 18 May 2024 23:16:09 -0600 Subject: [PATCH 018/550] ui: use material transitions on some shapes These look a lot better than the old ones. --- .../home/fastscroll/FastScrollRecyclerView.kt | 90 +++++++++++++++---- .../service/ExoPlaybackStateHolder.kt | 1 - .../playback/ui/AnimatedMaterialButton.kt | 19 ++-- 3 files changed, 83 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index 991fc6f4e..781974a2f 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -31,15 +31,17 @@ import android.view.WindowInsets import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.core.view.isInvisible +import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.R as MR +import com.google.android.material.motion.MotionUtils import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.list.recycler.AuxioRecyclerView import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDrawableCompat -import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.isRtl import org.oxycblt.auxio.util.isUnder import org.oxycblt.auxio.util.systemBarInsetsCompat @@ -79,10 +81,28 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // Thumb private val thumbView = View(context).apply { - alpha = 0f + scaleX = 0f background = context.getDrawableCompat(R.drawable.ui_scroll_thumb) } + private val thumbEnterInterpolator = + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingEmphasizedDecelerateInterpolator, + FastOutSlowInInterpolator()) + + private val thumbEnterDuration = + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 300) + + private val thumbExitInterpolator = + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingEmphasizedAccelerateInterpolator, + FastOutSlowInInterpolator()) + + private val thumbExitDuration = + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100) + private val thumbWidth = thumbView.background.intrinsicWidth private val thumbHeight = thumbView.background.intrinsicHeight private val thumbPadding = Rect(0, 0, 0, 0) @@ -98,6 +118,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // Popup private val popupView = FastScrollPopupView(context).apply { + scaleX = 0f + scaleY = 0f + alpha = 0f layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) @@ -107,6 +130,24 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } + private val popupEnterInterpolator = + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingEmphasizedDecelerateInterpolator, + FastOutSlowInInterpolator()) + + private val popupEnterDuration = + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium1, 300) + + private val popupExitInterpolator = + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingEmphasizedAccelerateInterpolator, + FastOutSlowInInterpolator()) + + private val popupExitDuration = + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100) + private var showingPopup = false // Touch @@ -417,7 +458,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingThumb = true - animateViewIn(thumbView) + thumbView + .animate() + .scaleX(1f) + .setInterpolator(thumbEnterInterpolator) + .setDuration(thumbEnterDuration.toLong()) + .start() } private fun hideScrollbar() { @@ -426,7 +472,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingThumb = false - animateViewOut(thumbView) + thumbView + .animate() + .scaleX(0f) + .setInterpolator(thumbExitInterpolator) + .setDuration(thumbExitDuration.toLong()) + .start() } private fun showPopup() { @@ -434,8 +485,18 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr return } + popupView.scaleX = 0f + popupView.scaleY = 0f + + popupView.alpha = 1f showingPopup = true - animateViewIn(popupView) + popupView + .animate() + .scaleX(1f) + .scaleY(1f) + .setInterpolator(popupEnterInterpolator) + .setDuration(popupEnterDuration.toLong()) + .start() } private fun hidePopup() { @@ -444,22 +505,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingPopup = false - animateViewOut(popupView) - } - - private fun animateViewIn(view: View) { - view - .animate() - .alpha(1f) - .setDuration(context.getInteger(R.integer.anim_fade_enter_duration).toLong()) - .start() - } - - private fun animateViewOut(view: View) { - view + popupView .animate() .alpha(0f) - .setDuration(context.getInteger(R.integer.anim_fade_exit_duration).toLong()) + .scaleX(0.75f) + .scaleY(0.75f) + .setInterpolator(popupExitInterpolator) + .setDuration(popupExitDuration.toLong()) .start() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index e8a108d0a..8413738b9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -21,7 +21,6 @@ package org.oxycblt.auxio.playback.service import android.content.Context import android.content.Intent import android.media.audiofx.AudioEffect -import android.os.Bundle import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index 87380055d..c688d1eb9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -21,11 +21,11 @@ package org.oxycblt.auxio.playback.ui import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet -import android.view.animation.AnimationUtils +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import com.google.android.material.R as MR import com.google.android.material.button.MaterialButton -import org.oxycblt.auxio.R +import com.google.android.material.motion.MotionUtils import org.oxycblt.auxio.ui.RippleFixMaterialButton -import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.logD /** @@ -48,6 +48,13 @@ class AnimatedMaterialButton : RippleFixMaterialButton { private var currentCornerRadiusRatio = 0f private var animator: ValueAnimator? = null + private val matInterpolator = + MotionUtils.resolveThemeInterpolator( + context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()) + + private val matDuration = + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300) + override fun setActivated(activated: Boolean) { super.setActivated(activated) @@ -64,10 +71,8 @@ class AnimatedMaterialButton : RippleFixMaterialButton { animator?.cancel() animator = ValueAnimator.ofFloat(currentCornerRadiusRatio, targetRadius).apply { - duration = context.getInteger(R.integer.anim_fade_enter_duration).toLong() - interpolator = - AnimationUtils.loadInterpolator( - context, android.R.interpolator.fast_out_slow_in) + duration = matDuration.toLong() + interpolator = matInterpolator addUpdateListener { updateCornerRadiusRatio(animatedValue as Float) } start() } From d293cc86b0015d96f381628fbf6e89c0828d7428 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 20 May 2024 12:08:32 -0600 Subject: [PATCH 019/550] ui: clean out self-rolled dimens Lots of cruft has built up with my dimensions, partially collapse them into a more consistent set of re-usable dimens (within reason) and try to delegate to MDC as much as possible. --- .../java/org/oxycblt/auxio/MainFragment.kt | 11 +- .../home/fastscroll/FastScrollPopupView.kt | 10 +- .../home/fastscroll/FastScrollRecyclerView.kt | 3 +- .../list/recycler/MaterialDragCallback.kt | 5 +- .../playback/PlaybackBottomSheetBehavior.kt | 9 +- .../auxio/playback/PlaybackPanelFragment.kt | 3 - .../queue/QueueBottomSheetBehavior.kt | 8 +- .../auxio/playback/ui/PlaybackPagerAdapter.kt | 121 ------------------ .../auxio/ui/BaseBottomSheetBehavior.kt | 4 +- .../ui/accent/AccentGridLayoutManager.kt | 2 +- .../oxycblt/auxio/widgets/WidgetComponent.kt | 2 +- .../ui_remote_fab_container_paused.xml | 2 +- .../ui_remote_fab_container_playing.xml | 2 +- .../layout-h480dp/fragment_playback_panel.xml | 10 +- .../fragment_playback_panel.xml | 6 +- .../main/res/layout/dialog_error_details.xml | 2 +- app/src/main/res/layout/dialog_pre_amp.xml | 4 +- app/src/main/res/layout/fragment_main.xml | 2 +- .../res/layout/fragment_playback_panel.xml | 6 +- .../main/res/layout/widget_docked_thin.xml | 12 +- .../main/res/layout/widget_docked_wide.xml | 20 +-- app/src/main/res/layout/widget_pane_thin.xml | 12 +- app/src/main/res/layout/widget_pane_wide.xml | 20 +-- app/src/main/res/layout/widget_wafer_thin.xml | 12 +- app/src/main/res/layout/widget_wafer_wide.xml | 20 +-- app/src/main/res/values/dimens.xml | 40 ++---- app/src/main/res/values/styles_android.xml | 10 +- app/src/main/res/values/styles_ui.xml | 44 +++---- 28 files changed, 137 insertions(+), 265 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/playback/ui/PlaybackPagerAdapter.kt diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index eb5235476..97f51320f 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -101,7 +101,7 @@ class MainFragment : val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? - elevationNormal = binding.context.getDimen(R.dimen.elevation_normal) + elevationNormal = binding.context.getDimen(MR.dimen.m3_sys_elevation_level1) // Currently all back press callbacks are handled in MainFragment, as it's not guaranteed // that instantiating these callbacks in their respective fragments would result in the @@ -146,12 +146,11 @@ class MainFragment : // Emulate the elevated bottom sheet style. background = MaterialShapeDrawable.createWithElevationOverlay(context).apply { - val cornerSize = - context.resources.getDimension(R.dimen.size_corners_mid_large) shapeAppearanceModel = - ShapeAppearanceModel.builder() - .setTopLeftCornerSize(cornerSize) - .setTopRightCornerSize(cornerSize) + ShapeAppearanceModel.builder( + context, + MR.style.ShapeAppearance_Material3_Corner_ExtraLarge, + MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) .build() fillColor = context.getAttrColorCompat(MR.attr.colorSurfaceContainerHigh) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt index ae546d137..c96ceaeb6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt @@ -50,8 +50,8 @@ class FastScrollPopupView constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) : MaterialTextView(context, attrs, defStyleRes) { init { - minimumWidth = context.getDimenPixels(R.dimen.fast_scroll_popup_min_width) - minimumHeight = context.getDimenPixels(R.dimen.fast_scroll_popup_min_height) + minimumWidth = context.getDimenPixels(R.dimen.size_touchable_mid_huge) + minimumHeight = context.getDimenPixels(R.dimen.size_touchable_large) TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge) setTextColor(context.getAttrColorCompat(MR.attr.colorOnSecondary)) @@ -60,7 +60,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) includeFontPadding = false alpha = 0f - elevation = context.getDimenPixels(R.dimen.elevation_normal).toFloat() + elevation = context.getDimenPixels(MR.dimen.m3_sys_elevation_level2).toFloat() background = FastScrollPopupDrawable(context) } @@ -78,8 +78,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) private val path = Path() private val matrix = Matrix() - private val paddingStart = context.getDimenPixels(R.dimen.fast_scroll_popup_padding_start) - private val paddingEnd = context.getDimenPixels(R.dimen.fast_scroll_popup_padding_end) + private val paddingStart = context.getDimenPixels(R.dimen.spacing_medium) + private val paddingEnd = context.getDimenPixels(R.dimen.spacing_mid_huge) override fun draw(canvas: Canvas) { canvas.drawPath(path, paint) diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index 781974a2f..c973a8cfb 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -151,8 +151,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private var showingPopup = false // Touch - private val minTouchTargetSize = - context.getDimenPixels(R.dimen.fast_scroll_thumb_touch_target_size) + private val minTouchTargetSize = context.getDimenPixels(R.dimen.size_touchable_small) private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop private var downX = 0f diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt index 6ccf789b4..a9ffbcd6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt @@ -25,6 +25,7 @@ import android.view.animation.AccelerateDecelerateInterpolator import androidx.core.view.isInvisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.R as MR import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -96,7 +97,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { logD("Lifting ViewHolder") val bg = holder.background - val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal) + val elevation = recyclerView.context.getDimen(MR.dimen.m3_sys_elevation_level4) holder.root .animate() .translationZ(elevation) @@ -138,7 +139,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { logD("Lifting ViewHolder") val bg = holder.background - val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal) + val elevation = recyclerView.context.getDimen(MR.dimen.m3_sys_elevation_level4) holder.root .animate() .translationZ(0f) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt index 4a0af6d92..3adfe27c6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt @@ -27,7 +27,7 @@ import android.view.WindowInsets import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable -import org.oxycblt.auxio.R +import com.google.android.material.shape.ShapeAppearanceModel import org.oxycblt.auxio.ui.BaseBottomSheetBehavior import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat @@ -43,7 +43,12 @@ class PlaybackBottomSheetBehavior(context: Context, attributeSet: Attr val sheetBackgroundDrawable = MaterialShapeDrawable.createWithElevationOverlay(context).apply { fillColor = context.getAttrColorCompat(MR.attr.colorSurfaceContainerLow) - setCornerSize(context.resources.getDimension(R.dimen.size_corners_mid_large)) + shapeAppearanceModel = + ShapeAppearanceModel.builder( + context, + MR.style.ShapeAppearance_Material3_Corner_ExtraLarge, + MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) + .build() } init { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 044577933..ca78a57c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -38,7 +38,6 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.playback.ui.PlaybackPagerAdapter import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.playback.ui.SwipeCoverView import org.oxycblt.auxio.ui.ViewBindingFragment @@ -65,7 +64,6 @@ class PlaybackPanelFragment : private val detailModel: DetailViewModel by activityViewModels() private val listModel: ListViewModel by activityViewModels() private var equalizerLauncher: ActivityResultLauncher? = null - private var coverAdapter: PlaybackPagerAdapter? = null override fun onCreateBinding(inflater: LayoutInflater) = FragmentPlaybackPanelBinding.inflate(inflater) @@ -129,7 +127,6 @@ class PlaybackPanelFragment : override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) { equalizerLauncher = null - coverAdapter = null binding.playbackSong.isSelected = false binding.playbackToolbar.setOnMenuItemClickListener(null) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt index 6da05243f..c09462906 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt @@ -25,6 +25,7 @@ import android.view.WindowInsets import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.R as MR import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.BaseBottomSheetBehavior import org.oxycblt.auxio.util.getAttrColorCompat @@ -65,7 +66,12 @@ class QueueBottomSheetBehavior(context: Context, attributeSet: Attribu MaterialShapeDrawable.createWithElevationOverlay(context).apply { // The queue sheet's background is a static elevated background. fillColor = context.getAttrColorCompat(MR.attr.colorSurfaceContainerHigh) - setCornerSize(context.resources.getDimension(R.dimen.size_corners_mid_large)) + shapeAppearanceModel = + ShapeAppearanceModel.builder( + context, + MR.style.ShapeAppearance_Material3_Corner_ExtraLarge, + MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) + .build() } override fun applyWindowInsets(child: View, insets: WindowInsets): WindowInsets { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/PlaybackPagerAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/PlaybackPagerAdapter.kt deleted file mode 100644 index f98498c71..000000000 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/PlaybackPagerAdapter.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * PlaybackPagerAdapter.kt is part of Auxio. - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.oxycblt.auxio.playback.ui - -import android.view.ViewGroup -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import kotlin.jvm.internal.Intrinsics -import org.oxycblt.auxio.databinding.ItemPlaybackSongBinding -import org.oxycblt.auxio.list.adapter.FlexibleListAdapter -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.util.inflater - -/** @author Koitharu, Alexander Capehart (OxygenCobalt) */ -class PlaybackPagerAdapter(private val listener: Listener) : - FlexibleListAdapter(CoverViewHolder.DIFF_CALLBACK) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoverViewHolder { - return CoverViewHolder.from(parent) - } - - override fun onBindViewHolder(holder: CoverViewHolder, position: Int) { - holder.bind(getItem(position), listener) - } - - override fun onViewRecycled(holder: CoverViewHolder) { - holder.recycle() - super.onViewRecycled(holder) - } - - interface Listener { - fun navigateToCurrentArtist() - - fun navigateToCurrentAlbum() - - fun navigateToCurrentSong() - - fun navigateToMenu() - } -} - -class CoverViewHolder private constructor(private val binding: ItemPlaybackSongBinding) : - RecyclerView.ViewHolder(binding.root), DefaultLifecycleObserver { - init { - binding.root.layoutParams = - RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT) - } - - /** - * Bind new data to this instance. - * - * @param item The new [Song] to bind. - */ - fun bind(item: Song, listener: PlaybackPagerAdapter.Listener) { - val context = binding.root.context - binding.playbackCover.bind(item) - // binding.playbackCover.bind(item) - binding.playbackSong.apply { text = item.name.resolve(context) } - binding.playbackArtist.apply { - text = item.artists.resolveNames(context) - setOnClickListener { listener.navigateToCurrentArtist() } - } - binding.playbackAlbum.apply { - text = item.album.name.resolve(context) - setOnClickListener { listener.navigateToCurrentAlbum() } - } - setSelected(true) - } - - fun recycle() { - // Marquee elements leak if they are not disabled when the views are destroyed. - // TODO: Move to TextView impl to avoid having to deal with lifecycle here - setSelected(false) - } - - private fun setSelected(value: Boolean) { - binding.playbackSong.isSelected = value - binding.playbackArtist.isSelected = value - binding.playbackAlbum.isSelected = value - } - - companion object { - /** - * Create a new instance. - * - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: ViewGroup) = - CoverViewHolder(ItemPlaybackSongBinding.inflate(parent.context.inflater)) - - /** A comparator that can be used with DiffUtil. */ - val DIFF_CALLBACK = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Song, newItem: Song) = - oldItem.uid == newItem.uid - - override fun areContentsTheSame(oldItem: Song, newItem: Song): Boolean { - return Intrinsics.areEqual(oldItem, newItem) - } - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt index 0299a6af8..a36c20302 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -25,8 +25,8 @@ import android.view.View import android.view.ViewGroup import android.view.WindowInsets import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.R as MR import com.google.android.material.bottomsheet.BackportBottomSheetBehavior -import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.systemGestureInsetsCompat @@ -87,7 +87,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: logD("Not initialized, setting up child") child.apply { // Set up compat elevation attributes. These are only shown below API 28. - translationZ = context.getDimen(R.dimen.elevation_normal) + translationZ = context.getDimen(MR.dimen.m3_sys_elevation_level1) // Background differs depending on concrete implementation. background = createBackground(context) setOnApplyWindowInsetsListener(::applyWindowInsets) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt index d4fa6c00c..aae66a179 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt @@ -39,7 +39,7 @@ class AccentGridLayoutManager( ) : GridLayoutManager(context, attrs, defStyleAttr, defStyleRes) { // We use 56dp here since that's the rough size of the accent item. // This will need to be modified if this is used beyond the accent dialog. - private var columnWidth = context.getDimenPixels(R.dimen.size_accent_item) + private var columnWidth = context.getDimenPixels(R.dimen.size_touchable_large) private var lastWidth = -1 private var lastHeight = -1 diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 44731e706..8179e2ab8 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -89,7 +89,7 @@ constructor( } else if (uiSettings.roundMode) { // < Android 12, but the user still enabled round mode. logD("Using default corner radius") - context.getDimenPixels(R.dimen.size_corners_medium) + context.getDimenPixels(R.dimen.m3_shape_corners_large) } else { // User did not enable round mode. logD("Using no corner radius") diff --git a/app/src/main/res/drawable/ui_remote_fab_container_paused.xml b/app/src/main/res/drawable/ui_remote_fab_container_paused.xml index 82362866f..7e9d09828 100644 --- a/app/src/main/res/drawable/ui_remote_fab_container_paused.xml +++ b/app/src/main/res/drawable/ui_remote_fab_container_paused.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_remote_fab_container_playing.xml b/app/src/main/res/drawable/ui_remote_fab_container_playing.xml index f5e00b30c..9090ea9b3 100644 --- a/app/src/main/res/drawable/ui_remote_fab_container_playing.xml +++ b/app/src/main/res/drawable/ui_remote_fab_container_playing.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml index 6cca9cf82..f9bb8ffd4 100644 --- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml @@ -38,7 +38,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_medium" android:layout_marginHorizontal="@dimen/spacing_tiny" - android:layout_marginEnd="@dimen/spacing_mid_medium" + android:layout_marginEnd="@dimen/spacing_medium" app:layout_constraintBottom_toTopOf="@+id/playback_artist" app:layout_constraintEnd_toStartOf="@+id/playback_more" app:layout_constraintStart_toStartOf="parent" @@ -77,11 +77,11 @@ @@ -101,7 +101,7 @@ diff --git a/app/src/main/res/layout/dialog_pre_amp.xml b/app/src/main/res/layout/dialog_pre_amp.xml index 24ca0a85e..9ad5a99b9 100644 --- a/app/src/main/res/layout/dialog_pre_amp.xml +++ b/app/src/main/res/layout/dialog_pre_amp.xml @@ -40,7 +40,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="@dimen/spacing_large" android:gravity="center" - android:minWidth="@dimen/size_pre_amp_ticker" + android:minWidth="@dimen/size_touchable_medium" android:textAppearance="@style/TextAppearance.Auxio.BodySmall" app:layout_constraintBottom_toBottomOf="@+id/with_tags_slider" app:layout_constraintEnd_toEndOf="parent" @@ -77,7 +77,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="@dimen/spacing_large" android:gravity="center" - android:minWidth="@dimen/size_pre_amp_ticker" + android:minWidth="@dimen/size_touchable_medium" android:textAppearance="@style/TextAppearance.Auxio.BodySmall" app:layout_constraintBottom_toBottomOf="@+id/without_tags_slider" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 6297fe740..137612186 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -60,7 +60,7 @@ @@ -82,8 +82,8 @@ @@ -95,8 +95,8 @@ diff --git a/app/src/main/res/layout/widget_docked_wide.xml b/app/src/main/res/layout/widget_docked_wide.xml index d344799f4..646348f70 100644 --- a/app/src/main/res/layout/widget_docked_wide.xml +++ b/app/src/main/res/layout/widget_docked_wide.xml @@ -56,8 +56,8 @@ @@ -69,8 +69,8 @@ @@ -82,8 +82,8 @@ @@ -95,8 +95,8 @@ @@ -108,8 +108,8 @@ diff --git a/app/src/main/res/layout/widget_pane_thin.xml b/app/src/main/res/layout/widget_pane_thin.xml index 2b2361971..8b5be1f4e 100644 --- a/app/src/main/res/layout/widget_pane_thin.xml +++ b/app/src/main/res/layout/widget_pane_thin.xml @@ -80,8 +80,8 @@ @@ -93,8 +93,8 @@ @@ -106,8 +106,8 @@ diff --git a/app/src/main/res/layout/widget_pane_wide.xml b/app/src/main/res/layout/widget_pane_wide.xml index 07c693564..14c671bf7 100644 --- a/app/src/main/res/layout/widget_pane_wide.xml +++ b/app/src/main/res/layout/widget_pane_wide.xml @@ -82,8 +82,8 @@ @@ -95,8 +95,8 @@ @@ -108,8 +108,8 @@ @@ -121,8 +121,8 @@ @@ -134,8 +134,8 @@ diff --git a/app/src/main/res/layout/widget_wafer_thin.xml b/app/src/main/res/layout/widget_wafer_thin.xml index db12288cf..385a39fe7 100644 --- a/app/src/main/res/layout/widget_wafer_thin.xml +++ b/app/src/main/res/layout/widget_wafer_thin.xml @@ -40,8 +40,8 @@ @@ -55,8 +55,8 @@ @@ -73,8 +73,8 @@ diff --git a/app/src/main/res/layout/widget_wafer_wide.xml b/app/src/main/res/layout/widget_wafer_wide.xml index d773fc5f0..bd922107a 100644 --- a/app/src/main/res/layout/widget_wafer_wide.xml +++ b/app/src/main/res/layout/widget_wafer_wide.xml @@ -39,8 +39,8 @@ @@ -60,8 +60,8 @@ @@ -75,8 +75,8 @@ @@ -93,8 +93,8 @@ @@ -114,8 +114,8 @@ diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e8c1b441f..a419aee10 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -7,32 +7,29 @@ 16dp 20dp 24dp + 28dp 32dp - 48dp - 56dp - 128dp - 192dp - 256dp + 48dp + 56dp + 60dp + 64dp + 76dp + 80dp - 12dp - 14dp - 24dp - - 48dp - 60dp - 56dp - 64dp - 64dp - 80dp + 128dp + 192dp + 256dp 24dp 32dp 40dp 48dp - 56dp + + 16dp + 128dp 14sp 22sp @@ -41,17 +38,6 @@ 10dp 24dp - - 1dp - - 78dp - 64dp - @dimen/spacing_medium - 28dp - 48dp - - 6dp - 88dp 128dp diff --git a/app/src/main/res/values/styles_android.xml b/app/src/main/res/values/styles_android.xml index 98812140d..e31f6660e 100644 --- a/app/src/main/res/values/styles_android.xml +++ b/app/src/main/res/values/styles_android.xml @@ -7,7 +7,7 @@ 56dp to 48dp. --> @@ -85,13 +85,13 @@ diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 933e33950..26fe9d5c5 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -11,7 +11,7 @@ @@ -64,35 +64,35 @@ @@ -205,7 +205,7 @@ @dimen/spacing_small @dimen/spacing_small @dimen/spacing_medium - @dimen/size_btn + @dimen/size_touchable_small center_vertical ?attr/colorSecondary @style/TextAppearance.Auxio.LabelLarge @@ -248,8 +248,8 @@ - From 294c558b9326234373f083c66311d5e77c62578b Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 5 Jul 2024 12:11:50 -0600 Subject: [PATCH 044/550] playback: fix brief pause when adding songs to playlists --- CHANGELOG.md | 1 + .../service/ExoPlaybackStateHolder.kt | 28 +++++++++++++++---- .../playback/state/PlaybackStateHolder.kt | 8 +++++- .../playback/state/PlaybackStateManager.kt | 19 +++++++++---- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ccedd7b..ce0284a29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Sorting songs by date now uses songs date first, before the earliest album date #### What's Fixed +- Playback no longer briefly pauses when adding songs to playlists - Music loader no longer spawns thousands of threads when scanning - Excessive CPU no longer spent showing music loading process - Fixed playback sheet flickering on warm start diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 058b33403..adf3a782c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -49,6 +49,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.service.toMediaItem import org.oxycblt.auxio.music.service.toSong import org.oxycblt.auxio.playback.PlaybackSettings +import org.oxycblt.auxio.playback.msToSecs import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.state.DeferredPlayback @@ -365,13 +366,21 @@ class ExoPlaybackStateHolder( override fun applySavedState( parent: MusicParent?, rawQueue: RawQueue, + positionMs: Long, + repeatMode: RepeatMode, ack: StateAck.NewPlayback? ) { - logD("Applying saved state") - var sendEvent = false + val resolve = resolveQueue() + logD("${rawQueue.heap == resolve.heap}") + logD("${rawQueue.shuffledMapping == resolve.shuffledMapping}") + logD("${rawQueue.heapIndex == resolve.heapIndex}") + logD("${rawQueue.isShuffled == resolve.isShuffled}") + logD("${rawQueue == resolve}") + var sendNewPlaybackEvent = false + var shouldSeek = false if (this.parent != parent) { this.parent = parent - sendEvent = true + sendNewPlaybackEvent = true } if (rawQueue != resolveQueue()) { player.setMediaItems(rawQueue.heap.map { it.toMediaItem(context, null) }) @@ -384,9 +393,18 @@ class ExoPlaybackStateHolder( player.seekTo(rawQueue.heapIndex, C.TIME_UNSET) player.prepare() player.pause() - sendEvent = true + sendNewPlaybackEvent = true + shouldSeek = true } - if (sendEvent) { + + repeatMode(repeatMode) + // Positions in milliseconds will drift during tight restores (i.e what the music loader + // does to sanitize the state), compare by seconds instead. + if (positionMs.msToSecs() != player.currentPosition.msToSecs() || shouldSeek) { + player.seekTo(positionMs) + } + + if (sendNewPlaybackEvent) { ack?.let { playbackManager.ack(this, it) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt index 857ac6898..01ff1e520 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt @@ -145,7 +145,13 @@ interface PlaybackStateHolder { * @param ack The [StateAck] to return to [PlaybackStateManager]. If null, do not return any * ack. */ - fun applySavedState(parent: MusicParent?, rawQueue: RawQueue, ack: StateAck.NewPlayback?) + fun applySavedState( + parent: MusicParent?, + rawQueue: RawQueue, + positionMs: Long, + repeatMode: RepeatMode, + ack: StateAck.NewPlayback? + ) /** End whatever ongoing playback session may be going on */ fun endSession() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 494ab2c0e..948be122e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -24,6 +24,7 @@ import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -416,9 +417,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { this.stateHolder = stateHolder if (isInitialized && currentSong != null) { - stateHolder.applySavedState(stateMirror.parent, stateMirror.rawQueue, null) - stateHolder.seekTo(stateMirror.progression.calculateElapsedPositionMs()) - stateHolder.playing(false) + stateHolder.applySavedState( + stateMirror.parent, + stateMirror.rawQueue, + stateMirror.progression.calculateElapsedPositionMs(), + stateMirror.repeatMode, + null) } pendingDeferredPlayback?.let(stateHolder::handleDeferred) } @@ -795,9 +799,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { index }) - stateHolder.applySavedState(savedState.parent, rawQueue, StateAck.NewPlayback) - stateHolder.seekTo(savedState.positionMs) - stateHolder.repeatMode(savedState.repeatMode) + stateHolder.applySavedState( + savedState.parent, + rawQueue, + savedState.positionMs, + savedState.repeatMode, + StateAck.NewPlayback) isInitialized = true } From 82a015c1e121c61a74972fc3214ed4b435e6f0a4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 5 Jul 2024 17:32:39 -0600 Subject: [PATCH 045/550] music: handle null mediastore album name Mostly a band-aid to make null album names correspond to a folder name (the standard MediaStore behavior). --- .../java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index e0e989133..d0776e4a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -283,7 +283,10 @@ private class MediaStoreExtractorImpl( // the file is not actually in the root internal storage directory. We can't do // anything to fix this, really. We also can't really filter it out, since how can we // know when it corresponds to the folder and not, say, Low Roar's breakout album "0"? - rawSong.albumName = cursor.getString(albumIndex) + // Also, on some devices it's literally just null. To maintain behavior sanity just + // replicate the majority behavior described prior. + rawSong.albumName = cursor.getStringOrNull(albumIndex) + ?: requireNotNull(rawSong.path?.name) { "Invalid raw: No path" } // Android does not make a non-existent artist tag null, it instead fills it in // as , which makes absolutely no sense given how other columns default // to null if they are not present. If this column is such, null it so that From 86e2fd7a89af7215143de060d43febd1118df065 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 11:19:18 -0600 Subject: [PATCH 046/550] detail: make album view use collapsing toolbar --- .../auxio/detail/AlbumDetailFragment.kt | 120 +++++++---- .../oxycblt/auxio/detail/DetailViewModel.kt | 1 - .../detail/header/AlbumDetailHeaderAdapter.kt | 114 ---------- .../res/layout-w600dp/fragment_detail2.xml | 203 ++++++++++++++++++ app/src/main/res/layout/fragment_detail2.xml | 187 ++++++++++++++++ 5 files changed, 474 insertions(+), 151 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt create mode 100644 app/src/main/res/layout-w600dp/fragment_detail2.xml create mode 100644 app/src/main/res/layout/fragment_detail2.xml diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index b49b206ec..56b271310 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -23,14 +23,16 @@ import android.view.LayoutInflater import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.header.AlbumDetailHeaderAdapter +import org.oxycblt.auxio.databinding.FragmentDetail2Binding import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter import org.oxycblt.auxio.detail.list.DetailListAdapter import org.oxycblt.auxio.list.Divider @@ -46,12 +48,14 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.info.Disc +import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.util.canScroll +import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.getDimenPixels +import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.overrideOnOverflowMenuClick @@ -66,9 +70,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ @AndroidEntryPoint class AlbumDetailFragment : - ListFragment(), - AlbumDetailHeaderAdapter.Listener, - DetailListAdapter.Listener { + ListFragment(), + DetailListAdapter.Listener, + AppBarLayout.OnOffsetChangedListener { private val detailModel: DetailViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() @@ -77,9 +81,10 @@ class AlbumDetailFragment : // Information about what album to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an album. private val args: AlbumDetailFragmentArgs by navArgs() - private val albumHeaderAdapter = AlbumDetailHeaderAdapter(this) private val albumListAdapter = AlbumDetailListAdapter(this) + private var spacingSmall = 0 + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Detail transitions are always on the X axis. Shared element transitions are more @@ -90,15 +95,18 @@ class AlbumDetailFragment : reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) } - override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) + override fun onCreateBinding(inflater: LayoutInflater) = + FragmentDetail2Binding.inflate(inflater) - override fun getSelectionToolbar(binding: FragmentDetailBinding) = + override fun getSelectionToolbar(binding: FragmentDetail2Binding) = binding.detailSelectionToolbar - override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) // --- UI SETUP -- + binding.detailAppbar.addOnOffsetChangedListener(this) + binding.detailNormalToolbar.apply { setNavigationOnClickListener { findNavController().navigateUp() } overrideOnOverflowMenuClick { @@ -108,17 +116,23 @@ class AlbumDetailFragment : } binding.detailRecycler.apply { - adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter) + adapter = albumListAdapter + (layoutManager as GridLayoutManager).setFullWidthLookup { if (it != 0) { - val item = detailModel.albumSongList.value[it - 1] - item is Divider || item is Header || item is Disc + val item = + detailModel.genreSongList.value.getOrElse(it - 1) { + return@setFullWidthLookup false + } + item is Divider || item is Header } else { true } } } + spacingSmall = requireContext().getDimenPixels(R.dimen.spacing_small) + // -- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. detailModel.setAlbum(args.albumUid) @@ -134,15 +148,31 @@ class AlbumDetailFragment : collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) } - override fun onDestroyBinding(binding: FragmentDetailBinding) { + override fun onDestroyBinding(binding: FragmentDetail2Binding) { super.onDestroyBinding(binding) - binding.detailNormalToolbar.setOnMenuItemClickListener(null) binding.detailRecycler.adapter = null // Avoid possible race conditions that could cause a bad replace instruction to be consumed // during list initialization and crash the app. Could happen if the user is fast enough. detailModel.albumSongInstructions.consume() } + override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { + val binding = requireBinding() + val range = appBarLayout.totalScrollRange + val ratio = abs(verticalOffset.toFloat()) / range.toFloat() + + val outRatio = min(ratio * 2, 1f) + val detailHeader = binding.detailHeader + detailHeader.scaleX = 1 - 0.05f * outRatio + detailHeader.scaleY = 1 - 0.05f * outRatio + detailHeader.alpha = 1 - outRatio + + val inRatio = max(ratio - 0.5f, 0f) * 2 + val detailContent = binding.detailToolbarContent + detailContent.alpha = inRatio + detailContent.translationY = spacingSmall * (1 - inRatio) + } + override fun onRealClick(item: Song) { playbackModel.play(item, detailModel.playInAlbumWith) } @@ -151,30 +181,50 @@ class AlbumDetailFragment : listModel.openMenu(R.menu.album_song, item, detailModel.playInAlbumWith) } - override fun onPlay() { - playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value)) - } - - override fun onShuffle() { - playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value)) - } - override fun onOpenSortMenu() { findNavController().navigateSafe(AlbumDetailFragmentDirections.sort()) } - override fun onNavigateToParentArtist() { - detailModel.showArtist(unlikelyToBeNull(detailModel.currentAlbum.value)) - } - private fun updateAlbum(album: Album?) { if (album == null) { logD("No album to show, navigating away") findNavController().navigateUp() return } - requireBinding().detailNormalToolbar.title = album.name.resolve(requireContext()) - albumHeaderAdapter.setParent(album) + + val binding = requireBinding() + + binding.detailToolbarTitle.text = album.name.resolve(requireContext()) + binding.detailCover.bind(album) + // The type text depends on the release type (Album, EP, Single, etc.) + binding.detailType.text = getString(album.releaseType.stringRes) + binding.detailName.text = album.name.resolve(requireContext()) + // Artist name maps to the subhead text + binding.detailSubhead.apply { + text = album.artists.resolveNames(context) + + // Add a QoL behavior where navigation to the artist will occur if the artist + // name is pressed. + setOnClickListener { + detailModel.showArtist(unlikelyToBeNull(detailModel.currentAlbum.value)) + } + } + + // Date, song count, and duration map to the info text + binding.detailInfo.apply { + // Fall back to a friendlier "No date" text if the album doesn't have date information + val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date) + val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size) + val duration = album.durationMs.formatDurationMs(true) + text = context.getString(R.string.fmt_three, date, songCount, duration) + } + + binding.detailPlayButton.setOnClickListener { + playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value)) + } + binding.detailShuffleButton.setOnClickListener { + playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value)) + } } private fun updateList(list: List) { @@ -318,6 +368,9 @@ class AlbumDetailFragment : if (pos != -1) { // Only scroll if the song is within this album. val binding = requireBinding() + // RecyclerView will scroll assuming it has the total height of the screen (i.e a + // collapsed appbar), so we need to collapse the appbar if that's the case. + binding.detailAppbar.setExpanded(false) binding.detailRecycler.post { // Use a custom smooth scroller that will settle the item in the middle of // the screen rather than the end. @@ -340,11 +393,6 @@ class AlbumDetailFragment : // Make sure to increment the position to make up for the detail header binding.detailRecycler.layoutManager?.startSmoothScroll(centerSmoothScroller) - - // If the recyclerview can scroll, its certain that it will have to scroll to - // correctly center the playing item, so make sure that the Toolbar is lifted in - // that case. - binding.detailAppbar.isLifted = binding.detailRecycler.canScroll() } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 3613d96c6..a9695df55 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -556,7 +556,6 @@ constructor( logD("Refreshing album list") val list = mutableListOf() val header = SortHeader(R.string.lbl_songs) - list.add(Divider(header)) list.add(header) val instructions = if (replace) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt deleted file mode 100644 index 41c12d9d6..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * AlbumDetailHeaderAdapter.kt is part of Auxio. - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.oxycblt.auxio.detail.header - -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.playback.formatDurationMs -import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.inflater - -/** - * A [DetailHeaderAdapter] that shows [Album] information. - * - * @param listener [DetailHeaderAdapter.Listener] to bind interactions to. - * @author Alexander Capehart (OxygenCobalt) - */ -class AlbumDetailHeaderAdapter(private val listener: Listener) : - DetailHeaderAdapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - AlbumDetailHeaderViewHolder.from(parent) - - override fun onBindHeader(holder: AlbumDetailHeaderViewHolder, parent: Album) = - holder.bind(parent, listener) - - /** An extended listener for [DetailHeaderAdapter] implementations. */ - interface Listener : DetailHeaderAdapter.Listener { - - /** - * Called when the artist name in the [Album] header was clicked, requesting navigation to - * it's parent artist. - */ - fun onNavigateToParentArtist() - } -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to - * create an instance. - * - * @author Alexander Capehart (OxygenCobalt) - */ -class AlbumDetailHeaderViewHolder -private constructor(private val binding: ItemDetailHeaderBinding) : - RecyclerView.ViewHolder(binding.root) { - - /** - * Bind new data to this instance. - * - * @param album The new [Album] to bind. - * @param listener A [AlbumDetailHeaderAdapter.Listener] to bind interactions to. - */ - fun bind(album: Album, listener: AlbumDetailHeaderAdapter.Listener) { - binding.detailCover.bind(album) - - // The type text depends on the release type (Album, EP, Single, etc.) - binding.detailType.text = binding.context.getString(album.releaseType.stringRes) - - binding.detailName.text = album.name.resolve(binding.context) - - // Artist name maps to the subhead text - binding.detailSubhead.apply { - text = album.artists.resolveNames(context) - - // Add a QoL behavior where navigation to the artist will occur if the artist - // name is pressed. - setOnClickListener { listener.onNavigateToParentArtist() } - } - - // Date, song count, and duration map to the info text - binding.detailInfo.apply { - // Fall back to a friendlier "No date" text if the album doesn't have date information - val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date) - val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size) - val duration = album.durationMs.formatDurationMs(true) - text = context.getString(R.string.fmt_three, date, songCount, duration) - } - - binding.detailPlayButton.setOnClickListener { listener.onPlay() } - binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } - } - - companion object { - /** - * Create a new instance. - * - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - AlbumDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater)) - } -} diff --git a/app/src/main/res/layout-w600dp/fragment_detail2.xml b/app/src/main/res/layout-w600dp/fragment_detail2.xml new file mode 100644 index 000000000..1ffac5630 --- /dev/null +++ b/app/src/main/res/layout-w600dp/fragment_detail2.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_detail2.xml b/app/src/main/res/layout/fragment_detail2.xml new file mode 100644 index 000000000..4bab91c03 --- /dev/null +++ b/app/src/main/res/layout/fragment_detail2.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 3286a94b1a71f873708d5b44150ad3e22bbcffa8 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 11:19:58 -0600 Subject: [PATCH 047/550] playback: fix various playback layout issues --- .../layout-h480dp/fragment_playback_panel.xml | 110 ++++++----- .../fragment_playback_panel.xml | 179 ------------------ .../fragment_main.xml | 0 3 files changed, 59 insertions(+), 230 deletions(-) delete mode 100644 app/src/main/res/layout-sw600dp/fragment_playback_panel.xml rename app/src/main/res/{layout-w670dp => layout-w720dp}/fragment_main.xml (100%) diff --git a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml index f9bb8ffd4..bcf2e70a8 100644 --- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml @@ -12,12 +12,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:menu="@menu/toolbar_playback" - app:titleCentered="true" - app:subtitleCentered="true" - app:titleTextAppearance="@style/TextAppearance.Auxio.LabelLarge" - app:subtitleTextAppearance="@style/TextAppearance.Auxio.BodySmall" app:navigationIcon="@drawable/ic_down_24" + app:subtitleCentered="true" + app:subtitleTextAppearance="@style/TextAppearance.Auxio.BodySmall" app:title="@string/lbl_playback" + app:titleCentered="true" + app:titleTextAppearance="@style/TextAppearance.Auxio.LabelLarge" tools:subtitle="@string/lbl_all_songs" /> + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" + app:layout_constraintVertical_chainStyle="packed" /> - - - - - + app:layout_constraintEnd_toStartOf="@+id/playback_more" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" + app:layout_constraintVertical_bias="1.0" + app:layout_constraintVertical_chainStyle="packed"> + + + + + + + + + - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/playback_info_container" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintWidth_max="400dp"> - - \ No newline at end of file + diff --git a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml deleted file mode 100644 index 2d5e62307..000000000 --- a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-w670dp/fragment_main.xml b/app/src/main/res/layout-w720dp/fragment_main.xml similarity index 100% rename from app/src/main/res/layout-w670dp/fragment_main.xml rename to app/src/main/res/layout-w720dp/fragment_main.xml From 04265d52858a8a925bc6d7b1092adfe22515a376 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 11:20:55 -0600 Subject: [PATCH 048/550] home: remove logging spamming the console --- app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 74c71630b..0781560e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -311,7 +311,6 @@ constructor( } fun setSheetObscuresFab(sheetRising: Boolean) { - logD("Updating sheet rising state: $sheetRising") _sheetObscuresFab.value = sheetRising } From 0eb3ede8ec5c78c7c227db4022d79b211bfbee29 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 12:54:04 -0600 Subject: [PATCH 049/550] detail: make artist view use collapsing toolbar --- .../auxio/detail/AlbumDetailFragment.kt | 88 ++---------- .../auxio/detail/ArtistDetailFragment.kt | 125 +++++++++-------- .../oxycblt/auxio/detail/DetailFragment.kt | 131 ++++++++++++++++++ .../oxycblt/auxio/detail/DetailViewModel.kt | 1 - .../header/ArtistDetailHeaderAdapter.kt | 120 ---------------- .../auxio/music/fs/MediaStoreExtractor.kt | 5 +- 6 files changed, 214 insertions(+), 256 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 56b271310..c80f1cda6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -23,20 +23,11 @@ import android.view.LayoutInflater import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller -import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetail2Binding import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter -import org.oxycblt.auxio.detail.list.DetailListAdapter -import org.oxycblt.auxio.list.Divider -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel @@ -54,12 +45,9 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe -import org.oxycblt.auxio.util.overrideOnOverflowMenuClick -import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull @@ -69,10 +57,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class AlbumDetailFragment : - ListFragment(), - DetailListAdapter.Listener, - AppBarLayout.OnOffsetChangedListener { +class AlbumDetailFragment : DetailFragment() { private val detailModel: DetailViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() @@ -83,56 +68,17 @@ class AlbumDetailFragment : private val args: AlbumDetailFragmentArgs by navArgs() private val albumListAdapter = AlbumDetailListAdapter(this) - private var spacingSmall = 0 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - // Detail transitions are always on the X axis. Shared element transitions are more - // semantically correct, but are also too buggy to be sensible. - enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - } - override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetail2Binding.inflate(inflater) + override fun getDetailListAdapter() = albumListAdapter + override fun getSelectionToolbar(binding: FragmentDetail2Binding) = binding.detailSelectionToolbar override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) - // --- UI SETUP -- - binding.detailAppbar.addOnOffsetChangedListener(this) - - binding.detailNormalToolbar.apply { - setNavigationOnClickListener { findNavController().navigateUp() } - overrideOnOverflowMenuClick { - listModel.openMenu( - R.menu.detail_album, unlikelyToBeNull(detailModel.currentAlbum.value)) - } - } - - binding.detailRecycler.apply { - adapter = albumListAdapter - - (layoutManager as GridLayoutManager).setFullWidthLookup { - if (it != 0) { - val item = - detailModel.genreSongList.value.getOrElse(it - 1) { - return@setFullWidthLookup false - } - item is Divider || item is Header - } else { - true - } - } - } - - spacingSmall = requireContext().getDimenPixels(R.dimen.spacing_small) - // -- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. detailModel.setAlbum(args.albumUid) @@ -150,33 +96,19 @@ class AlbumDetailFragment : override fun onDestroyBinding(binding: FragmentDetail2Binding) { super.onDestroyBinding(binding) - binding.detailRecycler.adapter = null // Avoid possible race conditions that could cause a bad replace instruction to be consumed // during list initialization and crash the app. Could happen if the user is fast enough. detailModel.albumSongInstructions.consume() } - override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { - val binding = requireBinding() - val range = appBarLayout.totalScrollRange - val ratio = abs(verticalOffset.toFloat()) / range.toFloat() - - val outRatio = min(ratio * 2, 1f) - val detailHeader = binding.detailHeader - detailHeader.scaleX = 1 - 0.05f * outRatio - detailHeader.scaleY = 1 - 0.05f * outRatio - detailHeader.alpha = 1 - outRatio - - val inRatio = max(ratio - 0.5f, 0f) * 2 - val detailContent = binding.detailToolbarContent - detailContent.alpha = inRatio - detailContent.translationY = spacingSmall * (1 - inRatio) - } - override fun onRealClick(item: Song) { playbackModel.play(item, detailModel.playInAlbumWith) } + override fun onOpenParentMenu() { + listModel.openMenu(R.menu.album, unlikelyToBeNull(detailModel.currentAlbum.value)) + } + override fun onOpenMenu(item: Song) { listModel.openMenu(R.menu.album_song, item, detailModel.playInAlbumWith) } @@ -193,12 +125,14 @@ class AlbumDetailFragment : } val binding = requireBinding() + val context = requireContext() + val name = album.name.resolve(context) - binding.detailToolbarTitle.text = album.name.resolve(requireContext()) + binding.detailToolbarTitle.text = name binding.detailCover.bind(album) // The type text depends on the release type (Album, EP, Single, etc.) binding.detailType.text = getString(album.releaseType.stringRes) - binding.detailName.text = album.name.resolve(requireContext()) + binding.detailName.text = name // Artist name maps to the subhead text binding.detailSubhead.apply { text = album.artists.resolveNames(context) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 5fdd4e7f7..af65b9a27 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -20,21 +20,15 @@ package org.oxycblt.auxio.detail import android.os.Bundle import android.view.LayoutInflater +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.header.ArtistDetailHeaderAdapter -import org.oxycblt.auxio.detail.header.DetailHeaderAdapter +import org.oxycblt.auxio.databinding.FragmentDetail2Binding import org.oxycblt.auxio.detail.list.ArtistDetailListAdapter -import org.oxycblt.auxio.detail.list.DetailListAdapter -import org.oxycblt.auxio.list.Divider -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel @@ -47,14 +41,15 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe -import org.oxycblt.auxio.util.overrideOnOverflowMenuClick -import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull @@ -64,18 +59,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class ArtistDetailFragment : - ListFragment(), - DetailHeaderAdapter.Listener, - DetailListAdapter.Listener { +class ArtistDetailFragment : DetailFragment() { private val detailModel: DetailViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() + // Information about what artist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an artist. private val args: ArtistDetailFragmentArgs by navArgs() - private val artistHeaderAdapter = ArtistDetailHeaderAdapter(this) private val artistListAdapter = ArtistDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { @@ -88,39 +80,17 @@ class ArtistDetailFragment : reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) } - override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) + override fun onCreateBinding(inflater: LayoutInflater) = + FragmentDetail2Binding.inflate(inflater) - override fun getSelectionToolbar(binding: FragmentDetailBinding) = + override fun getSelectionToolbar(binding: FragmentDetail2Binding) = binding.detailSelectionToolbar - override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { + override fun getDetailListAdapter() = artistListAdapter + + override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) - // --- UI SETUP --- - binding.detailNormalToolbar.apply { - setNavigationOnClickListener { findNavController().navigateUp() } - setOnMenuItemClickListener(this@ArtistDetailFragment) - overrideOnOverflowMenuClick { - listModel.openMenu( - R.menu.detail_parent, unlikelyToBeNull(detailModel.currentArtist.value)) - } - } - - binding.detailRecycler.apply { - adapter = ConcatAdapter(artistHeaderAdapter, artistListAdapter) - (layoutManager as GridLayoutManager).setFullWidthLookup { - if (it != 0) { - val item = - detailModel.artistSongList.value.getOrElse(it - 1) { - return@setFullWidthLookup false - } - item is Divider || item is Header - } else { - true - } - } - } - // --- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. detailModel.setArtist(args.artistUid) @@ -136,10 +106,8 @@ class ArtistDetailFragment : collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) } - override fun onDestroyBinding(binding: FragmentDetailBinding) { + override fun onDestroyBinding(binding: FragmentDetail2Binding) { super.onDestroyBinding(binding) - binding.detailNormalToolbar.setOnMenuItemClickListener(null) - binding.detailRecycler.adapter = null // Avoid possible race conditions that could cause a bad replace instruction to be consumed // during list initialization and crash the app. Could happen if the user is fast enough. detailModel.artistSongInstructions.consume() @@ -153,6 +121,10 @@ class ArtistDetailFragment : } } + override fun onOpenParentMenu() { + listModel.openMenu(R.menu.detail_parent, unlikelyToBeNull(detailModel.currentArtist.value)) + } + override fun onOpenMenu(item: Music) { when (item) { is Song -> listModel.openMenu(R.menu.artist_song, item, detailModel.playInArtistWith) @@ -161,14 +133,6 @@ class ArtistDetailFragment : } } - override fun onPlay() { - playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value)) - } - - override fun onShuffle() { - playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value)) - } - override fun onOpenSortMenu() { findNavController().navigateSafe(ArtistDetailFragmentDirections.sort()) } @@ -179,8 +143,57 @@ class ArtistDetailFragment : findNavController().navigateUp() return } - requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext()) - artistHeaderAdapter.setParent(artist) + val binding = requireBinding() + val context = requireContext() + val name = artist.name.resolve(context) + binding.detailToolbarTitle.text = name + + binding.detailCover.bind(artist) + binding.detailType.text = context.getString(R.string.lbl_artist) + binding.detailName.text = name + + // Song and album counts map to the info + binding.detailInfo.text = + context.getString( + R.string.fmt_two, + if (artist.explicitAlbums.isNotEmpty()) { + context.getPlural(R.plurals.fmt_album_count, artist.explicitAlbums.size) + } else { + context.getString(R.string.def_album_count) + }, + if (artist.songs.isNotEmpty()) { + context.getPlural(R.plurals.fmt_song_count, artist.songs.size) + } else { + context.getString(R.string.def_song_count) + }) + + if (artist.songs.isNotEmpty()) { + // Information about the artist's genre(s) map to the sub-head text + binding.detailSubhead.apply { + isVisible = true + text = artist.genres.resolveNames(context) + } + + // In the case that this header used to he configured to have no songs, + // we want to reset the visibility of all information that was hidden. + binding.detailPlayButton.isVisible = true + binding.detailShuffleButton.isVisible = true + } else { + // The artist does not have any songs, so hide functionality that makes no sense. + // ex. Play and Shuffle, Song Counts, and Genre Information. + // Artists are always guaranteed to have albums however, so continue to show those. + logD("Artist is empty, disabling genres and playback") + binding.detailSubhead.isVisible = false + binding.detailPlayButton.isEnabled = false + binding.detailShuffleButton.isEnabled = false + } + + binding.detailPlayButton.setOnClickListener { + playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value)) + } + binding.detailShuffleButton.setOnClickListener { + playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value)) + } } private fun updateList(list: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt new file mode 100644 index 000000000..334142172 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 Auxio Project + * DetailFragment.kt is part of Auxio. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.oxycblt.auxio.detail + +import android.os.Bundle +import android.view.LayoutInflater +import androidx.fragment.app.activityViewModels +import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.transition.MaterialSharedAxis +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentDetail2Binding +import org.oxycblt.auxio.detail.list.DetailListAdapter +import org.oxycblt.auxio.list.Divider +import org.oxycblt.auxio.list.Header +import org.oxycblt.auxio.list.ListFragment +import org.oxycblt.auxio.list.ListViewModel +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.util.getDimenPixels +import org.oxycblt.auxio.util.overrideOnOverflowMenuClick +import org.oxycblt.auxio.util.setFullWidthLookup + +abstract class DetailFragment

: + ListFragment(), + DetailListAdapter.Listener, + AppBarLayout.OnOffsetChangedListener { + private val detailModel: DetailViewModel by activityViewModels() + override val listModel: ListViewModel by activityViewModels() + override val musicModel: MusicViewModel by activityViewModels() + override val playbackModel: PlaybackViewModel by activityViewModels() + + private var spacingSmall = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Detail transitions are always on the X axis. Shared element transitions are more + // semantically correct, but are also too buggy to be sensible. + enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + } + + override fun onCreateBinding(inflater: LayoutInflater) = + FragmentDetail2Binding.inflate(inflater) + + abstract fun getDetailListAdapter(): DetailListAdapter + + override fun getSelectionToolbar(binding: FragmentDetail2Binding) = + binding.detailSelectionToolbar + + override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { + super.onBindingCreated(binding, savedInstanceState) + + // --- UI SETUP --- + binding.detailAppbar.addOnOffsetChangedListener(this) + + binding.detailNormalToolbar.apply { + setNavigationOnClickListener { findNavController().navigateUp() } + setOnMenuItemClickListener(this@DetailFragment) + overrideOnOverflowMenuClick { onOpenParentMenu() } + } + + binding.detailRecycler.apply { + adapter = getDetailListAdapter() + (layoutManager as GridLayoutManager).setFullWidthLookup { + if (it != 0) { + val item = + detailModel.artistSongList.value.getOrElse(it - 1) { + return@setFullWidthLookup false + } + item is Divider || item is Header + } else { + true + } + } + } + + spacingSmall = requireContext().getDimenPixels(R.dimen.spacing_small) + } + + override fun onDestroyBinding(binding: FragmentDetail2Binding) { + super.onDestroyBinding(binding) + binding.detailAppbar.removeOnOffsetChangedListener(this) + binding.detailNormalToolbar.setOnMenuItemClickListener(null) + binding.detailRecycler.adapter = null + } + + override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { + val binding = requireBinding() + val range = appBarLayout.totalScrollRange + val ratio = abs(verticalOffset.toFloat()) / range.toFloat() + + val outRatio = min(ratio * 2, 1f) + val detailHeader = binding.detailHeader + detailHeader.scaleX = 1 - 0.05f * outRatio + detailHeader.scaleY = 1 - 0.05f * outRatio + detailHeader.alpha = 1 - outRatio + + val inRatio = max(ratio - 0.5f, 0f) * 2 + val detailContent = binding.detailToolbarContent + detailContent.alpha = inRatio + detailContent.translationY = spacingSmall * (1 - inRatio) + } + + abstract fun onOpenParentMenu() +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index a9695df55..0e1de5d35 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -625,7 +625,6 @@ constructor( for (entry in grouping.entries) { val header = BasicHeader(entry.key.headerTitleRes) - list.add(Divider(header)) list.add(header) list.addAll(ARTIST_ALBUM_SORT.albums(entry.value)) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt deleted file mode 100644 index e85c892e7..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * ArtistDetailHeaderAdapter.kt is part of Auxio. - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.oxycblt.auxio.detail.header - -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.inflater -import org.oxycblt.auxio.util.logD - -/** - * A [DetailHeaderAdapter] that shows [Artist] information. - * - * @param listener [DetailHeaderAdapter.Listener] to bind interactions to. - * @author Alexander Capehart (OxygenCobalt) - */ -class ArtistDetailHeaderAdapter(private val listener: Listener) : - DetailHeaderAdapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - ArtistDetailHeaderViewHolder.from(parent) - - override fun onBindHeader(holder: ArtistDetailHeaderViewHolder, parent: Artist) = - holder.bind(parent, listener) -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [from] to - * create an instance. - * - * @author Alexander Capehart (OxygenCobalt) - */ -class ArtistDetailHeaderViewHolder -private constructor(private val binding: ItemDetailHeaderBinding) : - RecyclerView.ViewHolder(binding.root) { - - /** - * Bind new data to this instance. - * - * @param artist The new [Artist] to bind. - * @param listener A [DetailHeaderAdapter.Listener] to bind interactions to. - */ - fun bind(artist: Artist, listener: DetailHeaderAdapter.Listener) { - binding.detailCover.bind(artist) - binding.detailType.text = binding.context.getString(R.string.lbl_artist) - binding.detailName.text = artist.name.resolve(binding.context) - - // Song and album counts map to the info - binding.detailInfo.text = - binding.context.getString( - R.string.fmt_two, - if (artist.explicitAlbums.isNotEmpty()) { - binding.context.getPlural(R.plurals.fmt_album_count, artist.explicitAlbums.size) - } else { - binding.context.getString(R.string.def_album_count) - }, - if (artist.songs.isNotEmpty()) { - binding.context.getPlural(R.plurals.fmt_song_count, artist.songs.size) - } else { - binding.context.getString(R.string.def_song_count) - }) - - if (artist.songs.isNotEmpty()) { - // Information about the artist's genre(s) map to the sub-head text - binding.detailSubhead.apply { - isVisible = true - text = artist.genres.resolveNames(context) - } - - // In the case that this header used to he configured to have no songs, - // we want to reset the visibility of all information that was hidden. - binding.detailPlayButton.isVisible = true - binding.detailShuffleButton.isVisible = true - } else { - // The artist does not have any songs, so hide functionality that makes no sense. - // ex. Play and Shuffle, Song Counts, and Genre Information. - // Artists are always guaranteed to have albums however, so continue to show those. - logD("Artist is empty, disabling genres and playback") - binding.detailSubhead.isVisible = false - binding.detailPlayButton.isEnabled = false - binding.detailShuffleButton.isEnabled = false - } - - binding.detailPlayButton.setOnClickListener { listener.onPlay() } - binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } - } - - companion object { - /** - * Create a new instance. - * - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - ArtistDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater)) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index d0776e4a6..3972718e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -285,8 +285,9 @@ private class MediaStoreExtractorImpl( // know when it corresponds to the folder and not, say, Low Roar's breakout album "0"? // Also, on some devices it's literally just null. To maintain behavior sanity just // replicate the majority behavior described prior. - rawSong.albumName = cursor.getStringOrNull(albumIndex) - ?: requireNotNull(rawSong.path?.name) { "Invalid raw: No path" } + rawSong.albumName = + cursor.getStringOrNull(albumIndex) + ?: requireNotNull(rawSong.path?.name) { "Invalid raw: No path" } // Android does not make a non-existent artist tag null, it instead fills it in // as , which makes absolutely no sense given how other columns default // to null if they are not present. If this column is such, null it so that From 6ea72336266777823d94bd7db27ae4270dc47944 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 13:00:10 -0600 Subject: [PATCH 050/550] detail: make genre view use collapsing toolbar --- .../oxycblt/auxio/detail/DetailViewModel.kt | 1 - .../auxio/detail/GenreDetailFragment.kt | 96 +++++++------------ .../detail/header/GenreDetailHeaderAdapter.kt | 88 ----------------- 3 files changed, 35 insertions(+), 150 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 0e1de5d35..452b050cd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -653,7 +653,6 @@ constructor( val list = mutableListOf() // Genre is guaranteed to always have artists and songs. val artistHeader = BasicHeader(R.string.lbl_artists) - list.add(Divider(artistHeader)) list.add(artistHeader) list.addAll(GENRE_ARTIST_SORT.artists(genre.artists)) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 07e15faa5..8938328e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -19,22 +19,15 @@ package org.oxycblt.auxio.detail import android.os.Bundle -import android.view.LayoutInflater +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.header.DetailHeaderAdapter -import org.oxycblt.auxio.detail.header.GenreDetailHeaderAdapter -import org.oxycblt.auxio.detail.list.DetailListAdapter +import org.oxycblt.auxio.databinding.FragmentDetail2Binding import org.oxycblt.auxio.detail.list.GenreDetailListAdapter -import org.oxycblt.auxio.list.Divider -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel @@ -51,10 +44,9 @@ import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe -import org.oxycblt.auxio.util.overrideOnOverflowMenuClick -import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull @@ -64,18 +56,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class GenreDetailFragment : - ListFragment(), - DetailHeaderAdapter.Listener, - DetailListAdapter.Listener { +class GenreDetailFragment : DetailFragment() { private val detailModel: DetailViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() + // Information about what genre to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an genre. private val args: GenreDetailFragmentArgs by navArgs() - private val genreHeaderAdapter = GenreDetailHeaderAdapter(this) private val genreListAdapter = GenreDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { @@ -86,43 +75,15 @@ class GenreDetailFragment : reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) } - override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) + override fun getDetailListAdapter() = genreListAdapter - override fun getSelectionToolbar(binding: FragmentDetailBinding) = - binding.detailSelectionToolbar - - override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) - // --- UI SETUP --- - binding.detailNormalToolbar.apply { - setNavigationOnClickListener { findNavController().navigateUp() } - setOnMenuItemClickListener(this@GenreDetailFragment) - overrideOnOverflowMenuClick { - listModel.openMenu( - R.menu.detail_parent, unlikelyToBeNull(detailModel.currentGenre.value)) - } - } - - binding.detailRecycler.apply { - adapter = ConcatAdapter(genreHeaderAdapter, genreListAdapter) - (layoutManager as GridLayoutManager).setFullWidthLookup { - if (it != 0) { - val item = - detailModel.genreSongList.value.getOrElse(it - 1) { - return@setFullWidthLookup false - } - item is Divider || item is Header - } else { - true - } - } - } - // --- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. detailModel.setGenre(args.genreUid) - collectImmediately(detailModel.currentGenre, ::updatePlaylist) + collectImmediately(detailModel.currentGenre, ::updateGenre) collectImmediately(detailModel.genreSongList, ::updateList) collect(detailModel.toShow.flow, ::handleShow) collect(listModel.menu.flow, ::handleMenu) @@ -134,10 +95,8 @@ class GenreDetailFragment : collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) } - override fun onDestroyBinding(binding: FragmentDetailBinding) { + override fun onDestroyBinding(binding: FragmentDetail2Binding) { super.onDestroyBinding(binding) - binding.detailNormalToolbar.setOnMenuItemClickListener(null) - binding.detailRecycler.adapter = null // Avoid possible race conditions that could cause a bad replace instruction to be consumed // during list initialization and crash the app. Could happen if the user is fast enough. detailModel.genreSongInstructions.consume() @@ -151,6 +110,10 @@ class GenreDetailFragment : } } + override fun onOpenParentMenu() { + listModel.openMenu(R.menu.detail_parent, unlikelyToBeNull(detailModel.currentGenre.value)) + } + override fun onOpenMenu(item: Music) { when (item) { is Artist -> listModel.openMenu(R.menu.parent, item) @@ -159,26 +122,37 @@ class GenreDetailFragment : } } - override fun onPlay() { - playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value)) - } - - override fun onShuffle() { - playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value)) - } - override fun onOpenSortMenu() { findNavController().navigateSafe(GenreDetailFragmentDirections.sort()) } - private fun updatePlaylist(genre: Genre?) { + private fun updateGenre(genre: Genre?) { if (genre == null) { logD("No genre to show, navigating away") findNavController().navigateUp() return } - requireBinding().detailNormalToolbar.title = genre.name.resolve(requireContext()) - genreHeaderAdapter.setParent(genre) + val binding = requireBinding() + val context = requireContext() + val name = genre.name.resolve(context) + binding.detailToolbarTitle.text = name + binding.detailCover.bind(genre) + binding.detailType.text = context.getString(R.string.lbl_genre) + binding.detailName.text = genre.name.resolve(context) + // Nothing about a genre is applicable to the sub-head text. + binding.detailSubhead.isVisible = false + // The song and artist count of the genre maps to the info text. + binding.detailInfo.text = + context.getString( + R.string.fmt_two, + context.getPlural(R.plurals.fmt_artist_count, genre.artists.size), + context.getPlural(R.plurals.fmt_song_count, genre.songs.size)) + binding.detailPlayButton.setOnClickListener { + playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value)) + } + binding.detailShuffleButton.setOnClickListener { + playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value)) + } } private fun updateList(list: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt deleted file mode 100644 index 42e2b4f09..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * GenreDetailHeaderAdapter.kt is part of Auxio. - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.oxycblt.auxio.detail.header - -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.inflater - -/** - * A [DetailHeaderAdapter] that shows [Genre] information. - * - * @param listener [DetailHeaderAdapter.Listener] to bind interactions to. - * @author Alexander Capehart (OxygenCobalt) - */ -class GenreDetailHeaderAdapter(private val listener: Listener) : - DetailHeaderAdapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - GenreDetailHeaderViewHolder.from(parent) - - override fun onBindHeader(holder: GenreDetailHeaderViewHolder, parent: Genre) = - holder.bind(parent, listener) -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Genre] header in the detail view. Use [from] to - * create an instance. - * - * @author Alexander Capehart (OxygenCobalt) - */ -class GenreDetailHeaderViewHolder -private constructor(private val binding: ItemDetailHeaderBinding) : - RecyclerView.ViewHolder(binding.root) { - /** - * Bind new data to this instance. - * - * @param genre The new [Genre] to bind. - * @param listener A [DetailHeaderAdapter.Listener] to bind interactions to. - */ - fun bind(genre: Genre, listener: DetailHeaderAdapter.Listener) { - binding.detailCover.bind(genre) - binding.detailType.text = binding.context.getString(R.string.lbl_genre) - binding.detailName.text = genre.name.resolve(binding.context) - // Nothing about a genre is applicable to the sub-head text. - binding.detailSubhead.isVisible = false - // The song and artist count of the genre maps to the info text. - binding.detailInfo.text = - binding.context.getString( - R.string.fmt_two, - binding.context.getPlural(R.plurals.fmt_artist_count, genre.artists.size), - binding.context.getPlural(R.plurals.fmt_song_count, genre.songs.size)) - binding.detailPlayButton.setOnClickListener { listener.onPlay() } - binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } - } - - companion object { - /** - * Create a new instance. - * - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - GenreDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater)) - } -} From d909f2d98e00e397f88a5555ef96e1bc3178ed1b Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 13:13:56 -0600 Subject: [PATCH 051/550] detail: make playlist view use collapsing toolbar --- .../oxycblt/auxio/detail/DetailViewModel.kt | 1 - .../auxio/detail/PlaylistDetailFragment.kt | 125 +++++++++------- .../detail/header/DetailHeaderAdapter.kt | 84 ----------- .../header/PlaylistDetailHeaderAdapter.kt | 141 ------------------ 4 files changed, 68 insertions(+), 283 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 452b050cd..090c838a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -683,7 +683,6 @@ constructor( val songs = editedPlaylist.value ?: playlist.songs if (songs.isNotEmpty()) { val header = EditHeader(R.string.lbl_songs) - list.add(Divider(header)) list.add(header) list.addAll(songs) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index dce61f05c..6693e3bfd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -19,27 +19,21 @@ package org.oxycblt.auxio.detail import android.os.Bundle -import android.view.LayoutInflater import android.view.MenuItem import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.header.DetailHeaderAdapter -import org.oxycblt.auxio.detail.header.PlaylistDetailHeaderAdapter +import org.oxycblt.auxio.databinding.FragmentDetail2Binding import org.oxycblt.auxio.detail.list.PlaylistDetailListAdapter import org.oxycblt.auxio.detail.list.PlaylistDragCallback -import org.oxycblt.auxio.list.Divider -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel @@ -54,14 +48,15 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.external.M3U import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.ui.DialogAwareNavigationListener import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe -import org.oxycblt.auxio.util.overrideOnOverflowMenuClick -import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull @@ -72,9 +67,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ @AndroidEntryPoint class PlaylistDetailFragment : - ListFragment(), - DetailHeaderAdapter.Listener, - PlaylistDetailListAdapter.Listener { + DetailFragment(), PlaylistDetailListAdapter.Listener { private val detailModel: DetailViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() @@ -82,7 +75,6 @@ class PlaylistDetailFragment : // Information about what playlist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an playlist. private val args: PlaylistDetailFragmentArgs by navArgs() - private val playlistHeaderAdapter = PlaylistDetailHeaderAdapter(this) private val playlistListAdapter = PlaylistDetailListAdapter(this) private var touchHelper: ItemTouchHelper? = null private var editNavigationListener: DialogAwareNavigationListener? = null @@ -97,12 +89,9 @@ class PlaylistDetailFragment : reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) } - override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) + override fun getDetailListAdapter() = playlistListAdapter - override fun getSelectionToolbar(binding: FragmentDetailBinding) = - binding.detailSelectionToolbar - - override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) editNavigationListener = DialogAwareNavigationListener(detailModel::dropPlaylistEdit) @@ -119,14 +108,6 @@ class PlaylistDetailFragment : } // --- UI SETUP --- - binding.detailNormalToolbar.apply { - setNavigationOnClickListener { findNavController().navigateUp() } - setOnMenuItemClickListener(this@PlaylistDetailFragment) - overrideOnOverflowMenuClick { - listModel.openMenu( - R.menu.detail_playlist, unlikelyToBeNull(detailModel.currentPlaylist.value)) - } - } binding.detailEditToolbar.apply { setNavigationOnClickListener { detailModel.dropPlaylistEdit() } @@ -134,28 +115,18 @@ class PlaylistDetailFragment : } binding.detailRecycler.apply { - adapter = ConcatAdapter(playlistHeaderAdapter, playlistListAdapter) + adapter = playlistListAdapter touchHelper = ItemTouchHelper(PlaylistDragCallback(detailModel)).also { it.attachToRecyclerView(this) } - (layoutManager as GridLayoutManager).setFullWidthLookup { - if (it != 0) { - val item = - detailModel.playlistSongList.value.getOrElse(it - 1) { - return@setFullWidthLookup false - } - item is Divider || item is Header - } else { - true - } - } } // --- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. detailModel.setPlaylist(args.playlistUid) - collectImmediately(detailModel.currentPlaylist, ::updatePlaylist) + collectImmediately( + detailModel.currentPlaylist, detailModel.editedPlaylist, ::updatePlaylist) collectImmediately(detailModel.playlistSongList, ::updateList) collectImmediately(detailModel.editedPlaylist, ::updateEditedList) collect(detailModel.toShow.flow, ::handleShow) @@ -195,7 +166,7 @@ class PlaylistDetailFragment : .release(findNavController()) } - override fun onDestroyBinding(binding: FragmentDetailBinding) { + override fun onDestroyBinding(binding: FragmentDetail2Binding) { super.onDestroyBinding(binding) binding.detailNormalToolbar.setOnMenuItemClickListener(null) touchHelper = null @@ -210,41 +181,82 @@ class PlaylistDetailFragment : playbackModel.play(item, detailModel.playInPlaylistWith) } + override fun onStartEdit() { + detailModel.startPlaylistEdit() + } + override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder) } + override fun onOpenParentMenu() { + listModel.openMenu(R.menu.playlist, unlikelyToBeNull(detailModel.currentPlaylist.value)) + } + override fun onOpenMenu(item: Song) { listModel.openMenu(R.menu.playlist_song, item, detailModel.playInPlaylistWith) } - override fun onPlay() { - playbackModel.play(unlikelyToBeNull(detailModel.currentPlaylist.value)) - } - - override fun onShuffle() { - playbackModel.shuffle(unlikelyToBeNull(detailModel.currentPlaylist.value)) - } - - override fun onStartEdit() { - detailModel.startPlaylistEdit() - } - override fun onOpenSortMenu() { findNavController().navigateSafe(PlaylistDetailFragmentDirections.sort()) } - private fun updatePlaylist(playlist: Playlist?) { + private fun updatePlaylist(playlist: Playlist?, editedPlaylist: List?) { if (playlist == null) { // Playlist we were showing no longer exists. findNavController().navigateUp() return } val binding = requireBinding() - binding.detailNormalToolbar.title = playlist.name.resolve(requireContext()) + binding.detailToolbarTitle.text = playlist.name.resolve(requireContext()) binding.detailEditToolbar.title = getString(R.string.fmt_editing, playlist.name.resolve(requireContext())) - playlistHeaderAdapter.setParent(playlist) + + if (editedPlaylist != null) { + logD("Binding edited playlist image") + binding.detailCover.bind( + editedPlaylist, + binding.context.getString(R.string.desc_playlist_image, playlist.name), + R.drawable.ic_playlist_24) + } else { + binding.detailCover.bind(playlist) + } + + binding.detailType.text = binding.context.getString(R.string.lbl_playlist) + binding.detailName.text = playlist.name.resolve(binding.context) + // Nothing about a playlist is applicable to the sub-head text. + binding.detailSubhead.isVisible = false + + val songs = editedPlaylist ?: playlist.songs + val durationMs = editedPlaylist?.sumOf { it.durationMs } ?: playlist.durationMs + // The song count of the playlist maps to the info text. + binding.detailInfo.text = + if (songs.isNotEmpty()) { + binding.context.getString( + R.string.fmt_two, + binding.context.getPlural(R.plurals.fmt_song_count, songs.size), + durationMs.formatDurationMs(true)) + } else { + binding.context.getString(R.string.def_song_count) + } + + val playable = playlist.songs.isNotEmpty() && editedPlaylist == null + if (!playable) { + logD("Playlist is being edited or is empty, disabling playback options") + } + + binding.detailPlayButton.apply { + isEnabled = playable + setOnClickListener { + playbackModel.play(unlikelyToBeNull(detailModel.currentPlaylist.value)) + } + } + binding.detailShuffleButton.apply { + isEnabled = playable + setOnClickListener { + playbackModel.shuffle(unlikelyToBeNull(detailModel.currentPlaylist.value)) + } + } } private fun updateList(list: List) { @@ -253,7 +265,6 @@ class PlaylistDetailFragment : private fun updateEditedList(editedPlaylist: List?) { playlistListAdapter.setEditing(editedPlaylist != null) - playlistHeaderAdapter.setEditedPlaylist(editedPlaylist) listModel.dropSelection() if (editedPlaylist != null) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt deleted file mode 100644 index 4afabb6c3..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * DetailHeaderAdapter.kt is part of Auxio. - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.oxycblt.auxio.detail.header - -import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.util.logD - -/** - * A [RecyclerView.Adapter] that implements shared behavior between each parent header view. - * - * @author Alexander Capehart (OxygenCobalt) - */ -abstract class DetailHeaderAdapter : - RecyclerView.Adapter() { - private var currentParent: T? = null - - final override fun getItemCount() = 1 - - final override fun onBindViewHolder(holder: VH, position: Int) = - onBindHeader(holder, requireNotNull(currentParent)) - - /** - * Bind the created header [RecyclerView.ViewHolder] with the current [parent]. - * - * @param holder The [RecyclerView.ViewHolder] to bind. - * @param parent The current [MusicParent] to bind. - */ - abstract fun onBindHeader(holder: VH, parent: T) - - /** - * Update the [MusicParent] shown in the header. - * - * @param parent The new [MusicParent] to show. - */ - fun setParent(parent: T) { - logD("Updating parent [old: $currentParent new: $parent]") - currentParent = parent - rebindParent() - } - - /** - * Forces the parent [RecyclerView.ViewHolder] to rebind as soon as possible, with no animation. - */ - protected fun rebindParent() { - logD("Rebinding parent") - notifyItemChanged(0, PAYLOAD_UPDATE_HEADER) - } - - /** A listener for [DetailHeaderAdapter] implementations. */ - interface Listener { - /** - * Called when the play button in a detail header is pressed, requesting that the current - * item should be played. - */ - fun onPlay() - - /** - * Called when the shuffle button in a detail header is pressed, requesting that the current - * item should be shuffled - */ - fun onShuffle() - } - - private companion object { - val PAYLOAD_UPDATE_HEADER = Any() - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt deleted file mode 100644 index 67f3b82ec..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/PlaylistDetailHeaderAdapter.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * PlaylistDetailHeaderAdapter.kt is part of Auxio. - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.oxycblt.auxio.detail.header - -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding -import org.oxycblt.auxio.music.Playlist -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.playback.formatDurationMs -import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.inflater -import org.oxycblt.auxio.util.logD - -/** - * A [DetailHeaderAdapter] that shows [Playlist] information. - * - * @param listener [DetailHeaderAdapter.Listener] to bind interactions to. - * @author Alexander Capehart (OxygenCobalt) - */ -class PlaylistDetailHeaderAdapter(private val listener: Listener) : - DetailHeaderAdapter() { - private var editedPlaylist: List? = null - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - PlaylistDetailHeaderViewHolder.from(parent) - - override fun onBindHeader(holder: PlaylistDetailHeaderViewHolder, parent: Playlist) = - holder.bind(parent, editedPlaylist, listener) - - /** - * Indicate to this adapter that editing is ongoing with the current state of the editing - * process. This will make the header immediately update to reflect information about the edited - * playlist. - */ - fun setEditedPlaylist(songs: List?) { - if (editedPlaylist == songs) { - // Nothing to do. - return - } - logD("Updating editing state [old: ${editedPlaylist?.size} new: ${songs?.size}") - editedPlaylist = songs - rebindParent() - } -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Playlist] header in the detail view. Use [from] to - * create an instance. - * - * @author Alexander Capehart (OxygenCobalt) - */ -class PlaylistDetailHeaderViewHolder -private constructor(private val binding: ItemDetailHeaderBinding) : - RecyclerView.ViewHolder(binding.root) { - /** - * Bind new data to this instance. - * - * @param playlist The new [Playlist] to bind. - * @param editedPlaylist The current edited state of the playlist, if it exists. - * @param listener A [DetailHeaderAdapter.Listener] to bind interactions to. - */ - fun bind( - playlist: Playlist, - editedPlaylist: List?, - listener: DetailHeaderAdapter.Listener - ) { - if (editedPlaylist != null) { - logD("Binding edited playlist image") - binding.detailCover.bind( - editedPlaylist, - binding.context.getString(R.string.desc_playlist_image, playlist.name), - R.drawable.ic_playlist_24) - } else { - binding.detailCover.bind(playlist) - } - - binding.detailType.text = binding.context.getString(R.string.lbl_playlist) - binding.detailName.text = playlist.name.resolve(binding.context) - // Nothing about a playlist is applicable to the sub-head text. - binding.detailSubhead.isVisible = false - - val songs = editedPlaylist ?: playlist.songs - val durationMs = editedPlaylist?.sumOf { it.durationMs } ?: playlist.durationMs - // The song count of the playlist maps to the info text. - binding.detailInfo.text = - if (songs.isNotEmpty()) { - binding.context.getString( - R.string.fmt_two, - binding.context.getPlural(R.plurals.fmt_song_count, songs.size), - durationMs.formatDurationMs(true)) - } else { - binding.context.getString(R.string.def_song_count) - } - - val playable = playlist.songs.isNotEmpty() && editedPlaylist == null - if (!playable) { - logD("Playlist is being edited or is empty, disabling playback options") - } - - binding.detailPlayButton.apply { - isEnabled = playable - setOnClickListener { listener.onPlay() } - } - binding.detailShuffleButton.apply { - isEnabled = playable - setOnClickListener { listener.onShuffle() } - } - } - - companion object { - /** - * Create a new instance. - * - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - PlaylistDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater)) - } -} From 2f21b12beb562ab430cfcbdf700f3c1901e8d8d0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 13:32:06 -0600 Subject: [PATCH 052/550] ui: make multitoolbar transition m3 --- .../java/org/oxycblt/auxio/ui/MultiToolbar.kt | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt index 137e9abe9..cefabee45 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt @@ -27,8 +27,11 @@ import androidx.annotation.IdRes import androidx.appcompat.widget.Toolbar import androidx.core.view.children import androidx.core.view.isInvisible -import org.oxycblt.auxio.R -import org.oxycblt.auxio.util.getInteger +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import com.google.android.material.R as MR +import com.google.android.material.motion.MotionUtils +import kotlin.math.max +import kotlin.math.min import org.oxycblt.auxio.util.logD class MultiToolbar @@ -37,6 +40,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr FrameLayout(context, attrs, defStyleAttr) { private var fadeThroughAnimator: ValueAnimator? = null private var currentlyVisible = 0 + private val matInterpolator = + MotionUtils.resolveThemeInterpolator( + context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()) + private val matDuration = + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium3, 300).toLong() override fun onFinishInflate() { super.onFinishInflate() @@ -61,16 +69,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // Set up the target transitions for both the inner and selection toolbars. val targetFromAlpha = 0f val targetToAlpha = 1f - val targetDuration = - // Since this view starts with the lowest toolbar index, - if (from < to) { - logD("Moving higher, use an entrance animation") - context.getInteger(R.integer.anim_fade_enter_duration).toLong() - } else { - logD("Moving lower, use an exit animation") - context.getInteger(R.integer.anim_fade_exit_duration).toLong() - } - val fromView = getChildAt(from) as Toolbar val toView = getChildAt(to) as Toolbar @@ -91,7 +89,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr fadeThroughAnimator?.cancel() fadeThroughAnimator = ValueAnimator.ofFloat(fromView.alpha, targetFromAlpha).apply { - duration = targetDuration + duration = matDuration + interpolator = matInterpolator addUpdateListener { setToolbarsAlpha(fromView, toView, it.animatedValue as Float) } start() } @@ -99,15 +98,21 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr return true } - private fun setToolbarsAlpha(from: Toolbar, to: Toolbar, innerAlpha: Float) { - from.apply { - alpha = innerAlpha - isInvisible = innerAlpha == 0f + private fun setToolbarsAlpha(from: Toolbar, to: Toolbar, ratio: Float) { + val outRatio = min(ratio * 2, 1f) + to.apply { + scaleX = 1 - 0.05f * outRatio + scaleY = 1 - 0.05f * outRatio + alpha = 1 - outRatio + isInvisible = alpha == 0f } - to.apply { - alpha = 1 - innerAlpha - isInvisible = innerAlpha == 1f + val inRatio = max(ratio - 0.5f, 0f) * 2 + from.apply { + scaleX = 1 - 0.05f * (1 - inRatio) + scaleY = 1 - 0.05f * (1 - inRatio) + alpha = inRatio + isInvisible = alpha == 0f } } } From 80dac7d9e9944d65bf3e4f25ce2c6c516f4426eb Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 13:48:58 -0600 Subject: [PATCH 053/550] detail: eliminate dead code --- .../auxio/detail/AlbumDetailFragment.kt | 22 +- .../auxio/detail/ArtistDetailFragment.kt | 33 +-- .../oxycblt/auxio/detail/DetailFragment.kt | 16 +- .../auxio/detail/GenreDetailFragment.kt | 24 +-- .../auxio/detail/PlaylistDetailFragment.kt | 34 +-- .../fragment_detail.xml} | 0 .../res/layout-h600dp/item_detail_header.xml | 88 -------- .../fragment_detail.xml} | 0 .../res/layout-land/item_detail_header.xml | 103 --------- .../res/layout-sw600dp/fragment_detail.xml | 203 ++++++++++++++++++ .../res/layout-sw600dp/item_detail_header.xml | 105 --------- .../res/layout-sw840dp/item_detail_header.xml | 100 --------- app/src/main/res/layout/fragment_detail.xml | 181 +++++++++++++--- .../main/res/layout/item_detail_header.xml | 90 -------- 14 files changed, 380 insertions(+), 619 deletions(-) rename app/src/main/res/{layout/fragment_detail2.xml => layout-h480dp/fragment_detail.xml} (100%) delete mode 100644 app/src/main/res/layout-h600dp/item_detail_header.xml rename app/src/main/res/{layout-w600dp/fragment_detail2.xml => layout-land/fragment_detail.xml} (100%) delete mode 100644 app/src/main/res/layout-land/item_detail_header.xml create mode 100644 app/src/main/res/layout-sw600dp/fragment_detail.xml delete mode 100644 app/src/main/res/layout-sw600dp/item_detail_header.xml delete mode 100644 app/src/main/res/layout-sw840dp/item_detail_header.xml delete mode 100644 app/src/main/res/layout/item_detail_header.xml diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index c80f1cda6..d14d75f8f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -19,29 +19,24 @@ package org.oxycblt.auxio.detail import android.os.Bundle -import android.view.LayoutInflater -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearSmoothScroller import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetail2Binding +import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.PlaybackDecision -import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately @@ -58,25 +53,14 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ @AndroidEntryPoint class AlbumDetailFragment : DetailFragment() { - private val detailModel: DetailViewModel by activityViewModels() - override val listModel: ListViewModel by activityViewModels() - override val musicModel: MusicViewModel by activityViewModels() - override val playbackModel: PlaybackViewModel by activityViewModels() - // Information about what album to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an album. private val args: AlbumDetailFragmentArgs by navArgs() private val albumListAdapter = AlbumDetailListAdapter(this) - override fun onCreateBinding(inflater: LayoutInflater) = - FragmentDetail2Binding.inflate(inflater) - override fun getDetailListAdapter() = albumListAdapter - override fun getSelectionToolbar(binding: FragmentDetail2Binding) = - binding.detailSelectionToolbar - - override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) // -- VIEWMODEL SETUP --- @@ -94,7 +78,7 @@ class AlbumDetailFragment : DetailFragment() { collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) } - override fun onDestroyBinding(binding: FragmentDetail2Binding) { + override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) // Avoid possible race conditions that could cause a bad replace instruction to be consumed // during list initialization and crash the app. Could happen if the user is fast enough. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index af65b9a27..4ea8ffb73 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -19,31 +19,25 @@ package org.oxycblt.auxio.detail import android.os.Bundle -import android.view.LayoutInflater import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetail2Binding +import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.list.ArtistDetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.PlaybackDecision -import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.context @@ -60,35 +54,14 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ @AndroidEntryPoint class ArtistDetailFragment : DetailFragment() { - private val detailModel: DetailViewModel by activityViewModels() - override val listModel: ListViewModel by activityViewModels() - override val musicModel: MusicViewModel by activityViewModels() - override val playbackModel: PlaybackViewModel by activityViewModels() - // Information about what artist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an artist. private val args: ArtistDetailFragmentArgs by navArgs() private val artistListAdapter = ArtistDetailListAdapter(this) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - // Detail transitions are always on the X axis. Shared element transitions are more - // semantically correct, but are also too buggy to be sensible. - enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - } - - override fun onCreateBinding(inflater: LayoutInflater) = - FragmentDetail2Binding.inflate(inflater) - - override fun getSelectionToolbar(binding: FragmentDetail2Binding) = - binding.detailSelectionToolbar - override fun getDetailListAdapter() = artistListAdapter - override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) // --- VIEWMODEL SETUP --- @@ -106,7 +79,7 @@ class ArtistDetailFragment : DetailFragment() { collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) } - override fun onDestroyBinding(binding: FragmentDetail2Binding) { + override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) // Avoid possible race conditions that could cause a bad replace instruction to be consumed // during list initialization and crash the app. Could happen if the user is fast enough. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 334142172..5213f08b0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -22,7 +22,6 @@ import android.os.Bundle import android.view.LayoutInflater import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController -import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.transition.MaterialSharedAxis @@ -30,7 +29,7 @@ import kotlin.math.abs import kotlin.math.max import kotlin.math.min import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetail2Binding +import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.list.DetailListAdapter import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Header @@ -45,10 +44,10 @@ import org.oxycblt.auxio.util.overrideOnOverflowMenuClick import org.oxycblt.auxio.util.setFullWidthLookup abstract class DetailFragment

: - ListFragment(), + ListFragment(), DetailListAdapter.Listener, AppBarLayout.OnOffsetChangedListener { - private val detailModel: DetailViewModel by activityViewModels() + protected val detailModel: DetailViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -65,15 +64,14 @@ abstract class DetailFragment

: reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) } - override fun onCreateBinding(inflater: LayoutInflater) = - FragmentDetail2Binding.inflate(inflater) + override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) abstract fun getDetailListAdapter(): DetailListAdapter - override fun getSelectionToolbar(binding: FragmentDetail2Binding) = + override fun getSelectionToolbar(binding: FragmentDetailBinding) = binding.detailSelectionToolbar - override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) // --- UI SETUP --- @@ -103,7 +101,7 @@ abstract class DetailFragment

: spacingSmall = requireContext().getDimenPixels(R.dimen.spacing_small) } - override fun onDestroyBinding(binding: FragmentDetail2Binding) { + override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) binding.detailAppbar.removeOnOffsetChangedListener(this) binding.detailNormalToolbar.setOnMenuItemClickListener(null) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 8938328e9..ec65f97bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -20,28 +20,23 @@ package org.oxycblt.auxio.detail import android.os.Bundle import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetail2Binding +import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.list.GenreDetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackDecision -import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getPlural @@ -57,27 +52,14 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ @AndroidEntryPoint class GenreDetailFragment : DetailFragment() { - private val detailModel: DetailViewModel by activityViewModels() - override val listModel: ListViewModel by activityViewModels() - override val musicModel: MusicViewModel by activityViewModels() - override val playbackModel: PlaybackViewModel by activityViewModels() - // Information about what genre to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an genre. private val args: GenreDetailFragmentArgs by navArgs() private val genreListAdapter = GenreDetailListAdapter(this) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - } - override fun getDetailListAdapter() = genreListAdapter - override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) // --- VIEWMODEL SETUP --- @@ -95,7 +77,7 @@ class GenreDetailFragment : DetailFragment() { collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) } - override fun onDestroyBinding(binding: FragmentDetail2Binding) { + override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) // Avoid possible race conditions that could cause a bad replace instruction to be consumed // during list initialization and crash the app. Could happen if the user is fast enough. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 6693e3bfd..b5f1fb8e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -23,31 +23,26 @@ import android.view.MenuItem import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.FragmentDetail2Binding +import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.list.PlaylistDetailListAdapter import org.oxycblt.auxio.detail.list.PlaylistDragCallback import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.external.M3U import org.oxycblt.auxio.playback.PlaybackDecision -import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.ui.DialogAwareNavigationListener import org.oxycblt.auxio.util.collect @@ -68,10 +63,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull @AndroidEntryPoint class PlaylistDetailFragment : DetailFragment(), PlaylistDetailListAdapter.Listener { - private val detailModel: DetailViewModel by activityViewModels() - override val listModel: ListViewModel by activityViewModels() - override val musicModel: MusicViewModel by activityViewModels() - override val playbackModel: PlaybackViewModel by activityViewModels() // Information about what playlist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an playlist. private val args: PlaylistDetailFragmentArgs by navArgs() @@ -81,17 +72,9 @@ class PlaylistDetailFragment : private var getContentLauncher: ActivityResultLauncher? = null private var pendingImportTarget: Playlist? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - } - override fun getDetailListAdapter() = playlistListAdapter - override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) editNavigationListener = DialogAwareNavigationListener(detailModel::dropPlaylistEdit) @@ -114,13 +97,10 @@ class PlaylistDetailFragment : setOnMenuItemClickListener(this@PlaylistDetailFragment) } - binding.detailRecycler.apply { - adapter = playlistListAdapter - touchHelper = - ItemTouchHelper(PlaylistDragCallback(detailModel)).also { - it.attachToRecyclerView(this) - } - } + touchHelper = + ItemTouchHelper(PlaylistDragCallback(detailModel)).also { + it.attachToRecyclerView(binding.detailRecycler) + } // --- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. @@ -166,7 +146,7 @@ class PlaylistDetailFragment : .release(findNavController()) } - override fun onDestroyBinding(binding: FragmentDetail2Binding) { + override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) binding.detailNormalToolbar.setOnMenuItemClickListener(null) touchHelper = null diff --git a/app/src/main/res/layout/fragment_detail2.xml b/app/src/main/res/layout-h480dp/fragment_detail.xml similarity index 100% rename from app/src/main/res/layout/fragment_detail2.xml rename to app/src/main/res/layout-h480dp/fragment_detail.xml diff --git a/app/src/main/res/layout-h600dp/item_detail_header.xml b/app/src/main/res/layout-h600dp/item_detail_header.xml deleted file mode 100644 index cdaf0e484..000000000 --- a/app/src/main/res/layout-h600dp/item_detail_header.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-w600dp/fragment_detail2.xml b/app/src/main/res/layout-land/fragment_detail.xml similarity index 100% rename from app/src/main/res/layout-w600dp/fragment_detail2.xml rename to app/src/main/res/layout-land/fragment_detail.xml diff --git a/app/src/main/res/layout-land/item_detail_header.xml b/app/src/main/res/layout-land/item_detail_header.xml deleted file mode 100644 index 432f6dc3e..000000000 --- a/app/src/main/res/layout-land/item_detail_header.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-sw600dp/fragment_detail.xml b/app/src/main/res/layout-sw600dp/fragment_detail.xml new file mode 100644 index 000000000..53bd566bd --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_detail.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/item_detail_header.xml b/app/src/main/res/layout-sw600dp/item_detail_header.xml deleted file mode 100644 index 4b354bc58..000000000 --- a/app/src/main/res/layout-sw600dp/item_detail_header.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-sw840dp/item_detail_header.xml b/app/src/main/res/layout-sw840dp/item_detail_header.xml deleted file mode 100644 index d66af44bb..000000000 --- a/app/src/main/res/layout-sw840dp/item_detail_header.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index 8d46398f8..b94cec508 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -2,52 +2,179 @@ - - + android:layout_height="match_parent" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" + app:titleEnabled="false" + app:toolbarId="@+id/detail_toolbar"> - + + + + + + + + + + + + + + + + + + android:layout_gravity="bottom" + app:layout_collapseMode="pin" /> - + app:layout_collapseMode="pin" > - + - - + + + + + + + + + + + + + + + + + + tools:listitem="@layout/item_song" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_detail_header.xml b/app/src/main/res/layout/item_detail_header.xml deleted file mode 100644 index 0ea6a4111..000000000 --- a/app/src/main/res/layout/item_detail_header.xml +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - From f3b73a51961598a2dc65ce1a029c151adc558761 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 14:52:03 -0600 Subject: [PATCH 054/550] home: extract fab system to home --- .../java/org/oxycblt/auxio/MainFragment.kt | 290 ++++++++++++++++-- .../org/oxycblt/auxio/home/HomeFragment.kt | 185 +---------- .../org/oxycblt/auxio/home/HomeViewModel.kt | 21 -- .../main/res/layout-w720dp/fragment_main.xml | 54 +++- app/src/main/res/layout/fragment_home.xml | 32 -- app/src/main/res/layout/fragment_main.xml | 64 +++- 6 files changed, 363 insertions(+), 283 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 513357ca9..f1e1764f2 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -20,6 +20,8 @@ package org.oxycblt.auxio import android.os.Bundle import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View import android.view.ViewTreeObserver import android.view.WindowInsets import androidx.activity.BackEventCompat @@ -32,10 +34,14 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import com.google.android.material.R as MR import com.google.android.material.bottomsheet.BackportBottomSheetBehavior +import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.transition.MaterialFadeThrough +import com.leinardi.android.speeddial.SpeedDialActionItem +import com.leinardi.android.speeddial.SpeedDialView import dagger.hilt.android.AndroidEntryPoint +import java.lang.reflect.Method import kotlin.math.max import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentMainBinding @@ -44,7 +50,10 @@ import org.oxycblt.auxio.detail.Show import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.Outer import org.oxycblt.auxio.list.ListViewModel +import org.oxycblt.auxio.music.IndexingState import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicType +import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.OpenPanel import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior @@ -58,6 +67,8 @@ import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimen +import org.oxycblt.auxio.util.isUnder +import org.oxycblt.auxio.util.lazyReflectedMethod import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.unlikelyToBeNull @@ -69,7 +80,10 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ @AndroidEntryPoint class MainFragment : - ViewBindingFragment(), ViewTreeObserver.OnPreDrawListener { + ViewBindingFragment(), + ViewTreeObserver.OnPreDrawListener, + SpeedDialView.OnActionSelectedListener { + private val musicModel: MusicViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels() private val listModel: ListViewModel by activityViewModels() @@ -78,11 +92,12 @@ class MainFragment : private var detailBackCallback: DetailBackPressedCallback? = null private var selectionBackCallback: SelectionBackPressedCallback? = null private var speedDialBackCallback: SpeedDialBackPressedCallback? = null - private var selectionNavigationListener: DialogAwareNavigationListener? = null + private var navigationListener: DialogAwareNavigationListener? = null private var lastInsets: WindowInsets? = null private var elevationNormal = 0f private var normalCornerSize = 0f private var maxScaleXDistance = 0f + private var sheetRising: Boolean? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -113,10 +128,9 @@ class MainFragment : DetailBackPressedCallback(detailModel).also { detailBackCallback = it } val selectionBackCallback = SelectionBackPressedCallback(listModel).also { selectionBackCallback = it } - val speedDialBackCallback = - SpeedDialBackPressedCallback(homeModel).also { speedDialBackCallback = it } + speedDialBackCallback = SpeedDialBackPressedCallback(homeModel) - selectionNavigationListener = DialogAwareNavigationListener(listModel::dropSelection) + navigationListener = DialogAwareNavigationListener(::onExploreNavigate) // --- UI SETUP --- val context = requireActivity() @@ -162,8 +176,22 @@ class MainFragment : binding.playbackSheet.elevation = 0f - binding.mainScrim.setOnClickListener { homeModel.setSpeedDialOpen(false) } - binding.sheetScrim.setOnClickListener { homeModel.setSpeedDialOpen(false) } + binding.mainScrim.setOnClickListener { binding.homeNewPlaylistFab.close() } + binding.sheetScrim.setOnClickListener { binding.homeNewPlaylistFab.close() } + binding.homeShuffleFab.setOnClickListener { playbackModel.shuffleAll() } + binding.homeNewPlaylistFab.apply { + inflate(R.menu.new_playlist_actions) + setOnActionSelectedListener(this@MainFragment) + setChangeListener(::updateSpeedDial) + } + + forceHideAllFabs() + updateSpeedDial(false) + updateFabVisibility( + binding, + homeModel.songList.value, + homeModel.isFastScrolling.value, + homeModel.currentTabType.value) // --- VIEWMODEL SETUP --- // This has to be done here instead of the playback panel to make sure that it's prioritized @@ -173,7 +201,9 @@ class MainFragment : collect(detailModel.toShow.flow, ::handleShow) collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled) collectImmediately(homeModel.showOuter.flow, ::handleShowOuter) - collectImmediately(homeModel.speedDialOpen, ::handleSpeedDialState) + collectImmediately(homeModel.currentTabType, ::updateCurrentTab) + collectImmediately(homeModel.songList, homeModel.isFastScrolling, ::updateFab) + collectImmediately(musicModel.indexingState, ::updateIndexerState) collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled) collectImmediately(playbackModel.song, ::updateSong) collectImmediately(playbackModel.openPanel.flow, ::handlePanel) @@ -184,7 +214,7 @@ class MainFragment : val binding = requireBinding() // Once we add the destination change callback, we will receive another initialization call, // so handle that by resetting the flag. - requireNotNull(selectionNavigationListener) { "NavigationListener was not available" } + requireNotNull(navigationListener) { "NavigationListener was not available" } .attach(binding.exploreNavHost.findNavController()) // Listener could still reasonably fire even if we clear the binding, attach/detach // our pre-draw listener our listener in onStart/onStop respectively. @@ -202,12 +232,23 @@ class MainFragment : addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback)) addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback)) } + + // Stock bottom sheet overlay won't work with our nested UI setup, have to replicate + // it ourselves. + requireBinding().root.rootView.apply { + findViewById(R.id.main_scrim).setOnTouchListener { _, event -> + handleSpeedDialBoundaryTouch(event) + } + findViewById(R.id.sheet_scrim).setOnTouchListener { _, event -> + handleSpeedDialBoundaryTouch(event) + } + } } override fun onStop() { super.onStop() val binding = requireBinding() - requireNotNull(selectionNavigationListener) { "NavigationListener was not available" } + requireNotNull(navigationListener) { "NavigationListener was not available" } .release(binding.exploreNavHost.findNavController()) binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this) } @@ -218,7 +259,9 @@ class MainFragment : sheetBackCallback = null detailBackCallback = null selectionBackCallback = null - selectionNavigationListener = null + navigationListener = null + binding.homeNewPlaylistFab.setChangeListener(null) + binding.homeNewPlaylistFab.setOnActionSelectedListener(null) } override fun onPreDraw(): Boolean { @@ -236,13 +279,18 @@ class MainFragment : binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f) - if (playbackRatio > 0f && homeModel.speedDialOpen.value) { - // Stupid hack to prevent you from sliding the sheet up without closing the speed - // dial. Filtering out ACTION_MOVE events will cause back gestures to close the - // speed dial, which is super finicky behavior. - homeModel.setSpeedDialOpen(false) + // Stupid hack to prevent you from sliding the sheet up without closing the speed + // dial. Filtering out ACTION_MOVE events will cause back gestures to close the + // speed dial, which is super finicky behavior. + val rising = playbackRatio > 0f + if (rising != sheetRising) { + sheetRising = rising + updateFabVisibility( + binding, + homeModel.songList.value, + homeModel.isFastScrolling.value, + homeModel.currentTabType.value) } - homeModel.setSheetObscuresFab(playbackRatio > 0f) val playbackOutRatio = 1 - min(playbackRatio * 2, 1f) val playbackInRatio = max(playbackRatio - 0.5f, 0f) * 2 @@ -330,6 +378,193 @@ class MainFragment : return true } + override fun onActionSelected(actionItem: SpeedDialActionItem): Boolean { + when (actionItem.id) { + R.id.action_new_playlist -> { + logD("Creating playlist") + musicModel.createPlaylist() + } + R.id.action_import_playlist -> { + logD("Importing playlist") + musicModel.importPlaylist() + } + else -> {} + } + // Returning false to close the speed dial results in no animation, manually close instead. + // Adapted from Material Files: https://github.com/zhanghai/MaterialFiles + requireBinding().homeNewPlaylistFab.close() + return true + } + + private fun onExploreNavigate() { + listModel.dropSelection() + updateFabVisibility( + requireBinding(), + homeModel.songList.value, + homeModel.isFastScrolling.value, + homeModel.currentTabType.value) + } + + private fun updateCurrentTab(tabType: MusicType) { + val binding = requireBinding() + updateFabVisibility( + binding, homeModel.songList.value, homeModel.isFastScrolling.value, tabType) + } + + private fun updateIndexerState(state: IndexingState?) { + // TODO: Make music loading experience a bit more pleasant + // 1. Loading placeholder for item lists + // 2. Rework the "No Music" case to not be an error and instead result in a placeholder + if (state is IndexingState.Completed && state.error == null) { + logD("Received ok response") + val binding = requireBinding() + updateFabVisibility( + binding, + homeModel.songList.value, + homeModel.isFastScrolling.value, + homeModel.currentTabType.value) + } + } + + private fun updateFab(songs: List, isFastScrolling: Boolean) { + val binding = requireBinding() + updateFabVisibility(binding, songs, isFastScrolling, homeModel.currentTabType.value) + } + + private fun updateFabVisibility( + binding: FragmentMainBinding, + songs: List, + isFastScrolling: Boolean, + tabType: MusicType + ) { + // If there are no songs, it's likely that the library has not been loaded, so + // displaying the shuffle FAB makes no sense. We also don't want the fast scroll + // popup to overlap with the FAB, so we hide the FAB when fast scrolling too. + if (shouldHideAllFabs(binding, songs, isFastScrolling)) { + logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]") + forceHideAllFabs() + } else { + if (tabType != MusicType.PLAYLISTS) { + if (binding.homeShuffleFab.isOrWillBeShown) { + return + } + + if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { + logD("Animating transition") + binding.homeNewPlaylistFab.hide( + object : FloatingActionButton.OnVisibilityChangedListener() { + override fun onHidden(fab: FloatingActionButton) { + super.onHidden(fab) + if (shouldHideAllFabs( + binding, + homeModel.songList.value, + homeModel.isFastScrolling.value)) { + return + } + binding.homeShuffleFab.show() + } + }) + } else { + logD("Showing immediately") + binding.homeShuffleFab.show() + } + } else { + logD("Showing playlist button") + if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { + return + } + + if (binding.homeShuffleFab.isOrWillBeShown) { + logD("Animating transition") + binding.homeShuffleFab.hide( + object : FloatingActionButton.OnVisibilityChangedListener() { + override fun onHidden(fab: FloatingActionButton) { + super.onHidden(fab) + if (shouldHideAllFabs( + binding, + homeModel.songList.value, + homeModel.isFastScrolling.value)) { + return + } + binding.homeNewPlaylistFab.show() + } + }) + } else { + logD("Showing immediately") + binding.homeNewPlaylistFab.show() + } + } + } + } + + private fun shouldHideAllFabs( + binding: FragmentMainBinding, + songs: List, + isFastScrolling: Boolean + ) = + binding.exploreNavHost.findNavController().currentDestination?.id != R.id.home_fragment || + sheetRising == true || + songs.isEmpty() || + isFastScrolling + + private fun forceHideAllFabs() { + val binding = requireBinding() + if (binding.homeShuffleFab.isOrWillBeShown) { + FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeShuffleFab, null, false) + } + if (binding.homeNewPlaylistFab.isOpen) { + binding.homeNewPlaylistFab.close() + } + if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { + FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeNewPlaylistFab.mainFab, null, false) + } + } + + private fun updateSpeedDial(open: Boolean) { + requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" } + .invalidateEnabled(open) + val binding = requireBinding() + logD(open) + binding.mainScrim.isVisible = open + binding.sheetScrim.isVisible = open + if (open) { + binding.homeNewPlaylistFab.open(true) + } else { + binding.homeNewPlaylistFab.close(true) + } + } + + private fun handleSpeedDialBoundaryTouch(event: MotionEvent): Boolean { + val binding = binding ?: return false + + if (binding.homeNewPlaylistFab.isOpen && + binding.homeNewPlaylistFab.isUnder(event.x, event.y)) { + // Convert absolute coordinates to relative coordinates + val offsetX = event.x - binding.homeNewPlaylistFab.x + val offsetY = event.y - binding.homeNewPlaylistFab.y + + // Create a new MotionEvent with relative coordinates + val relativeEvent = + MotionEvent.obtain( + event.downTime, + event.eventTime, + event.action, + offsetX, + offsetY, + event.metaState) + + // Dispatch the relative MotionEvent to the target child view + val handled = binding.homeNewPlaylistFab.dispatchTouchEvent(relativeEvent) + + // Recycle the relative MotionEvent + relativeEvent.recycle() + + return handled + } + + return false + } + private fun handleShow(show: Show?) { when (show) { is Show.SongAlbumDetails, @@ -355,13 +590,6 @@ class MainFragment : homeModel.showOuter.consume() } - private fun handleSpeedDialState(open: Boolean) { - requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" } - .invalidateEnabled(open) - requireBinding().mainScrim.isVisible = open - requireBinding().sheetScrim.isVisible = open - } - private fun updateSong(song: Song?) { if (song != null) { tryShowSheets() @@ -566,8 +794,9 @@ class MainFragment : private inner class SpeedDialBackPressedCallback(private val homeModel: HomeViewModel) : OnBackPressedCallback(false) { override fun handleOnBackPressed() { - if (homeModel.speedDialOpen.value) { - homeModel.setSpeedDialOpen(false) + val binding = requireBinding() + if (binding.homeNewPlaylistFab.isOpen) { + binding.homeNewPlaylistFab.close() } } @@ -575,4 +804,13 @@ class MainFragment : isEnabled = open } } + + private companion object { + val FAB_HIDE_FROM_USER_FIELD: Method by + lazyReflectedMethod( + FloatingActionButton::class, + "hide", + FloatingActionButton.OnVisibilityChangedListener::class, + Boolean::class) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 0290b7fc2..bad5d0ec6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -22,7 +22,6 @@ import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem -import android.view.MotionEvent import android.view.View import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -41,8 +40,6 @@ import com.google.android.material.appbar.AppBarLayout import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.transition.MaterialSharedAxis -import com.leinardi.android.speeddial.SpeedDialActionItem -import com.leinardi.android.speeddial.SpeedDialView import dagger.hilt.android.AndroidEntryPoint import java.lang.reflect.Field import java.lang.reflect.Method @@ -72,14 +69,12 @@ import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistMessage -import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.external.M3U import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getColorCompat -import org.oxycblt.auxio.util.isUnder import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedMethod import org.oxycblt.auxio.util.logD @@ -95,9 +90,7 @@ import org.oxycblt.auxio.util.showToast */ @AndroidEntryPoint class HomeFragment : - SelectionFragment(), - AppBarLayout.OnOffsetChangedListener, - SpeedDialView.OnActionSelectedListener { + SelectionFragment(), AppBarLayout.OnOffsetChangedListener { override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -190,27 +183,9 @@ class HomeFragment : // re-creating the ViewPager. setupPager(binding) - binding.homeShuffleFab.setOnClickListener { playbackModel.shuffleAll() } - - binding.homeNewPlaylistFab.apply { - inflate(R.menu.new_playlist_actions) - setOnActionSelectedListener(this@HomeFragment) - setChangeListener(homeModel::setSpeedDialOpen) - } - - hideAllFabs() - updateFabVisibility( - homeModel.songList.value, - homeModel.isFastScrolling.value, - homeModel.sheetObscuresFab.value, - homeModel.currentTabType.value) - // --- VIEWMODEL SETUP --- collect(homeModel.recreateTabs.flow, ::handleRecreate) collectImmediately(homeModel.currentTabType, ::updateCurrentTab) - collectImmediately( - homeModel.songList, homeModel.isFastScrolling, homeModel.sheetObscuresFab, ::updateFab) - collect(homeModel.speedDialOpen, ::updateSpeedDial) collect(detailModel.toShow.flow, ::handleShow) collect(listModel.menu.flow, ::handleMenu) collectImmediately(listModel.selected, ::updateSelection) @@ -220,28 +195,11 @@ class HomeFragment : collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) } - override fun onResume() { - super.onResume() - - // Stock bottom sheet overlay won't work with our nested UI setup, have to replicate - // it ourselves. - requireBinding().root.rootView.apply { - findViewById(R.id.main_scrim).setOnTouchListener { _, event -> - handleSpeedDialBoundaryTouch(event) - } - findViewById(R.id.sheet_scrim).setOnTouchListener { _, event -> - handleSpeedDialBoundaryTouch(event) - } - } - } - override fun onDestroyBinding(binding: FragmentHomeBinding) { super.onDestroyBinding(binding) storagePermissionLauncher = null binding.homeAppbar.removeOnOffsetChangedListener(this) binding.homeNormalToolbar.setOnMenuItemClickListener(null) - binding.homeNewPlaylistFab.setChangeListener(null) - binding.homeNewPlaylistFab.setOnActionSelectedListener(null) } override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { @@ -299,24 +257,6 @@ class HomeFragment : } } - override fun onActionSelected(actionItem: SpeedDialActionItem): Boolean { - when (actionItem.id) { - R.id.action_new_playlist -> { - logD("Creating playlist") - musicModel.createPlaylist() - } - R.id.action_import_playlist -> { - logD("Importing playlist") - musicModel.importPlaylist() - } - else -> {} - } - // Returning false to close th speed dial results in no animation, manually close instead. - // Adapted from Material Files: https://github.com/zhanghai/MaterialFiles - requireBinding().homeNewPlaylistFab.close() - return true - } - private fun setupPager(binding: FragmentHomeBinding) { binding.homePager.adapter = HomePagerAdapter(homeModel.currentTabTypes, childFragmentManager, viewLifecycleOwner) @@ -358,12 +298,6 @@ class HomeFragment : MusicType.GENRES -> R.id.home_genre_recycler MusicType.PLAYLISTS -> R.id.home_playlist_recycler } - - updateFabVisibility( - homeModel.songList.value, - homeModel.isFastScrolling.value, - homeModel.sheetObscuresFab.value, - tabType) } private fun handleRecreate(recreate: Unit?) { @@ -395,11 +329,6 @@ class HomeFragment : private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) { if (error == null) { logD("Received ok response") - updateFabVisibility( - homeModel.songList.value, - homeModel.isFastScrolling.value, - homeModel.sheetObscuresFab.value, - homeModel.currentTabType.value) binding.homeIndexingContainer.visibility = View.INVISIBLE return } @@ -544,118 +473,6 @@ class HomeFragment : } } - private fun updateFab(songs: List, isFastScrolling: Boolean, sheetRising: Boolean) { - updateFabVisibility(songs, isFastScrolling, sheetRising, homeModel.currentTabType.value) - } - - private fun updateFabVisibility( - songs: List, - isFastScrolling: Boolean, - sheetRising: Boolean, - tabType: MusicType - ) { - val binding = requireBinding() - // If there are no songs, it's likely that the library has not been loaded, so - // displaying the shuffle FAB makes no sense. We also don't want the fast scroll - // popup to overlap with the FAB, so we hide the FAB when fast scrolling too. - if (songs.isEmpty() || isFastScrolling || sheetRising) { - logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]") - hideAllFabs() - } else { - if (tabType != MusicType.PLAYLISTS) { - logD("Showing shuffle button") - if (binding.homeShuffleFab.isOrWillBeShown) { - logD("Nothing to do") - return - } - - if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { - logD("Animating transition") - binding.homeNewPlaylistFab.hide( - object : FloatingActionButton.OnVisibilityChangedListener() { - override fun onHidden(fab: FloatingActionButton) { - super.onHidden(fab) - binding.homeShuffleFab.show() - } - }) - } else { - logD("Showing immediately") - binding.homeShuffleFab.show() - } - } else { - logD("Showing playlist button") - if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { - logD("Nothing to do") - return - } - - if (binding.homeShuffleFab.isOrWillBeShown) { - logD("Animating transition") - binding.homeShuffleFab.hide( - object : FloatingActionButton.OnVisibilityChangedListener() { - override fun onHidden(fab: FloatingActionButton) { - super.onHidden(fab) - binding.homeNewPlaylistFab.show() - } - }) - } else { - logD("Showing immediately") - binding.homeNewPlaylistFab.show() - } - } - } - } - - private fun hideAllFabs() { - val binding = requireBinding() - if (binding.homeShuffleFab.isOrWillBeShown) { - FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeShuffleFab, null, false) - } - if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { - FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeNewPlaylistFab.mainFab, null, false) - } - } - - private fun updateSpeedDial(open: Boolean) { - val binding = requireBinding() - - if (open) { - binding.homeNewPlaylistFab.open(true) - } else { - binding.homeNewPlaylistFab.close(true) - } - } - - private fun handleSpeedDialBoundaryTouch(event: MotionEvent): Boolean { - val binding = binding ?: return false - - if (homeModel.speedDialOpen.value && binding.homeNewPlaylistFab.isUnder(event.x, event.y)) { - // Convert absolute coordinates to relative coordinates - val offsetX = event.x - binding.homeNewPlaylistFab.x - val offsetY = event.y - binding.homeNewPlaylistFab.y - - // Create a new MotionEvent with relative coordinates - val relativeEvent = - MotionEvent.obtain( - event.downTime, - event.eventTime, - event.action, - offsetX, - offsetY, - event.metaState) - - // Dispatch the relative MotionEvent to the target child view - val handled = binding.homeNewPlaylistFab.dispatchTouchEvent(relativeEvent) - - // Recycle the relative MotionEvent - relativeEvent.recycle() - - return handled - } - - return false - } - private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 0781560e6..bb9311c84 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -156,13 +156,6 @@ constructor( /** A marker for whether the user is fast-scrolling in the home view or not. */ val isFastScrolling: StateFlow = _isFastScrolling - private val _speedDialOpen = MutableStateFlow(false) - /** A marker for whether the speed dial is open or not. */ - val speedDialOpen: StateFlow = _speedDialOpen - - private val _sheetObscuresFab = MutableStateFlow(false) - val sheetObscuresFab: StateFlow = _sheetObscuresFab - private val _showOuter = MutableEvent() val showOuter: Event get() = _showOuter @@ -300,20 +293,6 @@ constructor( _isFastScrolling.value = isFastScrolling } - /** - * Update whether the speed dial is open or not. - * - * @param speedDialOpen true if the speed dial is open, false otherwise. - */ - fun setSpeedDialOpen(speedDialOpen: Boolean) { - logD("Updating speed dial state: $speedDialOpen") - _speedDialOpen.value = speedDialOpen - } - - fun setSheetObscuresFab(sheetRising: Boolean) { - _sheetObscuresFab.value = sheetRising - } - fun showSettings() { _showOuter.put(Outer.Settings) } diff --git a/app/src/main/res/layout-w720dp/fragment_main.xml b/app/src/main/res/layout-w720dp/fragment_main.xml index 726c9c74a..8fabc42f2 100644 --- a/app/src/main/res/layout-w720dp/fragment_main.xml +++ b/app/src/main/res/layout-w720dp/fragment_main.xml @@ -7,15 +7,55 @@ android:layout_height="match_parent" android:background="?attr/colorSurface"> - + app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"> + + + + + + + + + + + + + - - - - - - - - diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index ebaa7bc32..ed20f341f 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -8,15 +8,53 @@ android:background="?attr/colorSurface" android:transitionGroup="true"> - + app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"> + + + + + + + + + + + + + android:background="?attr/colorSurfaceContainerLow" /> Date: Sat, 20 Jul 2024 15:01:23 -0600 Subject: [PATCH 055/550] detail: fix no divider rendering w/artist albums --- .../main/java/org/oxycblt/auxio/detail/DetailViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 090c838a1..d97b54b09 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -623,8 +623,11 @@ constructor( logD("Release groups for this artist: ${grouping.keys}") - for (entry in grouping.entries) { + for ((i, entry) in grouping.entries.withIndex()) { val header = BasicHeader(entry.key.headerTitleRes) + if (i > 0) { + list.add(Divider(header)) + } list.add(header) list.addAll(ARTIST_ALBUM_SORT.albums(entry.value)) } From 95469a554cd620811906c29391db0a75c908c7e6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 15:17:27 -0600 Subject: [PATCH 056/550] ui: fix multitoolbar animation error Was misusing the material animation, this should be in line w/spec --- .../java/org/oxycblt/auxio/ui/MultiToolbar.kt | 88 ++++++++++++------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt index cefabee45..c343bc06b 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.ui +import android.animation.AnimatorSet import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet @@ -30,21 +31,28 @@ import androidx.core.view.isInvisible import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.google.android.material.R as MR import com.google.android.material.motion.MotionUtils -import kotlin.math.max -import kotlin.math.min import org.oxycblt.auxio.util.logD class MultiToolbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { - private var fadeThroughAnimator: ValueAnimator? = null + private var animator: AnimatorSet? = null private var currentlyVisible = 0 - private val matInterpolator = + private val outInterpolator = MotionUtils.resolveThemeInterpolator( - context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()) - private val matDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium3, 300).toLong() + context, + MR.attr.motionEasingEmphasizedAccelerateInterpolator, + FastOutSlowInInterpolator()) + private val outDuration = + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 250).toLong() + private val inInterpolator = + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingEmphasizedDecelerateInterpolator, + FastOutSlowInInterpolator()) + private val inDuration = + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium1, 300).toLong() override fun onFinishInflate() { super.onFinishInflate() @@ -81,38 +89,54 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // Not laid out, just change it immediately while are not shown to the user. // This is an initialization, so we return false despite changing. logD("Not laid out, immediately updating visibility") - setToolbarsAlpha(fromView, toView, targetFromAlpha) + fromView.apply { + alpha = 0f + isInvisible = true + } + toView.apply { + alpha = 1f + isInvisible = false + } return false } logD("Changing toolbar visibility $from -> 0f, $to -> 1f") - fadeThroughAnimator?.cancel() - fadeThroughAnimator = - ValueAnimator.ofFloat(fromView.alpha, targetFromAlpha).apply { - duration = matDuration - interpolator = matInterpolator - addUpdateListener { setToolbarsAlpha(fromView, toView, it.animatedValue as Float) } + animator?.cancel() + val outAnimator = + ValueAnimator.ofFloat(fromView.alpha, 0f).apply { + duration = outDuration + interpolator = outInterpolator + addUpdateListener { + val ratio = it.animatedValue as Float + fromView.apply { + scaleX = 1 - 0.05f * (1 - ratio) + scaleY = 1 - 0.05f * (1 - ratio) + alpha = ratio + isInvisible = alpha == 0f + } + } + } + val inAnimator = + ValueAnimator.ofFloat(toView.alpha, 1f).apply { + duration = inDuration + interpolator = inInterpolator + startDelay = outDuration + addUpdateListener { + val ratio = it.animatedValue as Float + toView.apply { + scaleX = 1 - 0.05f * (1 - ratio) + scaleY = 1 - 0.05f * (1 - ratio) + alpha = ratio + isInvisible = alpha == 0f + } + } + } + animator = + AnimatorSet().apply { + playTogether(outAnimator, inAnimator) start() } return true } - - private fun setToolbarsAlpha(from: Toolbar, to: Toolbar, ratio: Float) { - val outRatio = min(ratio * 2, 1f) - to.apply { - scaleX = 1 - 0.05f * outRatio - scaleY = 1 - 0.05f * outRatio - alpha = 1 - outRatio - isInvisible = alpha == 0f - } - - val inRatio = max(ratio - 0.5f, 0f) * 2 - from.apply { - scaleX = 1 - 0.05f * (1 - inRatio) - scaleY = 1 - 0.05f * (1 - inRatio) - alpha = inRatio - isInvisible = alpha == 0f - } - } } From 106194fa524730d6a6409d75c57910eedbeab870 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 20 Jul 2024 16:06:56 -0600 Subject: [PATCH 057/550] playback: add split screen playback form factor --- .../auxio/playback/PlaybackPanelFragment.kt | 16 +- .../layout-h240dp/fragment_playback_panel.xml | 176 ++++++++++++++++++ .../res/layout/fragment_playback_panel.xml | 85 +++------ app/src/main/res/values/styles_ui.xml | 12 ++ 4 files changed, 224 insertions(+), 65 deletions(-) create mode 100644 app/src/main/res/layout-h240dp/fragment_playback_panel.xml diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 80f9a99d0..c73a1b814 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -102,12 +102,12 @@ class PlaybackPanelFragment : isSelected = true setOnClickListener { navigateToCurrentArtist() } } - binding.playbackAlbum.apply { + binding.playbackAlbum?.apply { isSelected = true setOnClickListener { navigateToCurrentAlbum() } } - binding.playbackSeekBar.listener = this + binding.playbackSeekBar?.listener = this // Set up actions // TODO: Add better playback button accessibility @@ -116,7 +116,7 @@ class PlaybackPanelFragment : binding.playbackPlayPause.setOnClickListener { playbackModel.togglePlaying() } binding.playbackSkipNext.setOnClickListener { playbackModel.next() } binding.playbackShuffle.setOnClickListener { playbackModel.toggleShuffled() } - binding.playbackMore.setOnClickListener { + binding.playbackMore?.setOnClickListener { playbackModel.song.value?.let { listModel.openMenu(R.menu.playback_song, it, PlaySong.ByItself) } @@ -135,7 +135,7 @@ class PlaybackPanelFragment : equalizerLauncher = null binding.playbackSong.isSelected = false binding.playbackArtist.isSelected = false - binding.playbackAlbum.isSelected = false + binding.playbackAlbum?.isSelected = false binding.playbackToolbar.setOnMenuItemClickListener(null) } @@ -185,10 +185,10 @@ class PlaybackPanelFragment : val context = requireContext() logD("Updating song display: $song") binding.playbackCover.bind(song) - binding.playbackSong.text = song.name.resolve(context) + binding.playbackSong?.text = song.name.resolve(context) binding.playbackArtist.text = song.artists.resolveNames(context) - binding.playbackAlbum.text = song.album.name.resolve(context) - binding.playbackSeekBar.durationDs = song.durationMs.msToDs() + binding.playbackAlbum?.text = song.album.name.resolve(context) + binding.playbackSeekBar?.durationDs = song.durationMs.msToDs() } private fun updateParent(parent: MusicParent?) { @@ -199,7 +199,7 @@ class PlaybackPanelFragment : } private fun updatePosition(positionDs: Long) { - requireBinding().playbackSeekBar.positionDs = positionDs + requireBinding().playbackSeekBar?.positionDs = positionDs } private fun updateRepeat(repeatMode: RepeatMode) { diff --git a/app/src/main/res/layout-h240dp/fragment_playback_panel.xml b/app/src/main/res/layout-h240dp/fragment_playback_panel.xml new file mode 100644 index 000000000..b73de1fd9 --- /dev/null +++ b/app/src/main/res/layout-h240dp/fragment_playback_panel.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_playback_panel.xml b/app/src/main/res/layout/fragment_playback_panel.xml index 07ba982bd..fa845fb33 100644 --- a/app/src/main/res/layout/fragment_playback_panel.xml +++ b/app/src/main/res/layout/fragment_playback_panel.xml @@ -12,95 +12,67 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:menu="@menu/toolbar_playback" - app:titleCentered="true" - app:subtitleCentered="true" - app:titleTextAppearance="@style/TextAppearance.Auxio.LabelLarge" - app:subtitleTextAppearance="@style/TextAppearance.Auxio.BodySmall" app:navigationIcon="@drawable/ic_down_24" + app:subtitleCentered="true" + app:subtitleTextAppearance="@style/TextAppearance.Auxio.BodySmall" app:title="@string/lbl_playback" + app:titleCentered="true" + app:titleTextAppearance="@style/TextAppearance.Auxio.LabelLarge" tools:subtitle="@string/lbl_all_songs" /> - + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" /> - - - - - - - + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/playback_cover" + app:layout_constraintTop_toBottomOf="@+id/playback_info_container"> - diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 0b8603e51..000a8e899 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -277,6 +277,18 @@ @color/m3_text_button_foreground_color_selector + + + + + + - diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 16eacfc8a..583944f7b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -77,7 +77,6 @@ Albüm Yıl Süre - Durum kaydedildi Alexander Capehart tarafından geliştirildi Siyah tema Kitaplık istatistikleri @@ -145,8 +144,6 @@ Önceki şarkıya atlamadan önce geri sar Bir şarkı tekrarlandığında duraklat İçerik - Çalma durumunu kaydet - Mevcut çalma durumunu şimdi kaydet Karıştırmayı hatırla Yeni bir şarkı çalarken karışık çalmayı açık tut Müziği yenile @@ -176,11 +173,7 @@ Camgöbeği Deniz mavisi Ek arayüz öğelerinde yuvarlatılmış köşeleri etkinleştirir (Albüm kapaklarının yuvarlatılmış olmasını gerektirir) - Durum geri yüklendi - Önceden kayıtlı çalma durumunu geri getir (varsa) Yuvarlak mod - Çalma durumunu eski haline getir - Durum geri getirelemedi Tekrarda duraklat Müzik yükleniyor Müzik kitaplığı denetleniyor @@ -209,7 +202,6 @@ EP\'ler EP Karışık kasetler - Durum temizlendi Remiksler Film Müzikleri Film Müziği @@ -219,8 +211,6 @@ Albüm kapakları Kapalı Hızlı - Çalma durumunu temizle - Önceki kayıtlı çalma durumunu temizle (varsa) %d Seçili %d sanatçı @@ -235,7 +225,6 @@ Noktalı virgül (;) Artı (+) Ve (&) - Durum kaydedilemedi Çalmayı durdur Viki Müzikleri yeniden tara @@ -248,7 +237,6 @@ Podcast\'ler gibi müzik olmayan ses dosyalarını yok say Uyarı: Bu ayarın kullanılması bazı etiketlerin yanlışlıkla birden fazla değere sahip olarak yorumlanmasına neden olabilir. Bunu, istenmeyen ayırıcı karakterlerin önüne ters eğik çizgi (\\) koyarak çözebilirsiniz. Müzik olmayanları hariç tut - Durum temizlenemedi ReplayGain stratejisi Bu şarkıyı kuyrukta taşı %1$s, %2$s @@ -258,7 +246,6 @@ Sesi ve oynatma davranışını yapılandırın Oynatma Kütüphane - Kalıcılık Azalan Uygulamanın temasını ve renklerini değiştirin Klasörler @@ -270,7 +257,6 @@ çalma listeleri Sıralama yaparken makaleleri yoksay Ada göre sıralarken \"the\" gibi kelimeleri yok sayın (en iyi ingilizce müzikle çalışır) - Yeni bir oynatma listesi oluştur Yeni Oynatma Listesi Sil Yeniden Adlandır diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 98613352d..6bf66ebc8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -142,7 +142,6 @@ Відтворити альбом При відтворенні з бібліотеки Віддавати перевагу альбому, якщо він відтворюється - Стан відтворення очищено Використовувати повністю чорну тему Показувати лише тих виконавців, які безпосередньо зазначені в альбомі (найкраще працює в добре позначених бібліотеках) Увага: Встановлення високих позитивних значень попереднього підсилювача може призвести до спотворення звуку в деяких піснях. @@ -152,11 +151,9 @@ Перезавантажувати бібліотеку при виявленні змін (потрібне постійне сповіщення) Музика не буде завантажена з вибраних папок. Налаштуйте символи, які позначають кілька значень тегів - Стан відтворення збережено За альбомом За піснею Зміст - Очистити раніше збережений стан відтворення (якщо є) Відстеження змін в музичній бібліотеці… Власна дія для панелі відтворення Регулювання без тегів @@ -165,20 +162,14 @@ Відтворити виконавця Відтворити жанр Перемотати назад перед відтворенням попередньої пісні - Зберегти поточний стан відтворення Пересканувати музику Попередження: Використання цього параметра може призвести до того, що деякі теги будуть неправильно інтерпретовані як такі, що мають кілька значень. Ви можете вирішити це, додавши перед небажаними символами-роздільниками зворотну скісну риску (\\). Кнопка в сповіщенні Багатозначні роздільники Музика буде завантажена тільки з вибраних папок. - Відновити раніше збережений стан відтворення (якщо є) Регулювання на основі тегів Налаштування ReplayGain - Зберегти стан відтворення - Очистити стан відтворення - Відновити стан відтворення Пісня - Стан відтворення відновлено Перегляд і керування відтворенням музики Перемотайте на початок пісні перед відтворенням попередньої Увімкнути заокруглені кути на додаткових елементах інтерфейсу (потрібно заокруглення обкладинок альбомів) @@ -194,7 +185,6 @@ Лаймовий Амперсанд (&) Немає папок - Не вдалось відновити статус відтворення Перейти до наступної пісні Ввімкніть або вимкніть перемішування Зупинити відтворення @@ -240,8 +230,6 @@ Дата відсутня Немає пісні Музика не грає - Не вдалось очистити статус відтворення - Не вдалось зберегти статус відтворення Перейти до попередньої пісні Червоний Звук MPEG-4 @@ -267,7 +255,6 @@ Відтворення Вирівнювання гучності Бібліотека - Стан відтворення Налаштуйте звук і поведінку при відтворенні Папки За спаданням @@ -276,7 +263,6 @@ Списки відтворення Інтелектуальне сортування Ігнорування таких слів, як \"the\", або цифр під час сортування за назвою (найкраще працює з англомовною музикою) - Створити новий список відтворення Новий список відтворення Список відтворення %d Додати до списку відтворення diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index eda8b077c..07d9b9fe6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -32,7 +32,6 @@ 已加入播放队列 查看艺术家 查看专辑 - 已保存播放进度 添加 保存 没有文件夹 @@ -73,8 +72,6 @@ 重复播放前暂停 曲目重复播放前暂停 内容 - 保存播放状态 - 立即保存当前播放状态 刷新音乐 重新加载音乐库,如有可能使用缓存的标签 @@ -182,19 +179,12 @@ 采样率 好的 取消 - 清除播放状态 - 清除此前保存的播放状态(如果有) - 无法恢复状态 - 恢复播放状态 - 恢复此前保存的播放状态(如果有) 自动重载 只要发生更改就重新加载曲库(需要持久性通知) 打开队列 正在加载音乐 正在监测曲库 正在监测您的曲库以查找更改… - 已清除状态 - 已恢复状态 EP 专辑 EP 专辑 单曲 @@ -243,8 +233,6 @@ %d 位艺术家 - 无法保存状态 - 无法清除状态 重新扫描音乐 清除标签缓存并完全重新加载音乐库(更慢,但更完整) 选中了 %d 首 @@ -254,7 +242,6 @@ 重置 行为 音量正常化 - 持久性 更改应用的主题和颜色 定制用户界面操控和行为 控制音乐和图片加载方式 @@ -270,7 +257,6 @@ %s 的播放列表图片 排序时忽略冠词 按名称排序时忽略类似“the”这样的冠词(对英文歌曲的效果最好) - 创建新的播放列表 新建播放列表 播放列表 %d 已创建播放列表 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a419aee10..3a5892b30 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -8,12 +8,10 @@ 20dp 24dp 28dp - 32dp 48dp 56dp - 60dp 64dp 76dp 80dp diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 3256b7dca..a45ef58ce 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -9,7 +9,6 @@ %d %1$s (%2$s) %s - %s - %1$s/%2$s Vorbis diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4431dacd..6408e9478 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -169,13 +169,6 @@ Use Windows-compatible paths - - State saved - - State cleared - - State restored - About Version Source code @@ -320,14 +313,6 @@ Rescan music Clear the tag cache and fully reload the music library (slower, but more complete) - Persistence - Save playback state - Save the current playback state now - Clear playback state - Clear the previously saved playback state (if any) - Restore playback state - Restore the previously saved playback state (if any) - No music found Music loading failed @@ -338,12 +323,6 @@ No folders This folder is not supported - - Unable to restore state - - Unable to clear state - - Unable to save state @@ -355,7 +334,6 @@ Change repeat mode Turn shuffle on or off Shuffle all songs - Create a new playlist Stop playback Remove this song diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 60c3c1071..b9e7e8a60 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -264,7 +264,7 @@ @dimen/spacing_small @dimen/spacing_small - @color/m3_text_button_foreground_color_selector + @color/m3_text_button_foreground_color_selector - - \ No newline at end of file From 7d8efce28b93c205e2ae2ffabee8e836f432f38d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 22 Jul 2024 19:02:22 -0600 Subject: [PATCH 067/550] ui: use 360 for extremely short layout --- .../main/res/{layout-h280dp => layout-h360dp}/fragment_detail.xml | 0 .../{layout-h240dp => layout-h360dp}/fragment_playback_panel.xml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/res/{layout-h280dp => layout-h360dp}/fragment_detail.xml (100%) rename app/src/main/res/{layout-h240dp => layout-h360dp}/fragment_playback_panel.xml (100%) diff --git a/app/src/main/res/layout-h280dp/fragment_detail.xml b/app/src/main/res/layout-h360dp/fragment_detail.xml similarity index 100% rename from app/src/main/res/layout-h280dp/fragment_detail.xml rename to app/src/main/res/layout-h360dp/fragment_detail.xml diff --git a/app/src/main/res/layout-h240dp/fragment_playback_panel.xml b/app/src/main/res/layout-h360dp/fragment_playback_panel.xml similarity index 100% rename from app/src/main/res/layout-h240dp/fragment_playback_panel.xml rename to app/src/main/res/layout-h360dp/fragment_playback_panel.xml From b020285e9fec9e813e845474556494d269597b83 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 22 Jul 2024 19:06:39 -0600 Subject: [PATCH 068/550] main: simplify speed dial management --- .../java/org/oxycblt/auxio/MainFragment.kt | 63 ++----------------- .../oxycblt/auxio/home/ThemedSpeedDialView.kt | 2 +- .../main/res/layout-w720dp/fragment_main.xml | 38 ++++++----- app/src/main/res/layout/fragment_main.xml | 14 +++-- 4 files changed, 37 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 2dac07833..849618337 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -18,17 +18,15 @@ package org.oxycblt.auxio +import android.animation.ValueAnimator import android.os.Bundle import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View import android.view.ViewTreeObserver import android.view.WindowInsets import androidx.activity.BackEventCompat import androidx.activity.OnBackPressedCallback import androidx.core.view.ViewCompat import androidx.core.view.isInvisible -import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController @@ -67,7 +65,6 @@ import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimen -import org.oxycblt.auxio.util.isUnder import org.oxycblt.auxio.util.lazyReflectedMethod import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe @@ -232,19 +229,6 @@ class MainFragment : addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback)) addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback)) } - - // Stock bottom sheet overlay won't work with our nested UI setup, have to replicate - // it ourselves. - requireBinding().root.rootView.apply { - findViewById(R.id.main_scrim).setOnTouchListener { v, event -> - v.performClick() - handleSpeedDialBoundaryTouch(event) - } - findViewById(R.id.sheet_scrim).setOnTouchListener { v, event -> - v.performClick() - handleSpeedDialBoundaryTouch(event) - } - } } override fun onStop() { @@ -522,49 +506,14 @@ class MainFragment : } } + private var scrimAnimator: ValueAnimator? = null + private fun updateSpeedDial(open: Boolean) { requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" } .invalidateEnabled(open) val binding = requireBinding() - logD(open) - binding.mainScrim.isVisible = open - binding.sheetScrim.isVisible = open - if (open) { - binding.homeNewPlaylistFab.open(true) - } else { - binding.homeNewPlaylistFab.close(true) - } - } - - private fun handleSpeedDialBoundaryTouch(event: MotionEvent): Boolean { - val binding = binding ?: return false - - if (binding.homeNewPlaylistFab.isOpen && - binding.homeNewPlaylistFab.isUnder(event.x, event.y)) { - // Convert absolute coordinates to relative coordinates - val offsetX = event.x - binding.homeNewPlaylistFab.x - val offsetY = event.y - binding.homeNewPlaylistFab.y - - // Create a new MotionEvent with relative coordinates - val relativeEvent = - MotionEvent.obtain( - event.downTime, - event.eventTime, - event.action, - offsetX, - offsetY, - event.metaState) - - // Dispatch the relative MotionEvent to the target child view - val handled = binding.homeNewPlaylistFab.dispatchTouchEvent(relativeEvent) - - // Recycle the relative MotionEvent - relativeEvent.recycle() - - return handled - } - - return false + binding.mainScrim.isInvisible = !open + binding.sheetScrim.isInvisible = !open } private fun handleShow(show: Show?) { @@ -793,7 +742,7 @@ class MainFragment : } } - private inner class SpeedDialBackPressedCallback() : OnBackPressedCallback(false) { + private inner class SpeedDialBackPressedCallback : OnBackPressedCallback(false) { override fun handleOnBackPressed() { val binding = requireBinding() if (binding.homeNewPlaylistFab.isOpen) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt index f0b4e32c0..2b1d0c862 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt @@ -85,7 +85,7 @@ class ThemedSpeedDialView : SpeedDialView { context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()) private val matDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300) + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 350) init { // Work around ripple bug on Android 12 when useCompatPadding = true. diff --git a/app/src/main/res/layout-w720dp/fragment_main.xml b/app/src/main/res/layout-w720dp/fragment_main.xml index 8685296cc..3a51e8cf6 100644 --- a/app/src/main/res/layout-w720dp/fragment_main.xml +++ b/app/src/main/res/layout-w720dp/fragment_main.xml @@ -77,17 +77,22 @@ android:focusable="true" app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior"> - + android:layout_height="match_parent"> - + + + + + - - - - + + + diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index ed20f341f..682ce60ae 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -22,6 +22,12 @@ app:navGraph="@navigation/inner" tools:layout="@layout/fragment_home" /> + + - - + android:layout_height="match_parent" + android:visibility="invisible" /> From 9b272bbdfe8f97738ce93639bd482809b8a2d1f6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 22 Jul 2024 19:11:32 -0600 Subject: [PATCH 069/550] home: fix broken sheet --- .../main/res/layout-w720dp/fragment_main.xml | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/app/src/main/res/layout-w720dp/fragment_main.xml b/app/src/main/res/layout-w720dp/fragment_main.xml index 3a51e8cf6..a364dd452 100644 --- a/app/src/main/res/layout-w720dp/fragment_main.xml +++ b/app/src/main/res/layout-w720dp/fragment_main.xml @@ -77,22 +77,17 @@ android:focusable="true" app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior"> - + android:layout_height="wrap_content" /> - - - + - - + - - - - + + + From 9bc27a49eb4926733bf6af02d7a06e33aa691c51 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 29 Jul 2024 18:25:06 -0600 Subject: [PATCH 070/550] music: start indexing after bind/start command --- app/src/main/java/org/oxycblt/auxio/AuxioService.kt | 7 ++++--- .../main/java/org/oxycblt/auxio/music/MusicRepository.kt | 3 --- .../oxycblt/auxio/music/service/IndexerServiceFragment.kt | 6 ++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index 64121e6d1..a8cb344cf 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -43,24 +43,25 @@ class AuxioService : MediaLibraryService(), ForegroundListener { } override fun onBind(intent: Intent?): IBinder? { - handleIntent(intent) + start(intent) return super.onBind(intent) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // TODO: Start command occurring from a foreign service basically implies a detached // service, we might need more handling here. - handleIntent(intent) + start(intent) return super.onStartCommand(intent, flags, startId) } - private fun handleIntent(intent: Intent?) { + private fun start(intent: Intent?) { val nativeStart = intent?.getBooleanExtra(INTENT_KEY_NATIVE_START, false) ?: false if (!nativeStart) { // Some foreign code started us, no guarantees about foreground stability. Figure // out what to do. mediaSessionFragment.handleNonNativeStart() } + indexingFragment.start() } override fun onTaskRemoved(rootIntent: Intent?) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index b8a3d2b26..7d60f825d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -280,9 +280,6 @@ constructor( } logD("Registering worker $worker") indexingWorker = worker - if (indexingState == null) { - worker.requestIndex(true) - } } @Synchronized diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt index 6362def6b..571e96ca7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt @@ -79,6 +79,12 @@ constructor( foregroundListener = null } + fun start() { + if (musicRepository.indexingState == null) { + requestIndex(true) + } + } + fun createNotification(post: (IndexerNotification?) -> Unit) { val state = musicRepository.indexingState if (state is IndexingState.Indexing) { From e351a91a9c27b51cb4b3b14b380a812bd0f1d088 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 29 Jul 2024 18:27:56 -0600 Subject: [PATCH 071/550] playback: do not leak indexerservicefragment --- .../oxycblt/auxio/music/service/IndexerServiceFragment.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt index 571e96ca7..c6277ba7c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt @@ -72,10 +72,10 @@ constructor( fun release() { contentObserver.release() - musicSettings.registerListener(this) - musicRepository.addIndexingListener(this) - musicRepository.addUpdateListener(this) + musicRepository.unregisterWorker(this) musicRepository.removeIndexingListener(this) + musicRepository.removeUpdateListener(this) + musicSettings.unregisterListener(this) foregroundListener = null } From 9299e03e955ab317b4644c448e63e43a5baa86a6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 29 Jul 2024 19:10:19 -0600 Subject: [PATCH 072/550] widget: mitigate bitmap size calculation bug --- .../auxio/music/fs/MediaStoreExtractor.kt | 5 +- .../oxycblt/auxio/widgets/WidgetProvider.kt | 59 +++++++++++++++---- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index d0776e4a6..3972718e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -285,8 +285,9 @@ private class MediaStoreExtractorImpl( // know when it corresponds to the folder and not, say, Low Roar's breakout album "0"? // Also, on some devices it's literally just null. To maintain behavior sanity just // replicate the majority behavior described prior. - rawSong.albumName = cursor.getStringOrNull(albumIndex) - ?: requireNotNull(rawSong.path?.name) { "Invalid raw: No path" } + rawSong.albumName = + cursor.getStringOrNull(albumIndex) + ?: requireNotNull(rawSong.path?.name) { "Invalid raw: No path" } // Android does not make a non-existent artist tag null, it instead fills it in // as , which makes absolutely no sense given how other columns default // to null if they are not present. If this column is such, null it so that diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 2ee0fcafd..80b8758e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -101,17 +101,51 @@ class WidgetProvider : AppWidgetProvider() { SizeF(180f, 272f) to newThinPaneLayout(context, uiSettings, state), SizeF(304f, 272f) to newWidePaneLayout(context, uiSettings, state)) + // This is the order in which we will disable cover art layouts if they exceed the + // maximum bitmap memory usage. (See the comment in the loop below for more info.) + val victims = + mutableSetOf( + R.layout.widget_wafer_thin, + R.layout.widget_wafer_wide, + R.layout.widget_pane_thin, + R.layout.widget_pane_wide, + R.layout.widget_docked_thin, + R.layout.widget_docked_wide, + ) + // Manually update AppWidgetManager with the new views. val awm = AppWidgetManager.getInstance(context) val component = ComponentName(context, this::class.java) - try { - awm.updateAppWidgetCompat(context, component, views) - logD("Successfully updated RemoteViews layout") - } catch (e: Exception) { - // Layout update failed, gracefully degrade to the default widget. - logW("Unable to update widget: $e") - reset(context, uiSettings) + while (victims.size > 0) { + try { + awm.updateAppWidgetCompat(context, component, views) + logD("Successfully updated RemoteViews layout") + return + } catch (e: IllegalArgumentException) { + val msg = e.message ?: return + if (!msg.startsWith( + "RemoteViews for widget update exceeds maximum bitmap memory usage")) { + throw e + } + // Some android devices on Android 12-14 suffer from a bug where the maximum bitmap + // size calculation does not factor in bitmaps shared across multiple RemoteView + // forms. + // To mitigate an outright crash, progressively disable layouts that contain cover + // art + // in order of least to most commonly used until it actually works. + val victim = victims.first() + val view = views.entries.find { it.value.layoutId == victim } ?: continue + view.value.discardCover(context) + victims.remove(victim) + } catch (e: Exception) { + // Layout update failed, gracefully degrade to the default widget. + logW("Unable to update widget: $e") + reset(context, uiSettings) + } } + // We flat-out cannot fit the bitmap into the widget. Weird. + logW("Unable to update widget: Bitmap too large") + reset(context, uiSettings) } /** @@ -288,16 +322,17 @@ class WidgetProvider : AppWidgetProvider() { context.getString( R.string.desc_album_cover, state.song.album.name.resolve(context))) } else { - // We are unable to use the typical placeholder cover with the song item due to - // limitations with the corner radius. Instead use a custom-made album icon as the - // placeholder. - setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24) - setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover)) + discardCover(context) } return this } + private fun RemoteViews.discardCover(context: Context) { + setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24) + setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover)) + } + private fun RemoteViews.setupFillingCover(uiSettings: UISettings): RemoteViews { // Below API 31, enable a rounded background only if round mode is enabled. // On API 31+, the background should always be round in order to fit in with other From 86d9d957a2da6c9cf708fb1ef978f5201f27b0d6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 29 Jul 2024 20:57:23 -0600 Subject: [PATCH 073/550] music: propose file name as playlist name --- .../music/external/ExternalPlaylistManager.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt index a1171efee..81d8720cc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.music.external import android.content.Context import android.net.Uri import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.fs.Components import org.oxycblt.auxio.music.fs.DocumentPathFactory import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.util.logE +import javax.inject.Inject /** * Generic playlist file importing abstraction. @@ -92,7 +92,20 @@ constructor( return try { context.contentResolverSafe.openInputStream(uri)?.use { - return m3u.read(it, filePath.directory) + val imported = m3u.read(it, filePath.directory) ?: return null + val name = imported.name + if (name != null) { + return imported + } + // Strip extension + val fileName = filePath.name ?: return imported + val split = fileName.split(".") + var newName = split[0] + // Replace delimiters with space + newName = newName.replace(Regex("[_-]"), " ") + // Replace long stretches of whitespace with one space + newName = newName.replace(Regex("\\s+"), " ") + return ImportedPlaylist(newName, imported.paths) } } catch (e: Exception) { logE("Failed to import playlist: $e") From 4c20ca2a5c8f8971b9503a178e3cfcbc6124cdf6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 29 Jul 2024 21:40:21 -0600 Subject: [PATCH 074/550] info: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0284a29..83750a61f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Refreshed playback design #### What's Improved +- M3U playlist file name is now proposed if one cannot be found within the file - Sorting songs by date now uses songs date first, before the earliest album date #### What's Fixed From d55db7bde4974ac74b46b8660b2a7f8c02f78335 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 1 Aug 2024 01:49:08 +0000 Subject: [PATCH 075/550] actions: use checkout v4 --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 61ec47e0c..72dedd3ad 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Clone submodules run: git submodule update --init --recursive --remote - name: Set up JDK 17 From ba56fe1a4a5bed57bf950ee246088e5dcf90c232 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 1 Aug 2024 02:34:34 +0000 Subject: [PATCH 076/550] actions: update java & artifact to v4 --- .github/workflows/android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 72dedd3ad..b39dd4cfc 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -16,7 +16,7 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive --remote - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' @@ -28,7 +28,7 @@ jobs: - name: Build debug APK with Gradle run: ./gradlew app:packageDebug - name: Upload debug APK artifact - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v4 with: name: Auxio_Canary path: ./app/build/outputs/apk/debug/app-debug.apk From cf28adc5aac7d050eda8c4c3dca735b766afe0d7 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 14 Aug 2024 05:28:26 +0200 Subject: [PATCH 077/550] Translations update from Hosted Weblate (#820) * Translated using Weblate (Spanish) Currently translated at 100.0% (48 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/es/ * Translated using Weblate (Bulgarian) Currently translated at 100.0% (48 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/bg/ * Translated using Weblate (Punjabi) Currently translated at 100.0% (48 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/pa/ * Translated using Weblate (Hindi) Currently translated at 100.0% (48 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/hi/ * Translated using Weblate (German) Currently translated at 100.0% (314 of 314 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/de/ * Translated using Weblate (Portuguese (Portugal)) Currently translated at 100.0% (314 of 314 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/pt_PT/ * Translated using Weblate (Portuguese) Currently translated at 100.0% (314 of 314 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/pt/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (314 of 314 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (48 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/hu/ * Translated using Weblate (Welsh) Currently translated at 82.8% (260 of 314 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/cy/ * Translated using Weblate (Welsh) Currently translated at 89.4% (281 of 314 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/cy/ * Translated using Weblate (Greek) Currently translated at 97.9% (47 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/el/ * Translated using Weblate (French) Currently translated at 100.0% (314 of 314 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fr/ * Translated using Weblate (French) Currently translated at 100.0% (48 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/fr/ * Translated using Weblate (Korean) Currently translated at 100.0% (48 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/ko/ --------- Co-authored-by: gallegonovato Co-authored-by: trunars Co-authored-by: ShareASmile Co-authored-by: min7-i Co-authored-by: ssantos Co-authored-by: zalna Rs Co-authored-by: fin-w Co-authored-by: mpt.c Co-authored-by: Victor Lamoine Co-authored-by: Yurical Co-authored-by: Alexander Capehart --- app/src/main/res/values-cy/strings.xml | 49 ++- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-fr/strings.xml | 4 +- app/src/main/res/values-hu/strings.xml | 39 +- app/src/main/res/values-pt-rPT/strings.xml | 32 +- app/src/main/res/values-pt/strings.xml | 336 ++++++++++++++---- .../metadata/android/bg/full_description.txt | 5 +- .../metadata/android/el/short_description.txt | 1 + .../android/es-ES/full_description.txt | 41 ++- .../android/fr-FR/full_description.txt | 1 + .../metadata/android/hi/full_description.txt | 1 + .../metadata/android/hu/full_description.txt | 25 ++ .../metadata/android/ko/full_description.txt | 1 + .../metadata/android/pa/full_description.txt | 1 + 14 files changed, 439 insertions(+), 101 deletions(-) create mode 100644 fastlane/metadata/android/el/short_description.txt create mode 100644 fastlane/metadata/android/hu/full_description.txt diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 831085e58..06b23bdd6 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -34,7 +34,7 @@ Trac Didoli Didoli gan - Ffolderau + Ffolderi Allgáu Rhestri chwarae Rhestr chwarae newydd @@ -87,7 +87,7 @@ Sain I ffwrdd Llyfrgell - Ffolderau cerddoriaeth + Ffolderi cerddoriaeth Modd Cynnwys Adnewyddu cerddoriaeth @@ -107,7 +107,7 @@ Porffor dwfn Glas Glas dwfn - Gwyrddlas + Gwyrddlas 1 Gwyrdd Gwyrdd dwfn Leim @@ -165,7 +165,7 @@ Defnyddio llwybrau Windows Ynghylch Fersiwn - Côd + Cod ffynhonnell Wici Trwyddedau Defnyddio thema ddu pur @@ -179,7 +179,7 @@ Tywyll Lliw Thema ddu - Galluogi corneli crwn ar elfennau UI ychwanegol (angen i gloriau albwm fod yn crwn) + Galluogi corneli crwn ar elfennau UI ychwanegol (angen cloriau albwm i fod yn grwn) Personoleiddio Arddangos Tabiau\'r llyfrgell @@ -215,7 +215,7 @@ Llun y dewis Sain MPEG-1 Sain Ogg - %d kb/e + %d cilobeit yr eiliad %d artistiaid %d artist @@ -258,4 +258,41 @@ %d chân %d cân + Senglau + Sengl + Demo + Demos + Ymddengys ar + Artist + Genre + Genres + Didradd + Plws (+) + Chwarae + Indigo + Gwyrddlas 2 + +%.1f desibelau + -%.1f desibelau + %d Hz + Sengl byw + Sengl ail-gymysgiad + Cyfeiriad + Gradd samplu + Alexander Capehart + Allgáu pethau sydd ddim yn gerddoriaeth + Seibio os yn ailadrodd + Seibio pan fydd cân yn ailadrodd + pre-amp ReplayGain + Cadw cyflwr chwarae + Clirio cyflwr chwarae + Dyfalbarhad + Adfer cyflwr chwarae + Coma (,) + Gwahannod (;) + Blaenslaes (/) + Didoli\'n ddeallus + Didoli enwau sydd yn dechrau gyda rhifau neu enwau megis \"the\" yn gywir (mae\'n gweithio orau gyda Saesneg) + Chwarae\'n awtomatig gyda chlustffon + Ail-sganio cerddoriaeth + %1$s, %2$s \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d807a2b09..da02c01db 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -257,8 +257,8 @@ Wiedergabelistenbild für %s Wiedergabeliste Wiedergabelisten - Artikel beim Sortieren ignorieren - Wörter wie „the“ ignorieren (funktioniert am besten mit englischsprachiger Musik) + Intelligente Sortierung + Korrekte Sortierung von Namen, die mit Nummern oder Wörtern wie „the“ beginnen (funktioniert am besten mit englischsprachiger Musik) Neue Wiedergabeliste Zur Wiedergabeliste hinzugefügt Zur Wiedergabeliste hinzufügen diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b5b01959d..15089eb0b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -260,12 +260,12 @@ %d ko/s +%.1f dB -%.1f dB - Supprimer %s&nbsp;\? Cette opération ne peut pas être annulée. + Supprimer %s ? Cette opération ne peut pas être annulée. Avertissement&nbsp;: Le fait de régler le préamplificateur sur une valeur positive élevée peut entraîner l\'apparition des distortions sur certaines pistes audio. Liste de lecture %d Apparaît sur Renommer la liste de lecture - Supprimer la liste de lecture&nbsp;\? + Supprimer la liste de lecture ? Partager Retirer cette chanson Déplacer cette chanson diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 2deeff420..2ec3f80fd 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -9,7 +9,7 @@ Dalok Összes dal Keresés - Szűrő + Szűrés Összes Rendezés Növekvő @@ -45,7 +45,7 @@ Lejátszás/szünet Vörös - Pink + Rózsaszín Bíbor Mély bíbor Indigókék @@ -62,11 +62,11 @@ %d dal - %d dal + %d dalok %d album - %d album + %d albumok EP-k EP @@ -82,7 +82,7 @@ Ismétlő mód módosítása Keverés be/ki kapcsolása %s album borítója - Visszajátszás + Hangerő normalizálás Mappa eltávolítása Lejátszólistához ad Formátum @@ -230,7 +230,7 @@ Per jel (/) %d előadó - %d előadó + %d előadók Ekvalizer Könyvtár statisztika @@ -289,4 +289,31 @@ Másolva Jelentés Hiba információ + Ki + Importált lejátszási lista + Lejátszási lista exportálva + Demó + ReplayGain Track módosítása + ReplayGain Album módosítása + Szerző + Adományozás + Támogatók + Adományozzon a projektnek, hogy itt legyen a neved! + Szünet megjegyzése + A lejátszás/szünet megmarad a várólista kihagyásakor vagy szerkesztésekor + Cím + Nincsenek albumok + Lejátszási lista exportálása + Használja a Windows-kompatibilis útvonalakat + Path stílus + Export + Import + Lejátszási lista importálása + Demók + Üres lejátszási lista + Abszolút + Relatív + Lejátszási lista importálva + Nem sikerült importálni a lejátszási listát ebből a fájlból + Nem sikerült exportálni a lejátszási listát ebbe a fájlba \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index a401f8890..0544ccc66 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -210,7 +210,7 @@ Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas) %d Selecionadas Misturas DJ - DJ Mix + Mixagem de DJ Aleatório Ocultar colaboradores Limpa os metadados em cache e recarrega totalmente a biblioteca de música (lento, porém mais completo) @@ -243,7 +243,7 @@ %d artistas %d artistas - Configurar ganho de repetição + Normalização de volume Descendente Mudar o tema e cores da app Personalize os controlos e o comportamento do interface do utilizador @@ -282,6 +282,7 @@ Ordenar por Visualizar Música + Apagar lista de reprodução? Eliminar lista de reprodução Lista de reprodução eliminada Relatório @@ -292,4 +293,31 @@ Renomear lista de reprodução Excluir %s\? Não pode ser desfeito. Só aparecer + Desligado + Incapaz de exportar a lista de reprodução para este ficheiro + Lista de reprodução importada + Não foi possível importar uma lista de reprodução deste ficheiro + Demo + Demos + Ajuste de ReplayGain da faixa + Autor + Doar + Apoiadores + Lista de reprodução exportada + Doe para o projeto para ter o seu nome adicionado aqui! + Lembrar pausa + Continuar a reproduzir/pausado quando ao pular ou editar a fila + Lista de reprodução vazia + Importar lista de reprodução + Sem álbuns + Caminho + Importar + Exportar + Exportar lista de reprodução + Estilo de caminho + Absoluto + Lista de reprodução importada + Ajuste de ReplayGain do álbum + Relativo + Usar caminhos compatíveis com Windows \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 202c45c1f..673b23874 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,115 +1,327 @@ - Playlist importada - Monitorando biblioteca de música + Lista de reprodução importada + A monitorizar a biblioteca de música Tentar novamente Mais - Um simples e funcional player de música para o android. - Conceder + Um reprodutor de música simples e racional para Android. + Permitir Música - Todas músicas + Todas as músicas Álbuns Álbum Compilações Importar Gênero - Playlist importada - Gêneros - Tocar - Tocar o próximo - Adicionar a fila - Adicionar a playlist - Propriedades de música + Lista de reprodução importada + Géneros + Reproduzir + Reproduzir a próxima + Adicionar à fila + Adicionar à lista de reprodução + Propriedades da música Formato Fila - Resetar - Fonte do código + Repor + Código fonte Seleção - Carregando sua biblioteca de música… + A carregar biblioteca de músicas… Licenças Estatísticas da biblioteca Artistas - Playlist - Playlists - Adicionar a fila - Adicionado à fila + Lista de reprodução + Listas de reprodução + Adicionada à fila + Adicionado à lista de reprodução Autor - Doação - Monitorando mudanças na sus biblioteca de música… - Playlist exportada - Doe para o projeto para ter seu nome adicionado aqui! - Configurações - Guias da biblioteca - Alterar a visibilidade e a ordem das guias da biblioteca - Ação de notificação personalizada - Playlist vazia - Importar playlist + Doar + A Monitorizar alterações na sua biblioteca de músicas… + Lista de reprodução exportada + Doe para o projeto para ter o seu nome adicionado aqui! + Definições + Abas da biblioteca + Altere a visibilidade e a ordem das abas da biblioteca + Personalizar notificações + Lista de reprodução vazia + Importar lista de reprodução Procurar Filtrar Duração - Qtde. de músicas - Track + Contagem de músicas + Faixa Data adicionada Organizar - Organizar por - Tocando agora + Ordenar por + A tocar agora Equalizador - Embaralhar + Misturar Ir para o artista Ir para o álbum - Ver propriedades - Ver + Propriedades + Visualizar Tudo Data - Carregando música - Carregando música + A carregar música + A carregar música Músicas Álbum ao vivo - Remix de álbum + Álbum remix Single Compilação Artista Renomear - Deletar - Deletar playlist? - Nova Playlist - Renomear playlist + Eliminar + Apagar lista de reprodução? + Nova lista de reprodução + Renomear lista de reprodução Editar Nome Cancelar Adicionar Salvar - Mudar o tema e as cores do app + Estado salvo + Mudar o tema e cores da app Tema Claro Automático Caminho - Erro de informação + Informações de erro Copiado - Reportar - Ver e controlar reprodução de música - Playlist criada - Veja e sinta - Tema escuro - Usar um tema escuro pure-black + Relatório + Vêr e controlar a reprodução da música + Lista de reprodução criada + Aparência + Tema preto + Utilizar tema preto puro Modo redondo - Habilite cantos arredondados em elementos adicionais da interface do usuário (requer que as capas dos álbuns sejam arredondadas) + Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas) Personalizar - Personalize os controles e o comportamento da IU + Personalize os controlos e o comportamento do interface do utilizador Escuro Esquema de cores - Pular para o próximo - Modo de loop + Avançar para o próximo + Modo de repetição Comportamento - Exportar playlist - Compartilhar + Exportar lista de reprodução + Partilhar Tamanho - Embaralhar - Embaralhar tudo + Aleatório + Misturar tudo Exportar Sobre Versão - Playlist renomeada - Playlist deletada - Procurar sua biblioteca… + Lista de reprodução renomeada + Lista de reprodução eliminada + Procurar na biblioteca… + Desligado + Configurar onde a música deve ser carregada + Pastas + Modo + Ativar ou desativar a reprodução aleatória + Seleção de imagem + Verde + Disco %d + %d Hz + Músicas carregado: %d + Álbuns carregados: %d + Artistas carregados: %d + Preferir álbum + + %d Música + %d Músicas + %d Músicas + + Demo + Normalização de volume + Não foi possível limpar a lista + Ajuste de ReplayGain da faixa + Ajuste de ReplayGain do álbum + Desenvolvido por Alexander Capehart + Apoiadores + Mostrar + Personalizar a barra de reprodução + Forçar capas em formato quadrado + Recortar à capa dos álbuns numa proporção de 1:1 + Configurar o som e comportamento da reprodução + Reprodução + Retroceder antes de voltar + Retrocede a música antes de voltar para a anterior + Pausar na repetição + Pausar quando uma música é repetida + Pastas de música + Lembrar pausa + Continuar a reproduzir/pausado quando ao pular ou editar a fila + Estratégia do ganho de repetição + Prefira o álbum se estiver a tocar + +%.1f dB + -%.1f dB + %d kbps + Gêneros carregados: %d + Direção + Ascendente + Descendente + Desligado + A música não será carregada das pastas que adicionar. + A música será somente carregada das pastas que adicionar. + Recarregar música + Miniálbuns + EP + EP ao vivo + Álbum de Remix + Singles + Single ao vivo + Single remix + Compilação ao vivo + Mistura de compilações + Trilhas sonoras + Trilha sonora + Mixtapes + Mixagem de DJ + Ao vivo + Remixes + Disco + Wiki + Ignorar ficheiros de áudio que não são música, tal como podcasts + Classificação inteligente + Ignore palavras como \"the\" ao classificar por nome (funciona melhor com músicas em inglês) + Imagens + Áudio + Pré-amplificação da normalização de volume + O pré-amplificador é aplicado ao ajuste existente durante a reprodução + Ajustar com etiquetas + Ajustar sem etiquetas + Persistência + Gravar estado da reprodução + Salvar o estado de reprodução atual + Limpar estado de reprodução + Restaurar o estado de reprodução + Restaurar o estado de reprodução salvo anteriormente (se houver) + Nenhuma música encontrada + Falha ao carregar música + O Auxio precisa de permissão para ler a sua biblioteca de músicas + Música %d + Reproduzir ou pausar + Pular para a próxima música + Pular para a última música + Embaralhar todas as músicas + Ícone do Auxio + Capa do álbum + Nenhum disco + Nenhuma faixa + Nenhuma música + Nenhuma música tocando + Vermelho + Rosa + Roxo + Roxo profundo + Índigo + Azul + Verde profundo + Verde-amarelo + Amarelo + Laranja + Moreno + + %d Álbum + %d Álbuns + %d Álbuns + + + %d artista + %d artistas + %d artistas + + Ao tocar da biblioteca + Ao tocar a partir dos detalhes do item + Reproduzir do artista + Tocar a partir do gênero + Tocar música sozinha + Memorizar música misturada + Mantenha a reprodução aleatória ao reproduzir uma nova música + Ponto-e-vírgula (;) + Barra (/) + Mais (+) + Aviso: Usar essa configuração pode resultar em algumas etiquetas serem interpretadas incorretamente como tendo múltiplos valores. Pode resolver isso pré-definindo caracteres de separador indesejados com uma barra invertida (\\). + Ocultar colaboradores + Mostrar apenas artistas que foram creditados diretamente no álbum (funciona melhor em músicas com metadados completos) + Capas de álbuns + Rápido + Gênero desconhecido + Sem data + Codec de Audio Gratuito Sem Perdas (FLAC) + Ciano + Lista de reprodução %d + A carregar a sua biblioteca de músicas… (%1$d/%2$d) + Excluir %s? Não pode ser desfeito. + Taxa de bits + Taxa de amostragem + OK + Estilo de caminho + Absoluto + Relativo + Usar caminhos compatíveis com Windows + Codificação de Audio Avançada (AAC) + A editar %s + Demos + Misturas DJ + Só aparecer + Mixtape + Reproduzir a partir do item mostrado + Reproduzir de todas as músicas + Reproduzir do álbum + Conteúdo + Controlar como a música e as imagens são carregadas + Música + Recarregamento automático + Recarrega a biblioteca de músicas sempre que ela mudar (requer notificação fixa) + Excluir não-música + Separadores multi-valor + Configurar caracteres que denotam múltiplos valores de etiqueta + Vírgula (,) + E comercial (&) + Qualidade alta + Reprodução automática dos auscultadores + Iniciar música quando os auscultadores forem conectados (pode não funcionar em todos os aparelhos) + Preferir faixa + Aviso: Alterar o pré-amplificador para um valor positivo alto pode resultar em picos em algumas faixas de áudio. + Biblioteca + Excluir + Incluir + Recarrega a biblioteca de músicas usando metadados salvos em cache quando possível + Procurar músicas novamente + Limpa os metadados em cache e recarrega totalmente a biblioteca de música (lento, porém mais completo) + Limpar o estado de reprodução salvo anteriormente (se houver) + Não foi possível importar uma lista de reprodução deste ficheiro + Incapaz de exportar a lista de reprodução para este ficheiro + Nenhuma aplicação encontrada que possa executar esta tarefa + Sem pastas + Esta pasta não é compatível + Nenhuma lista pode ser restaurada + Não foi possível gravar a lista + Alterar o modo de repetição + Criar nova lista de reprodução + Parar reprodução + Remover esta música de fila + Mover esta música da fila + Abra a fila + Mover esta guia + Limpar consulta de pesquisa + Remover pasta + Capa do álbum para %s + Imagem do artista para %s + Imagem de gênero para %s + Imagem da lista de reprodução de %s + Artista desconhecido + Sem álbuns + Áudio MPEG-1 + Áudio MPEG-4 + Áudio Ogg + Áudio Matroska + Azul profundo + Azul-verde + Grisalho + Dinâmico + %1$s, %2$s + %d Selecionadas + Duração total: %s \ No newline at end of file diff --git a/fastlane/metadata/android/bg/full_description.txt b/fastlane/metadata/android/bg/full_description.txt index efa66010a..b959b6bc8 100644 --- a/fastlane/metadata/android/bg/full_description.txt +++ b/fastlane/metadata/android/bg/full_description.txt @@ -1,4 +1,4 @@ -Auxio е локален музикален плейър с бърз, надежден UI/UX без много безполезни функции, присъстващи в други музикални плейъри. Изграден от модерни библиотеки за възпроизвеждане на мултимедия, Auxio има превъзходна поддръжка на библиотека и качество на слушане в сравнение с други приложения, които използват остаряла функционалност на Android. Накратко, Възпроизвежда музика. +Auxio е локален музикален плейър с бърз, надежден UI/UX без много безполезни функции, присъстващи в други музикални плейъри. Изграден от модерни библиотеки за възпроизвеждане на мултимедия, Auxio има превъзходна поддръжка на библиотека и качество на слушане в сравнение с други приложения, които използват остаряла функционалност на Android. Накратко,Възпроизвежда музика. Характеристики @@ -8,10 +8,11 @@ Auxio е локален музикален плейър с бърз, надеж - Персонализирано поведение - Поддръжка за номера на дискове, множество изпълнители, типове издания, точни/оригинални дати, етикети за сортиране и др -- Разширена система за изпълнители, която обединява изпълнители и изпълнители на албуми +- Усъвършенствана система за изпълнители, която обединява изпълнители и изпълнители на албуми - Управление на папки с SD карта - Надеждна функционалност за плейлисти - Устойчивост на състоянието на възпроизвеждане +- Android автоматична поддръжка - Автоматично възпроизвеждане без пропуски - Пълна поддръжка на ReplayGain (на MP3, FLAC, OGG, OPUS и MP4 файлове) - Поддръжка на външен еквалайзер (напр. Wavelet) diff --git a/fastlane/metadata/android/el/short_description.txt b/fastlane/metadata/android/el/short_description.txt new file mode 100644 index 000000000..752cd28a5 --- /dev/null +++ b/fastlane/metadata/android/el/short_description.txt @@ -0,0 +1 @@ +Μία απλή, λογική συσκευή αναπαραγωγής μουσικής diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt index fe6073caa..1ffe22d7e 100644 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -1,22 +1,25 @@ -Auxio es un reproductor de música nativo con una UI/UX rápida y sólida sin las muchas funciones inútiles que se encuentran en otros reproductores de música. En comparación con otras aplicaciones que utilizan la API nativa de MediaPlayer, Auxio se basa en Exoplayer, con una mejor experiencia auditiva y mejor compatibilidad con la biblioteca de música, exactamente como debería ser un reproductor de música. +Auxio es un reproductor de música local con una interfaz de usuario y una experiencia de usuario rápidas y fiables sin las numerosas funciones inútiles presentes en otros reproductores de música. Creado a partir de bibliotecas de reproducción de medios modernas, Auxio tiene un soporte de biblioteca y una calidad de escucha superiores en comparación con otras aplicaciones que utilizan funciones obsoletas de Android. En resumen, reproduce música. Características -- Reproducción basada en ExoPlayer -- Interfaz inteligente derivada de las últimas especificaciones de diseño de Material You -- Una experiencia de usuario única que prioriza la facilidad de uso -- Comportamiento del reproductor personalizable -- Admite número de disco, múltiples artistas, tipo de lanzamiento, fecha exacta/original, -Clasificación de pestañas y más -- Sistema avanzado de "Artista" que unifica "Artista" y "Artista del álbum" -- La función de administración de carpetas admite tarjeta SD -- Diseño confiable y persistente del progreso de la reproducción. -- Soporte completo para ReplayGain (incluidos archivos MP3, FLAC, OGG, OPUS y MP4) -- Soporte para ecualizadores externos (aplicaciones como Wavelet) -- Diseño de borde a borde -- Soporte de cubierta integrado -- buscar función -- Reproducción automática cuando los auriculares están conectados -- Widgets estilizados que se adaptan al tamaño de la oficina. -- Completamente fuera de línea y privado -- Carátula del álbum sin esquinas redondeadas (también puedes tenerla si quieres) +- Reproducción basada en ExoPlayer media 3 +- Interfaz de usuario ágil basada en las últimas directrices de Material Design +- Experiencia de usuario que prioriza la facilidad de uso sobre los casos extremos +- Comportamiento personalizable +- Soporte para números de disco, múltiples artistas, tipos de lanzamiento +fechas precisas/originales, etiquetas de clasificación, etc. +- Sistema avanzado de artistas que unifica artistas y artistas de álbumes +- Gestión de carpetas compatible con tarjetas SD +- Funcionalidad fiable de listas de reproducción +- Persistencia del estado de reproducción +- Compatibilidad con Android auto +- Reproducción automática sin pausas +- Compatibilidad total con ReplayGain (en archivos MP3, FLAC, OGG, OPUS y MP4) +- Compatibilidad con ecualizadores externos (por ejemplo, Wavelet) +- Borde a borde +- Compatibilidad con carátulas incrustadas +- Función de búsqueda +- Reproducción automática con auriculares +- Widgets elegantes que se adaptan automáticamente a su tamaño +- Completamente privado y sin conexión +- Sin carátulas redondeadas (por defecto) diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt index aeb864c64..a2e737b5a 100644 --- a/fastlane/metadata/android/fr-FR/full_description.txt +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -12,6 +12,7 @@ les dates précises/originales, le classement par tags, and plus encore - Carte SD reconnue par le système de dossiers - Fonction de liste de lecture efficace - Statut de lecture persistant +- Support d'Android auto - Support complet de ReplayGain (pour les fichiers MP3, FLAC, OGG, OPUS, et MP4) - Support pour égaliseur externe (ex. Wavelet) - Navigation bord-à-bord diff --git a/fastlane/metadata/android/hi/full_description.txt b/fastlane/metadata/android/hi/full_description.txt index be3a02a95..73982b4f2 100644 --- a/fastlane/metadata/android/hi/full_description.txt +++ b/fastlane/metadata/android/hi/full_description.txt @@ -12,6 +12,7 @@ Auxio एक तेज़, विश्वसनीय UI/UX वाला एक - एसडी कार्ड-जागरूक फ़ोल्डर प्रबंधन - विश्वसनीय प्लेलिस्टिंग कार्यक्षमता - प्लेबैक अवस्था दृढ़ता +- एंड्रॉयड ऑटो समर्थन - स्वचालित गैपलेस प्लेबैक - पूर्ण रीप्लेगैन समर्थन (MP3, FLAC, OGG, OPUS और MP4 फ़ाइलों पर) - बाहरी तुल्यकारक समर्थन (उदा: वेवलेट) diff --git a/fastlane/metadata/android/hu/full_description.txt b/fastlane/metadata/android/hu/full_description.txt new file mode 100644 index 000000000..3cb5b16cd --- /dev/null +++ b/fastlane/metadata/android/hu/full_description.txt @@ -0,0 +1,25 @@ +Az Auxio egy helyi zenelejátszó gyors, megbízható felhasználói felülettel/UX-szel, anélkül, hogy a többi zenelejátszóban jelen lévő sok haszontalan funkciót tartalmazna. A modern médialejátszó könyvtárakra épülő Auxio kiváló könyvtártámogatással és zenehallgatási minőséggel rendelkezik, mint más, elavult Android-funkciókat használó alkalmazások. Röviden: Zenét játszik. + +Funkciók + +- Lejátszás Media3 ExoPlayer alapú +- A legfrissebb anyagtervezési irányelvekből származó, lendületes felhasználói felület +- Véleményes UX, amely előnyben részesíti a könnyű használatot a szélső esetekben +- Testreszabható viselkedés +- Lemezszámok, több előadó, kiadástípusok támogatása, +pontos/eredeti dátumok, címkék rendezése stb +- Fejlett előadói rendszer, amely egyesíti az előadókat és az album előadókat +- SD-kártya-tudatos mappakezelés +- Megbízható lejátszási lista funkció +- Lejátszási állapot fennmaradása +- Android automatikus támogatás +- Automatikus hézagmentes lejátszás +- Teljes ReplayGain támogatás (MP3, FLAC, OGG, OPUS és MP4 fájlokon) +- Külső hangszínszabályzó támogatás (pl. Wavelet) +- Éltől szélig +- Beágyazott borítók támogatása +- Keresés funkció +- Headset automatikus lejátszása +- Stílusos kütyük, amelyek automatikusan alkalmazkodnak a méretükhöz +- Teljesen privát és offline +- Nincsenek lekerekített albumborítók (alapértelmezés szerint) diff --git a/fastlane/metadata/android/ko/full_description.txt b/fastlane/metadata/android/ko/full_description.txt index 45f7cc6ca..c03243343 100644 --- a/fastlane/metadata/android/ko/full_description.txt +++ b/fastlane/metadata/android/ko/full_description.txt @@ -12,6 +12,7 @@ Auxio는 다른 음악 플레이어에 존재하는 쓸모없는 기능 없이, - SD 카드를 지원하는 폴더 관리 기능 - 안정적인 재생 목록 기능 - 이전 재생 상태 기억 +- Android Auto 지원 - 자동 갭리스 재생 지원 - ReplayGain 완벽 지원 (MP3, FLAC, OGG, OPUS, MP4) - 외부 이퀄라이저 지원 (Wavelet 등) diff --git a/fastlane/metadata/android/pa/full_description.txt b/fastlane/metadata/android/pa/full_description.txt index 646c41be8..25d95cc5c 100644 --- a/fastlane/metadata/android/pa/full_description.txt +++ b/fastlane/metadata/android/pa/full_description.txt @@ -12,6 +12,7 @@ Auxio ਇੱਕ ਤੇਜ਼, ਭਰੋਸੇਮੰਦ UI/UX ਵਾਲਾ ਇੱ - ਭਰੋਸੇਯੋਗ ਪਲੇਅਲਿਸਟਿੰਗ ਕਾਰਜਕੁਸ਼ਲਤਾ - ਭਰੋਸੇਯੋਗ ਪਲੇਅਬੈਕ ਸਥਿਤੀ ਸਥਿਰਤਾ +- ਐਂਡਰੌਇਡ ਆਟੋ ਸਪੋਰਟ - ਆਟੋਮੈਟਿਕ ਗੈਪਲੈੱਸ ਪਲੇਅਬੈਕ - ਪੂਰਾ ਰੀਪਲੇਅ-ਗੇਨ ਸਮਰਥਨ (MP3, FLAC, OGG, OPUS, ਅਤੇ MP4 ਫਾਈਲਾਂ 'ਤੇ) - ਬਾਹਰੀ ਈਕੋਲਾਈਜ਼ਰ ਦਾ ਸਮਰਥਨ (ਉਦਾਹਰਨ. ਵੇਵਲੇਟ) From 2e9647d1dcb2fbf6fe34c262103077d9c61f8447 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 21 Aug 2024 11:34:46 -0600 Subject: [PATCH 078/550] ui: fix duplicate string --- app/src/main/res/values-pt-rPT/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 0544ccc66..6ec67d5b0 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -283,7 +283,6 @@ Visualizar Música Apagar lista de reprodução? - Eliminar lista de reprodução Lista de reprodução eliminada Relatório Nova lista de reprodução From b8e54b3707abf01acef71cbb8ab1ceccd09af8ad Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 26 Aug 2024 11:42:50 -0600 Subject: [PATCH 079/550] info: remove sponsor --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0eca7d6ea..365464252 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,6 @@ You can support Auxio's development through [my Github Sponsors page](https://gi

-

From 94c48406720f9a1c8e2e45d05836177e3469cdc2 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 17 Sep 2024 23:42:29 +0200 Subject: [PATCH 080/550] Translations update from Hosted Weblate (#850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ * Translated using Weblate (Croatian) Currently translated at 100.0% (48 of 48 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/hr/ * Translated using Weblate (Croatian) Currently translated at 100.0% (300 of 300 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/hr/ * Translated using Weblate (Spanish) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/es/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/zh_Hans/ * Translated using Weblate (Czech) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/cs/ * Translated using Weblate (Finnish) Currently translated at 92.0% (278 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fi/ * Translated using Weblate (German) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/de/ * Translated using Weblate (Hindi) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/hi/ * Translated using Weblate (German) Currently translated at 100.0% (50 of 50 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/de/ * Translated using Weblate (Punjabi) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/pa/ * Translated using Weblate (Finnish) Currently translated at 92.7% (280 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fi/ * Translated using Weblate (Finnish) Currently translated at 98.6% (298 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fi/ * Translated using Weblate (Finnish) Currently translated at 100.0% (50 of 50 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/fi/ * Translated using Weblate (Bulgarian) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/bg/ * Translated using Weblate (Korean) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ko/ * Translated using Weblate (Croatian) Currently translated at 99.6% (301 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/hr/ * Translated using Weblate (Welsh) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/cy/ * Translated using Weblate (Croatian) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/hr/ * Translated using Weblate (Russian) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ru/ * Translated using Weblate (Interlingua) Currently translated at 68.8% (208 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ia/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/uk/ * Translated using Weblate (Belarusian) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/be/ * Translated using Weblate (Lithuanian) Currently translated at 100.0% (302 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/lt/ * Added translation using Weblate (Estonian) * Translated using Weblate (Estonian) Currently translated at 46.6% (141 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/et/ * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (50 of 50 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/zh_Hant/ * Translated using Weblate (Russian) Currently translated at 100.0% (50 of 50 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/ru/ * Translated using Weblate (Estonian) Currently translated at 55.9% (169 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/et/ * Translated using Weblate (Estonian) Currently translated at 66.2% (200 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/et/ * Translated using Weblate (Estonian) Currently translated at 77.4% (234 of 302 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/et/ --------- Co-authored-by: Milo Ivir Co-authored-by: gallegonovato Co-authored-by: 大王叫我来巡山 Co-authored-by: Fjuro Co-authored-by: Riku Co-authored-by: qwerty287 Co-authored-by: ShareASmile Co-authored-by: trunars Co-authored-by: Yurical Co-authored-by: fin-w Co-authored-by: Макар Разин Co-authored-by: Software In Interlingua Co-authored-by: Vaclovas Intas Co-authored-by: Priit Jõerüüt Co-authored-by: abc0922001 Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com> --- app/src/main/res/values-be/strings.xml | 4 + app/src/main/res/values-bg/strings.xml | 4 + app/src/main/res/values-cs/strings.xml | 4 + app/src/main/res/values-cy/strings.xml | 56 +++- app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values-es/strings.xml | 4 + app/src/main/res/values-et/strings.xml | 239 ++++++++++++++++++ app/src/main/res/values-fi/strings.xml | 23 +- app/src/main/res/values-hi/strings.xml | 4 + app/src/main/res/values-hr/strings.xml | 12 +- app/src/main/res/values-ia/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 4 + app/src/main/res/values-lt/strings.xml | 4 + app/src/main/res/values-pa/strings.xml | 4 + app/src/main/res/values-pt/strings.xml | 12 - app/src/main/res/values-ru/strings.xml | 4 + app/src/main/res/values-uk/strings.xml | 4 + app/src/main/res/values-zh-rCN/strings.xml | 4 + .../metadata/android/de/full_description.txt | 1 + .../metadata/android/fi/full_description.txt | 23 ++ .../metadata/android/hr/full_description.txt | 2 + .../metadata/android/ru/full_description.txt | 1 + .../android/zh-Hant/full_description.txt | 1 + 23 files changed, 389 insertions(+), 30 deletions(-) create mode 100644 app/src/main/res/values-et/strings.xml create mode 100644 fastlane/metadata/android/fi/full_description.txt diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index a28d02db7..6c4d965d5 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -315,4 +315,8 @@ Запамінаць паўзу Пакідаць прайграванне/паўзу падчас пропуску або рэдагаванні чаргі Адкл. + Пачаць прайграванне + Запускаць auxio, выкарыстоўваючы раней захаваны стан. Калі захаваны стан недаступны, усе песні будуць ператасаваныя. Прайграванне пачнецца неадкладна. +\n +\nПапярэджанне: будзьце асцярожныя пры кіраванні гэтай службай, калі вы закрыеце яе, а затым паспрабуеце выкарыстоўваць зноў, вы, верагодна, прывядзеце да збою прыкладання. \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 01aebdd0e..946e4d3f7 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -309,4 +309,8 @@ Advanced Audio Coding (AAC) %d Hz Неизвестен жанр + Стартира Auxio, използвайки предварително запазеното състояние. Ако няма налично запазено състояние, всички песни ще бъдат разбъркани. Възпроизвеждането ще започне веднага. +\n +\nПРЕДУПРЕЖДЕНИЕ: Бъдете внимателни, когато контролирате тази услуга, ако я затворите и след това опитате да я използвате отново, вероятно ще сринете приложението. + Стартирай възпроизвеждането \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4f9cac540..8c4845613 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -326,4 +326,8 @@ Zůstat ve stavu přehrávání/pozastavení při přeskakování nebo úpravě fronty Zapamatovat pozastavení Vypnuto + Spustit přehrávání + Spustí Auxio pomocí naposledy uloženého stavu. Pokud není dostupný žádný uložený stav, budou náhodně přehrány všechny skladby. Přehrávání se spustí ihned. +\n +\nVAROVÁNÍ: při ovládání této služby buďte opatrní, pokud ji zavřete a pak se ji znovu pokusíte použít, aplikace nejspíše spadne. \ No newline at end of file diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 06b23bdd6..691a6e407 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -92,7 +92,7 @@ Cynnwys Adnewyddu cerddoriaeth Trac %d - Cymysgu holl ganeuon + Chwarae pob cân ar hap Artist anhysbys Genre anhysbys Dim dyddiad @@ -142,7 +142,7 @@ Yn chwarae nawr Gosodiadau sain Chwarae - Cymysgu + Chwarae ar hap Ciwio Esgynnol Chwarae nesaf @@ -154,8 +154,8 @@ Gweld Rhannu Llwybr - Cymysgu - Cymysgu i gyd + Chwarae ar hap + Chwarae pob cân ar hap Iawn Diddymu Cadw @@ -183,7 +183,7 @@ Personoleiddio Arddangos Tabiau\'r llyfrgell - Symud i\'r nesaf + Neidio i\'r nesaf Modd ailadrodd Ymarweddiad Wrth chwarae o\'r llyfrgell @@ -207,8 +207,8 @@ Mae ar Auxio angen caniatâd i ddarllen eich llyfrgell gerddoriaeth Methwyd dod o hyd ap sydd yn gallu gwneud y tasg hon Dim ffolderi - Symud i\'r gân nesaf - Symud i\'r gân ddiwethaf + Neidio i\'r gân nesaf + Neidio i\'r gân ddiwethaf Newid y modd ail-chwarae Symud y gân hon Llun rhestr chwarae %s @@ -232,7 +232,7 @@ %d albwm %d albwm - Cofio\'r cymysgu + Cofio y modd chwarae ar hap Methwyd llwytho rhestr chwarae o\'r ffeil hon Methwyd allforio\'r rhestr chwarae i\'r ffeil hon Symud y tab hwn @@ -243,7 +243,7 @@ Llun artist %s Llun genre %s Chwarae neu seibio - Troi\'r modd cymysgu ymlaen neu\'i ddiffodd + Troi\'r modd chwarae ar hap ymlaen neu i ffwrdd Stopio\'r chwarae Tynnu\'r gân hon Agor y ciw @@ -283,10 +283,6 @@ Seibio os yn ailadrodd Seibio pan fydd cân yn ailadrodd pre-amp ReplayGain - Cadw cyflwr chwarae - Clirio cyflwr chwarae - Dyfalbarhad - Adfer cyflwr chwarae Coma (,) Gwahannod (;) Blaenslaes (/) @@ -295,4 +291,38 @@ Chwarae\'n awtomatig gyda chlustffon Ail-sganio cerddoriaeth %1$s, %2$s + Rheoli o ble i lwytho cerddoriaeth + Free Lossless Audio Codec (FLAC) + Addasu rheolaethau\'r UI ac ymarweddiad + Gweithred addasedig ar y bar chwarae + Gweithred hysbysiad addasedig + Gwahanydd aml-werth + Tocio pob clawr albwm i gymhareb agwedd 1:1 + Rhybudd: Gall newid y pre-amp i lefel bositif uchel yn achosi uchafbwyntiau ar rai traciau sain. + Ni fydd cerddoriaeth yn cael ei llwytho o\'r ffolderi rydych yn eu hychwanegu. + Bydd cerddoriaeth yn cael ei llwytho dim ond o\'r ffolderi rydych yn eu hychwanegu. + Ail-lwytho\'r llyfrgell cerddoriaeth, gan ddefnyddio tagiau wedi\'u cadw\'n barod pan fo modd + Dileu\'r tagiau wedi\'u cadw\'n barod ac ail-lwytho\'r llyfrgell gerddoriaeth yn llawn (yn arafach, ond yn fwy cyflawn) + Ni chefnogir y ffolder hon + Advanced Audio Coding (AAC) + Addasu Albwm ReplayGain + Addasu Trac ReplayGain + Mynd i ddechrau\'r gân cyn neidio\'n ôl + Mynd i ddechrau\'r gân cyn neidio i\'r gân flaenorol + Parhau i chwarae / oedi wrth neidio neu wrth golygu\'r ciw + Dechrau chwarae + Diystyru ffeiliau sain sydd ddim yn gerddoriaeth, megis podlediadau + Rhybudd: Gall ddefnyddio\'r gosodiad hwn achosi i rai tagiau i gael eu dehongli\'n anghywir fel bod â gwerthoedd lluosog. Gallwch ddatrys hyn trwy ragddodi llythrennau gwahanydd diangen ag adlach (\\). + Dangos dim ond artistiaid sy\'n cael eu cydnabyddiaeth ar albwm (mae\'n gweithio gorau ar llyfrgelloedd gyda thagiau da) + Dechrau chwarae bob amser pan fydd clustffon wedi\'i gysylltu (efallai na fydd yn gweithio gyda phob un dyfeisiau) + Mae\'r pre-amp yn cael ei gymhwyso i\'r addasiad presennol yn ystod chwarae + Yn dechrau Auxio yn y cyflwr wedi\'i gadw\'n barod. Os nad yw cyflwr sydd wedi\'i gadw ar gael, bydd pob cân yn cael eu chwarae ar hap. Bydd chwarae yn dechrau\'n syth. +\n +\nRHYBUDD: Byddwch yn ofalus wrth reoli\'r gwasanaeth hwn. Os ydych yn ei gau ac yna\'n ceisio ei ddefnyddio, mae\'n debygol y byddwch yn chwalu\'r ap. + Cadw y modd chwarae ar hap wrth chwarae cân newydd + Rheoli sut mae cerddoriaeth a lluniau\'n cael eu llwytho + Ail-lwytho\'r llyfrgell cerddoriaeth pryd bynnag y bydd yn ei newid (mae angen hysbysiad di-ball) + Ffurfweddu llythrennau sydd yn dynodi mwy nag un gwerth tag + Ampersand (&) + Ffurfweddu sain ac ymarweddiad chwarae \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index da02c01db..4ccf94c1c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -317,4 +317,8 @@ Pause merken Wiedergabe/Pause beim Springen oder Bearbeiten der Warteschlange beibehalten Aus + Wiedergabe starten + Startet Auxio mit dem vorher gespeicherten Zustand. Wenn kein gespeicherter Zustand vorhanden ist, werden alle Lieder zufällig abgespielt. Die Wiedergabe startet sofort. +\n +\nACHTUNG: Sei vorsichtig bei der Steuerung dieses Dienstes. Wenn Sie ihn schließen und dann erneut versuchen, ihn zu nutzen, stürzt die App wahrscheinlich ab. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 27fe726d7..16405269a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -321,4 +321,8 @@ Recordar la pausa Permanecer en reproducción/pausa al saltar o editar la cola Apagar + Iniciar reproducción + Inicia Auxio utilizando el estado previamente guardado. Si no hay ningún estado guardado, todas las canciones se reproducirán aleatoriamente. La reproducción comenzará inmediatamente. +\n +\nADVERTENCIA: Ten cuidado al controlar este servicio, si lo cierras y luego intentas usarlo de nuevo, probablemente se bloqueará la aplicación. \ No newline at end of file diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml new file mode 100644 index 000000000..33de06d4a --- /dev/null +++ b/app/src/main/res/values-et/strings.xml @@ -0,0 +1,239 @@ + + + Muusika on laadimisel + Jälgime muudatusi muusikakogus + Veel + Anna õigused + Pala + Albumid + Album elavas esituses muusikaga + EP + EP elavas esituses muusikaga + EP remiksidega + Singel + Singel elavas esituses muusikaga + Singel remiksidega + Kogumikud + Kogumik elavas esituses muusikaga + Kogumik remiksidega + Filmimuusika palad + Filmimuusika + Mixtape-kogumik + Mixtape + Demo + Elavas esituses + Esineb + Esitajaid + Žanr + Esitusloendid + Uus esitusloend + Imporditud esitusloend + Impordi esitusloend + Muuda nime + Kustuta + Otsi + Kõik + Nimi + Kuupäev + Palade arv + Plaat + Rada + Lisamise kuupäev + Suund + Kasvavalt + Esita järgmisena + Lisa esitusjärjekorda + Mine esitaja juurde + Mine albumi juurde + Loo teave + Asukoht + Vorming + Bitikiirus + Diskreetimissagedus + Lookohane esitusvaljuse tundlikkus + Albumikohane esitusvaljuse tundlikkus + Sega lood + Sega kõik lood + Alusta taasesitust + Katkesta + Salvesta + Lisa + Asukoha kuvamise viis + Absoluutne + Rakenduse teave + Versioon + Viki + Litsentsid + Muusikakogu statistika + Valik + Veateave + Kopeeritud lõikelauale + Teata veast + Autor + Arenduse eestvedaja Alexander Capehart + Toeta arendajat + Toetajad + Laadime sinu muusikakogu… + Jälgime muudatusi sinu muusikakogus… + Esitusloend on loodud + Esitusloend on imporditud + Esitusloendi nimi on muudetud + Esitusloend on eksporditud + Esitusloend on kustutatud + Lisasime esitusloendisse + Seadistused + Välimus ja tunnetus + Muuda rakenduse kujundust ja värve + Kujundus + Automaatne kujundus + Ümarad nurgad + Kasutajaliidese elementide puhul pruugi ümaraid nurki (eeldab, et ka kaanepiltide nurgad on ümarad) + Laadime muusikat + Lihtne ja ratsionaalne muusikamängija Androidi jaoks. + Album + Proovi uuesti + Palad + Kõik palad + Album remiksidega + EP-albumid + Singlid + Kogumik + Demo-kogumik + DJ-miksid + DJ-miks + Remiksid + Esitaja + Ekspordi esitusloend + Žanrid + Esitusloend + Tühi esitusloend + Muuda esitusloendi nime + Impordi + Ekspordi + Kas kustutame esitusloendi? + Muuda + Filtreeri + Kestus + Järjesta + Sega lood + Järjestuse alus + Kahanevalt + Hetkel esitamisel + Esita + Esitusjärjekord + Vaata teavet + Ekvalaiser + Lisa esitusloendisse + Vaata + Jaga + Suurus + Sobib + Suhteline + Kasuta Windowsiga ühilduvaid asukohti + Lähtekood + Vaata ja halda muusika esitamist + Lähtesta + Lisatud esitusjärjekorda + Kui soovid siin näha oma nime, siis toeta rahaliselt meie arendust! + Otsi oma muusikakogust… + Hele kujundus + Must kujundus + Tume kujundus + Tumeda kujunduse puhul kasuta päris musta kujundust + Värviteema + Purpurpunane + Sügav purpurpunane + Kollane + Rohekassinine + %d. esitusloend + Sinine + Sügavsinine + Free Lossless Audio Codec (FLAC) + Sinakasroheline + %d kbps + Punane + Indigosinine + Roheline + Sügavroheline + Pruun + Roosa + Hall + Laimiroheline + Oranž + Dünaamiline värv + %1$s, %2$s + -%.1f dB + %d valitud + Muudame esitusloendit %s + %d. plaat + +%.1f dB + %d Hz + Laadime sinu muusikakogu… (%1$d/%2$d) + Auxio käivitub varem salvestatud olekus. Kui sellist olekut ei leidu, siis segatakse kõikide lugude järjekord ning taasesitus algab koheselt. +\n +\nHOIATUS: Ole selle teenuse kasutamisel hoolikas - kui sa ta sulged ja siis proovid uuesti kasutada, siis rakendus ilmselt jookseb kokku. + Kohanda endale sobivaks + Seadista kasutajaliidese juhtnuppe ning käitumist + Ekraan + Muusikakogu kaardid + Muuda muusikakogu kaartide nähtavust ja järjekorda + Pildid + Kaanepildid + Pole kasutusel + Kuva kõrgekvaliteedilisena + Kuva kiirelt + Näita kaanepildid teravate nurkadega + Heli + Kadreeri kaanepildid 1:1 küljesuhte alusel + Seadista heli ja taasesitust + Taasesitus + Automaatne esitus kõrvaklappidest + Kõrvaklappide ühendamisel alusta koheselt meedia mängimist (ei pruugi toimida kõikide seadmetega) + Kordamisel tee paus + Enne tagasi hüppamist keri tagasi + Enne eelmise looni tagasi hüppamist keri jooksev lugu tagasi + Loo kordamisel tee esituse paus + Jäta paus meelde + Esitusjärjekorras vahelejätmisel või selle muutmisel jää senisesse olekusse (mängimine või paus) + Helivaljuse normaliseerimine + Esitusvaljuse tundlikkuse seadistamise strateegia + Pole kasutusel + Eelista palakohast esitusvaljuse tundlikkust + Eelista albumikohast esitusvaljuse tundlikkust + Kui album juba on esitamisel, siis eelista albumikohast esitusvaljuse tundlikkust + Esitusvaljuse tundlikkuse eelvõimendus + Kohandatud toimingud teavitusel + Kohandatud toimingud taasesitusribal + Esitamisel muusikakogust + Esita näidatud palast + Esita žanrist + Halda muusika ja piltide laadimist + Muusika + Muusikakogu automaatne uuendamine + Välista failid, kus pole muusikat + Järjesta muusikat nii, et arvesse ei lähe numbrid ja artiklid nagu „the“ (toimib kõige paremini ingliskeelse muusika puhul) + Peida kaasautorid + Hüppa järgmise juurde + Taasesituse viis + Esita albumist + Käitumine + Esitamisel pala detailsest vaatest + Esita kõikidest paladest + Esita esitajast + Esita samast palast + Jäta meelde segatud loend + Uue pala esitamisel jäta meelde segatud loendi olek + Sisu + Eira selliseid helifaile, kus pole muusikat (näiteks taskuhäälingud) + Uuenda muusikakogu alati, kui seal midagi muutub (eeldab püsiteavituse olemasolu) + Mitme väärtuse eraldajad siltides + Koma (,) + Semikoolon (;) + Pluss (+) + Ampersand (&) + Seadista tähemärke, mis eraldavad siltides mitut väärtust + Kaldkriips (/) + Hoiatus: selle seadistuse kasutamisel ei pruugi mitu väärtust siltides olla alati korralikult tuvastatud; seda olukorda saad proovida lahendada täiendava prefiksi lisamisega kurakaldkriipsu näol (\\). + Nutikas järjestamine + Näita vaid esitajaid, kes on otsesõnu nimetatud albumi autoriteks (eeldab, et muusikakogu on korralikult sildistatud) + \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 1b9253796..1122ade9d 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -202,7 +202,7 @@ Musiikki Kuvat Albumikannet - ReplayGain + Äänenvoimakkuuden normalisointi Suosi albumia ReplayGain-strategia Automaattinen uudelleenlataus @@ -286,4 +286,25 @@ Soittolistan tuonti tästä tiedostosta ei onnistu Soittolistan vienti tähän tiedostoon ei onnistu Valintakuva + Pois + Mukautettu toistopalkin toiminto + Aloita toisto + Mixtapet + Mixtape + Näytä vain esittäjät, jotka on suoraan osoitettu jonkin albumin tekijäksi (toimii parhaiten hyvin tagatuissa kirjastoissa) + Kelaa ennen takaisinsiirtymistä + Toista kappale itsenään + Varoitus: Tämän asetuksen käyttäminen voi johtaa joidenkin tagien virheelliseen tulkintaan useina arvoina. Voit ratkaista tämän liittämällä kenoviivan (\\) ei-toivottujen erotinmerkkien eteen. + Järjestä nimet, jotka alkavat numerolla tai sanoilla kuten \"the\", oikein (toimii parhaiten englanninkielisellä musiikilla) + Keskeytä uudelleentoistettaessa + Keskeytä kun kappale toistetaan uudelleen + Toista näytetystä kohteesta + Kelaa takaisin ennen edelliseen kappaleeseen siirtymistä + Demo + Demot + Virhetiedot + Pidä sekoitus päällä kun toistetaan uutta kappaletta + Pysy toisto/keskeytystilassa ohittaessa kappaleita tai muokatessa jonoa + Valitse merkit, jotka erottavat tagiarvot toisistaan + Varoitus: Esivahvistuksen asettaminen korkeaan positiiviseen arvoon saattaa johtaa säröilyyn joissain kappaleissa. \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index bb56c69fe..2080bc707 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -316,4 +316,8 @@ विराम याद रखें कतार छोड़ते या संपादित करते समय चलता/रोका रखिए बंद + प्लेबैक शुरू करें + पहले से सहेजे गए स्टेट का उपयोग करके Auxio को शुरू करता है। यदि कोई सहेजा हुआ स्टेट उपलब्ध नहीं है, तो सभी गाने शफल पर चलेंगे और प्लेबैक तुरंत शुरू हो जाएगा। +\n +\nचेतावनी: इस सेवा को नियंत्रित करते समय सावधान रहें, यदि आप इसे बंद करके फिर से उपयोग करने का प्रयास करते हैं, तो संभवतः ऐप क्रैश हो जाएगा। \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 5f28cdd1c..b5e00e131 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -171,8 +171,8 @@ Zvučni zapisi Traži Sve - Dodaj u redoslijed izvođenja - Dodano u popis pjesama + Dodaj u redoslijed + Dodano u redoslijed Pogledaj svojstva Idi na izvođača Idi na album @@ -197,7 +197,7 @@ Promijeni način ponavljanja Ljubičasto Matroska Zvuk - Otvori popis pjesama + Otvori redoslijed Žanr Zarez (,) Znak i (&) @@ -310,6 +310,10 @@ Podešavanje normalizacije glasnoće pjesme Podešavanje normalizacije glasnoće albuma Zapamti pauzu - Nastavi reprodukciju/pauziranje prilikom preskakanja ili uređivanja slijeda + Nastavi reprodukciju/pauziranje prilikom preskakanja ili uređivanja redoslijeda Isključeno + Pokreni reprodukciju + Pokreće Auxio koristeći prethodno spremljeno stanje. Ako nijedno spremljeno stanje nije dostupno, sve će se pjesme nasumično reproducirati. Reprodukcija će započeti odmah. +\n +\nUPOZORENJE: Oprez pri upravljanju ovom uslugom. Ako je zatvoriš i zatim je pokušaš ponovo koristiti, aplikacija će se vjerojatno zatvoriti. \ No newline at end of file diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index 6fef2ba3b..92e3c0240 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -216,4 +216,5 @@ Necun musica in reproduction Modificante %s Lista de reproduction %d + Initiar le reproduction \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 489ae6894..e0bd299ac 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -317,4 +317,8 @@ ReplayGain 앨범 조절값 일시 중지 기억 끄기 + 재생 시작 + 이전에 저장된 상태에 따라 Auxio를 시작합니다. 저장된 상태가 없으면 모든 곡을 무작위 재생합니다. 재생은 즉시 시작됩니다. +\n +\n경고: 이 서비스는 주의하여 사용하세요. 서비스를 닫은 뒤 다시 사용하려고 할 경우 앱이 충돌할 수 있습니다. \ No newline at end of file diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 25e54d3e4..b285a9d1f 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -315,4 +315,8 @@ Prisiminti pauzę Išlaikyk leidimą / pristabdymą, kai praleidžiama arba redaguojama eilė. Išjungta + Paleidžia „Auxio“ naudojant anksčiau išsaugotą būseną. Jei nėra išsaugotos būsenos, visos dainos bus išmaišytos. Įrašo perklausa bus pradėta iš karto. +\n +\nĮSPĖJIMAS: būk atsargus (-i) valdant šią paslaugą, jei ją uždarysi ir vėl bandysi naudoti, tikriausiai sutriks programėlė. + Paleisti įrašo perklausą \ No newline at end of file diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8ce1c518d..86de2a45e 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -309,4 +309,8 @@ ਕਤਾਰ ਨੂੰ ਛੱਡਣ ਜਾਂ ਸੰਪਾਦਿਤ ਕਰਨ ਵੇਲੇ ਚਲਾਉਂਦੇ/ਰੋਕੇ ਰਹੋ ਵਿਰਾਮ ਯਾਦ ਰੱਖੋ ਬੰਦ + ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਕਰੋ + ਪਹਿਲਾਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀ ਸਥਿਤੀ ਦੀ ਵਰਤੋਂ ਕਰਕੇ Auxio ਨੂੰ ਸ਼ੁਰੂ ਕਰਦਾ ਹੈ। ਜੇਕਰ ਕੋਈ ਰੱਖਿਅਤ ਸਥਿਤੀ ਉਪਲਬਧ ਨਹੀਂ ਹੈ, ਤਾਂ ਸਾਰੇ ਗੀਤ ਸ਼ਫਲ ਤੇ ਚੱਲਣਗੇ ਅਤੇ ਪਲੇਬੈਕ ਤੁਰੰਤ ਸ਼ੁਰੂ ਹੋ ਜਾਵੇਗਾ। +\n +\nਚੇਤਾਵਨੀ: ਇਸ ਸੇਵਾ ਨੂੰ ਨਿਯੰਤਰਿਤ ਕਰਨ ਵਿੱਚ ਸਾਵਧਾਨ ਰਹੋ, ਜੇਕਰ ਤੁਸੀਂ ਇਸਨੂੰ ਬੰਦ ਕਰਦੇ ਹੋ ਅਤੇ ਫਿਰ ਇਸਨੂੰ ਦੁਬਾਰਾ ਵਰਤਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰਦੇ ਹੋ, ਤਾਂ ਤੁਸੀਂ ਸ਼ਾਇਦ ਐਪ ਨੂੰ ਕ੍ਰੈਸ਼ ਕਰ ਦਿਓਗੇ। \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 673b23874..0627f9fc9 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -79,7 +79,6 @@ Cancelar Adicionar Salvar - Estado salvo Mudar o tema e cores da app Tema Claro @@ -133,7 +132,6 @@ Demo Normalização de volume - Não foi possível limpar a lista Ajuste de ReplayGain da faixa Ajuste de ReplayGain do álbum Desenvolvido por Alexander Capehart @@ -190,12 +188,6 @@ O pré-amplificador é aplicado ao ajuste existente durante a reprodução Ajustar com etiquetas Ajustar sem etiquetas - Persistência - Gravar estado da reprodução - Salvar o estado de reprodução atual - Limpar estado de reprodução - Restaurar o estado de reprodução - Restaurar o estado de reprodução salvo anteriormente (se houver) Nenhuma música encontrada Falha ao carregar música O Auxio precisa de permissão para ler a sua biblioteca de músicas @@ -290,16 +282,12 @@ Recarrega a biblioteca de músicas usando metadados salvos em cache quando possível Procurar músicas novamente Limpa os metadados em cache e recarrega totalmente a biblioteca de música (lento, porém mais completo) - Limpar o estado de reprodução salvo anteriormente (se houver) Não foi possível importar uma lista de reprodução deste ficheiro Incapaz de exportar a lista de reprodução para este ficheiro Nenhuma aplicação encontrada que possa executar esta tarefa Sem pastas Esta pasta não é compatível - Nenhuma lista pode ser restaurada - Não foi possível gravar a lista Alterar o modo de repetição - Criar nova lista de reprodução Parar reprodução Remover esta música de fila Mover esta música da fila diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 21311beb1..4d5064461 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -324,4 +324,8 @@ Оставлять воспроизведение/паузу во время пропуска или редактирования очереди Запоминать паузу Откл. + Запускать Auxio, используя ранее сохранённое состояние. если сохранённое состояние недоступно, все песни будут перетасованы. Воспроизведение начнётся немедленно. +\n +\nПредупреждение: будьте осторожны при управлении этой службой, если вы закроете её, а затем попытаетесь использовать снова, вы, вероятно, приведёте к сбою приложения. + Начать воспроизведение \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6bf66ebc8..4e9ccdcea 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -321,4 +321,8 @@ Залишати відтворення/паузу під час пропуску або редагування черги Запам\'ятовувати паузу Вимкнено + Почати відтворення + Запускати auxio, використовуючи раніше збережений стан. Якщо збережений стан недоступний, усі пісні будуть перетасовані. відтворення розпочнеться негайно. +\n +\nПопередження: будьте обережні при керуванні цією службою, якщо ви закриєте її, а потім спробуєте використовувати знову, ви, ймовірно, призведе до збою застосунка. \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 07d9b9fe6..ea3313ea6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -315,4 +315,8 @@ 跳过或编辑队列时保留播放/暂停状态 记住暂停状态 关闭 + 开始播放 + 使用之前使用的状态启动 Auxio。如果没有可用的已保存状态,将打乱所有歌曲的顺序。播放将立刻开始。 +\n +\n警告:请小心控制此服务,如果将其关闭然后试图再次使用可能造成应用崩溃。 \ No newline at end of file diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt index fc52bfe41..62f87e1ad 100644 --- a/fastlane/metadata/android/de/full_description.txt +++ b/fastlane/metadata/android/de/full_description.txt @@ -13,6 +13,7 @@ Auxio ist ein lokaler Musik-Player mit einer schnellen, verlässlichen UI/UX, ab - verlässliche Wiedergabelisten-Verwaltung - verlässliches Speichern des Wiedergabezustands - automatische lückenlose Wiedergabe +- Unterstützung für Android Auto - Vollständiger ReplayGain-Support (für MP3-, FLAC-, OGG-, OPUS- und MP4-Dateien) - Externer Equalizerunterstützung (z.B. Wavelet) - Edge-to-Edge diff --git a/fastlane/metadata/android/fi/full_description.txt b/fastlane/metadata/android/fi/full_description.txt new file mode 100644 index 000000000..fdbfee68a --- /dev/null +++ b/fastlane/metadata/android/fi/full_description.txt @@ -0,0 +1,23 @@ +Auxio on paikallisen musiikin soitin nopealla ja luotettavalla käyttöliittymällä ilman useita turhia ominaisuuksia kuten muissa soittimissa. Perustuen moderneihin mediantoistokirjastoihin, Auxiolla on parempi tuki kirjastoille ja äänenlaatu verrattuna muihin sovelluksiin, jotka käyttävät vanhentuneita android toimintoja. Lyhyesti, Se soittaa musiikkia. + +Ominaisuudet + +- Toisto perustuu Media3 ExoPlayeriin +- Tyylikäs käyttöliittymä johdettu viimeisimmistä Material Design ohjeista +- Omapäinen käyttökokemus joka asettaa helppokäyttöisyyden etusijalle reunatapauksien sijaan +- Mukautettava käyttäytyminen +- Tukee levyjen numeroita, moninkertaisia artisteja, julkaisutyyppejä, tarkkoja/alkuperäisiä päivämääriä, tagijärjestystä ynnä muuta +- Edistynyt esittäjäjärjestelmä, joka yhdistää artistit ja albumiartistit +- SD-korttitietoinen kansionhallinta +- Varmatoimiset soittolistatoiminnot +- Android auto-tuki +- Automaattinen katkoton toisto +- Täysi ReplayGain tuki (MP3, FLAC, OGG, OPUS ja MP4 tiedostoissa) +- Tuki ulkoiselle taajuuskorjaimelle (esim. Wavelet) +- Reunasta reunaan +- Tukee kappaleen sisältämiä kansikuvia +- Hakutoiminto +- Automaattinen toisto kuulokkeilla +- Tyylikkäitä widgettejä, jotka mukautuvat automaattisesti kokoonsa +- Täysin yksityinen ja verkkoyhteydetön +- Pyöristämättömät albumin kansikuvat (oletusarvoisesti) diff --git a/fastlane/metadata/android/hr/full_description.txt b/fastlane/metadata/android/hr/full_description.txt index 6647c70f4..0d40db862 100644 --- a/fastlane/metadata/android/hr/full_description.txt +++ b/fastlane/metadata/android/hr/full_description.txt @@ -11,6 +11,8 @@ precizni/izvorni datumi, sortiranje oznaka i više - Napredni sustav izvođača koji ujedinjuje izvođače i izvođače albuma - Upravljanje mapama koje podržava SD karticu - Pouzdana funkcija popisa pjesama +- Automaska podrška za Android +- Automatska reprodukcija bez prekida - Postojanost stanja reprodukcije - Puna podrška za ReplayGain (na MP3, FLAC, OGG, OPUS i MP4 datotekama) - Podrška za vanjski ekvilizator (npr. Wavelet) diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt index 1b3700f14..c54e911c6 100644 --- a/fastlane/metadata/android/ru/full_description.txt +++ b/fastlane/metadata/android/ru/full_description.txt @@ -12,6 +12,7 @@ Auxio — это локальный музыкальный плеер с быс - Управление папками на SD-карте - Надежная функция составления плейлистов - Сохранение состояния воспроизведения +- Поддержка Android Auto - Автоматическое воспроизведение без пауз - Полная поддержка ReplayGain (в файлах MP3, FLAC, OGG, OPUS и MP4) - Поддержка внешнего эквалайзера (например, Wavelet) diff --git a/fastlane/metadata/android/zh-Hant/full_description.txt b/fastlane/metadata/android/zh-Hant/full_description.txt index 26596541a..efd745793 100644 --- a/fastlane/metadata/android/zh-Hant/full_description.txt +++ b/fastlane/metadata/android/zh-Hant/full_description.txt @@ -10,6 +10,7 @@ Auxio 是一款本機音樂播放器,擁有快速且可靠的 UI/UX,不含 - 進階藝術家系統,統一藝術家與專輯藝術家 - 支援 SD 卡的資料夾管理 - 可靠的播放列表功能 +- 支援 Android Auto - 播放狀態持久性 - 完整的 ReplayGain 支援(適用於 MP3、FLAC、OGG、OPUS 和 MP4 檔案) - 外部均衡器支援(例如 Wavelet) From d540d6f14ccdcff597d116d7346f9d3207e42ee2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 14 Oct 2024 14:35:33 -0600 Subject: [PATCH 081/550] build: initial android 15 upgrade --- app/build.gradle | 4 ++-- app/src/main/AndroidManifest.xml | 1 + app/src/main/java/org/oxycblt/auxio/AuxioService.kt | 2 ++ app/src/main/java/org/oxycblt/auxio/MainActivity.kt | 1 + app/src/main/java/org/oxycblt/auxio/tasker/Start.kt | 1 + app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt | 2 +- build.gradle | 2 +- 7 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f26c50e30..87b32dc83 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ plugins { } android { - compileSdk 34 + compileSdk 35 // NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify // it here so that binary stripping will work. // TODO: Eventually you might just want to start vendoring the FFMpeg extension so the @@ -25,7 +25,7 @@ android { versionCode 50 minSdk 24 - targetSdk 34 + targetSdk 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 308962b34..91ca80093 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,6 +48,7 @@ android:exported="true" android:icon="@mipmap/ic_launcher" android:launchMode="singleTask" + android:allowCrossUidActivitySwitchFromBelow="false" android:roundIcon="@mipmap/ic_launcher" android:windowSoftInputMode="adjustPan"> diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index da79b8a11..6fb29284b 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -155,6 +155,8 @@ class AuxioService : } companion object { + const val ACTION_START = BuildConfig.APPLICATION_ID + ".service.START" + var isForeground = false private set diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 530f3f14f..8ddd933a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -71,6 +71,7 @@ class MainActivity : AppCompatActivity() { startService( Intent(this, AuxioService::class.java) + .setAction(AuxioService.ACTION_START) .putExtra(AuxioService.INTENT_KEY_START_ID, IntegerTable.START_ID_ACTIVITY)) if (!startIntentAction(intent)) { diff --git a/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt b/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt index 1dd5c8997..174ffa884 100644 --- a/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt +++ b/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt @@ -61,6 +61,7 @@ class StartActionRunner : TaskerPluginRunnerActionNoOutputOrInput() { ContextCompat.startForegroundService( context, Intent(context, AuxioService::class.java) + .setAction(AuxioService.ACTION_START) .putExtra(AuxioService.INTENT_KEY_START_ID, IntegerTable.START_ID_TASKER)) while (!AuxioService.isForeground) { Thread.sleep(100) diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index bf6d9313b..d2321ab5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -182,7 +182,7 @@ fun Context.newMainPendingIntent(): PendingIntent = PendingIntent.getActivity( this, IntegerTable.REQUEST_CODE, - Intent(this, MainActivity::class.java), + Intent(this, MainActivity::class.java).setAction(Intent.ACTION_MAIN), PendingIntent.FLAG_IMMUTABLE) /** diff --git a/build.gradle b/build.gradle index f3dda8969..575885787 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } plugins { - id "com.android.application" version '8.5.0' apply false + id "com.android.application" version '8.5.2' apply false id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false id "com.google.devtools.ksp" version '1.9.23-1.0.20' apply false From 190abd55889434c96648b7ce194e01ec5e3f6e64 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 14 Oct 2024 14:52:21 -0600 Subject: [PATCH 082/550] all: fix merge regressions --- .../oxycblt/auxio/detail/DetailViewModel.kt | 8 ++++-- .../org/oxycblt/auxio/image/BitmapProvider.kt | 4 +++ .../extractor/RoundedRectTransformation.kt | 6 ++++- .../music/external/ExternalPlaylistManager.kt | 4 +-- .../playback/state/PlaybackStateHolder.kt | 25 +++++++++++++++++++ 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 0fbaf22be..e125cb9f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -542,13 +542,17 @@ constructor( val header = if (section is DetailSection.Songs) SortHeader(section.stringRes) else BasicHeader(section.stringRes) - newList.add(Divider(header)) + if (newList.isNotEmpty()) { + newList.add(Divider(header)) + } newList.add(header) section.items } is DetailSection.Discs -> { val header = SortHeader(section.stringRes) - newList.add(Divider(header)) + if (newList.isNotEmpty()) { + newList.add(Divider(header)) + } newList.add(header) section.discs.flatMap { listOf(DiscHeader(it.key)) + it.value } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt index 06ebe3186..59dcb877d 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt @@ -73,6 +73,10 @@ constructor( private var currentRequest: Request? = null private var currentHandle = 0L + /** If this provider is currently attempting to load something. */ + val isBusy: Boolean + get() = currentRequest?.run { !disposable.isDisposed } ?: false + /** * Load the Album cover [Bitmap] from a [Song]. * diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt index 0d32d20de..f0574caf9 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/RoundedRectTransformation.kt @@ -65,7 +65,11 @@ class RoundedRectTransformation( val (outputWidth, outputHeight) = calculateOutputSize(input, size) - val output = createBitmap(outputWidth, outputHeight, input.config) + val output = + createBitmap( + outputWidth, + outputHeight, + requireNotNull(input.config) { "unsupported bitmap format" }) output.applyCanvas { drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt index 81d8720cc..76703b6b6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.music.external import android.content.Context import android.net.Uri import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.fs.Components import org.oxycblt.auxio.music.fs.DocumentPathFactory import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.util.logE -import javax.inject.Inject /** * Generic playlist file importing abstraction. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt index 34c1d4927..4d1cdca98 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.playback.state import android.net.Uri import android.os.SystemClock +import android.support.v4.media.session.PlaybackStateCompat import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song @@ -324,6 +325,30 @@ private constructor( initPositionMs } + /** + * Load this instance into a [PlaybackStateCompat]. + * + * @param builder The [PlaybackStateCompat.Builder] to mutate. + * @return The same [PlaybackStateCompat.Builder] for easy chaining. + */ + fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder = + builder.setState( + // State represents the user's preference, not the actual player state. + // Doing this produces a better experience in the media control UI. + if (isPlaying) { + PlaybackStateCompat.STATE_PLAYING + } else { + PlaybackStateCompat.STATE_PAUSED + }, + initPositionMs, + if (isAdvancing) { + 1f + } else { + // Not advancing, so don't move the position. + 0f + }, + creationTime) + // Equality ignores the creation time to prevent functionally identical states // from being non-equal. From 97faa3f20ed1b921730ca8bddf54a9065d21cb7e Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 14 Oct 2024 18:25:20 -0600 Subject: [PATCH 083/550] detail: improve disc header design --- .../java/org/oxycblt/auxio/IntegerTable.kt | 4 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 12 ++++- .../detail/list/AlbumDetailListAdapter.kt | 44 +++++++++++++++++ app/src/main/res/layout/item_disc_header.xml | 48 ++++++++----------- 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index eb1a27d53..3708fb92e 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -49,8 +49,10 @@ object IntegerTable { const val VIEW_TYPE_ARTIST_SONG = 0xA00A /** DiscHeaderViewHolder */ const val VIEW_TYPE_DISC_HEADER = 0xA00B + /** DiscHeaderViewHolder */ + const val VIEW_TYPE_DISC_DIVIDER = 0xA00C /** EditHeaderViewHolder */ - const val VIEW_TYPE_EDIT_HEADER = 0xA00C + const val VIEW_TYPE_EDIT_HEADER = 0xA00D /** PlaylistSongViewHolder */ const val VIEW_TYPE_PLAYLIST_SONG = 0xA00E /** "Music playback" notification code */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index e125cb9f7..a2f502416 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.oxycblt.auxio.R +import org.oxycblt.auxio.detail.list.DiscDivider import org.oxycblt.auxio.detail.list.DiscHeader import org.oxycblt.auxio.detail.list.EditHeader import org.oxycblt.auxio.detail.list.SortHeader @@ -554,7 +555,16 @@ constructor( newList.add(Divider(header)) } newList.add(header) - section.discs.flatMap { listOf(DiscHeader(it.key)) + it.value } + buildList { + for (entry in section.discs) { + val discHeader = DiscHeader(inner = entry.key) + if (isNotEmpty()) { + add(DiscDivider(discHeader)) + } + add(discHeader) + addAll(entry.value) + } + } } } // Currently only the final section (songs, which can be sorted) are invalidatable diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt index c1134b207..2d09a1e4d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt @@ -24,6 +24,7 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.divider.MaterialDivider import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemAlbumSongBinding @@ -38,6 +39,7 @@ import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.resolveNumber import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.inflater /** @@ -52,6 +54,7 @@ class AlbumDetailListAdapter(private val listener: Listener) : when (getItem(position)) { // Support sub-headers for each disc, and special album songs. is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE + is DiscDivider -> DiscDividerViewHolder.VIEW_TYPE is Song -> AlbumSongViewHolder.VIEW_TYPE else -> super.getItemViewType(position) } @@ -59,6 +62,7 @@ class AlbumDetailListAdapter(private val listener: Listener) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.from(parent) + DiscDividerViewHolder.VIEW_TYPE -> DiscDividerViewHolder.from(parent) AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) } @@ -79,6 +83,8 @@ class AlbumDetailListAdapter(private val listener: Listener) : when { oldItem is Disc && newItem is Disc -> DiscHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + oldItem is DiscDivider && newItem is DiscDivider -> + DiscDividerViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is Song && newItem is Song -> AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) @@ -96,6 +102,8 @@ class AlbumDetailListAdapter(private val listener: Listener) : */ data class DiscHeader(val inner: Disc?) : Item +data class DiscDivider(val anchor: DiscHeader?) : Item + /** * A [RecyclerView.ViewHolder] that displays a [DiscHeader] to delimit different disc groups. Use * [from] to create an instance. @@ -140,6 +148,42 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : } } +/** + * A [RecyclerView.ViewHolder] that displays a [DiscHeader]. Use [from] to create an instance. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class DiscDividerViewHolder private constructor(divider: MaterialDivider) : + RecyclerView.ViewHolder(divider) { + + init { + divider.dividerColor = + divider.context + .getAttrColorCompat(com.google.android.material.R.attr.colorOutlineVariant) + .defaultColor + } + + companion object { + /** Unique ID for this ViewHolder type. */ + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_DISC_DIVIDER + + /** + * Create a new instance. + * + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun from(parent: View) = DiscDividerViewHolder(MaterialDivider(parent.context)) + + /** A comparator that can be used with DiffUtil. */ + val DIFF_CALLBACK = + object : SimpleDiffCallback() { + override fun areContentsTheSame(oldItem: DiscDivider, newItem: DiscDivider) = + oldItem.anchor == newItem.anchor + } + } +} + /** * A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Album]. Use [from] to * create an instance. diff --git a/app/src/main/res/layout/item_disc_header.xml b/app/src/main/res/layout/item_disc_header.xml index dbcc0e4f6..cae2a6e0e 100644 --- a/app/src/main/res/layout/item_disc_header.xml +++ b/app/src/main/res/layout/item_disc_header.xml @@ -6,57 +6,49 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:paddingStart="@dimen/spacing_medium" - android:paddingTop="@dimen/spacing_mid_medium" + android:paddingTop="@dimen/spacing_small" android:paddingEnd="@dimen/spacing_medium" - android:paddingBottom="@dimen/spacing_mid_medium"> + android:paddingBottom="@dimen/spacing_small"> - - - - - + app:tint="@color/sel_on_cover_bg" + tools:ignore="ContentDescription" /> + tools:text="Part 1" /> From d1e8cc3320a99781e92bae103d3cc6f558784407 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 14 Oct 2024 20:19:35 -0600 Subject: [PATCH 084/550] detail: fix playlist edit header update --- .../org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index 334f39703..6bd2ed44d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -99,7 +99,7 @@ class PlaylistDetailListAdapter(private val listener: Listener) : } logD("Updating editing state [old: $isEditing new: $editing]") this.isEditing = editing - notifyItemRangeChanged(1, currentList.size - 1, PAYLOAD_EDITING_CHANGED) + notifyItemRangeChanged(0, currentList.size, PAYLOAD_EDITING_CHANGED) } /** An extended [DetailListAdapter.Listener] for [PlaylistDetailListAdapter]. */ From 1ee5645780eab3f80a2188d3e8a1df0a9cfa0a2d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 17 Oct 2024 09:44:51 -0600 Subject: [PATCH 085/550] detail: continue scrolling even after toolbar collapses --- .../detail/ContinuousAppBarLayoutBehavior.kt | 116 ++++++++++++ .../auxio/detail/DetailAppBarLayout.kt | 169 ------------------ .../res/layout-h360dp/fragment_detail.xml | 1 + .../res/layout-h480dp/fragment_detail.xml | 1 + .../res/layout-sw600dp/fragment_detail.xml | 1 + .../res/layout-w600dp/fragment_detail.xml | 1 + app/src/main/res/layout/fragment_detail.xml | 1 + 7 files changed, 121 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/ContinuousAppBarLayoutBehavior.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ContinuousAppBarLayoutBehavior.kt b/app/src/main/java/org/oxycblt/auxio/detail/ContinuousAppBarLayoutBehavior.kt new file mode 100644 index 000000000..d2e074d8f --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/ContinuousAppBarLayoutBehavior.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2022 Auxio Project + * ContinuousAppBarLayoutBehavior.kt is part of Auxio. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.oxycblt.auxio.detail + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.ViewGroup +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.appbar.AppBarLayout + +class ContinuousAppBarLayoutBehavior +@JvmOverloads +constructor(context: Context? = null, attrs: AttributeSet? = null) : + AppBarLayout.Behavior(context, attrs) { + private var recycler: RecyclerView? = null + private var pointerId = -1 + private var velocityTracker: VelocityTracker? = null + + override fun onInterceptTouchEvent( + parent: CoordinatorLayout, + child: AppBarLayout, + ev: MotionEvent + ): Boolean { + val consumed = super.onInterceptTouchEvent(parent, child, ev) + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + ensureVelocityTracker() + findRecyclerView(child).stopScroll() + pointerId = ev.getPointerId(0) + } + MotionEvent.ACTION_CANCEL -> { + velocityTracker?.recycle() + velocityTracker = null + pointerId = -1 + } + else -> {} + } + return consumed + } + + override fun onTouchEvent( + parent: CoordinatorLayout, + child: AppBarLayout, + ev: MotionEvent + ): Boolean { + val consumed = super.onTouchEvent(parent, child, ev) + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + ensureVelocityTracker() + pointerId = ev.getPointerId(0) + } + MotionEvent.ACTION_UP -> { + findRecyclerView(child).fling(0, getYVelocity(ev)) + } + MotionEvent.ACTION_CANCEL -> { + velocityTracker?.recycle() + velocityTracker = null + pointerId = -1 + } + else -> {} + } + velocityTracker?.addMovement(ev) + return consumed + } + + private fun ensureVelocityTracker() { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + } + + private fun getYVelocity(event: MotionEvent): Int { + velocityTracker?.let { + it.addMovement(event) + it.computeCurrentVelocity(FLING_UNITS) + return -it.getYVelocity(pointerId).toInt() + } + return 0 + } + + private fun findRecyclerView(child: AppBarLayout): RecyclerView { + val recycler = recycler + if (recycler != null) { + return recycler + } + + // Use the scrolling view in order to find a RecyclerView to use. + val newRecycler = + (child.parent as ViewGroup).findViewById(child.liftOnScrollTargetViewId) + this.recycler = newRecycler + return newRecycler + } + + companion object { + private const val FLING_UNITS = 1000 // copied from base class + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt deleted file mode 100644 index 3c494cd96..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * DetailAppBarLayout.kt is part of Auxio. - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.oxycblt.auxio.detail - -import android.animation.ValueAnimator -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.annotation.AttrRes -import androidx.appcompat.widget.Toolbar -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.appbar.AppBarLayout -import java.lang.reflect.Field -import org.oxycblt.auxio.R -import org.oxycblt.auxio.ui.CoordinatorAppBarLayout -import org.oxycblt.auxio.util.getInteger -import org.oxycblt.auxio.util.lazyReflectedField -import org.oxycblt.auxio.util.logD - -/** - * An [CoordinatorAppBarLayout] that displays the title of a hidden [Toolbar] when the scrolling - * view goes beyond it's first item. - * - * This is intended for the detail views, in which the first item is the album/artist/genre header, - * and thus scrolling past them should make the toolbar show the name in order to give context on - * where the user currently is. - * - * @author Alexander Capehart (OxygenCobalt) - */ -class DetailAppBarLayout -@JvmOverloads -constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : - CoordinatorAppBarLayout(context, attrs, defStyleAttr) { - private var titleView: TextView? = null - private var recycler: RecyclerView? = null - - private var titleShown: Boolean? = null - private var titleAnimator: ValueAnimator? = null - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - if (!isInEditMode) { - (layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context) - } - } - - private fun findTitleView(): TextView { - val titleView = titleView - if (titleView != null) { - return titleView - } - - // Assume that we have a Toolbar with a detail_toolbar ID, as this view is only - // used within the detail layouts. - val toolbar = findViewById(R.id.detail_normal_toolbar) - - // The Toolbar's title view is actually hidden. To avoid having to create our own - // title view, we just reflect into Toolbar and grab the hidden field. - val newTitleView = - (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply { - // We can never properly initialize the title view's state before draw time, - // so we just set it's alpha to 0f to produce a less jarring initialization - // animation. - alpha = 0f - } - - this.titleView = newTitleView - return newTitleView - } - - private fun findRecyclerView(): RecyclerView { - val recycler = recycler - if (recycler != null) { - return recycler - } - - // Use the scrolling view in order to find a RecyclerView to use. - val newRecycler = (parent as ViewGroup).findViewById(liftOnScrollTargetViewId) - this.recycler = newRecycler - return newRecycler - } - - private fun setTitleVisibility(visible: Boolean) { - if (titleShown == visible) return - titleShown = visible - - // Emulate the AppBarLayout lift animation (Linear, alpha 0f -> 1f), but now with - // the title view's alpha instead of the AppBarLayout's elevation. - val titleView = findTitleView() - val from: Float - val to: Float - - if (visible) { - from = 0f - to = 1f - } else { - from = 1f - to = 0f - } - - if (titleView.alpha == to) { - // Nothing to do - return - } - - logD("Changing title visibility [from: $from to: $to]") - titleAnimator?.cancel() - titleAnimator = - ValueAnimator.ofFloat(from, to).apply { - addUpdateListener { titleView.alpha = it.animatedValue as Float } - duration = - if (titleShown == true) { - context.getInteger(R.integer.anim_fade_enter_duration).toLong() - } else { - context.getInteger(R.integer.anim_fade_exit_duration).toLong() - } - start() - } - } - - class Behavior - @JvmOverloads - constructor(context: Context? = null, attrs: AttributeSet? = null) : - AppBarLayout.Behavior(context, attrs) { - override fun onNestedPreScroll( - coordinatorLayout: CoordinatorLayout, - child: AppBarLayout, - target: View, - dx: Int, - dy: Int, - consumed: IntArray, - type: Int - ) { - super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) - - val appBarLayout = child as DetailAppBarLayout - val recycler = appBarLayout.findRecyclerView() - - // Title should be visible if we are no longer showing the top item - // (i.e the header) - appBarLayout.setTitleVisibility( - (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0) - } - } - - private companion object { - val TOOLBAR_TITLE_TEXT_FIELD: Field by lazyReflectedField(Toolbar::class, "mTitleTextView") - } -} diff --git a/app/src/main/res/layout-h360dp/fragment_detail.xml b/app/src/main/res/layout-h360dp/fragment_detail.xml index ad56ee758..a04d23b7b 100644 --- a/app/src/main/res/layout-h360dp/fragment_detail.xml +++ b/app/src/main/res/layout-h360dp/fragment_detail.xml @@ -11,6 +11,7 @@ diff --git a/app/src/main/res/layout-h480dp/fragment_detail.xml b/app/src/main/res/layout-h480dp/fragment_detail.xml index 71c041256..b219830c9 100644 --- a/app/src/main/res/layout-h480dp/fragment_detail.xml +++ b/app/src/main/res/layout-h480dp/fragment_detail.xml @@ -11,6 +11,7 @@ diff --git a/app/src/main/res/layout-sw600dp/fragment_detail.xml b/app/src/main/res/layout-sw600dp/fragment_detail.xml index d44f004f5..df3e5dfeb 100644 --- a/app/src/main/res/layout-sw600dp/fragment_detail.xml +++ b/app/src/main/res/layout-sw600dp/fragment_detail.xml @@ -11,6 +11,7 @@ diff --git a/app/src/main/res/layout-w600dp/fragment_detail.xml b/app/src/main/res/layout-w600dp/fragment_detail.xml index ad56ee758..a04d23b7b 100644 --- a/app/src/main/res/layout-w600dp/fragment_detail.xml +++ b/app/src/main/res/layout-w600dp/fragment_detail.xml @@ -11,6 +11,7 @@ diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index 327fe333d..82c95571a 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -12,6 +12,7 @@ android:id="@+id/detail_appbar" style="@style/Widget.Auxio.AppBarLayout" app:liftOnScroll="true" + app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior" app:liftOnScrollTargetViewId="@id/detail_recycler"> Date: Thu, 17 Oct 2024 09:57:47 -0600 Subject: [PATCH 086/550] list: tweak header/divider object hierarchy Make a new generic Header/Divider superclass that all headers derive. This allows disc headers to be recognized generically in places like the grid layout manager. --- .../org/oxycblt/auxio/detail/DetailFragment.kt | 6 +++--- .../org/oxycblt/auxio/detail/DetailViewModel.kt | 12 ++++++------ .../auxio/detail/list/AlbumDetailListAdapter.kt | 6 ++++-- .../auxio/detail/list/DetailListAdapter.kt | 10 +++++----- .../detail/list/PlaylistDetailListAdapter.kt | 6 +++--- app/src/main/java/org/oxycblt/auxio/list/Data.kt | 16 +++++++++++----- .../oxycblt/auxio/list/recycler/ViewHolders.kt | 8 ++++---- .../org/oxycblt/auxio/search/SearchAdapter.kt | 6 +++--- .../org/oxycblt/auxio/search/SearchFragment.kt | 6 +++--- .../org/oxycblt/auxio/search/SearchViewModel.kt | 10 +++++----- 10 files changed, 47 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 5213f08b0..675f5c198 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -31,10 +31,10 @@ import kotlin.math.min import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.list.DetailListAdapter -import org.oxycblt.auxio.list.Divider -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel +import org.oxycblt.auxio.list.PlainDivider +import org.oxycblt.auxio.list.PlainHeader import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel @@ -91,7 +91,7 @@ abstract class DetailFragment

: detailModel.artistSongList.value.getOrElse(it - 1) { return@setFullWidthLookup false } - item is Divider || item is Header + item is PlainDivider || item is PlainHeader } else { true } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 1d16d7992..5675b89f5 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -34,10 +34,10 @@ import org.oxycblt.auxio.detail.list.DiscHeader import org.oxycblt.auxio.detail.list.EditHeader import org.oxycblt.auxio.detail.list.SortHeader import org.oxycblt.auxio.list.BasicHeader -import org.oxycblt.auxio.list.Divider -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListSettings +import org.oxycblt.auxio.list.PlainDivider +import org.oxycblt.auxio.list.PlainHeader import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album @@ -531,7 +531,7 @@ constructor( list: MutableStateFlow>, instructions: MutableEvent, replace: Int?, - songHeader: (Int) -> Header = { SortHeader(it) } + songHeader: (Int) -> PlainHeader = { SortHeader(it) } ) { if (detail == null) { parent.value = null @@ -547,7 +547,7 @@ constructor( if (section is DetailSection.Songs) songHeader(section.stringRes) else BasicHeader(section.stringRes) if (newList.isNotEmpty()) { - newList.add(Divider(header)) + newList.add(PlainDivider(header)) } newList.add(header) section.items @@ -555,7 +555,7 @@ constructor( is DetailSection.Discs -> { val header = SortHeader(section.stringRes) if (newList.isNotEmpty()) { - newList.add(Divider(header)) + newList.add(PlainDivider(header)) } newList.add(header) buildList { @@ -600,7 +600,7 @@ constructor( val list = mutableListOf() if (edited.isNotEmpty()) { val header = EditHeader(R.string.lbl_songs) - list.add(Divider(header)) + list.add(PlainDivider(header)) list.add(header) list.addAll(edited) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt index 2d09a1e4d..1af1a595c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt @@ -29,6 +29,8 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemAlbumSongBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding +import org.oxycblt.auxio.list.Divider +import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter @@ -100,9 +102,9 @@ class AlbumDetailListAdapter(private val listener: Listener) : * * @author Alexander Capehart (OxygenCobalt) */ -data class DiscHeader(val inner: Disc?) : Item +data class DiscHeader(val inner: Disc?) : Header -data class DiscDivider(val anchor: DiscHeader?) : Item +data class DiscDivider(override val anchor: DiscHeader?) : Divider /** * A [RecyclerView.ViewHolder] that displays a [DiscHeader] to delimit different disc groups. Use diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt index 08293199f..87aebf5df 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt @@ -27,9 +27,9 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.list.BasicHeader -import org.oxycblt.auxio.list.Divider -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.PlainDivider +import org.oxycblt.auxio.list.PlainHeader import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback @@ -55,7 +55,7 @@ abstract class DetailListAdapter( override fun getItemViewType(position: Int) = when (getItem(position)) { // Implement support for headers and sort headers - is Divider -> DividerViewHolder.VIEW_TYPE + is PlainDivider -> DividerViewHolder.VIEW_TYPE is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE is SortHeader -> SortHeaderViewHolder.VIEW_TYPE else -> super.getItemViewType(position) @@ -91,7 +91,7 @@ abstract class DetailListAdapter( object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return when { - oldItem is Divider && newItem is Divider -> + oldItem is PlainDivider && newItem is PlainDivider -> DividerViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is BasicHeader && newItem is BasicHeader -> BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) @@ -110,7 +110,7 @@ abstract class DetailListAdapter( * @param titleRes The string resource to use as the header title * @author Alexander Capehart (OxygenCobalt) */ -data class SortHeader(@StringRes override val titleRes: Int) : Header +data class SortHeader(@StringRes override val titleRes: Int) : PlainHeader /** * A [RecyclerView.ViewHolder] that displays a [SortHeader] and it's actions. Use [from] to create diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index 6bd2ed44d..213c65b78 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -33,8 +33,8 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.databinding.ItemEditHeaderBinding import org.oxycblt.auxio.databinding.ItemEditableSongBinding import org.oxycblt.auxio.list.EditableListListener -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.PlainHeader import org.oxycblt.auxio.list.adapter.PlayingIndicatorAdapter import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback @@ -140,12 +140,12 @@ class PlaylistDetailListAdapter(private val listener: Listener) : } /** - * A [Header] variant that displays an edit button. + * A [PlainHeader] variant that displays an edit button. * * @param titleRes The string resource to use as the header title * @author Alexander Capehart (OxygenCobalt) */ -data class EditHeader(@StringRes override val titleRes: Int) : Header +data class EditHeader(@StringRes override val titleRes: Int) : PlainHeader /** * Displays an [EditHeader] and it's actions. Use [from] to create an instance. diff --git a/app/src/main/java/org/oxycblt/auxio/list/Data.kt b/app/src/main/java/org/oxycblt/auxio/list/Data.kt index 8636e1579..5507bb9fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt @@ -24,12 +24,14 @@ import androidx.annotation.StringRes /** A marker for something that is a RecyclerView item. Has no functionality on it's own. */ interface Item +interface Header : Item + /** * A "header" used for delimiting groups of data. * * @author Alexander Capehart (OxygenCobalt) */ -interface Header : Item { +interface PlainHeader : Header { /** The string resource used for the header's title. */ val titleRes: Int } @@ -40,12 +42,16 @@ interface Header : Item { * @param titleRes The string resource used for the header's title. * @author Alexander Capehart (OxygenCobalt) */ -data class BasicHeader(@StringRes override val titleRes: Int) : Header +data class BasicHeader(@StringRes override val titleRes: Int) : PlainHeader + +interface Divider : Item { + val anchor: T? +} /** * A divider decoration used to delimit groups of data. * - * @param anchor The [Header] this divider should be next to in a list. Used as a way to preserve - * divider continuity during list updates. + * @param anchor The [PlainHeader] this divider should be next to in a list. Used as a way to + * preserve divider continuity during list updates. */ -data class Divider(val anchor: Header?) : Item +data class PlainDivider(override val anchor: PlainHeader?) : Divider diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index 36565b63f..d7f5e1d23 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.list.BasicHeader -import org.oxycblt.auxio.list.Divider +import org.oxycblt.auxio.list.PlainDivider import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback @@ -360,7 +360,7 @@ class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderB } /** - * A [RecyclerView.ViewHolder] that displays a [Divider]. Use [from] to create an instance. + * A [RecyclerView.ViewHolder] that displays a [PlainDivider]. Use [from] to create an instance. * * @author Alexander Capehart (OxygenCobalt) */ @@ -381,8 +381,8 @@ class DividerViewHolder private constructor(divider: MaterialDivider) : /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Divider, newItem: Divider) = + object : SimpleDiffCallback() { + override fun areContentsTheSame(oldItem: PlainDivider, newItem: PlainDivider) = oldItem.anchor == newItem.anchor } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index a800221fb..7b9befdef 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -21,8 +21,8 @@ package org.oxycblt.auxio.search import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.list.BasicHeader -import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.PlainDivider import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback @@ -57,7 +57,7 @@ class SearchAdapter(private val listener: SelectableListListener) : is Artist -> ArtistViewHolder.VIEW_TYPE is Genre -> GenreViewHolder.VIEW_TYPE is Playlist -> PlaylistViewHolder.VIEW_TYPE - is Divider -> DividerViewHolder.VIEW_TYPE + is PlainDivider -> DividerViewHolder.VIEW_TYPE is BasicHeader -> BasicHeaderViewHolder.VIEW_TYPE else -> super.getItemViewType(position) } @@ -102,7 +102,7 @@ class SearchAdapter(private val listener: SelectableListListener) : GenreViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is Playlist && newItem is Playlist -> PlaylistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) - oldItem is Divider && newItem is Divider -> + oldItem is PlainDivider && newItem is PlainDivider -> DividerViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is BasicHeader && newItem is BasicHeader -> BasicHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index af6556c83..ddc3c754b 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -38,11 +38,11 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSearchBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.Show -import org.oxycblt.auxio.list.Divider -import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel +import org.oxycblt.auxio.list.PlainDivider +import org.oxycblt.auxio.list.PlainHeader import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -153,7 +153,7 @@ class SearchFragment : ListFragment() { searchModel.searchResults.value.getOrElse(it) { return@setFullWidthLookup false } - item is Divider || item is Header + item is PlainDivider || item is PlainHeader } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index fb60d7ff9..6eff6450e 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -30,8 +30,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.oxycblt.auxio.R import org.oxycblt.auxio.list.BasicHeader -import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.PlainDivider import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicType @@ -152,7 +152,7 @@ constructor( logD("Adding ${it.size} albums to search results") val header = BasicHeader(R.string.lbl_albums) if (isNotEmpty()) { - add(Divider(header)) + add(PlainDivider(header)) } add(header) @@ -162,7 +162,7 @@ constructor( logD("Adding ${it.size} playlists to search results") val header = BasicHeader(R.string.lbl_playlists) if (isNotEmpty()) { - add(Divider(header)) + add(PlainDivider(header)) } add(header) @@ -172,7 +172,7 @@ constructor( logD("Adding ${it.size} genres to search results") val header = BasicHeader(R.string.lbl_genres) if (isNotEmpty()) { - add(Divider(header)) + add(PlainDivider(header)) } add(header) @@ -182,7 +182,7 @@ constructor( logD("Adding ${it.size} songs to search results") val header = BasicHeader(R.string.lbl_songs) if (isNotEmpty()) { - add(Divider(header)) + add(PlainDivider(header)) } add(header) From a9a35c8055e19426b554cc3485fbf190826c5f39 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 17 Oct 2024 19:45:47 -0600 Subject: [PATCH 087/550] build: update deps nav -> 2.8.3 lifecycle -> 2.8.6 activity -> 1.9.3 --- app/build.gradle | 10 +++++----- build.gradle | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 075860a5a..bf666ec42 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -88,8 +88,8 @@ dependencies { // General implementation "androidx.core:core-ktx:1.13.1" implementation "androidx.appcompat:appcompat:1.7.0" - implementation "androidx.activity:activity-ktx:1.9.0" - // Disabled since 1.7+ has completely broken progressive back gestures. + implementation "androidx.activity:activity-ktx:1.9.3" + // Disabled until I can be secure that predictive back isn't utterly unstable. // noinspection GradleDependency implementation "androidx.fragment:fragment-ktx:1.6.2" @@ -105,7 +105,7 @@ dependencies { implementation "androidx.viewpager2:viewpager2:1.0.0" // Lifecycle - def lifecycle_version = "2.8.3" + def lifecycle_version = "2.8.6" implementation "androidx.lifecycle:lifecycle-common:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" @@ -135,7 +135,7 @@ dependencies { // Exoplayer (Vendored) implementation project(":media-lib-exoplayer") implementation project(":media-lib-decoder-ffmpeg") - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4" + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.2" // Image loading implementation 'io.coil-kt:coil-base:2.4.0' @@ -143,7 +143,7 @@ dependencies { // Material // TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just // PR a fix. - implementation "com.google.android.material:material:1.13.0-alpha04" + implementation "com.google.android.material:material:1.13.0-alpha07" // Dependency Injection implementation "com.google.dagger:dagger:$hilt_version" diff --git a/build.gradle b/build.gradle index 575885787..213148be0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.9.23' - navigation_version = "2.7.7" + navigation_version = "2.8.3" hilt_version = '2.51.1' } From 6d72240336e00553acae7d6bc657c4e695a82aa2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 17 Oct 2024 20:15:30 -0600 Subject: [PATCH 088/550] all: fully use timber for logging --- .../java/org/oxycblt/auxio/AuxioService.kt | 4 +- .../java/org/oxycblt/auxio/MainActivity.kt | 17 ++-- .../java/org/oxycblt/auxio/MainFragment.kt | 40 ++++----- .../auxio/detail/AlbumDetailFragment.kt | 26 +++--- .../auxio/detail/ArtistDetailFragment.kt | 24 +++--- .../oxycblt/auxio/detail/DetailGenerator.kt | 4 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 35 ++++---- .../auxio/detail/GenreDetailFragment.kt | 22 ++--- .../auxio/detail/PlaylistDetailFragment.kt | 47 +++++----- .../oxycblt/auxio/detail/SongDetailDialog.kt | 4 +- .../decision/DetailDecisionViewModel.kt | 13 ++- .../auxio/detail/decision/ShowArtistDialog.kt | 4 +- .../auxio/detail/list/DetailListAdapter.kt | 1 + .../detail/list/PlaylistDetailListAdapter.kt | 4 +- .../auxio/detail/sort/AlbumSongSortDialog.kt | 4 +- .../auxio/detail/sort/ArtistSongSortDialog.kt | 4 +- .../auxio/detail/sort/GenreSongSortDialog.kt | 4 +- .../detail/sort/PlaylistSongSortDialog.kt | 4 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 61 +++++++------ .../org/oxycblt/auxio/home/HomeGenerator.kt | 8 +- .../org/oxycblt/auxio/home/HomeSettings.kt | 12 +-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 6 +- .../java/org/oxycblt/auxio/home/tabs/Tab.kt | 9 +- .../org/oxycblt/auxio/home/tabs/TabAdapter.kt | 8 +- .../auxio/home/tabs/TabCustomizeDialog.kt | 6 +- .../org/oxycblt/auxio/image/ImageSettings.kt | 6 +- .../auxio/image/extractor/CoverExtractor.kt | 4 +- .../main/java/org/oxycblt/auxio/list/Data.kt | 1 + .../org/oxycblt/auxio/list/ListFragment.kt | 1 + .../org/oxycblt/auxio/list/ListViewModel.kt | 27 +++--- .../java/org/oxycblt/auxio/list/Listeners.kt | 1 + .../auxio/list/adapter/FlexibleListAdapter.kt | 10 +-- .../list/adapter/PlayingIndicatorAdapter.kt | 11 ++- .../list/adapter/SelectionIndicatorAdapter.kt | 4 +- .../auxio/list/adapter/SimpleDiffCallback.kt | 1 + .../auxio/list/menu/MenuDialogFragment.kt | 4 +- .../oxycblt/auxio/list/menu/MenuViewModel.kt | 4 +- .../list/recycler/MaterialDragCallback.kt | 6 +- .../java/org/oxycblt/auxio/music/Music.kt | 1 + .../oxycblt/auxio/music/MusicRepository.kt | 86 +++++++++---------- .../org/oxycblt/auxio/music/MusicSettings.kt | 6 +- .../org/oxycblt/auxio/music/MusicViewModel.kt | 43 +++++----- .../auxio/music/cache/CacheRepository.kt | 17 ++-- .../music/decision/AddToPlaylistDialog.kt | 4 +- .../music/decision/DeletePlaylistDialog.kt | 4 +- .../music/decision/ExportPlaylistDialog.kt | 11 ++- .../auxio/music/decision/NewPlaylistDialog.kt | 4 +- .../music/decision/PlaylistPickerViewModel.kt | 50 ++++++----- .../music/decision/RenamePlaylistDialog.kt | 4 +- .../auxio/music/device/DeviceLibrary.kt | 4 +- .../auxio/music/dirs/DirectoryAdapter.kt | 8 +- .../auxio/music/dirs/MusicDirsDialog.kt | 8 +- .../music/external/ExternalPlaylistManager.kt | 8 +- .../org/oxycblt/auxio/music/external/M3U.kt | 4 +- .../auxio/music/fs/MediaStoreExtractor.kt | 16 ++-- .../music/fs/MediaStorePathInterpreter.kt | 6 +- .../java/org/oxycblt/auxio/music/info/Date.kt | 4 +- .../auxio/music/metadata/AudioProperties.kt | 16 ++-- .../auxio/music/metadata/SeparatorsDialog.kt | 4 +- .../auxio/music/metadata/TagExtractor.kt | 17 ++-- .../auxio/music/metadata/TagInterpreter.kt | 12 +-- .../oxycblt/auxio/music/service/Indexer.kt | 14 +-- .../music/service/IndexerNotifications.kt | 6 +- .../music/service/MusicServiceFragment.kt | 11 ++- .../music/service/SystemContentObserver.kt | 4 +- .../oxycblt/auxio/music/user/UserLibrary.kt | 37 ++++---- .../auxio/playback/PlaybackBarFragment.kt | 8 +- .../auxio/playback/PlaybackPanelFragment.kt | 8 +- .../auxio/playback/PlaybackSettings.kt | 14 +-- .../auxio/playback/PlaybackViewModel.kt | 86 +++++++++---------- .../playback/decision/PlayFromArtistDialog.kt | 4 +- .../playback/decision/PlayFromGenreDialog.kt | 4 +- .../decision/PlaybackPickerViewModel.kt | 7 +- .../playback/persist/PersistenceRepository.kt | 19 ++-- .../auxio/playback/queue/QueueAdapter.kt | 8 +- .../auxio/playback/queue/QueueFragment.kt | 6 +- .../auxio/playback/queue/QueueViewModel.kt | 18 ++-- .../replaygain/PreAmpCustomizeDialog.kt | 4 +- .../replaygain/ReplayGainAudioProcessor.kt | 24 +++--- .../service/ExoPlaybackStateHolder.kt | 31 ++++--- .../playback/service/MediaButtonReceiver.kt | 4 +- .../playback/service/MediaSessionHolder.kt | 38 ++++---- .../playback/service/MediaSessionInterface.kt | 4 +- .../service/PlaybackServiceFragment.kt | 6 +- .../service/SystemPlaybackReceiver.kt | 24 +++--- .../auxio/playback/state/PlaybackCommand.kt | 1 + .../playback/state/PlaybackStateManager.kt | 63 +++++++------- .../playback/ui/AnimatedMaterialButton.kt | 6 +- .../auxio/playback/ui/StyledSeekBar.kt | 10 +-- .../org/oxycblt/auxio/search/SearchAdapter.kt | 4 +- .../org/oxycblt/auxio/search/SearchEngine.kt | 4 +- .../oxycblt/auxio/search/SearchFragment.kt | 49 ++++++----- .../oxycblt/auxio/search/SearchViewModel.kt | 24 +++--- .../auxio/settings/BasePreferenceFragment.kt | 4 +- .../auxio/settings/RootPreferenceFragment.kt | 10 +-- .../org/oxycblt/auxio/settings/Settings.kt | 13 ++- .../categories/AudioPreferenceFragment.kt | 4 +- .../categories/MusicPreferenceFragment.kt | 8 +- .../PersonalizePreferenceFragment.kt | 4 +- .../categories/UIPreferenceFragment.kt | 14 +-- .../auxio/ui/BaseBottomSheetBehavior.kt | 4 +- .../auxio/ui/BottomSheetContentBehavior.kt | 6 +- .../auxio/ui/CoordinatorAppBarLayout.kt | 4 +- .../java/org/oxycblt/auxio/ui/MultiToolbar.kt | 8 +- .../java/org/oxycblt/auxio/ui/UISettings.kt | 6 +- .../ViewBindingBottomSheetDialogFragment.kt | 6 +- .../oxycblt/auxio/ui/ViewBindingFragment.kt | 6 +- .../ui/ViewBindingMaterialDialogFragment.kt | 6 +- .../org/oxycblt/auxio/ui/accent/Accent.kt | 4 +- .../auxio/ui/accent/AccentCustomizeDialog.kt | 4 +- .../org/oxycblt/auxio/util/ContextUtil.kt | 1 + .../org/oxycblt/auxio/util/FrameworkUtil.kt | 19 ++-- .../java/org/oxycblt/auxio/util/LangUtil.kt | 1 + .../java/org/oxycblt/auxio/util/LogUtil.kt | 71 --------------- .../java/org/oxycblt/auxio/util/StateUtil.kt | 9 +- .../oxycblt/auxio/widgets/WidgetComponent.kt | 14 +-- .../oxycblt/auxio/widgets/WidgetProvider.kt | 15 ++-- .../org/oxycblt/auxio/widgets/WidgetUtil.kt | 6 +- 118 files changed, 749 insertions(+), 836 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index 6f1b1a68c..7edcad1e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -36,7 +36,7 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.music.service.MusicServiceFragment import org.oxycblt.auxio.playback.service.PlaybackServiceFragment -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T @AndroidEntryPoint class AuxioService : @@ -150,7 +150,7 @@ class AuxioService : } override fun invalidateMusic(mediaId: String) { - logD(mediaId) + T.d(mediaId) notifyChildrenChanged(mediaId) } diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 8ddd933a6..c745f8273 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -33,9 +33,8 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.state.DeferredPlayback import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isNight -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.systemBarInsetsCompat +import timber.log.Timber as T /** * Auxio's single [AppCompatActivity]. @@ -63,7 +62,7 @@ class MainActivity : AppCompatActivity() { val binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setupEdgeToEdge(binding.root) - logD("Activity created") + T.d("Activity created") } override fun onResume() { @@ -91,10 +90,10 @@ class MainActivity : AppCompatActivity() { // Apply the color scheme. The black theme requires it's own set of themes since // it's not possible to modify the themes at run-time. if (isNight && uiSettings.useBlackTheme) { - logD("Applying black theme [accent ${uiSettings.accent}]") + T.d("Applying black theme [accent ${uiSettings.accent}]") setTheme(uiSettings.accent.blackTheme) } else { - logD("Applying normal theme [accent ${uiSettings.accent}]") + T.d("Applying normal theme [accent ${uiSettings.accent}]") setTheme(uiSettings.accent.theme) } } @@ -121,7 +120,7 @@ class MainActivity : AppCompatActivity() { private fun startIntentAction(intent: Intent?): Boolean { if (intent == null) { // Nothing to do. - logD("No intent to handle") + T.d("No intent to handle") return false } @@ -130,7 +129,7 @@ class MainActivity : AppCompatActivity() { // This is because onStart can run multiple times, and thus we really don't // want to return false and override the original delayed action with a // RestoreState action. - logD("Already used this intent") + T.d("Already used this intent") return true } intent.putExtra(KEY_INTENT_USED, true) @@ -140,11 +139,11 @@ class MainActivity : AppCompatActivity() { Intent.ACTION_VIEW -> DeferredPlayback.Open(intent.data ?: return false) Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> DeferredPlayback.ShuffleAll else -> { - logW("Unexpected intent ${intent.action}") + T.w("Unexpected intent ${intent.action}") return false } } - logD("Translated intent to $action") + T.d("Translated intent to $action") playbackModel.playDeferred(action) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 849618337..58fd9f109 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -66,9 +66,9 @@ import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.lazyReflectedMethod -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A wrapper around the home fragment that shows the playback fragment and high-level navigation. @@ -145,13 +145,13 @@ class MainFragment : if (queueSheetBehavior != null) { // In portrait mode, set up click listeners on the stacked sheets. - logD("Configuring stacked bottom sheets") + T.d("Configuring stacked bottom sheets") unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener { playbackModel.openQueue() } } else { // Dual-pane mode, manually style the static queue sheet. - logD("Configuring dual-pane bottom sheet") + T.d("Configuring dual-pane bottom sheet") binding.queueSheet.apply { // Emulate the elevated bottom sheet style. background = @@ -367,11 +367,11 @@ class MainFragment : override fun onActionSelected(actionItem: SpeedDialActionItem): Boolean { when (actionItem.id) { R.id.action_new_playlist -> { - logD("Creating playlist") + T.d("Creating playlist") musicModel.createPlaylist() } R.id.action_import_playlist -> { - logD("Importing playlist") + T.d("Importing playlist") musicModel.importPlaylist() } else -> {} @@ -402,7 +402,7 @@ class MainFragment : // 1. Loading placeholder for item lists // 2. Rework the "No Music" case to not be an error and instead result in a placeholder if (state is IndexingState.Completed && state.error == null) { - logD("Received ok response") + T.d("Received ok response") val binding = requireBinding() updateFabVisibility( binding, @@ -427,7 +427,7 @@ class MainFragment : // displaying the shuffle FAB makes no sense. We also don't want the fast scroll // popup to overlap with the FAB, so we hide the FAB when fast scrolling too. if (shouldHideAllFabs(binding, songs, isFastScrolling)) { - logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]") + T.d("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]") forceHideAllFabs() } else { if (tabType != MusicType.PLAYLISTS) { @@ -436,7 +436,7 @@ class MainFragment : } if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { - logD("Animating transition") + T.d("Animating transition") binding.homeNewPlaylistFab.hide( object : FloatingActionButton.OnVisibilityChangedListener() { override fun onHidden(fab: FloatingActionButton) { @@ -451,17 +451,17 @@ class MainFragment : } }) } else { - logD("Showing immediately") + T.d("Showing immediately") binding.homeShuffleFab.show() } } else { - logD("Showing playlist button") + T.d("Showing playlist button") if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { return } if (binding.homeShuffleFab.isOrWillBeShown) { - logD("Animating transition") + T.d("Animating transition") binding.homeShuffleFab.hide( object : FloatingActionButton.OnVisibilityChangedListener() { override fun onHidden(fab: FloatingActionButton) { @@ -476,7 +476,7 @@ class MainFragment : } }) } else { - logD("Showing immediately") + T.d("Showing immediately") binding.homeNewPlaylistFab.show() } } @@ -551,7 +551,7 @@ class MainFragment : private fun handlePanel(panel: OpenPanel?) { if (panel == null) return - logD("Trying to update panel to $panel") + T.d("Trying to update panel to $panel") when (panel) { OpenPanel.MAIN -> tryClosePlaybackPanel() OpenPanel.PLAYBACK -> tryOpenPlaybackPanel() @@ -567,7 +567,7 @@ class MainFragment : if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) { // Playback sheet is not expanded and not hidden, we can expand it. - logD("Expanding playback sheet") + T.d("Expanding playback sheet") playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED return } @@ -578,7 +578,7 @@ class MainFragment : queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) { // Queue sheet and playback sheet is expanded, close the queue sheet so the // playback panel can shown. - logD("Collapsing queue sheet") + T.d("Collapsing queue sheet") queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED } } @@ -589,7 +589,7 @@ class MainFragment : binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) { // Playback sheet (and possibly queue) needs to be collapsed. - logD("Collapsing playback and queue sheets") + T.d("Collapsing playback and queue sheets") val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED @@ -615,7 +615,7 @@ class MainFragment : val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_HIDDEN) { - logD("Unhiding and enabling playback sheet") + T.d("Unhiding and enabling playback sheet") val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? // Queue sheet behavior is either collapsed or expanded, no hiding needed @@ -636,7 +636,7 @@ class MainFragment : val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? - logD("Hiding and disabling playback and queue sheets") + T.d("Hiding and disabling playback and queue sheets") // Make both bottom sheets non-draggable so the user can't halt the hiding event. queueSheetBehavior?.apply { @@ -720,7 +720,7 @@ class MainFragment : OnBackPressedCallback(false) { override fun handleOnBackPressed() { if (detailModel.dropPlaylistEdit()) { - logD("Dropped playlist edits") + T.d("Dropped playlist edits") } } @@ -733,7 +733,7 @@ class MainFragment : OnBackPressedCallback(false) { override fun handleOnBackPressed() { if (listModel.dropSelection()) { - logD("Dropped selection") + T.d("Dropped selection") } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index c6aa1e5c9..36488ba68 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -41,10 +41,10 @@ import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A [ListFragment] that shows information about an [Album]. @@ -103,7 +103,7 @@ class AlbumDetailFragment : DetailFragment() { private fun updateAlbum(album: Album?) { if (album == null) { - logD("No album to show, navigating away") + T.d("No album to show, navigating away") findNavController().navigateUp() return } @@ -153,7 +153,7 @@ class AlbumDetailFragment : DetailFragment() { val binding = requireBinding() when (show) { is Show.SongDetails -> { - logD("Navigating to ${show.song}") + T.d("Navigating to ${show.song}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showSong(show.song.uid)) } @@ -162,11 +162,11 @@ class AlbumDetailFragment : DetailFragment() { // fragment should be launched otherwise. is Show.SongAlbumDetails -> { if (unlikelyToBeNull(detailModel.currentAlbum.value) == show.song.album) { - logD("Navigating to a ${show.song} in this album") + T.d("Navigating to a ${show.song} in this album") scrollToAlbumSong(show.song) detailModel.toShow.consume() } else { - logD("Navigating to the album of ${show.song}") + T.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showAlbum(show.song.album.uid)) } @@ -176,27 +176,27 @@ class AlbumDetailFragment : DetailFragment() { // detail fragment. is Show.AlbumDetails -> { if (unlikelyToBeNull(detailModel.currentAlbum.value) == show.album) { - logD("Navigating to the top of this album") + T.d("Navigating to the top of this album") binding.detailRecycler.scrollToPosition(0) detailModel.toShow.consume() } else { - logD("Navigating to ${show.album}") + T.d("Navigating to ${show.album}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showAlbum(show.album.uid)) } } is Show.ArtistDetails -> { - logD("Navigating to ${show.artist}") + T.d("Navigating to ${show.artist}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - logD("Navigating to artist choices for ${show.song}") + T.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - logD("Navigating to artist choices for ${show.album}") + T.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showArtistChoices(show.album.uid)) } @@ -239,7 +239,7 @@ class AlbumDetailFragment : DetailFragment() { val directions = when (decision) { is PlaylistDecision.Add -> { - logD("Adding ${decision.songs.size} songs to a playlist") + T.d("Adding ${decision.songs.size} songs to a playlist") AlbumDetailFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -268,11 +268,11 @@ class AlbumDetailFragment : DetailFragment() { val directions = when (decision) { is PlaybackDecision.PlayFromArtist -> { - logD("Launching play from artist dialog for $decision") + T.d("Launching play from artist dialog for $decision") AlbumDetailFragmentDirections.playFromArtist(decision.song.uid) } is PlaybackDecision.PlayFromGenre -> { - logD("Launching play from artist dialog for $decision") + T.d("Launching play from artist dialog for $decision") AlbumDetailFragmentDirections.playFromGenre(decision.song.uid) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 78d38b191..cf86c72d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -42,10 +42,10 @@ import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A [ListFragment] that shows information about an [Artist]. @@ -112,7 +112,7 @@ class ArtistDetailFragment : DetailFragment() { private fun updateArtist(artist: Artist?) { if (artist == null) { - logD("No artist to show, navigating away") + T.d("No artist to show, navigating away") findNavController().navigateUp() return } @@ -155,7 +155,7 @@ class ArtistDetailFragment : DetailFragment() { // The artist does not have any songs, so hide functionality that makes no sense. // ex. Play and Shuffle, Song Counts, and Genre Information. // Artists are always guaranteed to have albums however, so continue to show those. - logD("Artist is empty, disabling genres and playback") + T.d("Artist is empty, disabling genres and playback") binding.detailSubhead.isVisible = false binding.detailPlayButton?.isEnabled = false binding.detailShuffleButton?.isEnabled = false @@ -177,14 +177,14 @@ class ArtistDetailFragment : DetailFragment() { val binding = requireBinding() when (show) { is Show.SongDetails -> { - logD("Navigating to ${show.song}") + T.d("Navigating to ${show.song}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showSong(show.song.uid)) } // Songs should be shown in their album, not in their artist. is Show.SongAlbumDetails -> { - logD("Navigating to the album of ${show.song}") + T.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showAlbum(show.song.album.uid)) } @@ -192,7 +192,7 @@ class ArtistDetailFragment : DetailFragment() { // Launch a new detail view for an album, even if it is part of // this artist. is Show.AlbumDetails -> { - logD("Navigating to ${show.album}") + T.d("Navigating to ${show.album}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showAlbum(show.album.uid)) } @@ -201,22 +201,22 @@ class ArtistDetailFragment : DetailFragment() { // scroll back to the top. Otherwise launch a new detail view. is Show.ArtistDetails -> { if (show.artist == detailModel.currentArtist.value) { - logD("Navigating to the top of this artist") + T.d("Navigating to the top of this artist") binding.detailRecycler.scrollToPosition(0) detailModel.toShow.consume() } else { - logD("Navigating to ${show.artist}") + T.d("Navigating to ${show.artist}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showArtist(show.artist.uid)) } } is Show.SongArtistDecision -> { - logD("Navigating to artist choices for ${show.song}") + T.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - logD("Navigating to artist choices for ${show.album}") + T.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showArtistChoices(show.album.uid)) } @@ -260,7 +260,7 @@ class ArtistDetailFragment : DetailFragment() { val directions = when (decision) { is PlaylistDecision.Add -> { - logD("Adding ${decision.songs.size} songs to a playlist") + T.d("Adding ${decision.songs.size} songs to a playlist") ArtistDetailFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -301,7 +301,7 @@ class ArtistDetailFragment : DetailFragment() { is PlaybackDecision.PlayFromArtist -> error("Unexpected playback decision $decision") is PlaybackDecision.PlayFromGenre -> { - logD("Launching play from artist dialog for $decision") + T.d("Launching play from artist dialog for $decision") ArtistDetailFragmentDirections.playFromGenre(decision.song.uid) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt index 348badcdb..f70ffe98e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt @@ -34,7 +34,7 @@ import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.ReleaseType -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T interface DetailGenerator { fun any(uid: Music.UID): Detail? @@ -159,7 +159,7 @@ private class DetailGeneratorImpl( // groupByTo normally returns a mapping to a MutableList mapping. Since MutableList // inherits list, we can cast upwards and save a copy by directly inserting the // implicit album list into the mapping. - logD("Implicit albums present, adding to list") + T.d("Implicit albums present, adding to list") @Suppress("UNCHECKED_CAST") (grouping as MutableMap>)[ DetailSection.Albums.Category.APPEARANCES] = artist.implicitAlbums diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 5675b89f5..0cd42fa29 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -54,9 +54,8 @@ import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the @@ -302,7 +301,7 @@ constructor( private fun showImpl(show: Show) { val existing = toShow.flow.value if (existing != null) { - logD("Already have pending show command $existing, ignoring $show") + T.d("Already have pending show command $existing, ignoring $show") return } _toShow.put(show) @@ -315,10 +314,10 @@ constructor( * @param uid The UID of the [Song] to load. Must be valid. */ fun setSong(uid: Music.UID) { - logD("Opening song $uid") + T.d("Opening song $uid") _currentSong.value = musicRepository.deviceLibrary?.findSong(uid)?.also(::refreshAudioInfo) if (_currentSong.value == null) { - logW("Given song UID was invalid") + T.w("Given song UID was invalid") } } @@ -329,14 +328,14 @@ constructor( * @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid. */ fun setAlbum(uid: Music.UID) { - logD("Opening album $uid") + T.d("Opening album $uid") if (uid === _currentAlbum.value?.uid) { return } val album = detailGenerator.album(uid) refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, null) if (_currentAlbum.value == null) { - logW("Given album UID was invalid") + T.w("Given album UID was invalid") } } @@ -356,7 +355,7 @@ constructor( * @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid. */ fun setArtist(uid: Music.UID) { - logD("Opening artist $uid") + T.d("Opening artist $uid") if (uid === _currentArtist.value?.uid) { return } @@ -380,7 +379,7 @@ constructor( * @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid. */ fun setGenre(uid: Music.UID) { - logD("Opening genre $uid") + T.d("Opening genre $uid") if (uid === _currentGenre.value?.uid) { return } @@ -404,7 +403,7 @@ constructor( * @param uid The [Music.UID] of the [Playlist] to update [currentPlaylist] to. Must be valid. */ fun setPlaylist(uid: Music.UID) { - logD("Opening playlist $uid") + T.d("Opening playlist $uid") if (uid === _currentPlaylist.value?.uid) { return } @@ -414,7 +413,7 @@ constructor( /** Start a playlist editing session. Does nothing if a playlist is not being shown. */ fun startPlaylistEdit() { val playlist = _currentPlaylist.value ?: return - logD("Starting playlist edit") + T.d("Starting playlist edit") _editedPlaylist.value = playlist.songs refreshPlaylist(playlist.uid) } @@ -426,7 +425,7 @@ constructor( fun savePlaylistEdit() { val playlist = _currentPlaylist.value ?: return val editedPlaylist = _editedPlaylist.value ?: return - logD("Committing playlist edits") + T.d("Committing playlist edits") viewModelScope.launch { musicRepository.rewritePlaylist(playlist, editedPlaylist) // TODO: The user could probably press some kind of button if they were fast enough. @@ -479,7 +478,7 @@ constructor( if (realFrom !in editedPlaylist.indices || realTo !in editedPlaylist.indices) { return false } - logD("Moving playlist song from $realFrom [$from] to $realTo [$to]") + T.d("Moving playlist song from $realFrom [$from] to $realTo [$to]") editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo)) _editedPlaylist.value = editedPlaylist refreshPlaylist(playlist.uid, UpdateInstructions.Move(from, to)) @@ -498,7 +497,7 @@ constructor( if (realAt !in editedPlaylist.indices) { return } - logD("Removing playlist song at $realAt [$at]") + T.d("Removing playlist song at $realAt [$at]") editedPlaylist.removeAt(realAt) _editedPlaylist.value = editedPlaylist refreshPlaylist( @@ -506,13 +505,13 @@ constructor( if (editedPlaylist.isNotEmpty()) { UpdateInstructions.Remove(at, 1) } else { - logD("Playlist will be empty after removal, removing header") + T.d("Playlist will be empty after removal, removing header") UpdateInstructions.Remove(at - 1, 3) }) } private fun refreshAudioInfo(song: Song) { - logD("Refreshing audio info") + T.d("Refreshing audio info") // Clear any previous job in order to avoid stale data from appearing in the UI. currentSongJob?.cancel() _songAudioProperties.value = null @@ -520,7 +519,7 @@ constructor( viewModelScope.launch(Dispatchers.IO) { val info = audioPropertiesFactory.extract(song) yield() - logD("Updating audio info to $info") + T.d("Updating audio info to $info") _songAudioProperties.value = info } } @@ -587,7 +586,7 @@ constructor( uid: Music.UID, instructions: UpdateInstructions = UpdateInstructions.Diff ) { - logD("Refreshing playlist list") + T.d("Refreshing playlist list") val edited = editedPlaylist.value if (edited == null) { val playlist = detailGenerator.playlist(uid) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 6081d6efd..8173d781a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -40,10 +40,10 @@ import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A [ListFragment] that shows information for a particular [Genre]. @@ -110,7 +110,7 @@ class GenreDetailFragment : DetailFragment() { private fun updateGenre(genre: Genre?) { if (genre == null) { - logD("No genre to show, navigating away") + T.d("No genre to show, navigating away") findNavController().navigateUp() return } @@ -144,7 +144,7 @@ class GenreDetailFragment : DetailFragment() { private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { - logD("Navigating to ${show.song}") + T.d("Navigating to ${show.song}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showSong(show.song.uid)) } @@ -152,7 +152,7 @@ class GenreDetailFragment : DetailFragment() { // Songs should be scrolled to if the album matches, or a new detail // fragment should be launched otherwise. is Show.SongAlbumDetails -> { - logD("Navigating to the album of ${show.song}") + T.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showAlbum(show.song.album.uid)) } @@ -160,29 +160,29 @@ class GenreDetailFragment : DetailFragment() { // If the album matches, no need to do anything. Otherwise launch a new // detail fragment. is Show.AlbumDetails -> { - logD("Navigating to ${show.album}") + T.d("Navigating to ${show.album}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showAlbum(show.album.uid)) } // Always launch a new ArtistDetailFragment. is Show.ArtistDetails -> { - logD("Navigating to ${show.artist}") + T.d("Navigating to ${show.artist}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - logD("Navigating to artist choices for ${show.song}") + T.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - logD("Navigating to artist choices for ${show.album}") + T.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showArtistChoices(show.album.uid)) } is Show.GenreDetails -> { - logD("Navigated to this genre") + T.d("Navigated to this genre") detailModel.toShow.consume() } is Show.PlaylistDetails -> { @@ -223,7 +223,7 @@ class GenreDetailFragment : DetailFragment() { val directions = when (decision) { is PlaylistDecision.Add -> { - logD("Adding ${decision.songs.size} songs to a playlist") + T.d("Adding ${decision.songs.size} songs to a playlist") GenreDetailFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -262,7 +262,7 @@ class GenreDetailFragment : DetailFragment() { val directions = when (decision) { is PlaybackDecision.PlayFromArtist -> { - logD("Launching play from artist dialog for $decision") + T.d("Launching play from artist dialog for $decision") GenreDetailFragmentDirections.playFromArtist(decision.song.uid) } is PlaybackDecision.PlayFromGenre -> error("Unexpected playback decision $decision") diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index bfe3c261c..1c8a0882d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -49,11 +49,10 @@ import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A [ListFragment] that shows information for a particular [Playlist]. @@ -82,11 +81,11 @@ class PlaylistDetailFragment : getContentLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) { - logW("No URI returned from file picker") + T.w("No URI returned from file picker") return@registerForActivityResult } - logD("Received playlist URI $uri") + T.d("Received playlist URI $uri") musicModel.importPlaylist(uri, pendingImportTarget) } @@ -194,7 +193,7 @@ class PlaylistDetailFragment : getString(R.string.fmt_editing, playlist.name.resolve(requireContext())) if (editedPlaylist != null) { - logD("Binding edited playlist image") + T.d("Binding edited playlist image") binding.detailCover.bind( editedPlaylist, binding.context.getString(R.string.desc_playlist_image, playlist.name), @@ -223,7 +222,7 @@ class PlaylistDetailFragment : val playable = playlist.songs.isNotEmpty() && editedPlaylist == null if (!playable) { - logD("Playlist is being edited or is empty, disabling playback options") + T.d("Playlist is being edited or is empty, disabling playback options") } binding.detailPlayButton?.apply { @@ -249,7 +248,7 @@ class PlaylistDetailFragment : listModel.dropSelection() if (editedPlaylist != null) { - logD("Updating save button state") + T.d("Updating save button state") requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).apply { isEnabled = editedPlaylist != detailModel.currentPlaylist.value?.songs } @@ -261,38 +260,38 @@ class PlaylistDetailFragment : private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { - logD("Navigating to ${show.song}") + T.d("Navigating to ${show.song}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showSong(show.song.uid)) } is Show.SongAlbumDetails -> { - logD("Navigating to the album of ${show.song}") + T.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showAlbum(show.song.album.uid)) } is Show.AlbumDetails -> { - logD("Navigating to ${show.album}") + T.d("Navigating to ${show.album}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showAlbum(show.album.uid)) } is Show.ArtistDetails -> { - logD("Navigating to ${show.artist}") + T.d("Navigating to ${show.artist}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - logD("Navigating to artist choices for ${show.song}") + T.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - logD("Navigating to artist choices for ${show.album}") + T.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe( PlaylistDetailFragmentDirections.showArtistChoices(show.album.uid)) } is Show.PlaylistDetails -> { - logD("Navigated to this playlist") + T.d("Navigated to this playlist") detailModel.toShow.consume() } is Show.GenreDetails -> { @@ -333,7 +332,7 @@ class PlaylistDetailFragment : val directions = when (decision) { is PlaylistDecision.Import -> { - logD("Importing playlist") + T.d("Importing playlist") pendingImportTarget = decision.target requireNotNull(getContentLauncher) { "Content picker launcher was not available" @@ -343,7 +342,7 @@ class PlaylistDetailFragment : return } is PlaylistDecision.Rename -> { - logD("Renaming ${decision.playlist}") + T.d("Renaming ${decision.playlist}") PlaylistDetailFragmentDirections.renamePlaylist( decision.playlist.uid, decision.template, @@ -351,15 +350,15 @@ class PlaylistDetailFragment : decision.reason) } is PlaylistDecision.Export -> { - logD("Exporting ${decision.playlist}") + T.d("Exporting ${decision.playlist}") PlaylistDetailFragmentDirections.exportPlaylist(decision.playlist.uid) } is PlaylistDecision.Delete -> { - logD("Deleting ${decision.playlist}") + T.d("Deleting ${decision.playlist}") PlaylistDetailFragmentDirections.deletePlaylist(decision.playlist.uid) } is PlaylistDecision.Add -> { - logD("Adding ${decision.songs.size} songs to a playlist") + T.d("Adding ${decision.songs.size} songs to a playlist") PlaylistDetailFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -385,11 +384,11 @@ class PlaylistDetailFragment : val directions = when (decision) { is PlaybackDecision.PlayFromArtist -> { - logD("Launching play from artist dialog for $decision") + T.d("Launching play from artist dialog for $decision") PlaylistDetailFragmentDirections.playFromArtist(decision.song.uid) } is PlaybackDecision.PlayFromGenre -> { - logD("Launching play from artist dialog for $decision") + T.d("Launching play from artist dialog for $decision") PlaylistDetailFragmentDirections.playFromGenre(decision.song.uid) } } @@ -400,15 +399,15 @@ class PlaylistDetailFragment : val id = when { detailModel.editedPlaylist.value != null -> { - logD("Currently editing playlist, showing edit toolbar") + T.d("Currently editing playlist, showing edit toolbar") R.id.detail_edit_toolbar } listModel.selected.value.isNotEmpty() -> { - logD("Currently selecting, showing selection toolbar") + T.d("Currently selecting, showing selection toolbar") R.id.detail_selection_toolbar } else -> { - logD("Using normal toolbar") + T.d("Using normal toolbar") R.id.detail_normal_toolbar } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index 9bad4e225..b89e33292 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -42,7 +42,7 @@ import org.oxycblt.auxio.playback.replaygain.formatDb import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.concatLocalized -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [ViewBindingMaterialDialogFragment] that shows information about a Song. @@ -76,7 +76,7 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment { - logD("Creating navigation choices for song") + T.d("Creating navigation choices for song") ArtistShowChoices.FromSong(music) } is Album -> { - logD("Creating navigation choices for album") + T.d("Creating navigation choices for album") ArtistShowChoices.FromAlbum(music) } else -> { - logW("Given song/album UID was invalid") + T.w("Given song/album UID was invalid") null } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt index 1f82ddfe2..7fddcd6b9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt @@ -35,7 +35,7 @@ import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A picker [ViewBindingMaterialDialogFragment] intended for when the [Artist] to show is ambiguous. @@ -85,7 +85,7 @@ class ShowArtistDialog : private fun updateChoices(choices: ArtistShowChoices?) { if (choices == null) { - logD("No choices to show, navigating away") + T.d("No choices to show, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt index 87aebf5df..c28dce1f2 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt @@ -38,6 +38,7 @@ import org.oxycblt.auxio.list.recycler.DividerViewHolder import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater +import timber.log.Timber as T /** * A [RecyclerView.Adapter] that implements shared behavior between lists of child items in the diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index 213c65b78..4a00f6fe8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -46,7 +46,7 @@ import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.inflater -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [DetailListAdapter] implementing the header, sub-items, and editing state for the [Playlist] @@ -97,7 +97,7 @@ class PlaylistDetailListAdapter(private val listener: Listener) : // Nothing to do. return } - logD("Updating editing state [old: $isEditing new: $editing]") + T.d("Updating editing state [old: $isEditing new: $editing]") this.isEditing = editing notifyItemRangeChanged(0, currentList.size, PAYLOAD_EDITING_CHANGED) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt index de8fe127d..19fdc2b36 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.SortDialog import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [SortDialog] that controls the [Sort] of [DetailViewModel.albumSongSort]. @@ -56,7 +56,7 @@ class AlbumSongSortDialog : SortDialog() { private fun updateAlbum(album: Album?) { if (album == null) { - logD("No album to sort, navigating away") + T.d("No album to sort, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt index d0d4e355a..c7961a3d6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.SortDialog import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [SortDialog] that controls the [Sort] of [DetailViewModel.artistSongSort]. @@ -57,7 +57,7 @@ class ArtistSongSortDialog : SortDialog() { private fun updateArtist(artist: Artist?) { if (artist == null) { - logD("No artist to sort, navigating away") + T.d("No artist to sort, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt index 88c69172b..e92bd867a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.SortDialog import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [SortDialog] that controls the [Sort] of [DetailViewModel.genreSongSort]. @@ -62,7 +62,7 @@ class GenreSongSortDialog : SortDialog() { private fun updateGenre(genre: Genre?) { if (genre == null) { - logD("No genre to sort, navigating away") + T.d("No genre to sort, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt index 923d41829..8c90759ba 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.SortDialog import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [SortDialog] that controls the [Sort] of [DetailViewModel.genreSongSort]. @@ -62,7 +62,7 @@ class PlaylistSongSortDialog : SortDialog() { private fun updatePlaylist(genre: Playlist?) { if (genre == null) { - logD("No genre to sort, navigating away") + T.d("No genre to sort, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index bad5d0ec6..850931796 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -77,10 +77,9 @@ import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedMethod -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast +import timber.log.Timber as T /** * The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation @@ -126,11 +125,11 @@ class HomeFragment : getContentLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) { - logW("No URI returned from file picker") + T.w("No URI returned from file picker") return@registerForActivityResult } - logD("Received playlist URI $uri") + T.d("Received playlist URI $uri") musicModel.importPlaylist(uri, pendingImportTarget) } @@ -221,17 +220,17 @@ class HomeFragment : return when (item.itemId) { // Handle main actions (Search, Settings, About) R.id.action_search -> { - logD("Navigating to search") + T.d("Navigating to search") findNavController().navigateSafe(HomeFragmentDirections.search()) true } R.id.action_settings -> { - logD("Navigating to preferences") + T.d("Navigating to preferences") homeModel.showSettings() true } R.id.action_about -> { - logD("Navigating to about") + T.d("Navigating to about") homeModel.showAbout() true } @@ -251,7 +250,7 @@ class HomeFragment : true } else -> { - logW("Unexpected menu item selected") + T.w("Unexpected menu item selected") false } } @@ -265,7 +264,7 @@ class HomeFragment : if (homeModel.currentTabTypes.size == 1) { // A single tab makes the tab layout redundant, hide it and disable the collapsing // behavior. - logD("Single tab shown, disabling TabLayout") + T.d("Single tab shown, disabling TabLayout") binding.homeTabs.isVisible = false binding.homeAppbar.setExpanded(true, false) toolbarParams.scrollFlags = 0 @@ -303,7 +302,7 @@ class HomeFragment : private fun handleRecreate(recreate: Unit?) { if (recreate == null) return val binding = requireBinding() - logD("Recreating ViewPager") + T.d("Recreating ViewPager") // Move back to position zero, as there must be a tab there. binding.homePager.currentItem = 0 // Make sure tabs are set up to also follow the new ViewPager configuration. @@ -320,7 +319,7 @@ class HomeFragment : is IndexingState.Completed -> setupCompleteState(binding, state.error) is IndexingState.Indexing -> setupIndexingState(binding, state.progress) null -> { - logD("Indexer is in indeterminate state") + T.d("Indexer is in indeterminate state") binding.homeIndexingContainer.visibility = View.INVISIBLE } } @@ -328,19 +327,19 @@ class HomeFragment : private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) { if (error == null) { - logD("Received ok response") + T.d("Received ok response") binding.homeIndexingContainer.visibility = View.INVISIBLE return } - logD("Received non-ok response") + T.d("Received non-ok response") val context = requireContext() binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingActions.visibility = View.VISIBLE when (error) { is NoAudioPermissionException -> { - logD("Showing permission prompt") + T.d("Showing permission prompt") binding.homeIndexingStatus.setText(R.string.err_no_perms) // Configure the action to act as a permission launcher. binding.homeIndexingTry.apply { @@ -355,7 +354,7 @@ class HomeFragment : binding.homeIndexingMore.visibility = View.GONE } is NoMusicException -> { - logD("Showing no music error") + T.d("Showing no music error") binding.homeIndexingStatus.setText(R.string.err_no_music) // Configure the action to act as a reload trigger. binding.homeIndexingTry.apply { @@ -366,7 +365,7 @@ class HomeFragment : binding.homeIndexingMore.visibility = View.GONE } else -> { - logD("Showing generic error") + T.d("Showing generic error") binding.homeIndexingStatus.setText(R.string.err_index_failed) // Configure the action to act as a reload trigger. binding.homeIndexingTry.apply { @@ -412,14 +411,14 @@ class HomeFragment : val directions = when (decision) { is PlaylistDecision.New -> { - logD("Creating new playlist") + T.d("Creating new playlist") HomeFragmentDirections.newPlaylist( decision.songs.map { it.uid }.toTypedArray(), decision.template, decision.reason) } is PlaylistDecision.Import -> { - logD("Importing playlist") + T.d("Importing playlist") pendingImportTarget = decision.target requireNotNull(getContentLauncher) { "Content picker launcher was not available" @@ -429,7 +428,7 @@ class HomeFragment : return } is PlaylistDecision.Rename -> { - logD("Renaming ${decision.playlist}") + T.d("Renaming ${decision.playlist}") HomeFragmentDirections.renamePlaylist( decision.playlist.uid, decision.template, @@ -437,15 +436,15 @@ class HomeFragment : decision.reason) } is PlaylistDecision.Export -> { - logD("Exporting ${decision.playlist}") + T.d("Exporting ${decision.playlist}") HomeFragmentDirections.exportPlaylist(decision.playlist.uid) } is PlaylistDecision.Delete -> { - logD("Deleting ${decision.playlist}") + T.d("Deleting ${decision.playlist}") HomeFragmentDirections.deletePlaylist(decision.playlist.uid) } is PlaylistDecision.Add -> { - logD("Adding ${decision.songs.size} to a playlist") + T.d("Adding ${decision.songs.size} to a playlist") HomeFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -476,38 +475,38 @@ class HomeFragment : private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { - logD("Navigating to ${show.song}") + T.d("Navigating to ${show.song}") findNavController().navigateSafe(HomeFragmentDirections.showSong(show.song.uid)) } is Show.SongAlbumDetails -> { - logD("Navigating to the album of ${show.song}") + T.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(HomeFragmentDirections.showAlbum(show.song.album.uid)) } is Show.AlbumDetails -> { - logD("Navigating to ${show.album}") + T.d("Navigating to ${show.album}") findNavController().navigateSafe(HomeFragmentDirections.showAlbum(show.album.uid)) } is Show.ArtistDetails -> { - logD("Navigating to ${show.artist}") + T.d("Navigating to ${show.artist}") findNavController().navigateSafe(HomeFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - logD("Navigating to artist choices for ${show.song}") + T.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(HomeFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - logD("Navigating to artist choices for ${show.album}") + T.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(HomeFragmentDirections.showArtistChoices(show.album.uid)) } is Show.GenreDetails -> { - logD("Navigating to ${show.genre}") + T.d("Navigating to ${show.genre}") findNavController().navigateSafe(HomeFragmentDirections.showGenre(show.genre.uid)) } is Show.PlaylistDetails -> { - logD("Navigating to ${show.playlist}") + T.d("Navigating to ${show.playlist}") findNavController() .navigateSafe(HomeFragmentDirections.showPlaylist(show.playlist.uid)) } @@ -535,7 +534,7 @@ class HomeFragment : binding.homeSelectionToolbar.title = getString(R.string.fmt_selected, selected.size) if (binding.homeToolbar.setVisible(R.id.home_selection_toolbar)) { // New selection started, show the AppBarLayout to indicate the new state. - logD("Significant selection occurred, expanding AppBar") + T.d("Significant selection occurred, expanding AppBar") binding.homeAppbar.expandWithScrollingRecycler() } } else { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt index 52135d3d4..140b5c573 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T interface HomeGenerator { fun attach() @@ -89,7 +89,7 @@ private class HomeGeneratorImpl( override fun onHideCollaboratorsChanged() { // Changes in the hide collaborator setting will change the artist contents // of the library, consider it a library update. - logD("Collaborator setting changed, forwarding update") + T.d("Collaborator setting changed, forwarding update") onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) } @@ -121,7 +121,7 @@ private class HomeGeneratorImpl( override fun onMusicChanges(changes: MusicRepository.Changes) { val deviceLibrary = musicRepository.deviceLibrary if (changes.deviceLibrary && deviceLibrary != null) { - logD("Refreshing library") + T.d("Refreshing library") // Get the each list of items in the library to use as our list data. // Applying the preferred sorting to them. invalidator.invalidateMusic(MusicType.SONGS, UpdateInstructions.Diff) @@ -132,7 +132,7 @@ private class HomeGeneratorImpl( val userLibrary = musicRepository.userLibrary if (changes.userLibrary && userLibrary != null) { - logD("Refreshing playlists") + T.d("Refreshing playlists") invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff) } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt index ec54942f3..fe88eee89 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt @@ -26,8 +26,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * User configuration specific to the home UI. @@ -68,17 +68,17 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) override fun migrate() { if (sharedPreferences.contains(OLD_KEY_LIB_TABS)) { - logD("Migrating tab setting") + T.d("Migrating tab setting") val oldTabs = Tab.fromIntCode(sharedPreferences.getInt(OLD_KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) ?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT)) - logD("Old tabs: $oldTabs") + T.d("Old tabs: $oldTabs") // The playlist tab is now parsed, but it needs to be made visible. val playlistIndex = oldTabs.indexOfFirst { it.type == MusicType.PLAYLISTS } check(playlistIndex > -1) // This should exist, otherwise we are in big trouble oldTabs[playlistIndex] = Tab.Visible(MusicType.PLAYLISTS) - logD("New tabs: $oldTabs") + T.d("New tabs: $oldTabs") sharedPreferences.edit { putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(oldTabs)) @@ -90,11 +90,11 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) override fun onSettingChanged(key: String, listener: HomeSettings.Listener) { when (key) { getString(R.string.set_key_home_tabs) -> { - logD("Dispatching tab setting change") + T.d("Dispatching tab setting change") listener.onTabsChanged() } getString(R.string.set_key_hide_collaborators) -> { - logD("Dispatching collaborator setting change") + T.d("Dispatching collaborator setting change") listener.onHideCollaboratorsChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 2dcf5b2a4..e90112c26 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * The ViewModel for managing the tab data and lists of the home view. @@ -249,7 +249,7 @@ constructor( * @param pagerPos The new position of the ViewPager2 instance. */ fun synchronizeTabPosition(pagerPos: Int) { - logD("Updating current tab to ${currentTabTypes[pagerPos]}") + T.d("Updating current tab to ${currentTabTypes[pagerPos]}") _currentTabType.value = currentTabTypes[pagerPos] } @@ -259,7 +259,7 @@ constructor( * @param isFastScrolling true if the user is currently fast scrolling, false otherwise. */ fun setFastScrolling(isFastScrolling: Boolean) { - logD("Updating fast scrolling state: $isFastScrolling") + T.d("Updating fast scrolling state: $isFastScrolling") _isFastScrolling.value = isFastScrolling } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index aee964e45..1c1ba6fe8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -19,8 +19,7 @@ package org.oxycblt.auxio.home.tabs import org.oxycblt.auxio.music.MusicType -import org.oxycblt.auxio.util.logE -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * A representation of a library tab suitable for configuration. @@ -86,7 +85,7 @@ sealed class Tab(open val type: MusicType) { // Like when deserializing, make sure there are no duplicate tabs for whatever reason. val distinct = tabs.distinctBy { it.type } if (tabs.size != distinct.size) { - logW( + T.w( "Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]") } @@ -133,13 +132,13 @@ sealed class Tab(open val type: MusicType) { // Make sure there are no duplicate tabs val distinct = tabs.distinctBy { it.type } if (tabs.size != distinct.size) { - logW( + T.w( "Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]") } // For safety, return null if we have an empty or larger-than-expected tab array. if (distinct.isEmpty() || distinct.size < MAX_SEQUENCE_IDX) { - logE("Sequence size was ${distinct.size}, which is invalid") + T.e("Sequence size was ${distinct.size}, which is invalid") return null } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index 736b5ba30..cb5c56384 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.util.inflater -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration. @@ -55,7 +55,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param newTabs The new array of tabs to show. */ fun submitTabs(newTabs: Array) { - logD("Force-updating tab information") + T.d("Force-updating tab information") tabs = newTabs @Suppress("NotifyDatasetChanged") notifyDataSetChanged() } @@ -67,7 +67,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param tab The new tab. */ fun setTab(at: Int, tab: Tab) { - logD("Updating tab [at: $at, tab: $tab]") + T.d("Updating tab [at: $at, tab: $tab]") tabs[at] = tab // Use a payload to avoid an item change animation. notifyItemChanged(at, PAYLOAD_TAB_CHANGED) @@ -80,7 +80,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param b The position of the second tab to swap. */ fun swapTabs(a: Int, b: Int) { - logD("Swapping tabs [a: $a, b: $b]") + T.d("Swapping tabs [a: $a, b: $b]") val tmp = tabs[b] tabs[b] = tabs[a] tabs[a] = tmp diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index 57bc73c15..e550fe297 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -31,7 +31,7 @@ import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.home.HomeSettings import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [ViewBindingMaterialDialogFragment] that allows the user to modify the home [Tab] @@ -52,7 +52,7 @@ class TabCustomizeDialog : builder .setTitle(R.string.set_lib_tabs) .setPositiveButton(R.string.lbl_ok) { _, _ -> - logD("Committing tab changes") + T.d("Committing tab changes") homeSettings.homeTabs = tabAdapter.tabs } .setNegativeButton(R.string.lbl_cancel, null) @@ -99,7 +99,7 @@ class TabCustomizeDialog : is Tab.Visible -> Tab.Invisible(old.type) is Tab.Invisible -> Tab.Visible(old.type) } - logD("Flipping tab visibility [from: $old to: $new]") + T.d("Flipping tab visibility [from: $old to: $new]") tabAdapter.setTab(index, new) // Prevent the user from saving if all the tabs are Invisible, as that's an invalid state. diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt index 232d903a1..d94b0faf4 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt @@ -24,7 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * User configuration specific to image loading. @@ -58,7 +58,7 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context // Show album covers and Ignore MediaStore covers were unified in 3.0.0 if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) || sharedPreferences.contains(OLD_KEY_QUALITY_COVERS)) { - logD("Migrating cover settings") + T.d("Migrating cover settings") val mode = when { @@ -79,7 +79,7 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context override fun onSettingChanged(key: String, listener: ImageSettings.Listener) { if (key == getString(R.string.set_key_cover_mode) || key == getString(R.string.set_key_square_covers)) { - logD("Dispatching image setting change") + T.d("Dispatching image setting change") listener.onImageSettingsChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index 7c57bdb74..ca0f82bcf 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -53,7 +53,7 @@ import okio.source import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.logE +import timber.log.Timber as T /** * Provides functionality for extracting album cover information. Meant for internal use only. @@ -153,7 +153,7 @@ constructor( } } } catch (e: Exception) { - logE("Unable to extract album cover due to an error: $e") + T.e("Unable to extract album cover due to an error: $e") null } diff --git a/app/src/main/java/org/oxycblt/auxio/list/Data.kt b/app/src/main/java/org/oxycblt/auxio/list/Data.kt index 5507bb9fc..9c874b5b1 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.list import androidx.annotation.StringRes +import timber.log.Timber as T // TODO: Consider breaking this up into sealed classes for individual adapters /** A marker for something that is a RecyclerView item. Has no functionality on it's own. */ diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 546b03a49..49636b594 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -21,6 +21,7 @@ package org.oxycblt.auxio.list import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import org.oxycblt.auxio.music.Music +import timber.log.Timber as T /** * A Fragment containing a selectable list. diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt index e223f439b..a648019f9 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt @@ -36,8 +36,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * A [ViewModel] that orchestrates menu dialogs and selection state. @@ -94,16 +93,16 @@ constructor(private val listSettings: ListSettings, private val musicRepository: */ fun select(music: Music) { if (music is MusicParent && music.songs.isEmpty()) { - logD("Cannot select empty parent, ignoring operation") + T.d("Cannot select empty parent, ignoring operation") return } val selected = _selected.value.toMutableList() if (!selected.remove(music)) { - logD("Adding $music to selection") + T.d("Adding $music to selection") selected.add(music) } else { - logD("Removed $music from selection") + T.d("Removed $music from selection") } _selected.value = selected @@ -131,7 +130,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @return A list of [Song]s collated from each item selected. */ fun takeSelection(): List { - logD("Taking selection") + T.d("Taking selection") return peekSelection().also { _selected.value = listOf() } } @@ -141,7 +140,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @return true if the prior selection was non-empty, false otherwise. */ fun dropSelection(): Boolean { - logD("Dropping selection [empty=${_selected.value.isEmpty()}]") + T.d("Dropping selection [empty=${_selected.value.isEmpty()}]") return _selected.value.isNotEmpty().also { _selected.value = listOf() } } @@ -155,7 +154,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * should do. */ fun openMenu(@MenuRes menuRes: Int, song: Song, playWith: PlaySong) { - logD("Opening menu for $song") + T.d("Opening menu for $song") openImpl(Menu.ForSong(menuRes, song, playWith)) } @@ -167,7 +166,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param album The [Album] to show. */ fun openMenu(@MenuRes menuRes: Int, album: Album) { - logD("Opening menu for $album") + T.d("Opening menu for $album") openImpl(Menu.ForAlbum(menuRes, album)) } @@ -179,7 +178,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param artist The [Artist] to show. */ fun openMenu(@MenuRes menuRes: Int, artist: Artist) { - logD("Opening menu for $artist") + T.d("Opening menu for $artist") openImpl(Menu.ForArtist(menuRes, artist)) } @@ -191,7 +190,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param genre The [Genre] to show. */ fun openMenu(@MenuRes menuRes: Int, genre: Genre) { - logD("Opening menu for $genre") + T.d("Opening menu for $genre") openImpl(Menu.ForGenre(menuRes, genre)) } @@ -203,7 +202,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param playlist The [Playlist] to show. */ fun openMenu(@MenuRes menuRes: Int, playlist: Playlist) { - logD("Opening menu for $playlist") + T.d("Opening menu for $playlist") openImpl(Menu.ForPlaylist(menuRes, playlist)) } @@ -215,14 +214,14 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param songs The [Song] selection to show. */ fun openMenu(@MenuRes menuRes: Int, songs: List) { - logD("Opening menu for ${songs.size} songs") + T.d("Opening menu for ${songs.size} songs") openImpl(Menu.ForSelection(menuRes, songs)) } private fun openImpl(menu: Menu) { val existing = _menu.flow.value if (existing != null) { - logW("Already opening $existing, ignoring $menu") + T.w("Already opening $existing, ignoring $menu") return } _menu.put(menu) diff --git a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt index c7704e503..0b0b701d7 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt @@ -21,6 +21,7 @@ package org.oxycblt.auxio.list import android.view.MotionEvent import android.view.View import androidx.recyclerview.widget.RecyclerView +import timber.log.Timber as T /** * A basic listener for list interactions. diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt index f01b47f41..99bd112f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -25,7 +25,7 @@ import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import java.util.concurrent.Executor -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A variant of ListDiffer with more flexible updates. @@ -57,7 +57,7 @@ abstract class FlexibleListAdapter( instructions: UpdateInstructions?, callback: (() -> Unit)? = null ) { - logD("Updating list to ${newList.size} items with $instructions") + T.d("Updating list to ${newList.size} items with $instructions") differ.update(newList, instructions, callback) } } @@ -171,7 +171,7 @@ private class FlexibleListDiffer( ) { // fast simple remove all if (newList.isEmpty()) { - logD("Short-circuiting diff to remove all") + T.d("Short-circuiting diff to remove all") val countRemoved = oldList.size currentList = emptyList() // notify last, after list is updated @@ -182,7 +182,7 @@ private class FlexibleListDiffer( // fast simple first insert if (oldList.isEmpty()) { - logD("Short-circuiting diff to insert all") + T.d("Short-circuiting diff to insert all") currentList = newList // notify last, after list is updated updateCallback.onInserted(0, newList.size) @@ -244,7 +244,7 @@ private class FlexibleListDiffer( mainThreadExecutor.execute { if (maxScheduledGeneration == runGeneration) { - logD("Applying calculated diff") + T.d("Applying calculated diff") currentList = newList result.dispatchUpdatesTo(updateCallback) callback?.invoke() diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt index 1beddc8ef..15e36be00 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt @@ -21,8 +21,7 @@ package org.oxycblt.auxio.list.adapter import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * A [RecyclerView.Adapter] that supports indicating the playback status of a particular item. @@ -59,7 +58,7 @@ abstract class PlayingIndicatorAdapter( * @param isPlaying Whether playback is ongoing or paused. */ fun setPlaying(item: T?, isPlaying: Boolean) { - logD("Updating playing item [old: $currentItem new: $item]") + T.d("Updating playing item [old: $currentItem new: $item]") var updatedItem = false if (currentItem != item) { @@ -72,7 +71,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - logW("oldItem was not in adapter data") + T.w("oldItem was not in adapter data") } } @@ -82,7 +81,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - logW("newItem was not in adapter data") + T.w("newItem was not in adapter data") } } @@ -100,7 +99,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - logW("newItem was not in adapter data") + T.w("newItem was not in adapter data") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt index 9339f78fc..0bd0a80ed 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt @@ -22,7 +22,7 @@ import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of @@ -55,7 +55,7 @@ abstract class SelectionIndicatorAdapter( // Nothing to do. return } - logD("Updating selection [old=${oldSelectedItems.size} new=${newSelectedItems.size}") + T.d("Updating selection [old=${oldSelectedItems.size} new=${newSelectedItems.size}") selectedItems = newSelectedItems for (i in currentList.indices) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt index d1fdc8a7a..53105d975 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.list.adapter import androidx.recyclerview.widget.DiffUtil import org.oxycblt.auxio.list.Item +import timber.log.Timber as T /** * A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. Use this diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt index 907cef049..ae8e49aaa 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.ui.ViewBindingBottomSheetDialogFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [ViewBindingBottomSheetDialogFragment] that displays basic music information and a series of @@ -102,7 +102,7 @@ abstract class MenuDialogFragment : private fun updateMenu(menu: Menu?) { if (menu == null) { - logD("No menu to show, navigating away") + T.d("No menu to show, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt index 18ff75ccc..220022b7b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.playback.PlaySong -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * Manages the state information for [MenuDialogFragment] implementations. @@ -55,7 +55,7 @@ class MenuViewModel @Inject constructor(private val musicRepository: MusicReposi fun setMenu(parcel: Menu.Parcel) { _currentMenu.value = unpackParcel(parcel) if (_currentMenu.value == null) { - logW("Given menu parcel $parcel was invalid") + T.w("Given menu parcel $parcel was invalid") } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt index a9ffbcd6d..daa032a59 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt @@ -34,7 +34,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.list.recycler.MaterialDragCallback.ViewHolder import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getInteger -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in editable UIs, @@ -94,7 +94,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { // this is only done once when the item is initially picked up. // TODO: I think this is possible to improve with a raw ValueAnimator. if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) { - logD("Lifting ViewHolder") + T.d("Lifting ViewHolder") val bg = holder.background val elevation = recyclerView.context.getDimen(MR.dimen.m3_sys_elevation_level4) @@ -136,7 +136,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { // This function can be called multiple times, so only start the animation when the view's // translationZ is already non-zero. if (holder.root.translationZ != 0f) { - logD("Lifting ViewHolder") + T.d("Lifting ViewHolder") val bg = holder.background val elevation = recyclerView.context.getDimen(MR.dimen.m3_sys_elevation_level4) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 359afd9c3..384151936 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -39,6 +39,7 @@ import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.toUuidOrNull +import timber.log.Timber as T /** * Abstract music data. This contains universal information about all concrete music diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 7d60f825d..89c61db05 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -44,9 +44,7 @@ import org.oxycblt.auxio.music.user.MutableUserLibrary import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.util.DEFAULT_TIMEOUT import org.oxycblt.auxio.util.forEachWithTimeout -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logE -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * Primary manager of music information and loading. @@ -244,51 +242,51 @@ constructor( @Synchronized override fun addUpdateListener(listener: MusicRepository.UpdateListener) { - logD("Adding $listener to update listeners") + T.d("Adding $listener to update listeners") updateListeners.add(listener) listener.onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = true)) } @Synchronized override fun removeUpdateListener(listener: MusicRepository.UpdateListener) { - logD("Removing $listener to update listeners") + T.d("Removing $listener to update listeners") if (!updateListeners.remove(listener)) { - logW("Update listener $listener was not added prior, cannot remove") + T.w("Update listener $listener was not added prior, cannot remove") } } @Synchronized override fun addIndexingListener(listener: MusicRepository.IndexingListener) { - logD("Adding $listener to indexing listeners") + T.d("Adding $listener to indexing listeners") indexingListeners.add(listener) listener.onIndexingStateChanged() } @Synchronized override fun removeIndexingListener(listener: MusicRepository.IndexingListener) { - logD("Removing $listener from indexing listeners") + T.d("Removing $listener from indexing listeners") if (!indexingListeners.remove(listener)) { - logW("Indexing listener $listener was not added prior, cannot remove") + T.w("Indexing listener $listener was not added prior, cannot remove") } } @Synchronized override fun registerWorker(worker: MusicRepository.IndexingWorker) { if (indexingWorker != null) { - logW("Worker is already registered") + T.w("Worker is already registered") return } - logD("Registering worker $worker") + T.d("Registering worker $worker") indexingWorker = worker } @Synchronized override fun unregisterWorker(worker: MusicRepository.IndexingWorker) { if (indexingWorker !== worker) { - logW("Given worker did not match current worker") + T.w("Given worker did not match current worker") return } - logD("Unregistering worker $worker") + T.d("Unregistering worker $worker") indexingWorker = null currentIndexingState = null } @@ -300,42 +298,42 @@ constructor( override suspend fun createPlaylist(name: String, songs: List) { val userLibrary = synchronized(this) { userLibrary ?: return } - logD("Creating playlist $name with ${songs.size} songs") + T.d("Creating playlist $name with ${songs.size} songs") userLibrary.createPlaylist(name, songs) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun renamePlaylist(playlist: Playlist, name: String) { val userLibrary = synchronized(this) { userLibrary ?: return } - logD("Renaming $playlist to $name") + T.d("Renaming $playlist to $name") userLibrary.renamePlaylist(playlist, name) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun deletePlaylist(playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } - logD("Deleting $playlist") + T.d("Deleting $playlist") userLibrary.deletePlaylist(playlist) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun addToPlaylist(songs: List, playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } - logD("Adding ${songs.size} songs to $playlist") + T.d("Adding ${songs.size} songs to $playlist") userLibrary.addToPlaylist(playlist, songs) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { val userLibrary = synchronized(this) { userLibrary ?: return } - logD("Rewriting $playlist with ${songs.size} songs") + T.d("Rewriting $playlist with ${songs.size} songs") userLibrary.rewritePlaylist(playlist, songs) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } @Synchronized override fun requestIndex(withCache: Boolean) { - logD("Requesting index operation [cache=$withCache]") + T.d("Requesting index operation [cache=$withCache]") indexingWorker?.requestIndex(withCache) } @@ -347,13 +345,13 @@ constructor( indexImpl(context, scope, withCache) } catch (e: CancellationException) { // Got cancelled, propagate upwards to top-level co-routine. - logD("Loading routine was cancelled") + T.d("Loading routine was cancelled") throw e } catch (e: Exception) { // Music loading process failed due to something we have not handled. // TODO: Still want to display this error eventually - logE("Music indexing failed") - logE(e.stackTraceToString()) + T.e("Music indexing failed") + T.e(e.stackTraceToString()) emitIndexingCompletion(e) } } @@ -366,7 +364,7 @@ constructor( // done at the UI level, but that intertwines logic and display too much. if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == PackageManager.PERMISSION_DENIED) { - logE("Permissions were not granted") + T.e("Permissions were not granted") throw NoAudioPermissionException() } @@ -385,7 +383,7 @@ constructor( // to figure out what songs are (probably) on the device, and the latter will be needed // for discovery (described later). These have no shared state, so they are done in // parallel. - logD("Starting MediaStore query") + T.d("Starting MediaStore query") emitIndexingProgress(IndexingProgress.Indeterminate) val mediaStoreQueryJob = @@ -406,18 +404,18 @@ constructor( // identical to calling async. val cache = if (withCache) { - logD("Reading cache") + T.d("Reading cache") cacheRepository.readCache() } else { null } - logD("Awaiting MediaStore query") + T.d("Awaiting MediaStore query") val query = mediaStoreQueryJob.await().getOrThrow() // We now have all the information required to start the "discovery" process. This // is the point at which Auxio starts scanning each file given from MediaStore and // transforming it into a music library. MediaStore normally - logD("Starting discovery") + T.d("Starting discovery") val incompleteSongs = Channel(Channel.UNLIMITED) // Not fully populated w/metadata val completeSongs = Channel(Channel.UNLIMITED) // Populated with quality metadata val processedSongs = Channel(Channel.UNLIMITED) // Transformed into SongImpl @@ -425,7 +423,7 @@ constructor( // MediaStoreExtractor discovers all music on the device, and forwards them to either // DeviceLibrary if cached metadata exists for it, or TagExtractor if cached metadata // does not exist. In the latter situation, it also applies it's own (inferior) metadata. - logD("Starting MediaStore discovery") + T.d("Starting MediaStore discovery") val mediaStoreJob = scope.async { try { @@ -434,7 +432,7 @@ constructor( // To prevent a deadlock, we want to close the channel with an exception // to cascade to and cancel all other routines before finally bubbling up // to the main extractor loop. - logE("MediaStore extraction failed: $e") + T.e("MediaStore extraction failed: $e") incompleteSongs.close( Exception("MediaStore extraction failed: ${e.stackTraceToString()}")) return@async @@ -444,13 +442,13 @@ constructor( // TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date // metadata for them, and then forwards it to DeviceLibrary. - logD("Starting tag extraction") + T.d("Starting tag extraction") val tagJob = scope.async { try { tagExtractor.consume(incompleteSongs, completeSongs) } catch (e: Exception) { - logE("Tag extraction failed: $e") + T.e("Tag extraction failed: $e") completeSongs.close( Exception("Tag extraction failed: ${e.stackTraceToString()}")) return@async @@ -460,7 +458,7 @@ constructor( // DeviceLibrary constructs music parent instances as song information is provided, // and then forwards them to the primary loading loop. - logD("Starting DeviceLibrary creation") + T.d("Starting DeviceLibrary creation") val deviceLibraryJob = scope.async(Dispatchers.Default) { val deviceLibrary = @@ -468,7 +466,7 @@ constructor( deviceLibraryFactory.create( completeSongs, processedSongs, separators, nameFactory) } catch (e: Exception) { - logE("DeviceLibrary creation failed: $e") + T.e("DeviceLibrary creation failed: $e") processedSongs.close( Exception("DeviceLibrary creation failed: ${e.stackTraceToString()}")) return@async Result.failure(e) @@ -499,14 +497,14 @@ constructor( // that the short-circuit occurs so quickly as to break the UI. // TODO: Do not error, instead just wipe the entire library. if (rawSongs.isEmpty()) { - logE("Music library was empty") + T.e("Music library was empty") throw NoMusicException() } // Now that the library is effectively loaded, we can start the finalization step, which // involves writing new cache information and creating more music data that is derived // from the library (e.g playlists) - logD("Discovered ${rawSongs.size} songs, starting finalization") + T.d("Discovered ${rawSongs.size} songs, starting finalization") // We have no idea how long the cache will take, and the playlist construction // will be too fast to indicate, so switch back to an indeterminate state. @@ -515,7 +513,7 @@ constructor( // The UserLibrary job is split into a query and construction step, a la MediaStore. // This way, we can start working on playlists even as DeviceLibrary might still be // working on parent information. - logD("Starting UserLibrary query") + T.d("Starting UserLibrary query") val userLibraryQueryJob = scope.async { val rawPlaylists = @@ -532,20 +530,20 @@ constructor( // since the playlist read will probably take some time. // TODO: Read/write from the cache incrementally instead of in bulk? if (cache == null || cache.invalidated) { - logD("Writing cache [why=${cache?.invalidated}]") + T.d("Writing cache [why=${cache?.invalidated}]") cacheRepository.writeCache(rawSongs) } // Create UserLibrary once we finally get the required components for it. - logD("Awaiting UserLibrary query") + T.d("Awaiting UserLibrary query") val rawPlaylists = userLibraryQueryJob.await().getOrThrow() - logD("Awaiting DeviceLibrary creation") + T.d("Awaiting DeviceLibrary creation") val deviceLibrary = deviceLibraryJob.await().getOrThrow() - logD("Starting UserLibrary creation") + T.d("Starting UserLibrary creation") val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary, nameFactory) // Loading process is functionally done, indicate such - logD( + T.d( "Successfully indexed music library [device=$deviceLibrary " + "user=$userLibrary time=${System.currentTimeMillis() - start}]") emitIndexingCompletion(null) @@ -561,7 +559,7 @@ constructor( deviceLibraryChanged = this.deviceLibrary != deviceLibrary userLibraryChanged = this.userLibrary != userLibrary if (!deviceLibraryChanged && !userLibraryChanged) { - logD("Library has not changed, skipping update") + T.d("Library has not changed, skipping update") return } @@ -591,7 +589,7 @@ constructor( synchronized(this) { previousCompletedState = IndexingState.Completed(error) currentIndexingState = null - logD("Dispatching completion state [error=$error]") + T.d("Dispatching completion state [error=$error]") for (listener in indexingListeners) { listener.onIndexingStateChanged() } @@ -601,7 +599,7 @@ constructor( @Synchronized private fun dispatchLibraryChange(device: Boolean, user: Boolean) { val changes = MusicRepository.Changes(device, user) - logD("Dispatching library change [changes=$changes]") + T.d("Dispatching library change [changes=$changes]") for (listener in updateListeners) { listener.onMusicChanges(changes) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index 58989ce1b..c937abfe0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.dirs.MusicDirectories import org.oxycblt.auxio.music.fs.DocumentPathFactory import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * User configuration specific to music system. @@ -108,11 +108,11 @@ constructor( getString(R.string.set_key_music_dirs_include), getString(R.string.set_key_separators), getString(R.string.set_key_auto_sort_names) -> { - logD("Dispatching indexing setting change for $key") + T.d("Dispatching indexing setting change for $key") listener.onIndexingSettingChanged() } getString(R.string.set_key_observing) -> { - logD("Dispatching observing setting change") + T.d("Dispatching observing setting change") listener.onObservingChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 99e3fee4d..b81a766b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -33,8 +33,7 @@ import org.oxycblt.auxio.music.external.ExportConfig import org.oxycblt.auxio.music.external.ExternalPlaylistManager import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logE +import timber.log.Timber as T /** * A [ViewModel] providing data specific to the music loading process. @@ -94,7 +93,7 @@ constructor( deviceLibrary.artists.size, deviceLibrary.genres.size, deviceLibrary.songs.sumOf { it.durationMs }) - logD("Updated statistics: ${_statistics.value}") + T.d("Updated statistics: ${_statistics.value}") } override fun onIndexingStateChanged() { @@ -103,13 +102,13 @@ constructor( /** Requests that the music library should be re-loaded while leveraging the cache. */ fun refresh() { - logD("Refreshing library") + T.d("Refreshing library") musicRepository.requestIndex(true) } /** Requests that the music library be re-loaded without the cache. */ fun rescan() { - logD("Rescanning library") + T.d("Rescanning library") musicRepository.requestIndex(false) } @@ -127,7 +126,7 @@ constructor( reason: PlaylistDecision.New.Reason = PlaylistDecision.New.Reason.NEW ) { if (name != null) { - logD("Creating $name with ${songs.size} songs]") + T.d("Creating $name with ${songs.size} songs]") viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) val message = @@ -139,7 +138,7 @@ constructor( _playlistMessage.put(message) } } else { - logD("Launching creation dialog for ${songs.size} songs") + T.d("Launching creation dialog for ${songs.size} songs") _playlistDecision.put(PlaylistDecision.New(songs, null, reason)) } } @@ -158,7 +157,7 @@ constructor( viewModelScope.launch(Dispatchers.IO) { val importedPlaylist = externalPlaylistManager.import(uri) if (importedPlaylist == null) { - logE("Could not import playlist") + T.e("Could not import playlist") _playlistMessage.put(PlaylistMessage.ImportFailed) return@launch } @@ -170,7 +169,7 @@ constructor( } if (songs.isEmpty()) { - logE("No songs found") + T.e("No songs found") _playlistMessage.put(PlaylistMessage.ImportFailed) return@launch } @@ -194,7 +193,7 @@ constructor( } } } else { - logD("Launching import picker") + T.d("Launching import picker") _playlistDecision.put(PlaylistDecision.Import(target)) } } @@ -207,7 +206,7 @@ constructor( */ fun exportPlaylist(playlist: Playlist, uri: Uri? = null, config: ExportConfig? = null) { if (uri != null && config != null) { - logD("Exporting playlist to $uri") + T.d("Exporting playlist to $uri") viewModelScope.launch(Dispatchers.IO) { if (externalPlaylistManager.export(playlist, uri, config)) { _playlistMessage.put(PlaylistMessage.ExportSuccess) @@ -216,7 +215,7 @@ constructor( } } } else { - logD("Launching export dialog") + T.d("Launching export dialog") _playlistDecision.put(PlaylistDecision.Export(playlist)) } } @@ -238,7 +237,7 @@ constructor( reason: PlaylistDecision.Rename.Reason = PlaylistDecision.Rename.Reason.ACTION ) { if (name != null) { - logD("Renaming $playlist to $name") + T.d("Renaming $playlist to $name") viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) if (applySongs.isNotEmpty()) { @@ -252,7 +251,7 @@ constructor( _playlistMessage.put(message) } } else { - logD("Launching rename dialog for $playlist") + T.d("Launching rename dialog for $playlist") _playlistDecision.put(PlaylistDecision.Rename(playlist, null, applySongs, reason)) } } @@ -267,13 +266,13 @@ constructor( */ fun deletePlaylist(playlist: Playlist, rude: Boolean = false) { if (rude) { - logD("Deleting $playlist") + T.d("Deleting $playlist") viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) _playlistMessage.put(PlaylistMessage.DeleteSuccess) } } else { - logD("Launching deletion dialog for $playlist") + T.d("Launching deletion dialog for $playlist") _playlistDecision.put(PlaylistDecision.Delete(playlist)) } } @@ -285,7 +284,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(song: Song, playlist: Playlist? = null) { - logD("Adding $song to playlist") + T.d("Adding $song to playlist") addToPlaylist(listOf(song), playlist) } @@ -296,7 +295,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(album: Album, playlist: Playlist? = null) { - logD("Adding $album to playlist") + T.d("Adding $album to playlist") addToPlaylist(listSettings.albumSongSort.songs(album.songs), playlist) } @@ -307,7 +306,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(artist: Artist, playlist: Playlist? = null) { - logD("Adding $artist to playlist") + T.d("Adding $artist to playlist") addToPlaylist(listSettings.artistSongSort.songs(artist.songs), playlist) } @@ -318,7 +317,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(genre: Genre, playlist: Playlist? = null) { - logD("Adding $genre to playlist") + T.d("Adding $genre to playlist") addToPlaylist(listSettings.genreSongSort.songs(genre.songs), playlist) } @@ -330,13 +329,13 @@ constructor( */ fun addToPlaylist(songs: List, playlist: Playlist? = null) { if (playlist != null) { - logD("Adding ${songs.size} songs to $playlist") + T.d("Adding ${songs.size} songs to $playlist") viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) _playlistMessage.put(PlaylistMessage.AddSuccess) } } else { - logD("Launching addition dialog for songs=${songs.size}") + T.d("Launching addition dialog for songs=${songs.size}") _playlistDecision.put(PlaylistDecision.Add(songs)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt index 441a9a7fa..d99c61ae5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt @@ -20,8 +20,7 @@ package org.oxycblt.auxio.music.cache import javax.inject.Inject import org.oxycblt.auxio.music.device.RawSong -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logE +import timber.log.Timber as T /** * A repository allowing access to cached metadata obtained in prior music loading operations. @@ -51,11 +50,11 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached // Faster to load the whole database into memory than do a query on each // populate call. val songs = cachedSongsDao.readSongs() - logD("Successfully read ${songs.size} songs from cache") + T.d("Successfully read ${songs.size} songs from cache") CacheImpl(songs) } catch (e: Exception) { - logE("Unable to load cache database.") - logE(e.stackTraceToString()) + T.e("Unable to load cache database.") + T.e(e.stackTraceToString()) null } @@ -63,12 +62,12 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached try { // Still write out whatever data was extracted. cachedSongsDao.nukeSongs() - logD("Successfully deleted old cache") + T.d("Successfully deleted old cache") cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw)) - logD("Successfully wrote ${rawSongs.size} songs to cache") + T.d("Successfully wrote ${rawSongs.size} songs to cache") } catch (e: Exception) { - logE("Unable to save cache database.") - logE(e.stackTraceToString()) + T.e("Unable to save cache database.") + T.e(e.stackTraceToString()) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/AddToPlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/AddToPlaylistDialog.kt index 602d1830f..22c9688e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/AddToPlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/AddToPlaylistDialog.kt @@ -36,8 +36,8 @@ import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe +import timber.log.Timber as T /** * A dialog that allows the user to pick a specific playlist to add song(s) to. @@ -105,7 +105,7 @@ class AddToPlaylistDialog : private fun updatePendingSongs(songs: List?) { if (songs == null) { - logD("No songs to show choices for, navigating away") + T.d("No songs to show choices for, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt index 0e97dea4b..b7e20adb7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt @@ -32,8 +32,8 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A [ViewBindingMaterialDialogFragment] that asks the user to confirm the deletion of a [Playlist]. @@ -76,7 +76,7 @@ class DeletePlaylistDialog : ViewBindingMaterialDialogFragment if (uri == null) { - logW("No URI returned from file picker") + T.w("No URI returned from file picker") return@registerForActivityResult } val playlist = pickerModel.currentPlaylistToExport.value if (playlist == null) { - logW("No playlist to export") + T.w("No playlist to export") findNavController().navigateUp() return@registerForActivityResult } - logD("Received playlist URI $uri") + T.d("Received playlist URI $uri") musicModel.exportPlaylist(playlist, uri, pickerModel.currentExportConfig.value) findNavController().navigateUp() } @@ -129,7 +128,7 @@ class ExportPlaylistDialog : ViewBindingMaterialDialogFragment @@ -110,7 +108,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M .ifEmpty { null } .also { refreshChoicesWith = it } } - logD("Updated songs to add: ${_currentSongsToAdd.value?.size} songs") + T.d("Updated songs to add: ${_currentSongsToAdd.value?.size} songs") } val chosenName = _chosenName.value @@ -122,7 +120,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M // Nothing to do. } } - logD("Updated chosen name to $chosenName") + T.d("Updated chosen name to $chosenName") refreshChoicesWith = refreshChoicesWith ?: _currentSongsToAdd.value // TODO: Add music syncing for other playlist states here @@ -131,7 +129,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M _currentPlaylistToExport.value?.let { playlist -> musicRepository.userLibrary?.findPlaylist(playlist.uid) } - logD("Updated playlist to export to ${_currentPlaylistToExport.value}") + T.d("Updated playlist to export to ${_currentPlaylistToExport.value}") } refreshChoicesWith?.let(::refreshPlaylistChoices) @@ -154,7 +152,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M template: String?, reason: PlaylistDecision.New.Reason ) { - logD("Opening ${songUids.size} songs to create a playlist from") + T.d("Opening ${songUids.size} songs to create a playlist from") val userLibrary = musicRepository.userLibrary ?: return val songs = musicRepository.deviceLibrary @@ -168,10 +166,10 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M var possibleName: String do { possibleName = context.getString(R.string.fmt_def_playlist, i) - logD("Trying $possibleName as a playlist name") + T.d("Trying $possibleName as a playlist name") ++i } while (userLibrary.playlists.any { it.name.resolve(context) == possibleName }) - logD("$possibleName is unique, using it as the playlist name") + T.d("$possibleName is unique, using it as the playlist name") possibleName } @@ -179,7 +177,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M if (possibleName != null && songs != null) { PendingNewPlaylist(possibleName, songs, template, reason) } else { - logW("Given song UIDs to create were invalid") + T.w("Given song UIDs to create were invalid") null } } @@ -195,7 +193,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M template: String?, reason: PlaylistDecision.Rename.Reason ) { - logD("Opening playlist $playlistUid to rename") + T.d("Opening playlist $playlistUid to rename") val playlist = musicRepository.userLibrary?.findPlaylist(playlistUid) val applySongs = musicRepository.deviceLibrary?.let { applySongUids.mapNotNull(it::findSong) } @@ -204,7 +202,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M if (playlist != null && applySongs != null) { PendingRenamePlaylist(playlist, applySongs, template, reason) } else { - logW("Given playlist UID to rename was invalid") + T.w("Given playlist UID to rename was invalid") null } } @@ -215,12 +213,12 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param playlistUid The [Music.UID] of the [Playlist] to export. */ fun setPlaylistToExport(playlistUid: Music.UID) { - logD("Opening playlist $playlistUid to export") + T.d("Opening playlist $playlistUid to export") // TODO: Add this guard to the rest of the methods here if (_currentPlaylistToExport.value?.uid == playlistUid) return _currentPlaylistToExport.value = musicRepository.userLibrary?.findPlaylist(playlistUid) if (_currentPlaylistToExport.value == null) { - logW("Given playlist UID to export was invalid") + T.w("Given playlist UID to export was invalid") } else { _currentExportConfig.value = DEFAULT_EXPORT_CONFIG } @@ -232,7 +230,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param exportConfig The new [ExportConfig] to use. */ fun setExportConfig(exportConfig: ExportConfig) { - logD("Setting export config to $exportConfig") + T.d("Setting export config to $exportConfig") _currentExportConfig.value = exportConfig } @@ -242,10 +240,10 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param playlistUid The [Music.UID] of the [Playlist] to delete. */ fun setPlaylistToDelete(playlistUid: Music.UID) { - logD("Opening playlist $playlistUid to delete") + T.d("Opening playlist $playlistUid to delete") _currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid) if (_currentPlaylistToDelete.value == null) { - logW("Given playlist UID to delete was invalid") + T.w("Given playlist UID to delete was invalid") } } @@ -255,25 +253,25 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param name The new user-inputted name, or null if not present. */ fun updateChosenName(name: String?) { - logD("Updating chosen name to $name") + T.d("Updating chosen name to $name") _chosenName.value = when { name.isNullOrEmpty() -> { - logE("Chosen name is empty") + T.e("Chosen name is empty") ChosenName.Empty } name.isBlank() -> { - logE("Chosen name is blank") + T.e("Chosen name is blank") ChosenName.Blank } else -> { val trimmed = name.trim() val userLibrary = musicRepository.userLibrary if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) { - logD("Chosen name is valid") + T.d("Chosen name is valid") ChosenName.Valid(trimmed) } else { - logD("Chosen name already exists in library") + T.d("Chosen name already exists in library") ChosenName.AlreadyExists(trimmed) } } @@ -286,19 +284,19 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param songUids The [Music.UID]s of songs to add to a playlist. */ fun setSongsToAdd(songUids: Array) { - logD("Opening ${songUids.size} songs to add to a playlist") + T.d("Opening ${songUids.size} songs to add to a playlist") _currentSongsToAdd.value = musicRepository.deviceLibrary ?.let { songUids.mapNotNull(it::findSong).ifEmpty { null } } ?.also(::refreshPlaylistChoices) if (_currentSongsToAdd.value == null || songUids.size != _currentSongsToAdd.value?.size) { - logW("Given song UIDs to add were (partially) invalid") + T.w("Given song UIDs to add were (partially) invalid") } } private fun refreshPlaylistChoices(songs: List) { val userLibrary = musicRepository.userLibrary ?: return - logD("Refreshing playlist choices") + T.d("Refreshing playlist choices") _playlistAddChoices.value = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map { val songSet = it.songs.toSet() diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt index da5265daf..f3ef3f9b1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt @@ -32,8 +32,8 @@ import org.oxycblt.auxio.databinding.DialogPlaylistNameBinding import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A dialog allowing the name of a new playlist to be chosen before committing it to the database. @@ -94,7 +94,7 @@ class RenamePlaylistDialog : ViewBindingMaterialDialogFragment) { - logD("Adding ${path.size} directories") + T.d("Adding ${path.size} directories") val oldLastIndex = path.lastIndex _dirs.addAll(path) notifyItemRangeInserted(oldLastIndex, path.size) @@ -78,7 +78,7 @@ class DirectoryAdapter(private val listener: Listener) : * @param path The [Path] to remove. Must exist in the list. */ fun remove(path: Path) { - logD("Removing $path") + T.d("Removing $path") val idx = _dirs.indexOf(path) _dirs.removeAt(idx) notifyItemRemoved(idx) diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt index 64c4bdc2e..85ef0a682 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt @@ -36,8 +36,8 @@ import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.fs.DocumentPathFactory import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast +import timber.log.Timber as T /** * Dialog that manages the music dirs setting. @@ -62,7 +62,7 @@ class MusicDirsDialog : .setPositiveButton(R.string.lbl_save) { _, _ -> val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) if (musicSettings.musicDirs != newDirs) { - logD("Committing changes") + T.d("Committing changes") musicSettings.musicDirs = newDirs } } @@ -76,7 +76,7 @@ class MusicDirsDialog : binding.dirsAdd.apply { ViewCompat.setTooltipText(this, contentDescription) setOnClickListener { - logD("Opening launcher") + T.d("Opening launcher") val launcher = requireNotNull(openDocumentTreeLauncher) { "Document tree launcher was not available" @@ -150,7 +150,7 @@ class MusicDirsDialog : private fun addDocumentTreeUriToDirs(uri: Uri?) { if (uri == null) { // A null URI means that the user left the file picker without picking a directory - logD("No URI given (user closed the dialog)") + T.d("No URI given (user closed the dialog)") return } diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt index 76703b6b6..a77676bbb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.music.fs.Components import org.oxycblt.auxio.music.fs.DocumentPathFactory import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.contentResolverSafe -import org.oxycblt.auxio.util.logE +import timber.log.Timber as T /** * Generic playlist file importing abstraction. @@ -108,7 +108,7 @@ constructor( return ImportedPlaylist(newName, imported.paths) } } catch (e: Exception) { - logE("Failed to import playlist: $e") + T.e("Failed to import playlist: $e") null } } @@ -124,7 +124,7 @@ constructor( return try { val outputStream = context.contentResolverSafe.openOutputStream(uri) if (outputStream == null) { - logE("Failed to export playlist: Could not open output stream") + T.e("Failed to export playlist: Could not open output stream") return false } outputStream.use { @@ -132,7 +132,7 @@ constructor( true } } catch (e: Exception) { - logE("Failed to export playlist: $e") + T.e("Failed to export playlist: $e") false } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index 211f6cea6..bb8318241 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -33,8 +33,8 @@ import org.oxycblt.auxio.music.fs.Volume import org.oxycblt.auxio.music.fs.VolumeManager import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * Minimal M3U file format implementation. @@ -116,7 +116,7 @@ constructor( } if (path == null) { - logE("Expected a path, instead got an EOF") + T.e("Expected a path, instead got an EOF") break@consumeFile } diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index 3972718e8..38b761013 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -32,8 +32,8 @@ import org.oxycblt.auxio.music.dirs.MusicDirectories import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.metadata.parseId3v2PositionField import org.oxycblt.auxio.music.metadata.transformPositionField -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.sendWithTimeout +import timber.log.Timber as T /** * The layer that loads music from the [MediaStore] database. This is an intermediate step in the @@ -127,7 +127,7 @@ private class MediaStoreExtractorImpl( // Filter out audio that is not music, if enabled. if (constraints.excludeNonMusic) { - logD("Excluding non-music") + T.d("Excluding non-music") uniSelector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1" } @@ -136,10 +136,10 @@ private class MediaStoreExtractorImpl( val pathSelector = mediaStorePathInterpreterFactory.createSelector(constraints.musicDirs.dirs) if (pathSelector != null) { - logD("Must select for directories") + T.d("Must select for directories") uniSelector += " AND " if (!constraints.musicDirs.shouldInclude) { - logD("Excluding directories in selector") + T.d("Excluding directories in selector") // Without a NOT, the query will be restricted to the specified paths, resulting // in the "Include" mode. With a NOT, the specified paths will not be included, // resulting in the "Exclude" mode. @@ -151,7 +151,7 @@ private class MediaStoreExtractorImpl( } // Now we can actually query MediaStore. - logD( + T.d( "Starting song query [proj=${projection.toList()}, selector=$uniSelector, args=$uniArgs]") val cursor = context.contentResolverSafe.safeQuery( @@ -159,7 +159,7 @@ private class MediaStoreExtractorImpl( projection, uniSelector, uniArgs.toTypedArray()) - logD("Successfully queried for ${cursor.count} songs") + T.d("Successfully queried for ${cursor.count} songs") val genreNamesMap = mutableMapOf() @@ -194,8 +194,8 @@ private class MediaStoreExtractorImpl( } } - logD("Read ${genreNamesMap.values.distinct().size} genres from MediaStore") - logD("Finished initialization in ${System.currentTimeMillis() - start}ms") + T.d("Read ${genreNamesMap.values.distinct().size} genres from MediaStore") + T.d("Finished initialization in ${System.currentTimeMillis() - start}ms") return QueryImpl( cursor, mediaStorePathInterpreterFactory.wrap(cursor), diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt index f3a6f6aa4..9b57a9fab 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt @@ -21,7 +21,7 @@ package org.oxycblt.auxio.music.fs import android.database.Cursor import android.os.Build import android.provider.MediaStore -import org.oxycblt.auxio.util.logE +import timber.log.Timber as T /** * Wrapper around a [Cursor] that interprets path information on a per-API/manufacturer basis. @@ -112,7 +112,7 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) : } } - logE("Could not find volume for $data [tried: ${volumes.map { it.components }}]") + T.e("Could not find volume for $data [tried: ${volumes.map { it.components }}]") return null } @@ -181,7 +181,7 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) : val displayName = cursor.getString(displayNameIndex) val volume = volumes.find { it.mediaStoreName == volumeName } if (volume == null) { - logE( + T.e( "Could not find volume for $volumeName:$relativePath/$displayName [tried: ${volumes.map { it.mediaStoreName }}]") return null } diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt index 4098d0c37..1518c4671 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt @@ -24,8 +24,8 @@ import java.text.SimpleDateFormat import kotlin.math.max import org.oxycblt.auxio.R import org.oxycblt.auxio.util.inRangeOrNull -import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.positiveOrNull +import timber.log.Timber as T /** * An ISO-8601/RFC 3339 Date. @@ -64,7 +64,7 @@ class Date private constructor(private val tokens: List) : Comparable try { format.parse("$year-$month") } catch (e: ParseException) { - logE("Unable to parse fine-grained date: $e") + T.e("Unable to parse fine-grained date: $e") return null } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt index 6e3646b62..c18db8d0c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt @@ -25,9 +25,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.MimeType -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logE -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * The properties of a [Song]'s file. @@ -75,8 +73,8 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. // Can feasibly fail with invalid file formats. Note that this isn't considered // an error condition in the UI, as there is still plenty of other song information // that we can show. - logW("Unable to extract song attributes.") - logW(e.stackTraceToString()) + T.w("Unable to extract song attributes.") + T.w(e.stackTraceToString()) return AudioProperties(null, null, song.mimeType) } @@ -92,7 +90,7 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. // Convert bytes-per-second to kilobytes-per-second. format.getInteger(MediaFormat.KEY_BIT_RATE) / 1000 } catch (e: NullPointerException) { - logD("Unable to extract bit rate field") + T.d("Unable to extract bit rate field") null } @@ -100,7 +98,7 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. try { format.getInteger(MediaFormat.KEY_SAMPLE_RATE) } catch (e: NullPointerException) { - logE("Unable to extract sample rate field") + T.e("Unable to extract sample rate field") null } @@ -110,13 +108,13 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. try { format.getString(MediaFormat.KEY_MIME) } catch (e: NullPointerException) { - logE("Unable to extract mime type field") + T.e("Unable to extract mime type field") null } extractor.release() - logD("Finished extracting audio properties") + T.d("Finished extracting audio properties") return AudioProperties( bitrate, diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt index 31195c408..1a4bfb0c7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * A [ViewBindingMaterialDialogFragment] that allows the user to configure the separator characters @@ -76,7 +76,7 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment binding.separatorSlash.isChecked = true Separators.PLUS -> binding.separatorPlus.isChecked = true Separators.AND -> binding.separatorAnd.isChecked = true - else -> logW("Unexpected separator in settings data") + else -> T.w("Unexpected separator in settings data") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt index ab37b6e83..bd6b9c725 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt @@ -41,9 +41,8 @@ import kotlinx.coroutines.yield import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.util.forEachWithTimeout -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.sendWithTimeout +import timber.log.Timber as T class TagExtractor @Inject @@ -67,7 +66,7 @@ constructor(private val mediaSourceFactory: Factory, private val tagInterpreter: songsIn++ } - logD("All incomplete songs exhausted, starting cleanup loop") + T.d("All incomplete songs exhausted, starting cleanup loop") while (!worker.idle()) { val completeRawSong = worker.pull() if (completeRawSong != null) { @@ -153,8 +152,8 @@ private class MetadataWorker( try { tagInterpreter.interpret(job.rawSong, job.future.get()) } catch (e: Exception) { - logE("Failed to extract metadata") - logE(e.stackTraceToString()) + T.e("Failed to extract metadata") + T.e(e.stackTraceToString()) } jobs[i] = null return job.rawSong @@ -177,7 +176,7 @@ private class MetadataWorker( mediaSource = currentMediaSource mediaSourceCaller = currentMediaSourceCaller } else { - logD("new media source yahoo") + T.d("new media source yahoo") mediaSource = mediaSourceFactory.createMediaSource(job.mediaItem) mediaSourceCaller = MediaSourceCaller(job) mediaSource.prepareSource( @@ -194,8 +193,8 @@ private class MetadataWorker( mediaPeriod.maybeThrowPrepareError() } } catch (e: Exception) { - logE("Failed to extract MediaSource") - logE(e.stackTraceToString()) + T.e("Failed to extract MediaSource") + T.e(e.stackTraceToString()) job.mediaPeriod?.let(mediaSource::releasePeriod) mediaSource.releaseSource(mediaSourceCaller) job.future.setException(e) @@ -248,7 +247,7 @@ private class MetadataWorker( // Ignore dynamic updates. return } - logD("yay source created") + T.d("yay source created") mediaPeriodCreated = true val mediaPeriod = source.createPeriod( diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt index f5c16f985..4746f1a50 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt @@ -26,8 +26,8 @@ import kotlin.math.min import org.oxycblt.auxio.image.extractor.CoverExtractor import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.info.Date -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull +import timber.log.Timber as T /** * An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on @@ -82,25 +82,25 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx // val gain = // (((header[16]).toInt() and 0xFF) or ((header[17].toInt() shl 8))) // .R128ToLUFS18() - // logD("Obtained opus base gain: $gain dB") + // T.d("Obtained opus base gain: $gain dB") // if (gain != 0f) { - // logD("Applying opus base gain") + // T.d("Applying opus base gain") // rawSong.replayGainTrackAdjustment = // (rawSong.replayGainTrackAdjustment ?: 0f) + gain // rawSong.replayGainAlbumAdjustment = // (rawSong.replayGainAlbumAdjustment ?: 0f) + gain // } else { - // logD("Ignoring opus base gain") + // T.d("Ignoring opus base gain") // } // } } else { - logD("No metadata could be extracted for ${rawSong.name}") + T.d("No metadata could be extracted for ${rawSong.name}") } } private fun populateWithId3v2(rawSong: RawSong, textFrames: Map>) { // Song - logD(textFrames) + T.d(textFrames) (textFrames["TXXX:musicbrainz release track id"] ?: textFrames["TXXX:musicbrainz_releasetrackid"]) ?.let { rawSong.musicBrainzId = it.first() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt index 5552aec69..661305f49 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.getSystemServiceCompat -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T class Indexer private constructor( @@ -117,7 +117,7 @@ private constructor( } } else if (musicSettings.shouldBeObserving) { // Not observing and done loading, exit foreground. - logD("Exiting foreground") + T.d("Exiting foreground") post(observingNotification) } else { post(null) @@ -125,7 +125,7 @@ private constructor( } override fun requestIndex(withCache: Boolean) { - logD("Starting new indexing job (previous=${currentIndexJob?.hashCode()})") + T.d("Starting new indexing job (previous=${currentIndexJob?.hashCode()})") // Cancel the previous music loading job. currentIndexJob?.cancel() // Start a new music loading job on a co-routine. @@ -146,7 +146,7 @@ private constructor( override fun onMusicChanges(changes: MusicRepository.Changes) { val deviceLibrary = musicRepository.deviceLibrary ?: return - logD("Music changed, updating shared objects") + T.d("Music changed, updating shared objects") // Wipe possibly-invalidated outdated covers imageLoader.memoryCache?.clear() // Clear invalid models from PlaybackStateManager. This is not connected @@ -175,7 +175,7 @@ private constructor( // setting changed. In such a case, the state will still be updated when // the music loading process ends. if (musicRepository.indexingState == null) { - logD("Not loading, updating idle session") + T.d("Not loading, updating idle session") foregroundListener.updateForeground(ForegroundListener.Change.INDEXER) } } @@ -184,7 +184,7 @@ private constructor( private fun PowerManager.WakeLock.acquireSafe() { // Avoid unnecessary acquire calls. if (!wakeLock.isHeld) { - logD("Acquiring wake lock") + T.d("Acquiring wake lock") // Time out after a minute, which is the average music loading time for a medium-sized // library. If this runs out, we will re-request the lock, and if music loading is // shorter than the timeout, it will be released early. @@ -196,7 +196,7 @@ private constructor( private fun PowerManager.WakeLock.releaseSafe() { // Avoid unnecessary release calls. if (wakeLock.isHeld) { - logD("Releasing wake lock") + T.d("Releasing wake lock") release() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt index 0e895196d..782109ca6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt @@ -26,8 +26,8 @@ import org.oxycblt.auxio.ForegroundServiceNotification import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.music.IndexingProgress -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newMainPendingIntent +import timber.log.Timber as T /** * A dynamic [ForegroundServiceNotification] that shows the current music loading state. @@ -66,7 +66,7 @@ class IndexingNotification(private val context: Context) : // Indeterminate state, use a vaguer description and in-determinate progress. // These events are not very frequent, and thus we don't need to safeguard // against rate limiting. - logD("Updating state to $progress") + T.d("Updating state to $progress") lastUpdateTime = -1 setContentText(context.getString(R.string.lng_indexing)) setProgress(0, 0, true) @@ -81,7 +81,7 @@ class IndexingNotification(private val context: Context) : return false } lastUpdateTime = SystemClock.elapsedRealtime() - logD("Updating state to $progress") + T.d("Updating state to $progress") setContentText( context.getString(R.string.fmt_indexing, progress.current, progress.total)) setProgress(progress.total, progress.current, false) diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt index 7cdd3fb9e..7494878e3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt @@ -31,8 +31,7 @@ import kotlinx.coroutines.launch import org.oxycblt.auxio.ForegroundListener import org.oxycblt.auxio.ForegroundServiceNotification import org.oxycblt.auxio.music.MusicRepository -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T class MusicServiceFragment @Inject @@ -124,11 +123,11 @@ constructor( try { val result = body() if (result == null) { - logW("Result is null") + T.w("Result is null") } sendResult(result) } catch (e: Exception) { - logD("Error while dispatching: $e") + T.d("Error while dispatching: $e") sendResult(null) } } @@ -139,11 +138,11 @@ constructor( try { val result = body() if (result == null) { - logW("Result is null") + T.w("Result is null") } sendResult(result) } catch (e: Exception) { - logD("Error while dispatching: $e") + T.d("Error while dispatching: $e") sendResult(null) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt b/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt index 5c9e1f120..b518e8f24 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt @@ -28,7 +28,7 @@ import javax.inject.Inject import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.fs.contentResolverSafe -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [ContentObserver] that observes the [MediaStore] music database for changes, a behavior known @@ -68,7 +68,7 @@ constructor( // Check here if we should even start a reindex. This is much less bug-prone than // registering and de-registering this component as this setting changes. if (musicSettings.shouldBeObserving) { - logD("MediaStore changed, starting re-index") + T.d("MediaStore changed, starting re-index") musicRepository.requestIndex(true) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 70943b7d3..bba60957c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -26,8 +26,7 @@ import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.info.Name -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logE +import timber.log.Timber as T /** * Organized library information controlled by the user. @@ -144,10 +143,10 @@ class UserLibraryFactoryImpl @Inject constructor(private val playlistDao: Playli override suspend fun query() = try { val rawPlaylists = playlistDao.readRawPlaylists() - logD("Successfully read ${rawPlaylists.size} playlists") + T.d("Successfully read ${rawPlaylists.size} playlists") rawPlaylists } catch (e: Exception) { - logE("Unable to read playlists: $e") + T.e("Unable to read playlists: $e") listOf() } @@ -193,11 +192,11 @@ private class UserLibraryImpl( return try { playlistDao.insertPlaylist(rawPlaylist) - logD("Successfully created playlist $name with ${songs.size} songs") + T.d("Successfully created playlist $name with ${songs.size} songs") playlistImpl } catch (e: Exception) { - logE("Unable to create playlist $name with ${songs.size} songs") - logE(e.stackTraceToString()) + T.e("Unable to create playlist $name with ${songs.size} songs") + T.e(e.stackTraceToString()) synchronized(this) { playlistMap.remove(playlistImpl.uid) } null } @@ -212,11 +211,11 @@ private class UserLibraryImpl( return try { playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name)) - logD("Successfully renamed $playlist to $name") + T.d("Successfully renamed $playlist to $name") true } catch (e: Exception) { - logE("Unable to rename $playlist to $name: $e") - logE(e.stackTraceToString()) + T.e("Unable to rename $playlist to $name: $e") + T.e(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } false } @@ -231,11 +230,11 @@ private class UserLibraryImpl( return try { playlistDao.deletePlaylist(playlist.uid) - logD("Successfully deleted $playlist") + T.d("Successfully deleted $playlist") true } catch (e: Exception) { - logE("Unable to delete $playlist: $e") - logE(e.stackTraceToString()) + T.e("Unable to delete $playlist: $e") + T.e(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } false } @@ -250,11 +249,11 @@ private class UserLibraryImpl( return try { playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) - logD("Successfully added ${songs.size} songs to $playlist") + T.d("Successfully added ${songs.size} songs to $playlist") true } catch (e: Exception) { - logE("Unable to add ${songs.size} songs to $playlist: $e") - logE(e.stackTraceToString()) + T.e("Unable to add ${songs.size} songs to $playlist: $e") + T.e(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } false } @@ -269,11 +268,11 @@ private class UserLibraryImpl( return try { playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) - logD("Successfully rewrote $playlist with ${songs.size} songs") + T.d("Successfully rewrote $playlist with ${songs.size} songs") true } catch (e: Exception) { - logE("Unable to rewrite $playlist with ${songs.size} songs: $e") - logE(e.stackTraceToString()) + T.e("Unable to rewrite $playlist with ${songs.size} songs: $e") + T.e(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } false } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index 7d0dfb1f9..283b2047c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getColorCompat -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [ViewBindingFragment] that shows the current playback state in a compact manner. @@ -128,7 +128,7 @@ class PlaybackBarFragment : ViewBindingFragment() { val binding = requireBinding() when (actionMode) { ActionMode.NEXT -> { - logD("Using skip next action") + T.d("Using skip next action") binding.playbackSecondaryAction.apply { if (tag != actionMode) { setIconResource(R.drawable.ic_skip_next_24) @@ -140,7 +140,7 @@ class PlaybackBarFragment : ViewBindingFragment() { } } ActionMode.REPEAT -> { - logD("Using repeat mode action") + T.d("Using repeat mode action") binding.playbackSecondaryAction.apply { if (tag != actionMode) { contentDescription = getString(R.string.desc_change_repeat) @@ -153,7 +153,7 @@ class PlaybackBarFragment : ViewBindingFragment() { } } ActionMode.SHUFFLE -> { - logD("Using shuffle action") + T.d("Using shuffle action") binding.playbackSecondaryAction.apply { if (tag != actionMode) { setIconResource(R.drawable.sel_shuffle_state_24) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index e5cf594ea..4cb692b2a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -43,9 +43,9 @@ import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.playback.ui.SwipeCoverView import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat +import timber.log.Timber as T /** * A [ViewBindingFragment] more information about the currently playing song, alongside all @@ -136,7 +136,7 @@ class PlaybackPanelFragment : override fun onStart() { super.onStart() - logD(requireBinding().playbackCover.width) + T.d(requireBinding().playbackCover.width) playbackModel.song.value?.let { requireBinding().playbackCover.bind(it) } requireBinding().root.viewTreeObserver.addOnGlobalLayoutListener(this) } @@ -180,7 +180,7 @@ class PlaybackPanelFragment : override fun onMenuItemClick(item: MenuItem): Boolean { if (item.itemId == R.id.action_open_equalizer) { // Launch the system equalizer app, if possible. - logD("Launching equalizer") + T.d("Launching equalizer") val equalizerIntent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) // Provide audio session ID so the equalizer can show options for this app @@ -221,7 +221,7 @@ class PlaybackPanelFragment : val binding = requireBinding() val context = requireContext() - logD("Updating song display: $song") + T.d("Updating song display: $song") binding.playbackCover.bind(song) binding.playbackSong.text = song.name.resolve(context) binding.playbackArtist.text = song.artists.resolveNames(context) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt index 9899477c3..b9dd5c73f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.playback.replaygain.ReplayGainMode import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * User configuration specific to the playback system. @@ -146,7 +146,7 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont } if (sharedPreferences.contains(OLD_KEY_LIB_MUSIC_PLAYBACK_MODE)) { - logD("Migrating $OLD_KEY_LIB_MUSIC_PLAYBACK_MODE") + T.d("Migrating $OLD_KEY_LIB_MUSIC_PLAYBACK_MODE") val mode = sharedPreferences @@ -162,7 +162,7 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont } if (sharedPreferences.contains(OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE)) { - logD("Migrating $OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE") + T.d("Migrating $OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE") val mode = sharedPreferences @@ -183,19 +183,19 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont getString(R.string.set_key_replay_gain), getString(R.string.set_key_pre_amp_with), getString(R.string.set_key_pre_amp_without) -> { - logD("Dispatching ReplayGain setting change") + T.d("Dispatching ReplayGain setting change") listener.onReplayGainSettingsChanged() } getString(R.string.set_key_notif_action) -> { - logD("Dispatching notification setting change") + T.d("Dispatching notification setting change") listener.onNotificationActionChanged() } getString(R.string.set_key_bar_action) -> { - logD("Dispatching bar action change") + T.d("Dispatching bar action change") listener.onBarActionChanged() } getString(R.string.set_key_repeat_pause) -> { - logD("Dispatching pause on repeat change") + T.d("Dispatching pause on repeat change") listener.onPauseOnRepeatChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index f0bc1d939..ffbb96319 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -43,7 +43,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * An [ViewModel] that provides a safe UI frontend for the current playback state. @@ -129,20 +129,20 @@ constructor( } override fun onIndexMoved(index: Int) { - logD("Index moved, updating current song") + T.d("Index moved, updating current song") _song.value = playbackManager.currentSong } override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { // Other types of queue changes preserve the current song. if (change.type == QueueChange.Type.SONG) { - logD("Queue changed, updating current song") + T.d("Queue changed, updating current song") _song.value = playbackManager.currentSong } } override fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) { - logD("Queue completely changed, updating current song") + T.d("Queue completely changed, updating current song") _isShuffled.value = isShuffled } @@ -152,14 +152,14 @@ constructor( index: Int, isShuffled: Boolean ) { - logD("New playback started, updating playback information") + T.d("New playback started, updating playback information") _song.value = playbackManager.currentSong _parent.value = parent _isShuffled.value = isShuffled } override fun onProgressionChanged(progression: Progression) { - logD("Player state changed, starting new position polling") + T.d("Player state changed, starting new position polling") _isPlaying.value = progression.isPlaying // Still need to update the position now due to co-routine launch delays _positionDs.value = progression.calculateElapsedPositionMs().msToDs() @@ -187,7 +187,7 @@ constructor( // --- PLAYING FUNCTIONS --- fun play(song: Song, with: PlaySong) { - logD("Playing $song with $with") + T.d("Playing $song with $with") playWithImpl(song, with, ShuffleMode.IMPLICIT) } @@ -201,7 +201,7 @@ constructor( /** Shuffle all songs in the music library. */ fun shuffleAll() { - logD("Shuffling all songs") + T.d("Shuffling all songs") playFromAllImpl(null, ShuffleMode.ON) } @@ -257,7 +257,7 @@ constructor( } private fun playFromAlbumImpl(song: Song, shuffle: ShuffleMode) { - logD("Playing $song from album") + T.d("Playing $song from album") playImpl(commandFactory.songFromAlbum(song, shuffle)) } @@ -267,7 +267,7 @@ constructor( playbackManager.play(params) return } - logD( + T.d( "Cannot use given artist parameter for $song [$artist from ${song.artists}], showing choice dialog") startPlaybackDecision(PlaybackDecision.PlayFromArtist(song)) } @@ -278,20 +278,20 @@ constructor( playbackManager.play(params) return } - logD( + T.d( "Cannot use given genre parameter for $song [$genre from ${song.genres}], showing choice dialog") startPlaybackDecision(PlaybackDecision.PlayFromArtist(song)) } private fun playFromPlaylistImpl(song: Song, playlist: Playlist, shuffle: ShuffleMode) { - logD("Playing $song from $playlist") + T.d("Playing $song from $playlist") playImpl(commandFactory.songFromPlaylist(song, playlist, shuffle)) } private fun startPlaybackDecision(decision: PlaybackDecision) { val existing = _playbackDecision.flow.value if (existing != null) { - logD("Already handling decision $existing, ignoring $decision") + T.d("Already handling decision $existing, ignoring $decision") return } _playbackDecision.put(decision) @@ -303,7 +303,7 @@ constructor( * @param album The [Album] to play. */ fun play(album: Album) { - logD("Playing $album") + T.d("Playing $album") playImpl(commandFactory.album(album, ShuffleMode.OFF)) } @@ -313,7 +313,7 @@ constructor( * @param album The [Album] to shuffle. */ fun shuffle(album: Album) { - logD("Shuffling $album") + T.d("Shuffling $album") playImpl(commandFactory.album(album, ShuffleMode.ON)) } @@ -323,7 +323,7 @@ constructor( * @param artist The [Artist] to play. */ fun play(artist: Artist) { - logD("Playing $artist") + T.d("Playing $artist") playImpl(commandFactory.artist(artist, ShuffleMode.OFF)) } @@ -333,7 +333,7 @@ constructor( * @param artist The [Artist] to shuffle. */ fun shuffle(artist: Artist) { - logD("Shuffling $artist") + T.d("Shuffling $artist") playImpl(commandFactory.artist(artist, ShuffleMode.ON)) } @@ -343,7 +343,7 @@ constructor( * @param genre The [Genre] to play. */ fun play(genre: Genre) { - logD("Playing $genre") + T.d("Playing $genre") playImpl(commandFactory.genre(genre, ShuffleMode.OFF)) } @@ -353,7 +353,7 @@ constructor( * @param genre The [Genre] to shuffle. */ fun shuffle(genre: Genre) { - logD("Shuffling $genre") + T.d("Shuffling $genre") playImpl(commandFactory.genre(genre, ShuffleMode.ON)) } @@ -363,7 +363,7 @@ constructor( * @param playlist The [Playlist] to play. */ fun play(playlist: Playlist) { - logD("Playing $playlist") + T.d("Playing $playlist") playImpl(commandFactory.playlist(playlist, ShuffleMode.OFF)) } @@ -373,7 +373,7 @@ constructor( * @param playlist The [Playlist] to shuffle. */ fun shuffle(playlist: Playlist) { - logD("Shuffling $playlist") + T.d("Shuffling $playlist") playImpl(commandFactory.playlist(playlist, ShuffleMode.ON)) } @@ -383,7 +383,7 @@ constructor( * @param songs The [Song]s to play. */ fun play(songs: List) { - logD("Playing ${songs.size} songs") + T.d("Playing ${songs.size} songs") playImpl(commandFactory.songs(songs, ShuffleMode.OFF)) } @@ -393,7 +393,7 @@ constructor( * @param songs The [Song]s to shuffle. */ fun shuffle(songs: List) { - logD("Shuffling ${songs.size} songs") + T.d("Shuffling ${songs.size} songs") playImpl(commandFactory.songs(songs, ShuffleMode.ON)) } @@ -408,7 +408,7 @@ constructor( * @param action The [DeferredPlayback] to perform eventually. */ fun playDeferred(action: DeferredPlayback) { - logD("Starting action $action") + T.d("Starting action $action") playbackManager.playDeferred(action) } @@ -420,7 +420,7 @@ constructor( * @param positionDs The position to seek to, in deci-seconds (1/10th of a second). */ fun seekTo(positionDs: Long) { - logD("Seeking to ${positionDs}ds") + T.d("Seeking to ${positionDs}ds") playbackManager.seekTo(positionDs.dsToMs()) } @@ -428,13 +428,13 @@ constructor( /** Skip to the next [Song]. */ fun next() { - logD("Skipping to next song") + T.d("Skipping to next song") playbackManager.next() } /** Skip to the previous [Song]. */ fun prev() { - logD("Skipping to previous song") + T.d("Skipping to previous song") playbackManager.prev() } @@ -444,7 +444,7 @@ constructor( * @param song The [Song] to add. */ fun playNext(song: Song) { - logD("Playing $song next") + T.d("Playing $song next") playbackManager.playNext(song) } @@ -454,7 +454,7 @@ constructor( * @param album The [Album] to add. */ fun playNext(album: Album) { - logD("Playing $album next") + T.d("Playing $album next") playbackManager.playNext(listSettings.albumSongSort.songs(album.songs)) } @@ -464,7 +464,7 @@ constructor( * @param artist The [Artist] to add. */ fun playNext(artist: Artist) { - logD("Playing $artist next") + T.d("Playing $artist next") playbackManager.playNext(listSettings.artistSongSort.songs(artist.songs)) } @@ -474,7 +474,7 @@ constructor( * @param genre The [Genre] to add. */ fun playNext(genre: Genre) { - logD("Playing $genre next") + T.d("Playing $genre next") playbackManager.playNext(listSettings.genreSongSort.songs(genre.songs)) } @@ -484,7 +484,7 @@ constructor( * @param playlist The [Playlist] to add. */ fun playNext(playlist: Playlist) { - logD("Playing $playlist next") + T.d("Playing $playlist next") playbackManager.playNext(playlist.songs) } @@ -494,7 +494,7 @@ constructor( * @param songs The [Song]s to add. */ fun playNext(songs: List) { - logD("Playing ${songs.size} songs next") + T.d("Playing ${songs.size} songs next") playbackManager.playNext(songs) } @@ -504,7 +504,7 @@ constructor( * @param song The [Song] to add. */ fun addToQueue(song: Song) { - logD("Adding $song to queue") + T.d("Adding $song to queue") playbackManager.addToQueue(song) } @@ -514,7 +514,7 @@ constructor( * @param album The [Album] to add. */ fun addToQueue(album: Album) { - logD("Adding $album to queue") + T.d("Adding $album to queue") playbackManager.addToQueue(listSettings.albumSongSort.songs(album.songs)) } @@ -524,7 +524,7 @@ constructor( * @param artist The [Artist] to add. */ fun addToQueue(artist: Artist) { - logD("Adding $artist to queue") + T.d("Adding $artist to queue") playbackManager.addToQueue(listSettings.artistSongSort.songs(artist.songs)) } @@ -534,7 +534,7 @@ constructor( * @param genre The [Genre] to add. */ fun addToQueue(genre: Genre) { - logD("Adding $genre to queue") + T.d("Adding $genre to queue") playbackManager.addToQueue(listSettings.genreSongSort.songs(genre.songs)) } @@ -544,7 +544,7 @@ constructor( * @param playlist The [Playlist] to add. */ fun addToQueue(playlist: Playlist) { - logD("Adding $playlist to queue") + T.d("Adding $playlist to queue") playbackManager.addToQueue(playlist.songs) } @@ -554,7 +554,7 @@ constructor( * @param songs The [Song]s to add. */ fun addToQueue(songs: List) { - logD("Adding ${songs.size} songs to queue") + T.d("Adding ${songs.size} songs to queue") playbackManager.addToQueue(songs) } @@ -562,13 +562,13 @@ constructor( /** Toggle [isPlaying] (i.e from playing to paused) */ fun togglePlaying() { - logD("Toggling playing state") + T.d("Toggling playing state") playbackManager.playing(!playbackManager.progression.isPlaying) } /** Toggle [isShuffled] (ex. from on to off) */ fun toggleShuffled() { - logD("Toggling shuffled state") + T.d("Toggling shuffled state") playbackManager.shuffled(!playbackManager.isShuffled) } @@ -578,7 +578,7 @@ constructor( * @see RepeatMode.increment */ fun toggleRepeatMode() { - logD("Toggling repeat mode") + T.d("Toggling repeat mode") playbackManager.repeatMode(playbackManager.repeatMode.increment()) } @@ -599,7 +599,7 @@ constructor( private fun openImpl(panel: OpenPanel) { val existing = openPanel.flow.value if (existing != null) { - logD("Already opening $existing, ignoring opening $panel") + T.d("Already opening $existing, ignoring opening $panel") return } _openPanel.put(panel) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromArtistDialog.kt index 67f295466..7ecea8b7b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromArtistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromArtistDialog.kt @@ -36,8 +36,8 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A picker [ViewBindingMaterialDialogFragment] intended for when [Artist] playback is ambiguous. @@ -88,7 +88,7 @@ class PlayFromArtistDialog : private fun updateSong(song: Song?) { if (song == null) { - logD("No song to show choices for, navigating away") + T.d("No song to show choices for, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromGenreDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromGenreDialog.kt index dfc522d3c..2679479c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromGenreDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromGenreDialog.kt @@ -36,8 +36,8 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A picker [ViewBindingMaterialDialogFragment] intended for when [Genre] playback is ambiguous. @@ -88,7 +88,7 @@ class PlayFromGenreDialog : private fun updateSong(song: Song?) { if (song == null) { - logD("No song to show choices for, navigating away") + T.d("No song to show choices for, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt index d8dae6ae4..2c6f990d2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt @@ -27,8 +27,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * A [ViewModel] that stores the choices shown in the playback picker dialogs. @@ -64,10 +63,10 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M * @param uid The [Music.UID] of the item to show. Must be a [Song]. */ fun setPickerSongUid(uid: Music.UID) { - logD("Opening picker for song $uid") + T.d("Opening picker for song $uid") _currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid) if (_currentPickerSong.value != null) { - logW("Given song UID was invalid") + T.w("Given song UID was invalid") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt index a291cc175..a64e49853 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt @@ -22,8 +22,7 @@ import javax.inject.Inject import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logE +import timber.log.Timber as T /** * Manages the persisted playback state in a structured manner. @@ -60,8 +59,8 @@ constructor( heapItems = queueDao.getHeap() mappingItems = queueDao.getShuffledMapping() } catch (e: Exception) { - logE("Unable read playback state") - logE(e.stackTraceToString()) + T.e("Unable read playback state") + T.e(e.stackTraceToString()) return null } @@ -85,12 +84,12 @@ constructor( queueDao.nukeHeap() queueDao.nukeShuffledMapping() } catch (e: Exception) { - logE("Unable to clear previous state") - logE(e.stackTraceToString()) + T.e("Unable to clear previous state") + T.e(e.stackTraceToString()) return false } - logD("Successfully cleared previous state") + T.d("Successfully cleared previous state") if (state != null) { // Transform saved state into raw state, which can then be written to the database. val playbackState = @@ -114,12 +113,12 @@ constructor( queueDao.insertHeap(heap) queueDao.insertShuffledMapping(shuffledMapping) } catch (e: Exception) { - logE("Unable to write new state") - logE(e.stackTraceToString()) + T.e("Unable to write new state") + T.e(e.stackTraceToString()) return false } - logD("Successfully wrote new state") + T.d("Successfully wrote new state") } return true diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 9bd554a59..1fe39292a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.inflater -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [RecyclerView.Adapter] that shows an editable list of queue items. @@ -80,7 +80,7 @@ class QueueAdapter(private val listener: EditClickListListener) : * @param isPlaying Whether playback is ongoing or paused. */ fun setPosition(index: Int, isPlaying: Boolean) { - logD("Updating index") + T.d("Updating index") val lastIndex = currentIndex currentIndex = index @@ -89,10 +89,10 @@ class QueueAdapter(private val listener: EditClickListListener) : // TODO: Optimize this by only updating the range between old and new indices? // TODO: Don't update when the index has not moved. if (currentIndex < lastIndex) { - logD("Moved backwards, must update items above last index") + T.d("Moved backwards, must update items above last index") notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_POSITION) } else { - logD("Moved forwards, update items after index") + T.d("Moved forwards, update items after index") notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_POSITION) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 7a70bc6e8..02b691e81 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -34,7 +34,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [ViewBindingFragment] that displays an editable queue. @@ -122,14 +122,14 @@ class QueueFragment : ViewBindingFragment(), EditClickList // dependent on where we have to scroll to get to the currently playing song. if (notInitialized || scrollTo < start) { // We need to scroll upwards, or initialize the scroll, no need to offset - logD("Not scrolling downwards, no offset needed") + T.d("Not scrolling downwards, no offset needed") binding.queueRecycler.scrollToPosition(scrollTo) } else if (scrollTo > end) { // We need to scroll downwards, we need to offset by a screen of songs. // This does have some error due to how many completely visible items on-screen // can vary. This is considered okay. val offset = scrollTo + (end - start) - logD("Scrolling downwards, offsetting by $offset") + T.d("Scrolling downwards, offsetting by $offset") binding.queueRecycler.scrollToPosition(min(queue.lastIndex, offset)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 1283fc909..85d37b366 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [ViewModel] that manages the current queue state and allows navigation through the queue. @@ -62,26 +62,26 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt } override fun onIndexMoved(index: Int) { - logD("Index moved, synchronizing and scrolling to new position") + T.d("Index moved, synchronizing and scrolling to new position") _scrollTo.put(index) _index.value = index } override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { // Queue changed trivially due to item mo -> Diff queue, stay at current index. - logD("Updating queue display") + T.d("Updating queue display") _queueInstructions.put(change.instructions) _queue.value = queue if (change.type != QueueChange.Type.MAPPING) { // Index changed, make sure it remains updated without actually scrolling to it. - logD("Index changed with queue, synchronizing new position") + T.d("Index changed with queue, synchronizing new position") _index.value = index } } override fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) { // Queue changed completely -> Replace queue, update index - logD("Queue changed completely, replacing queue and position") + T.d("Queue changed completely, replacing queue and position") _queueInstructions.put(UpdateInstructions.Replace(0)) _scrollTo.put(index) _queue.value = queue @@ -95,7 +95,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt isShuffled: Boolean ) { // Entirely new queue -> Replace queue, update index - logD("New playback, replacing queue and position") + T.d("New playback, replacing queue and position") _queueInstructions.put(UpdateInstructions.Replace(0)) _scrollTo.put(index) _queue.value = queue @@ -117,7 +117,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt if (adapterIndex !in queue.value.indices) { return } - logD("Going to position $adapterIndex in queue") + T.d("Going to position $adapterIndex in queue") playbackManager.goto(adapterIndex) } @@ -131,7 +131,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt if (adapterIndex !in queue.value.indices) { return } - logD("Removing item $adapterIndex in queue") + T.d("Removing item $adapterIndex in queue") playbackManager.removeQueueItem(adapterIndex) } @@ -146,7 +146,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt if (adapterFrom !in queue.value.indices || adapterTo !in queue.value.indices) { return false } - logD("Moving $adapterFrom to $adapterFrom in queue") + T.d("Moving $adapterFrom to $adapterFrom in queue") playbackManager.moveQueueItem(adapterFrom, adapterTo) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 8a98eb99a..9e52d1521 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * aa [ViewBindingMaterialDialogFragment] that allows user configuration of the current @@ -62,7 +62,7 @@ class PreAmpCustomizeDialog : ViewBindingMaterialDialogFragment { - logD("ReplayGain is off") + T.d("ReplayGain is off") null } // User wants track gain to be preferred. Default to album gain only if // there is no track gain. ReplayGainMode.TRACK -> { - logD("Using track strategy") + T.d("Using track strategy") gain.track ?: gain.album } // User wants album gain to be preferred. Default to track gain only if // here is no album gain. ReplayGainMode.ALBUM -> { - logD("Using album strategy") + T.d("Using album strategy") gain.album ?: gain.track } // User wants album gain to be used when in an album, track gain otherwise. ReplayGainMode.DYNAMIC -> { - logD("Using dynamic strategy") + T.d("Using dynamic strategy") gain.album?.takeIf { playbackManager.parent is Album && playbackManager.currentSong?.album == playbackManager.parent @@ -150,15 +150,15 @@ constructor( val amplifiedAdjustment = if (resolvedAdjustment != null) { // Successfully resolved an adjustment, apply the corresponding pre-amp - logD("Applying with pre-amp") + T.d("Applying with pre-amp") resolvedAdjustment + preAmp.with } else { // No adjustment found, use the corresponding user-defined pre-amp - logD("Applying without pre-amp") + T.d("Applying without pre-amp") preAmp.without } - logD("Applying ReplayGain adjustment ${amplifiedAdjustment}db") + T.d("Applying ReplayGain adjustment ${amplifiedAdjustment}db") // Final adjustment along the volume curve. volume = 10f.pow(amplifiedAdjustment / 20f) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 2947052c2..05ec3d9f2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -59,8 +59,7 @@ import org.oxycblt.auxio.playback.state.RawQueue import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.playback.state.StateAck -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logE +import timber.log.Timber as T class ExoPlaybackStateHolder( private val context: Context, @@ -152,7 +151,7 @@ class ExoPlaybackStateHolder( when (action) { // Restore state -> Start a new restoreState job is DeferredPlayback.RestoreState -> { - logD("Restoring playback state") + T.d("Restoring playback state") restoreScope.launch { val state = persistenceRepository.readState() withContext(Dispatchers.Main) { @@ -171,7 +170,7 @@ class ExoPlaybackStateHolder( } // Shuffle all -> Start new playback from all songs is DeferredPlayback.ShuffleAll -> { - logD("Shuffling all tracks") + T.d("Shuffling all tracks") playbackManager.play( requireNotNull(commandFactory.all(ShuffleMode.ON)) { "Invalid playback parameters" @@ -179,7 +178,7 @@ class ExoPlaybackStateHolder( } // Open -> Try to find the Song for the given file and then play it from all songs is DeferredPlayback.Open -> { - logD("Opening specified file") + T.d("Opening specified file") deviceLibrary.findSongForUri(context, action.uri)?.let { song -> playbackManager.play( requireNotNull(commandFactory.song(song, ShuffleMode.IMPLICIT)) { @@ -428,18 +427,18 @@ class ExoPlaybackStateHolder( if (player.playWhenReady) { // Mark that we have started playing so that the notification can now be posted. - logD("Player has started playing") + T.d("Player has started playing") sessionOngoing = true if (!openAudioEffectSession) { // Convention to start an audioeffect session on play/pause rather than // start/stop - logD("Opening audio effect session") + T.d("Opening audio effect session") broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) openAudioEffectSession = true } } else if (openAudioEffectSession) { // Make sure to close the audio session when we stop playback. - logD("Closing audio effect session") + T.d("Closing audio effect session") broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) openAudioEffectSession = false } @@ -471,7 +470,7 @@ class ExoPlaybackStateHolder( Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_POSITION_DISCONTINUITY)) { - logD("Player state changed, must synchronize state") + T.d("Player state changed, must synchronize state") playbackManager.ack(this, StateAck.ProgressionChanged) } } @@ -479,13 +478,13 @@ class ExoPlaybackStateHolder( override fun onPlayerError(error: PlaybackException) { // TODO: Replace with no skipping and a notification instead // If there's any issue, just go to the next song. - logE("Player error occurred") - logE(error.stackTraceToString()) + T.e("Player error occurred") + T.e(error.stackTraceToString()) playbackManager.next() } private fun broadcastAudioEffectAction(event: String) { - logD("Broadcasting AudioEffect event: $event") + T.d("Broadcasting AudioEffect event: $event") context.sendBroadcast( Intent(event) .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) @@ -498,7 +497,7 @@ class ExoPlaybackStateHolder( override fun onMusicChanges(changes: MusicRepository.Changes) { if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { // We now have a library, see if we have anything we need to do. - logD("Library obtained, requesting action") + T.d("Library obtained, requesting action") playbackManager.requestAction(this) } } @@ -524,17 +523,17 @@ class ExoPlaybackStateHolder( private fun deferSave() { saveJob { - logD("Waiting for save buffer") + T.d("Waiting for save buffer") delay(SAVE_BUFFER) yield() - logD("Committing saved state") + T.d("Committing saved state") persistenceRepository.saveState(playbackManager.toSavedState()) } } private fun saveJob(block: suspend () -> Unit) { currentSaveJob?.let { - logD("Discarding prior save job") + T.d("Discarding prior save job") it.cancel() } currentSaveJob = saveScope.launch { block() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt index 8a6e70312..2c32f25e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt @@ -27,7 +27,7 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.AuxioService import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to @@ -47,7 +47,7 @@ class MediaButtonReceiver : BroadcastReceiver() { // stupid this is with the state of foreground services on modern android. One // wrong action at the wrong time will result in the app crashing, and there is // nothing I can do about it. - logD("Delivering media button intent $intent") + T.d("Delivering media button intent $intent") intent.component = ComponentName(context, AuxioService::class.java) ContextCompat.startForegroundService(context, intent) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt index dc33ca4ab..d5b0f43b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt @@ -47,9 +47,9 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newBroadcastPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent +import timber.log.Timber as T /** * A component that mirrors the current playback state into the [MediaSessionCompat] and @@ -210,10 +210,10 @@ private constructor( * playback is currently occuring from all songs. */ private fun updateMediaMetadata(song: Song?, parent: MusicParent?) { - logD("Updating media metadata to $song with $parent") + T.d("Updating media metadata to $song with $parent") if (song == null) { // Nothing playing, reset the MediaSession and close the notification. - logD("Nothing playing, resetting media session") + T.d("Nothing playing, resetting media session") mediaSession.setMetadata(emptyMetadata) return } @@ -252,15 +252,15 @@ private constructor( MediaSessionUID.SingleItem(song.album.uid).toString()) // These fields are nullable and so we must check first before adding them to the fields. song.track?.let { - logD("Adding track information") + T.d("Adding track information") builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, it.toLong()) } song.disc?.let { - logD("Adding disc information") + T.d("Adding disc information") builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.number.toLong()) } song.date?.let { - logD("Adding date information") + T.d("Adding date information") builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) builder.putLong(MediaMetadataCompat.METADATA_KEY_YEAR, it.year.toLong()) } @@ -272,7 +272,7 @@ private constructor( song, object : BitmapProvider.Target { override fun onCompleted(bitmap: Bitmap?) { - logD("Bitmap loaded, applying media session and posting notification") + T.d("Bitmap loaded, applying media session and posting notification") if (bitmap != null) { builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap) builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) @@ -300,13 +300,13 @@ private constructor( // playback state. MediaSessionCompat.QueueItem(description, i.toLong()) } - logD("Uploading ${queueItems.size} songs to MediaSession queue") + T.d("Uploading ${queueItems.size} songs to MediaSession queue") mediaSession.setQueue(queueItems) } /** Invalidate the current [MediaSessionCompat]'s [PlaybackStateCompat]. */ private fun invalidateSessionState() { - logD("Updating media session playback state") + T.d("Updating media session playback state") val state = // InternalPlayer.State handles position/state information. @@ -322,7 +322,7 @@ private constructor( val secondaryAction = when (playbackSettings.notificationAction) { ActionMode.SHUFFLE -> { - logD("Using shuffle MediaSession action") + T.d("Using shuffle MediaSession action") PlaybackStateCompat.CustomAction.Builder( PlaybackActions.ACTION_INVERT_SHUFFLE, context.getString(R.string.desc_shuffle), @@ -333,7 +333,7 @@ private constructor( }) } else -> { - logD("Using repeat mode MediaSession action") + T.d("Using repeat mode MediaSession action") PlaybackStateCompat.CustomAction.Builder( PlaybackActions.ACTION_INC_REPEAT_MODE, context.getString(R.string.desc_change_repeat), @@ -356,22 +356,22 @@ private constructor( /** Invalidate the "secondary" action (i.e shuffle/repeat mode). */ private fun invalidateSecondaryAction() { - logD("Invalidating secondary action") + T.d("Invalidating secondary action") invalidateSessionState() when (playbackSettings.notificationAction) { ActionMode.SHUFFLE -> { - logD("Using shuffle notification action") + T.d("Using shuffle notification action") _notification.updateShuffled(playbackManager.isShuffled) } else -> { - logD("Using repeat mode notification action") + T.d("Using repeat mode notification action") _notification.updateRepeatMode(playbackManager.repeatMode) } } if (!bitmapProvider.isBusy) { - logD("Not loading a bitmap, post the notification") + T.d("Not loading a bitmap, post the notification") foregroundListener.updateForeground(ForegroundListener.Change.MEDIA_SESSION) } } @@ -423,7 +423,7 @@ private class PlaybackNotification( * @param metadata The [MediaMetadataCompat] to display in this notification. */ fun updateMetadata(metadata: MediaMetadataCompat) { - logD("Updating shown metadata") + T.d("Updating shown metadata") setLargeIcon(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)) setContentTitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)) setContentText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ARTIST)) @@ -436,7 +436,7 @@ private class PlaybackNotification( * @param isPlaying Whether playback should be indicated as ongoing or paused. */ fun updatePlaying(isPlaying: Boolean) { - logD("Updating playing state: $isPlaying") + T.d("Updating playing state: $isPlaying") mActions[2] = buildPlayPauseAction(context, isPlaying) } @@ -446,7 +446,7 @@ private class PlaybackNotification( * @param repeatMode The current [RepeatMode]. */ fun updateRepeatMode(repeatMode: RepeatMode) { - logD("Applying repeat mode action: $repeatMode") + T.d("Applying repeat mode action: $repeatMode") mActions[0] = buildRepeatAction(context, repeatMode) } @@ -456,7 +456,7 @@ private class PlaybackNotification( * @param isShuffled Whether the queue is currently shuffled or not. */ fun updateShuffled(isShuffled: Boolean) { - logD("Applying shuffle action: $isShuffled") + T.d("Applying shuffle action: $isShuffled") mActions[0] = buildShuffleAction(context, isShuffled) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt index 2ea4f8db2..d9ec16787 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt @@ -47,7 +47,7 @@ import org.oxycblt.auxio.playback.state.PlaybackCommand import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.ShuffleMode -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T class MediaSessionInterface @Inject @@ -85,7 +85,7 @@ constructor( val parentUid = extras?.getString(MusicBrowser.KEY_CHILD_OF)?.let { MediaSessionUID.fromString(it) } val command = expandUidIntoCommand(uid, parentUid) - logD(extras?.getString(MusicBrowser.KEY_CHILD_OF)) + T.d(extras?.getString(MusicBrowser.KEY_CHILD_OF)) playbackManager.play(requireNotNull(command) { "Invalid playback configuration" }) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt index 26ce16237..b21f43b97 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt @@ -27,8 +27,8 @@ import org.oxycblt.auxio.ForegroundServiceNotification import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.playback.state.DeferredPlayback import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.widgets.WidgetComponent +import timber.log.Timber as T class PlaybackServiceFragment private constructor( @@ -86,7 +86,7 @@ private constructor( fun start(startedBy: Int) { // At minimum we want to ensure an active playback state. // TODO: Possibly also force to go foreground? - logD("Handling non-native start.") + T.d("Handling non-native start.") val action = when (startedBy) { IntegerTable.START_ID_ACTIVITY -> null @@ -97,7 +97,7 @@ private constructor( else -> DeferredPlayback.RestoreState(play = false) } if (action != null) { - logD("Initing service fragment using action $action") + T.d("Initing service fragment using action $action") playbackManager.playDeferred(action) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt index 70f148c03..956800067 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt @@ -27,9 +27,9 @@ import androidx.core.content.ContextCompat import javax.inject.Inject import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetProvider +import timber.log.Timber as T /** * A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require an @@ -75,7 +75,7 @@ private constructor( // 3. Some internal framework thing that also handles bluetooth headsets // Just use ACTION_HEADSET_PLUG. AudioManager.ACTION_HEADSET_PLUG -> { - logD("Received headset plug event") + T.d("Received headset plug event") when (intent.getIntExtra("state", -1)) { 0 -> pauseFromHeadsetPlug() 1 -> playFromHeadsetPlug() @@ -84,37 +84,37 @@ private constructor( initialHeadsetPlugEventHandled = true } AudioManager.ACTION_AUDIO_BECOMING_NOISY -> { - logD("Received Headset noise event") + T.d("Received Headset noise event") pauseFromHeadsetPlug() } // --- AUXIO EVENTS --- PlaybackActions.ACTION_PLAY_PAUSE -> { - logD("Received play event") + T.d("Received play event") playbackManager.playing(!playbackManager.progression.isPlaying) } PlaybackActions.ACTION_INC_REPEAT_MODE -> { - logD("Received repeat mode event") + T.d("Received repeat mode event") playbackManager.repeatMode(playbackManager.repeatMode.increment()) } PlaybackActions.ACTION_INVERT_SHUFFLE -> { - logD("Received shuffle event") + T.d("Received shuffle event") playbackManager.shuffled(!playbackManager.isShuffled) } PlaybackActions.ACTION_SKIP_PREV -> { - logD("Received skip previous event") + T.d("Received skip previous event") playbackManager.prev() } PlaybackActions.ACTION_SKIP_NEXT -> { - logD("Received skip next event") + T.d("Received skip next event") playbackManager.next() } PlaybackActions.ACTION_EXIT -> { - logD("Received exit event") + T.d("Received exit event") playbackManager.endSession() } WidgetProvider.ACTION_WIDGET_UPDATE -> { - logD("Received widget update event") + T.d("Received widget update event") widgetComponent.update() } } @@ -127,14 +127,14 @@ private constructor( if (playbackSettings.headsetAutoplay && playbackManager.currentSong != null && initialHeadsetPlugEventHandled) { - logD("Device connected, resuming") + T.d("Device connected, resuming") playbackManager.playing(true) } } private fun pauseFromHeadsetPlug() { if (playbackManager.currentSong != null) { - logD("Device disconnected, pausing") + T.d("Device disconnected, pausing") playbackManager.playing(false) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt index 14b87c1dd..61ff8087e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackSettings +import timber.log.Timber as T /** * A playback command that can be passed to [PlaybackStateManager] to start new playback. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 174063b55..7aeb550e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -25,8 +25,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T /** * Core playback state controller class. @@ -388,11 +387,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun addListener(listener: Listener) { - logD("Adding $listener to listeners") + T.d("Adding $listener to listeners") listeners.add(listener) if (isInitialized) { - logD("Sending initial state to $listener") + T.d("Sending initial state to $listener") listener.onNewPlayback( stateMirror.parent, stateMirror.queue, stateMirror.index, stateMirror.isShuffled) listener.onProgressionChanged(stateMirror.progression) @@ -402,16 +401,16 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun removeListener(listener: Listener) { - logD("Removing $listener from listeners") + T.d("Removing $listener from listeners") if (!listeners.remove(listener)) { - logW("Listener $listener was not added prior, cannot remove") + T.w("Listener $listener was not added prior, cannot remove") } } @Synchronized override fun registerStateHolder(stateHolder: PlaybackStateHolder) { if (this.stateHolder != null) { - logW("Internal player is already registered") + T.w("Internal player is already registered") return } @@ -430,11 +429,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun unregisterStateHolder(stateHolder: PlaybackStateHolder) { if (this.stateHolder !== stateHolder) { - logW("Given internal player did not match current internal player") + T.w("Given internal player did not match current internal player") return } - logD("Unregistering internal player $stateHolder") + T.d("Unregistering internal player $stateHolder") this.stateHolder = null } @@ -444,7 +443,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun play(command: PlaybackCommand) { val stateHolder = stateHolder ?: return - logD("Playing $command") + T.d("Playing $command") // Played something, so we are initialized now isInitialized = true stateHolder.newPlayback(command) @@ -455,32 +454,32 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun next() { val stateHolder = stateHolder ?: return - logD("Going to next song") + T.d("Going to next song") stateHolder.next() } @Synchronized override fun prev() { val stateHolder = stateHolder ?: return - logD("Going to previous song") + T.d("Going to previous song") stateHolder.prev() } @Synchronized override fun goto(index: Int) { val stateHolder = stateHolder ?: return - logD("Going to index $index") + T.d("Going to index $index") stateHolder.goto(index) } @Synchronized override fun playNext(songs: List) { if (currentSong == null) { - logD("Nothing playing, short-circuiting to new playback") + T.d("Nothing playing, short-circuiting to new playback") play(QueueCommand(songs)) } else { val stateHolder = stateHolder ?: return - logD("Adding ${songs.size} songs to start of queue") + T.d("Adding ${songs.size} songs to start of queue") stateHolder.playNext(songs, StateAck.PlayNext(stateMirror.index + 1, songs.size)) } } @@ -488,11 +487,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun addToQueue(songs: List) { if (currentSong == null) { - logD("Nothing playing, short-circuiting to new playback") + T.d("Nothing playing, short-circuiting to new playback") play(QueueCommand(songs)) } else { val stateHolder = stateHolder ?: return - logD("Adding ${songs.size} songs to end of queue") + T.d("Adding ${songs.size} songs to end of queue") stateHolder.addToQueue(songs, StateAck.AddToQueue(queue.size, songs.size)) } } @@ -506,21 +505,21 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun moveQueueItem(src: Int, dst: Int) { val stateHolder = stateHolder ?: return - logD("Moving item $src to position $dst") + T.d("Moving item $src to position $dst") stateHolder.move(src, dst, StateAck.Move(src, dst)) } @Synchronized override fun removeQueueItem(at: Int) { val stateHolder = stateHolder ?: return - logD("Removing item at $at") + T.d("Removing item at $at") stateHolder.remove(at, StateAck.Remove(at)) } @Synchronized override fun shuffled(shuffled: Boolean) { val stateHolder = stateHolder ?: return - logD("Reordering queue [shuffled=$shuffled]") + T.d("Reordering queue [shuffled=$shuffled]") stateHolder.shuffled(shuffled) } @@ -530,7 +529,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override fun playDeferred(action: DeferredPlayback) { val stateHolder = stateHolder if (stateHolder == null || !stateHolder.handleDeferred(action)) { - logD("Internal player not present or did not consume action, waiting") + T.d("Internal player not present or did not consume action, waiting") pendingDeferredPlayback = action } } @@ -538,12 +537,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun requestAction(stateHolder: PlaybackStateHolder) { if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) { - logW("Given internal player did not match current internal player") + T.w("Given internal player did not match current internal player") return } if (pendingDeferredPlayback?.let(stateHolder::handleDeferred) == true) { - logD("Pending action consumed") + T.d("Pending action consumed") pendingDeferredPlayback = null } } @@ -551,35 +550,35 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun playing(isPlaying: Boolean) { val stateHolder = stateHolder ?: return - logD("Updating playing state to $isPlaying") + T.d("Updating playing state to $isPlaying") stateHolder.playing(isPlaying) } @Synchronized override fun repeatMode(repeatMode: RepeatMode) { val stateHolder = stateHolder ?: return - logD("Updating repeat mode to $repeatMode") + T.d("Updating repeat mode to $repeatMode") stateHolder.repeatMode(repeatMode) } @Synchronized override fun seekTo(positionMs: Long) { val stateHolder = stateHolder ?: return - logD("Seeking to ${positionMs}ms") + T.d("Seeking to ${positionMs}ms") stateHolder.seekTo(positionMs) } @Synchronized override fun endSession() { val stateHolder = stateHolder ?: return - logD("Ending session") + T.d("Ending session") stateHolder.endSession() } @Synchronized override fun ack(stateHolder: PlaybackStateHolder, ack: StateAck) { if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) { - logW("Given internal player did not match current internal player") + T.w("Given internal player did not match current internal player") return } @@ -730,7 +729,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { destructive: Boolean ) { if (isInitialized && !destructive) { - logW("Already initialized, cannot apply saved state") + T.w("Already initialized, cannot apply saved state") return } @@ -752,7 +751,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { } } - logD("Created adjustment mapping [max shift=$currentShift]") + T.d("Created adjustment mapping [max shift=$currentShift]") val shuffledMapping = savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { index -> @@ -767,7 +766,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { } else { heap.getOrNull(index) } - logD(currentSong) + T.d(currentSong) return currentSong?.uid == savedState.songUid } @@ -777,7 +776,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { index-- } - logD("Corrected index: ${savedState.index} -> $index") + T.d("Corrected index: ${savedState.index} -> $index") check(shuffledMapping.all { it in heap.indices }) { "Queue inconsistency detected: Shuffled mapping indices out of heap bounds" diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index c688d1eb9..48b8c209b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -26,7 +26,7 @@ import com.google.android.material.R as MR import com.google.android.material.button.MaterialButton import com.google.android.material.motion.MotionUtils import org.oxycblt.auxio.ui.RippleFixMaterialButton -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when @@ -62,12 +62,12 @@ class AnimatedMaterialButton : RippleFixMaterialButton { val targetRadius = if (activated) 0.3f else 0.5f if (!isLaidOut) { // Not laid out, initialize it without animation before drawing. - logD("Not laid out, immediately updating corner radius") + T.d("Not laid out, immediately updating corner radius") updateCornerRadiusRatio(targetRadius) return } - logD("Starting corner radius animation") + T.d("Starting corner radius animation") animator?.cancel() animator = ValueAnimator.ofFloat(currentCornerRadiusRatio, targetRadius).apply { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt index 20bbd9394..e7201ce97 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt @@ -25,7 +25,7 @@ import kotlin.math.max import org.oxycblt.auxio.databinding.ViewSeekBarBinding import org.oxycblt.auxio.playback.formatDurationDs import org.oxycblt.auxio.util.inflater -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A wrapper around [Slider] that shows position and duration values and sanitizes input to reduce @@ -81,11 +81,11 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 // zero, use 1 instead and disable the SeekBar. val to = max(value, 1) isEnabled = value > 0 - logD("Value sanitization finished [to=$to, enabled=$isEnabled]") + T.d("Value sanitization finished [to=$to, enabled=$isEnabled]") // Sanity check 2: If the current value exceeds the new duration value, clamp it // down so that we don't crash and instead have an annoying visual flicker. if (positionDs > to) { - logD("Clamping invalid position [current: $positionDs new max: $to]") + T.d("Clamping invalid position [current: $positionDs new max: $to]") binding.seekBarSlider.value = to.toFloat() } binding.seekBarSlider.valueTo = to.toFloat() @@ -93,14 +93,14 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 } override fun onStartTrackingTouch(slider: Slider) { - logD("Starting seek mode") + T.d("Starting seek mode") // User has begun seeking, place the SeekBar into a "Suspended" mode in which no // position updates are sent and is indicated by the position value turning accented. isActivated = true } override fun onStopTrackingTouch(slider: Slider) { - logD("Confirming seek") + T.d("Confirming seek") // End of seek event, send off new value to listener. isActivated = false listener?.onSeekConfirmed(slider.value.toLong()) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index 7b9befdef..f3ca192fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -39,7 +39,7 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * An adapter that displays search results. @@ -75,7 +75,7 @@ class SearchAdapter(private val listener: SelectableListListener) : } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - logD(position) + T.d(position) when (val item = getItem(position)) { is Song -> (holder as SongViewHolder).bind(item, listener) is Album -> (holder as AlbumViewHolder).bind(item, listener) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt index 32c6da8de..663f8accd 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Name -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * Implements the fuzzy-ish searching algorithm used in the search view. @@ -67,7 +67,7 @@ interface SearchEngine { class SearchEngineImpl @Inject constructor(@ApplicationContext private val context: Context) : SearchEngine { override suspend fun search(items: SearchEngine.Items, query: String): SearchEngine.Items { - logD("Launching search for $query") + T.d("Launching search for $query") return SearchEngine.Items( songs = items.songs?.searchListImpl(query) { q, song -> diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index ddc3c754b..148c71893 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -61,11 +61,10 @@ import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getSystemServiceCompat -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.showToast +import timber.log.Timber as T /** * The [ListFragment] providing search functionality for the music library. @@ -109,11 +108,11 @@ class SearchFragment : ListFragment() { getContentLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) { - logW("No URI returned from file picker") + T.w("No URI returned from file picker") return@registerForActivityResult } - logD("Received playlist URI $uri") + T.d("Received playlist URI $uri") musicModel.importPlaylist(uri, pendingImportTarget) } @@ -140,7 +139,7 @@ class SearchFragment : ListFragment() { if (!launchedKeyboard) { // Auto-open the keyboard when this view is shown - logD("Keyboard is not shown yet") + T.d("Keyboard is not shown yet") showKeyboard(this) launchedKeyboard = true } @@ -185,7 +184,7 @@ class SearchFragment : ListFragment() { if (item.itemId != R.id.submenu_filtering) { // Is a change in filter mode and not just a junk submenu click, update // the filtering within SearchViewModel. - logD("Filter mode selected") + T.d("Filter mode selected") item.isChecked = true searchModel.setFilterOptionId(item.itemId) return true @@ -223,7 +222,7 @@ class SearchFragment : ListFragment() { // I would make it so that the position is only scrolled back to the top when // the query actually changes instead of once every re-creation event, but sadly // that doesn't seem possible. - logD("Update finished, scrolling to top") + T.d("Update finished, scrolling to top") binding.searchRecycler.scrollToPosition(0) } } @@ -231,39 +230,39 @@ class SearchFragment : ListFragment() { private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { - logD("Navigating to ${show.song}") + T.d("Navigating to ${show.song}") findNavController().navigateSafe(SearchFragmentDirections.showSong(show.song.uid)) } is Show.SongAlbumDetails -> { - logD("Navigating to the album of ${show.song}") + T.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(SearchFragmentDirections.showAlbum(show.song.album.uid)) } is Show.AlbumDetails -> { - logD("Navigating to ${show.album}") + T.d("Navigating to ${show.album}") findNavController().navigateSafe(SearchFragmentDirections.showAlbum(show.album.uid)) } is Show.ArtistDetails -> { - logD("Navigating to ${show.artist}") + T.d("Navigating to ${show.artist}") findNavController() .navigateSafe(SearchFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - logD("Navigating to artist choices for ${show.song}") + T.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(SearchFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - logD("Navigating to artist choices for ${show.album}") + T.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(SearchFragmentDirections.showArtistChoices(show.album.uid)) } is Show.GenreDetails -> { - logD("Navigating to ${show.genre}") + T.d("Navigating to ${show.genre}") findNavController().navigateSafe(SearchFragmentDirections.showGenre(show.genre.uid)) } is Show.PlaylistDetails -> { - logD("Navigating to ${show.playlist}") + T.d("Navigating to ${show.playlist}") findNavController() .navigateSafe(SearchFragmentDirections.showPlaylist(show.playlist.uid)) } @@ -297,7 +296,7 @@ class SearchFragment : ListFragment() { binding.searchSelectionToolbar.title = getString(R.string.fmt_selected, selected.size) if (binding.searchToolbar.setVisible(R.id.search_selection_toolbar)) { // New selection started, show the keyboard to make selection easier. - logD("Significant selection occurred, hiding keyboard") + T.d("Significant selection occurred, hiding keyboard") hideKeyboard() } } else { @@ -310,7 +309,7 @@ class SearchFragment : ListFragment() { val directions = when (decision) { is PlaylistDecision.Import -> { - logD("Importing playlist") + T.d("Importing playlist") pendingImportTarget = decision.target requireNotNull(getContentLauncher) { "Content picker launcher was not available" @@ -320,7 +319,7 @@ class SearchFragment : ListFragment() { return } is PlaylistDecision.Rename -> { - logD("Renaming ${decision.playlist}") + T.d("Renaming ${decision.playlist}") SearchFragmentDirections.renamePlaylist( decision.playlist.uid, decision.template, @@ -328,15 +327,15 @@ class SearchFragment : ListFragment() { decision.reason) } is PlaylistDecision.Delete -> { - logD("Deleting ${decision.playlist}") + T.d("Deleting ${decision.playlist}") SearchFragmentDirections.deletePlaylist(decision.playlist.uid) } is PlaylistDecision.Export -> { - logD("Exporting ${decision.playlist}") + T.d("Exporting ${decision.playlist}") SearchFragmentDirections.exportPlaylist(decision.playlist.uid) } is PlaylistDecision.Add -> { - logD("Adding ${decision.songs.size} to a playlist") + T.d("Adding ${decision.songs.size} to a playlist") SearchFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -362,11 +361,11 @@ class SearchFragment : ListFragment() { val directions = when (decision) { is PlaybackDecision.PlayFromArtist -> { - logD("Launching play from artist dialog for $decision") + T.d("Launching play from artist dialog for $decision") SearchFragmentDirections.playFromArtist(decision.song.uid) } is PlaybackDecision.PlayFromGenre -> { - logD("Launching play from artist dialog for $decision") + T.d("Launching play from artist dialog for $decision") SearchFragmentDirections.playFromGenre(decision.song.uid) } } @@ -379,7 +378,7 @@ class SearchFragment : ListFragment() { * @param view The [View] to focus the keyboard on. */ private fun showKeyboard(view: View) { - logD("Launching keyboard") + T.d("Launching keyboard") view.apply { requestFocus() postDelayed(200) { @@ -391,7 +390,7 @@ class SearchFragment : ListFragment() { /** Safely hide the keyboard from this view. */ private fun hideKeyboard() { - logD("Hiding keyboard") + T.d("Hiding keyboard") requireNotNull(imm) { "InputMethodManager was not available" } .hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 6eff6450e..08cdb23b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -40,7 +40,7 @@ import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * An [ViewModel] that keeps performs search operations and tracks their results. @@ -79,7 +79,7 @@ constructor( override fun onMusicChanges(changes: MusicRepository.Changes) { if (changes.deviceLibrary || changes.userLibrary) { - logD("Music changed, re-searching library") + T.d("Music changed, re-searching library") search(lastQuery) } } @@ -98,13 +98,13 @@ constructor( val deviceLibrary = musicRepository.deviceLibrary val userLibrary = musicRepository.userLibrary if (query.isNullOrEmpty() || deviceLibrary == null || userLibrary == null) { - logD("Cannot search for the current query, aborting") + T.d("Cannot search for the current query, aborting") _searchResults.value = listOf() return } // Searching is time-consuming, so do it in the background. - logD("Searching music library for $query") + T.d("Searching music library for $query") currentSearchJob = viewModelScope.launch { _searchResults.value = @@ -122,7 +122,7 @@ constructor( val items = if (filter == null) { // A nulled filter type means to not filter anything. - logD("No filter specified, using entire library") + T.d("No filter specified, using entire library") SearchEngine.Items( deviceLibrary.songs, deviceLibrary.albums, @@ -130,7 +130,7 @@ constructor( deviceLibrary.genres, userLibrary.playlists) } else { - logD("Filter specified, reducing library") + T.d("Filter specified, reducing library") SearchEngine.Items( songs = if (filter == MusicType.SONGS) deviceLibrary.songs else null, albums = if (filter == MusicType.ALBUMS) deviceLibrary.albums else null, @@ -143,13 +143,13 @@ constructor( return buildList { results.artists?.let { - logD("Adding ${it.size} artists to search results") + T.d("Adding ${it.size} artists to search results") val header = BasicHeader(R.string.lbl_artists) add(header) addAll(SORT.artists(it)) } results.albums?.let { - logD("Adding ${it.size} albums to search results") + T.d("Adding ${it.size} albums to search results") val header = BasicHeader(R.string.lbl_albums) if (isNotEmpty()) { add(PlainDivider(header)) @@ -159,7 +159,7 @@ constructor( addAll(SORT.albums(it)) } results.playlists?.let { - logD("Adding ${it.size} playlists to search results") + T.d("Adding ${it.size} playlists to search results") val header = BasicHeader(R.string.lbl_playlists) if (isNotEmpty()) { add(PlainDivider(header)) @@ -169,7 +169,7 @@ constructor( addAll(SORT.playlists(it)) } results.genres?.let { - logD("Adding ${it.size} genres to search results") + T.d("Adding ${it.size} genres to search results") val header = BasicHeader(R.string.lbl_genres) if (isNotEmpty()) { add(PlainDivider(header)) @@ -179,7 +179,7 @@ constructor( addAll(SORT.genres(it)) } results.songs?.let { - logD("Adding ${it.size} songs to search results") + T.d("Adding ${it.size} songs to search results") val header = BasicHeader(R.string.lbl_songs) if (isNotEmpty()) { add(PlainDivider(header)) @@ -225,7 +225,7 @@ constructor( R.id.option_filter_all -> null else -> error("Invalid option ID provided") } - logD("Updating filter type to $newFilter") + T.d("Updating filter type to $newFilter") searchSettings.filterTo = newFilter search(lastQuery) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt index 14065ea47..4d300d25b 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt @@ -37,8 +37,8 @@ import org.oxycblt.auxio.settings.ui.IntListPreference import org.oxycblt.auxio.settings.ui.IntListPreferenceDialog import org.oxycblt.auxio.settings.ui.PreferenceHeaderItemDecoration import org.oxycblt.auxio.settings.ui.WrappedDialogPreference -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.systemBarInsetsCompat +import timber.log.Timber as T /** * Shared [PreferenceFragmentCompat] used across all preference screens. @@ -82,7 +82,7 @@ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) : preferenceManager.onDisplayPreferenceDialogListener = this preferenceScreen.children.forEach(::setupPreference) - logD("Fragment created") + T.d("Fragment created") } override fun onCreateRecyclerView( diff --git a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt index e8f5c9018..52b720927 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt @@ -29,8 +29,8 @@ import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.settings.ui.WrappedDialogPreference -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe +import timber.log.Timber as T /** * The [PreferenceFragmentCompat] that displays the root settings list. @@ -62,21 +62,21 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { // do one. when (preference.key) { getString(R.string.set_key_ui) -> { - logD("Navigating to UI preferences") + T.d("Navigating to UI preferences") findNavController().navigateSafe(RootPreferenceFragmentDirections.uiPreferences()) } getString(R.string.set_key_personalize) -> { - logD("Navigating to personalization preferences") + T.d("Navigating to personalization preferences") findNavController() .navigateSafe(RootPreferenceFragmentDirections.personalizePreferences()) } getString(R.string.set_key_music) -> { - logD("Navigating to music preferences") + T.d("Navigating to music preferences") findNavController() .navigateSafe(RootPreferenceFragmentDirections.musicPreferences()) } getString(R.string.set_key_audio) -> { - logD("Navigating to audio preferences") + T.d("Navigating to audio preferences") findNavController().navigateSafe(RootPreferenceFragmentDirections.audioPeferences()) } getString(R.string.set_key_reindex) -> musicModel.refresh() diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 8db338dd5..95cc281e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -22,9 +22,8 @@ import android.content.Context import android.content.SharedPreferences import androidx.annotation.StringRes import androidx.preference.PreferenceManager -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * Abstract user configuration information. This interface has no functionality whatsoever. Concrete @@ -74,19 +73,19 @@ interface Settings { override fun registerListener(listener: L) { if (this.listener == null) { // Registering a listener when it was null prior, attach the callback. - logD("Registering shared preference listener") + T.d("Registering shared preference listener") sharedPreferences.registerOnSharedPreferenceChangeListener(this) } - logD("Registering listener $listener") + T.d("Registering listener $listener") this.listener = listener } override fun unregisterListener(listener: L) { if (this.listener !== listener) { - logW("Given listener was not the current listener.") + T.w("Given listener was not the current listener.") return } - logD("Unregistering listener $listener") + T.d("Unregistering listener $listener") this.listener = null // No longer have a listener, detach from the preferences instance. sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) @@ -97,7 +96,7 @@ interface Settings { key: String? ) { // FIXME: Settings initialization firing the listener. - logD("Dispatching settings change $key") + T.d("Dispatching settings change $key") onSettingChanged(unlikelyToBeNull(key), unlikelyToBeNull(listener)) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt index 765c86987..7352c9c26 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt @@ -22,8 +22,8 @@ import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe +import timber.log.Timber as T /** * Audio settings interface. @@ -34,7 +34,7 @@ class AudioPreferenceFragment : BasePreferenceFragment(R.xml.preferences_audio) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_pre_amp)) { - logD("Navigating to pre-amp dialog") + T.d("Navigating to pre-amp dialog") findNavController().navigateSafe(AudioPreferenceFragmentDirections.preAmpSettings()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt index 19b507f1d..5f282a68d 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt @@ -26,8 +26,8 @@ import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe +import timber.log.Timber as T /** * "Content" settings. @@ -40,7 +40,7 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_separators)) { - logD("Navigating to separator dialog") + T.d("Navigating to separator dialog") findNavController().navigateSafe(MusicPreferenceFragmentDirections.separatorsSettings()) } } @@ -48,10 +48,10 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) override fun onSetupPreference(preference: Preference) { if (preference.key == getString(R.string.set_key_cover_mode) || preference.key == getString(R.string.set_key_square_covers)) { - logD("Configuring cover mode setting") + T.d("Configuring cover mode setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> - logD("Cover mode changed, resetting image memory cache") + T.d("Cover mode changed, resetting image memory cache") imageLoader.memoryCache?.clear() true } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt index 361ec1162..cd576ad81 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt @@ -22,8 +22,8 @@ import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe +import timber.log.Timber as T /** * Personalization settings interface. @@ -33,7 +33,7 @@ import org.oxycblt.auxio.util.navigateSafe class PersonalizePreferenceFragment : BasePreferenceFragment(R.xml.preferences_personalize) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_home_tabs)) { - logD("Navigating to home tab dialog") + T.d("Navigating to home tab dialog") findNavController().navigateSafe(PersonalizePreferenceFragmentDirections.tabSettings()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt index b756559dc..a52df3bdf 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt @@ -28,8 +28,8 @@ import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isNight -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.navigateSafe +import timber.log.Timber as T /** * Display preferences. @@ -42,7 +42,7 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_accent)) { - logD("Navigating to accent dialog") + T.d("Navigating to accent dialog") findNavController().navigateSafe(UIPreferenceFragmentDirections.accentSettings()) } } @@ -50,25 +50,25 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { override fun onSetupPreference(preference: Preference) { when (preference.key) { getString(R.string.set_key_theme) -> { - logD("Configuring theme setting") + T.d("Configuring theme setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value -> - logD("Theme changed, recreating") + T.d("Theme changed, recreating") AppCompatDelegate.setDefaultNightMode(value as Int) true } } getString(R.string.set_key_accent) -> { - logD("Configuring accent setting") + T.d("Configuring accent setting") preference.summary = getString(uiSettings.accent.name) } getString(R.string.set_key_black_theme) -> { - logD("Configuring black theme setting") + T.d("Configuring black theme setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> val activity = requireActivity() if (activity.isNight) { - logD("Black theme changed in night mode, recreating") + T.d("Black theme changed in night mode, recreating") activity.recreate() } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt index 4df169899..015305e0f 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -30,8 +30,8 @@ import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimenPixels -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.systemGestureInsetsCompat +import timber.log.Timber as T /** * A BottomSheetBehavior that resolves several issues with the default implementation, including: @@ -96,7 +96,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: val layout = super.onLayoutChild(parent, child, layoutDirection) // Don't repeat redundant initialization. if (!initalized) { - logD("Not initialized, setting up child") + T.d("Not initialized, setting up child") child.apply { // Set up compat elevation attributes. These are only shown below API 28. translationZ = context.getDimen(MR.dimen.m3_sys_elevation_level1) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt index 9b034dfe7..11440892b 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt @@ -26,9 +26,9 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import kotlin.math.abs import org.oxycblt.auxio.util.coordinatorLayoutBehavior -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat +import timber.log.Timber as T /** * A behavior that automatically re-layouts and re-insets content to align with the parent layout's @@ -61,12 +61,12 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri val behavior = dependency.coordinatorLayoutBehavior as BackportBottomSheetBehavior val consumed = behavior.calculateConsumedByBar() if (consumed == Int.MIN_VALUE) { - logD("Not laid out yet, cannot update dependent view") + T.d("Not laid out yet, cannot update dependent view") return false } if (consumed != lastConsumed) { - logD("Consumed amount changed, re-applying insets") + T.d("Consumed amount changed, re-applying insets") lastConsumed = consumed lastInsets?.let(child::dispatchApplyWindowInsets) measureContent(parent, child, consumed) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt index afa60e6fd..bc43c1cc8 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt @@ -30,7 +30,7 @@ import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout import org.oxycblt.auxio.util.coordinatorLayoutBehavior -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * An [AppBarLayout] that resolves two issues with the default implementation: @@ -76,7 +76,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr fun expandWithScrollingRecycler() { setExpanded(true) (findScrollingChild() as? RecyclerView)?.let { - logD("Found RecyclerView, expanding with it") + T.d("Found RecyclerView, expanding with it") addOnOffsetChangedListener(ExpansionHackListener(it)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt index c343bc06b..3eec71f1e 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt @@ -31,7 +31,7 @@ import androidx.core.view.isInvisible import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.google.android.material.R as MR import com.google.android.material.motion.MotionUtils -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T class MultiToolbar @JvmOverloads @@ -67,7 +67,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr fun setVisible(@IdRes viewId: Int): Boolean { val index = children.indexOfFirst { it.id == viewId } if (index == currentlyVisible) return false - logD("Switching toolbar visibility from $currentlyVisible -> $index") + T.d("Switching toolbar visibility from $currentlyVisible -> $index") return animateToolbarsVisibility(currentlyVisible, index).also { currentlyVisible = index } } @@ -88,7 +88,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (!isLaidOut) { // Not laid out, just change it immediately while are not shown to the user. // This is an initialization, so we return false despite changing. - logD("Not laid out, immediately updating visibility") + T.d("Not laid out, immediately updating visibility") fromView.apply { alpha = 0f isInvisible = true @@ -100,7 +100,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr return false } - logD("Changing toolbar visibility $from -> 0f, $to -> 1f") + T.d("Changing toolbar visibility $from -> 0f, $to -> 1f") animator?.cancel() val outAnimator = ValueAnimator.ofFloat(fromView.alpha, 0f).apply { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt index dc699498a..219f070e4 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt @@ -27,7 +27,7 @@ import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.accent.Accent -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * User configuration for the general app UI. @@ -76,7 +76,7 @@ class UISettingsImpl @Inject constructor(@ApplicationContext context: Context) : override fun migrate() { if (sharedPreferences.contains(OLD_KEY_ACCENT3)) { - logD("Migrating $OLD_KEY_ACCENT3") + T.d("Migrating $OLD_KEY_ACCENT3") var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -96,7 +96,7 @@ class UISettingsImpl @Inject constructor(@ApplicationContext context: Context) : override fun onSettingChanged(key: String, listener: UISettings.Listener) { if (key == getString(R.string.set_key_round_mode)) { - logD("Dispatching round mode setting change") + T.d("Dispatching round mode setting change") listener.onRoundModeChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt index 8abffb38f..9532aa6e5 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt @@ -31,8 +31,8 @@ import com.google.android.material.bottomsheet.BackportBottomSheetDialog import com.google.android.material.bottomsheet.BackportBottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.oxycblt.auxio.util.getDimenPixels -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A lifecycle-aware [DialogFragment] that automatically manages the [ViewBinding] lifecycle as a @@ -98,7 +98,7 @@ abstract class ViewBindingBottomSheetDialogFragment : final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) onBindingCreated(requireBinding(), savedInstanceState) - logD("Fragment created") + T.d("Fragment created") } final override fun onDestroyView() { @@ -106,7 +106,7 @@ abstract class ViewBindingBottomSheetDialogFragment : onDestroyBinding(unlikelyToBeNull(_binding)) // Clear binding _binding = null - logD("Fragment destroyed") + T.d("Fragment destroyed") } private inner class TweakedBottomSheetDialog diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt index c3a493bb5..aff6b3337 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt @@ -24,8 +24,8 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. @@ -87,7 +87,7 @@ abstract class ViewBindingFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // Configure binding onBindingCreated(requireBinding(), savedInstanceState) - logD("Fragment created") + T.d("Fragment created") } final override fun onDestroyView() { @@ -95,6 +95,6 @@ abstract class ViewBindingFragment : Fragment() { onDestroyBinding(unlikelyToBeNull(_binding)) // Clear binding _binding = null - logD("Fragment destroyed") + T.d("Fragment destroyed") } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingMaterialDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingMaterialDialogFragment.kt index f12fe1b2e..177ba3ff1 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingMaterialDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingMaterialDialogFragment.kt @@ -28,8 +28,8 @@ import androidx.fragment.app.DialogFragment import androidx.viewbinding.ViewBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.oxycblt.auxio.util.fixDoubleRipple -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A lifecycle-aware [DialogFragment] that automatically manages the [ViewBinding] lifecycle as a @@ -109,7 +109,7 @@ abstract class ViewBindingMaterialDialogFragment : DialogFragm onBindingCreated(requireBinding(), savedInstanceState) // Apply the newly-configured view to the dialog. (requireDialog() as AlertDialog).setView(view) - logD("Fragment created") + T.d("Fragment created") } override fun onStart() { @@ -127,6 +127,6 @@ abstract class ViewBindingMaterialDialogFragment : DialogFragm onDestroyBinding(unlikelyToBeNull(_binding)) // Clear binding _binding = null - logD("Fragment destroyed") + T.d("Fragment destroyed") } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt index 60ca375c0..d48b8e531 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt @@ -20,7 +20,7 @@ package org.oxycblt.auxio.ui.accent import android.os.Build import org.oxycblt.auxio.R -import org.oxycblt.auxio.util.logW +import timber.log.Timber as T private val accentNames = intArrayOf( @@ -142,7 +142,7 @@ class Accent private constructor(val index: Int) { */ fun from(index: Int): Accent { if (index !in 0 until MAX) { - logW("Accent is out of bounds [idx: $index]") + T.w("Accent is out of bounds [idx: $index]") return Accent(DEFAULT) } return Accent(index) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index c5ce7a5cc..f68777139 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -30,8 +30,8 @@ import org.oxycblt.auxio.databinding.DialogAccentBinding import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull +import timber.log.Timber as T /** * A [ViewBindingMaterialDialogFragment] that allows the user to configure the current [Accent]. @@ -55,7 +55,7 @@ class AccentCustomizeDialog : return@setPositiveButton } - logD("Applying new accent") + T.d("Applying new accent") uiSettings.accent = unlikelyToBeNull(accentAdapter.selectedAccent) requireActivity().recreate() dismiss() diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index d2321ab5c..c97ccf1c9 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -40,6 +40,7 @@ import kotlin.reflect.KClass import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.MainActivity import org.oxycblt.auxio.R +import timber.log.Timber as T /** * Get a [LayoutInflater] instance from this [Context]. diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 690714664..a23d33b8a 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -47,6 +47,7 @@ import java.lang.IllegalArgumentException import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song +import timber.log.Timber as T /** * Get if this [View] contains the given [PointF], with optional leeway. @@ -169,8 +170,8 @@ fun NavController.navigateSafe(directions: NavDirections) = navigate(directions) } catch (e: IllegalArgumentException) { // Nothing to do. - logE("Could not navigate from this destination.") - logE(e.stackTraceToString()) + T.e("Could not navigate from this destination.") + T.e(e.stackTraceToString()) } /** @@ -312,7 +313,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs) */ fun Context.share(songs: Collection) { if (songs.isEmpty()) return - logD("Showing sharesheet for ${songs.size} songs") + T.d("Showing sharesheet for ${songs.size} songs") val builder = ShareCompat.IntentBuilder(this) val mimeTypes = mutableSetOf() for (song in songs) { @@ -330,7 +331,7 @@ fun Context.share(songs: Collection) { */ fun Context.openInBrowser(uri: String) { fun openAppChooser(intent: Intent) { - logD("Opening app chooser for ${intent.action}") + T.d("Opening app chooser for ${intent.action}") val chooserIntent = Intent(Intent.ACTION_CHOOSER) .putExtra(Intent.EXTRA_INTENT, intent) @@ -338,7 +339,7 @@ fun Context.openInBrowser(uri: String) { startActivity(chooserIntent) } - logD("Opening $uri") + T.d("Opening $uri") val browserIntent = Intent(Intent.ACTION_VIEW, uri.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -346,7 +347,7 @@ fun Context.openInBrowser(uri: String) { // Android 11 seems to now handle the app chooser situations on its own now // [along with adding a new permission that breaks the old manual code], so // we just do a typical activity launch. - logD("Using API 30+ chooser") + T.d("Using API 30+ chooser") try { startActivity(browserIntent) } catch (e: ActivityNotFoundException) { @@ -358,7 +359,7 @@ fun Context.openInBrowser(uri: String) { // not work in all cases, especially when no default app was set. If that is the // case, we will try to manually handle these cases before we try to launch the // browser. - logD("Resolving browser activity for chooser") + T.d("Resolving browser activity for chooser") val pkgName = packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)?.run { activityInfo.packageName @@ -367,9 +368,9 @@ fun Context.openInBrowser(uri: String) { if (pkgName != null) { if (pkgName == "android") { // No default browser [Must open app chooser, may not be supported] - logD("No default browser found") + T.d("No default browser found") openAppChooser(browserIntent) - } else logD("Opening browser intent") + } else T.d("Opening browser intent") try { browserIntent.setPackage(pkgName) startActivity(browserIntent) diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index 8b8d8c6a1..e1e38784a 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -23,6 +23,7 @@ import java.util.UUID import kotlin.reflect.KClass import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.info.Date +import timber.log.Timber as T /** * Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull], diff --git a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt deleted file mode 100644 index bc1197af4..000000000 --- a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2021 Auxio Project - * LogUtil.kt is part of Auxio. - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package org.oxycblt.auxio.util - -import org.oxycblt.auxio.BuildConfig -import timber.log.Timber - -/** - * Log an object to the debug channel. Automatically handles tags. - * - * @param obj The object to log. - */ -inline fun logD(obj: Any?) = logD("$obj") - -/** - * Log a string message to the debug channel. Automatically handles tags. - * - * @param msg The message to log. - */ -inline fun logD(msg: String) { - if (BuildConfig.DEBUG && !copyleftNotice()) { - Timber.d(msg) - } -} - -/** - * Log a string message to the warning channel. Automatically handles tags. - * - * @param msg The message to log. - */ -inline fun logW(msg: String) = Timber.w(msg) - -/** - * Log a string message to the error channel. Automatically handles tags. - * - * @param msg The message to log. - */ -inline fun logE(msg: String) = Timber.e(msg) - -/** - * Please don't plagiarize Auxio! You are free to remove this as long as you continue to keep your - * source open. - */ -@Suppress("KotlinConstantConditions") -fun copyleftNotice(): Boolean { - if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && - BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") { - Timber.d( - "Auxio Project", - "Friendly reminder: Auxio is licensed under the " + - "GPLv3 and all derivative apps must be made open source!") - return true - } - return false -} diff --git a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt index 71927c70c..c7ea02c05 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import org.oxycblt.auxio.BuildConfig +import timber.log.Timber as T /** * A wrapper around [StateFlow] exposing a one-time consumable event. @@ -167,11 +168,11 @@ suspend fun SendChannel.sendWithTimeout(element: E, timeout: Long = DEFAU try { withTimeout(timeout) { send(element) } } catch (e: TimeoutCancellationException) { - logE("Failed to send element to channel $e in ${timeout}ms.") + T.e("Failed to send element to channel $e in ${timeout}ms.") if (BuildConfig.DEBUG) { throw TimeoutException("Timed out sending element to channel: $e") } else { - logE(e.stackTraceToString()) + T.e(e.stackTraceToString()) send(element) } } @@ -210,11 +211,11 @@ suspend fun ReceiveChannel.forEachWithTimeout( subsequent = true } } catch (e: TimeoutCancellationException) { - logE("Failed to send element to channel $e in ${timeout}ms.") + T.e("Failed to send element to channel $e in ${timeout}ms.") if (BuildConfig.DEBUG) { throw TimeoutException("Timed out sending element to channel: $e") } else { - logE(e.stackTraceToString()) + T.e(e.stackTraceToString()) handler() } } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 1ffe0d705..855dcbd5a 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getDimenPixels -import org.oxycblt.auxio.util.logD +import timber.log.Timber as T /** * A component that manages the "Now Playing" state. This is kept separate from the [WidgetProvider] @@ -77,7 +77,7 @@ private constructor( fun update() { val song = playbackManager.currentSong if (song == null) { - logD("No song, resetting widget") + T.d("No song, resetting widget") widgetProvider.update(context, uiSettings, null) return } @@ -87,7 +87,7 @@ private constructor( val repeatMode = playbackManager.repeatMode val isShuffled = playbackManager.isShuffled - logD("Updating widget with new playback state") + T.d("Updating widget with new playback state") bitmapProvider.load( song, object : BitmapProvider.Target { @@ -95,15 +95,15 @@ private constructor( val cornerRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12, always round the cover with the widget's inner radius - logD("Using android 12 corner radius") + T.d("Using android 12 corner radius") context.getDimenPixels(android.R.dimen.system_app_widget_inner_radius) } else if (uiSettings.roundMode) { // < Android 12, but the user still enabled round mode. - logD("Using default corner radius") + T.d("Using default corner radius") context.getDimenPixels(R.dimen.m3_shape_corners_large) } else { // User did not enable round mode. - logD("Using no corner radius") + T.d("Using no corner radius") 0 } @@ -124,7 +124,7 @@ private constructor( override fun onCompleted(bitmap: Bitmap?) { val state = PlaybackState(song, bitmap, isPlaying, repeatMode, isShuffled) - logD("Bitmap loaded, uploading state $state") + T.d("Bitmap loaded, uploading state $state") widgetProvider.update(context, uiSettings, state) } }) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 80b8758e8..e163d3aeb 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -35,9 +35,8 @@ import org.oxycblt.auxio.playback.service.PlaybackActions import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.UISettingsImpl -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.newBroadcastPendingIntent +import timber.log.Timber as T /** * The [AppWidgetProvider] for the "Now Playing" widget. This widget shows the current playback @@ -82,7 +81,7 @@ class WidgetProvider : AppWidgetProvider() { fun update(context: Context, uiSettings: UISettings, state: WidgetComponent.PlaybackState?) { if (state == null) { // No state, use the default widget. - logD("No state provided, returning to default") + T.d("No state provided, returning to default") reset(context, uiSettings) return } @@ -119,7 +118,7 @@ class WidgetProvider : AppWidgetProvider() { while (victims.size > 0) { try { awm.updateAppWidgetCompat(context, component, views) - logD("Successfully updated RemoteViews layout") + T.d("Successfully updated RemoteViews layout") return } catch (e: IllegalArgumentException) { val msg = e.message ?: return @@ -139,12 +138,12 @@ class WidgetProvider : AppWidgetProvider() { victims.remove(victim) } catch (e: Exception) { // Layout update failed, gracefully degrade to the default widget. - logW("Unable to update widget: $e") + T.w("Unable to update widget: $e") reset(context, uiSettings) } } // We flat-out cannot fit the bitmap into the widget. Weird. - logW("Unable to update widget: Bitmap too large") + T.w("Unable to update widget: Bitmap too large") reset(context, uiSettings) } @@ -154,7 +153,7 @@ class WidgetProvider : AppWidgetProvider() { * @param context [Context] required to update the widget layout. */ fun reset(context: Context, uiSettings: UISettings) { - logD("Using default layout") + T.d("Using default layout") AppWidgetManager.getInstance(context) .updateAppWidget( ComponentName(context, this::class.java), newDefaultLayout(context, uiSettings)) @@ -168,7 +167,7 @@ class WidgetProvider : AppWidgetProvider() { * @param context [Context] required to send update request broadcast. */ private fun requestUpdate(context: Context) { - logD("Sending update intent to PlaybackService") + T.d("Sending update intent to PlaybackService") val intent = Intent(ACTION_WIDGET_UPDATE).addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) context.sendBroadcast(intent) } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt index 953af14c8..5135b3526 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -28,8 +28,8 @@ import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.LayoutRes import org.oxycblt.auxio.util.isLandscape -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newMainPendingIntent +import timber.log.Timber as T /** * Create a [RemoteViews] instance with the specified layout and an automatic click handler to open @@ -103,7 +103,7 @@ fun AppWidgetManager.updateAppWidgetCompat( width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) } - logD("Assuming dimens are ${width}x$height") + T.d("Assuming dimens are ${width}x$height") // Find the layout with the greatest area that fits entirely within // the app widget. This is what we will use. Fall back to the smallest layout @@ -113,7 +113,7 @@ fun AppWidgetManager.updateAppWidgetCompat( .filter { it.width <= width && it.height <= height } .maxByOrNull { it.height * it.width } ?: views.minBy { it.key.width * it.key.height }.key - logD("Using layout $layout ${views.contains(layout)}") + T.d("Using layout $layout ${views.contains(layout)}") updateAppWidget(id, views[layout]) } From 745bff268f0916d9ef116364f3676a01ab48c9b0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 17 Oct 2024 20:37:22 -0600 Subject: [PATCH 089/550] build: move buildconfig to recipe --- app/build.gradle | 1 + gradle.properties | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index bf666ec42..58f9da579 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,6 +70,7 @@ android { buildFeatures { viewBinding true + buildConfig true } } diff --git a/gradle.properties b/gradle.properties index 87899875b..39c106d13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,6 +20,5 @@ android.enableJetifier=false # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.enableR8.fullMode=true -android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=true android.nonFinalResIds=true \ No newline at end of file From fbd94f1a213bfb0b8b3956e448fc68c77d0ed4ce Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 17 Oct 2024 21:06:35 -0600 Subject: [PATCH 090/550] all: fix invalid logs These are leftover debug logs --- .../java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt | 1 - .../java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt | 1 - .../org/oxycblt/auxio/playback/state/PlaybackStateManager.kt | 1 - app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt | 2 -- 4 files changed, 5 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt index 4746f1a50..04b23812c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt @@ -100,7 +100,6 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx private fun populateWithId3v2(rawSong: RawSong, textFrames: Map>) { // Song - T.d(textFrames) (textFrames["TXXX:musicbrainz release track id"] ?: textFrames["TXXX:musicbrainz_releasetrackid"]) ?.let { rawSong.musicBrainzId = it.first() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 4cb692b2a..b81ec6b79 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -136,7 +136,6 @@ class PlaybackPanelFragment : override fun onStart() { super.onStart() - T.d(requireBinding().playbackCover.width) playbackModel.song.value?.let { requireBinding().playbackCover.bind(it) } requireBinding().root.viewTreeObserver.addOnGlobalLayoutListener(this) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 7aeb550e0..4660d3210 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -766,7 +766,6 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { } else { heap.getOrNull(index) } - T.d(currentSong) return currentSong?.uid == savedState.songUid } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index f3ca192fc..fc27bc8dd 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -39,7 +39,6 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import timber.log.Timber as T /** * An adapter that displays search results. @@ -75,7 +74,6 @@ class SearchAdapter(private val listener: SelectableListListener) : } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - T.d(position) when (val item = getItem(position)) { is Song -> (holder as SongViewHolder).bind(item, listener) is Album -> (holder as AlbumViewHolder).bind(item, listener) From a46fa85d670b6bb6f63c1343a9f9cfeef7be196c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 17 Oct 2024 21:07:05 -0600 Subject: [PATCH 091/550] build: update to kotlin 2.0.0 --- .gitignore | 1 + build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c03b3271b..d461864a6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ captures/ .externalNativeBuild *.iml .cxx +.kotlin diff --git a/build.gradle b/build.gradle index 213148be0..13d37e8f4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.9.23' + kotlin_version = '2.0.21' navigation_version = "2.8.3" hilt_version = '2.51.1' } @@ -15,7 +15,7 @@ plugins { id "com.android.application" version '8.5.2' apply false id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false - id "com.google.devtools.ksp" version '1.9.23-1.0.20' apply false + id "com.google.devtools.ksp" version '2.0.21-1.0.25' apply false id "com.diffplug.spotless" version "6.20.0" apply false } From f7488f7b0d4ed37c06cbabd9cff69d89f8a836ee Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 17 Oct 2024 21:08:41 -0600 Subject: [PATCH 092/550] playback: fix deprecated constructors --- .../auxio/playback/service/ExoPlaybackStateHolder.kt | 4 ++-- .../org/oxycblt/auxio/playback/service/SystemModule.kt | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 05ec3d9f2..4c58aa533 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -30,6 +30,7 @@ import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.RenderersFactory import androidx.media3.exoplayer.audio.AudioCapabilities +import androidx.media3.exoplayer.audio.DefaultAudioSink import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer import androidx.media3.exoplayer.mediacodec.MediaCodecSelector import androidx.media3.exoplayer.source.MediaSource @@ -607,8 +608,7 @@ class ExoPlaybackStateHolder( MediaCodecSelector.DEFAULT, handler, audioListener, - AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES, - replayGainProcessor)) + DefaultAudioSink.Builder(context).setAudioProcessors(arrayOf(replayGainProcessor)).build())) } val exoPlayer = diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt index 3aade8f3e..cda467cfb 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt @@ -30,6 +30,7 @@ import androidx.media3.extractor.mp3.Mp3Extractor import androidx.media3.extractor.mp4.FragmentedMp4Extractor import androidx.media3.extractor.mp4.Mp4Extractor import androidx.media3.extractor.ogg.OggExtractor +import androidx.media3.extractor.text.DefaultSubtitleParserFactory import androidx.media3.extractor.ts.AdtsExtractor import androidx.media3.extractor.wav.WavExtractor import dagger.Module @@ -60,10 +61,10 @@ class SystemModule { arrayOf( FlacExtractor(), WavExtractor(), - FragmentedMp4Extractor(), - Mp4Extractor(), + FragmentedMp4Extractor(DefaultSubtitleParserFactory.UNSUPPORTED), + Mp4Extractor(DefaultSubtitleParserFactory.UNSUPPORTED), OggExtractor(), - MatroskaExtractor(), + MatroskaExtractor(DefaultSubtitleParserFactory.UNSUPPORTED), // Enable constant bitrate seeking so that certain MP3s/AACs are seekable AdtsExtractor(AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), Mp3Extractor(Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING)) From c1514d60296572e99417729b2f72cb4e3b93ef55 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 08:39:31 -0600 Subject: [PATCH 093/550] log: re-add copyleft notice --- app/src/main/java/org/oxycblt/auxio/Auxio.kt | 6 +++++- .../oxycblt/auxio/util/CopyleftNoticeTree.kt | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt diff --git a/app/src/main/java/org/oxycblt/auxio/Auxio.kt b/app/src/main/java/org/oxycblt/auxio/Auxio.kt index ebcffb5e2..d15f49647 100644 --- a/app/src/main/java/org/oxycblt/auxio/Auxio.kt +++ b/app/src/main/java/org/oxycblt/auxio/Auxio.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.home.HomeSettings import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.ui.UISettings +import org.oxycblt.auxio.util.CopyleftNoticeTree import timber.log.Timber /** @@ -45,7 +46,10 @@ class Auxio : Application() { override fun onCreate() { super.onCreate() - if (BuildConfig.DEBUG) { + @Suppress("KotlinConstantConditions") + if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") { + Timber.plant(CopyleftNoticeTree()) + } else if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } diff --git a/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt b/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt new file mode 100644 index 000000000..a191a4c14 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt @@ -0,0 +1,21 @@ +package org.oxycblt.auxio.util + +import timber.log.Timber + +class CopyleftNoticeTree : Timber.DebugTree() { + // Feel free to remove this if you are forking the project in good faith. + // + // However, if you are stealing the source code to repackage it into a new closed-source app, + // I will warn you that the One True, Living, Almighty God HATES thieves and WILL punish you + // ETERNALLY for what you are doing. However, God still loves you despite of your + // transgressions, and He provided a way out through Christ! Turn to Jesus and repent! Life + // with Jesus is so much better than a life revolving around taking other peoples work to + // arbitrage a few pennies from ad sales! + // + // Read more: John 3:16, Romans 6:23, Romans 9:10 + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + super.log(priority, tag, + "Hey! Auxio is an open-source project licensed under the GPLv3 license!" + + "You can fork this project and even add ads, but it still needs to be kept open-source with the same license!", t) + } +} \ No newline at end of file From 22ddda4e60c0d7e3a674fd242a49092ccfbea42f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 08:43:05 -0600 Subject: [PATCH 094/550] all: reformat --- app/src/main/java/org/oxycblt/auxio/Auxio.kt | 3 ++- .../oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/Auxio.kt b/app/src/main/java/org/oxycblt/auxio/Auxio.kt index d15f49647..2304a626b 100644 --- a/app/src/main/java/org/oxycblt/auxio/Auxio.kt +++ b/app/src/main/java/org/oxycblt/auxio/Auxio.kt @@ -47,7 +47,8 @@ class Auxio : Application() { override fun onCreate() { super.onCreate() @Suppress("KotlinConstantConditions") - if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") { + if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && + BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") { Timber.plant(CopyleftNoticeTree()) } else if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 4c58aa533..487bde8d4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -29,7 +29,6 @@ import androidx.media3.common.Player import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.RenderersFactory -import androidx.media3.exoplayer.audio.AudioCapabilities import androidx.media3.exoplayer.audio.DefaultAudioSink import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer import androidx.media3.exoplayer.mediacodec.MediaCodecSelector @@ -608,7 +607,9 @@ class ExoPlaybackStateHolder( MediaCodecSelector.DEFAULT, handler, audioListener, - DefaultAudioSink.Builder(context).setAudioProcessors(arrayOf(replayGainProcessor)).build())) + DefaultAudioSink.Builder(context) + .setAudioProcessors(arrayOf(replayGainProcessor)) + .build())) } val exoPlayer = From 7dfaea3a4b51a0ac5424d2d078411b57e96d29c8 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 15:41:06 -0600 Subject: [PATCH 095/550] all: cleanup --- app/src/main/AndroidManifest.xml | 3 +- .../java/org/oxycblt/auxio/AuxioService.kt | 2 - .../auxio/detail/ArtistDetailFragment.kt | 1 - .../auxio/detail/list/DetailListAdapter.kt | 1 - .../main/java/org/oxycblt/auxio/list/Data.kt | 1 - .../org/oxycblt/auxio/list/ListFragment.kt | 1 - .../java/org/oxycblt/auxio/list/Listeners.kt | 1 - .../auxio/list/adapter/FlexibleListAdapter.kt | 1 - .../list/adapter/PlayingIndicatorAdapter.kt | 1 - .../list/adapter/SelectionIndicatorAdapter.kt | 1 - .../auxio/list/adapter/SimpleDiffCallback.kt | 1 - .../auxio/list/recycler/ViewHolders.kt | 2 + .../java/org/oxycblt/auxio/music/Music.kt | 7 +-- .../org/oxycblt/auxio/music/external/M3U.kt | 4 +- .../java/org/oxycblt/auxio/music/info/Date.kt | 4 +- .../music/service/MusicServiceFragment.kt | 5 +- .../music/service/SystemContentObserver.kt | 2 +- .../playback/service/MediaButtonReceiver.kt | 2 +- .../playback/service/MediaSessionHolder.kt | 6 +- .../playback/service/MediaSessionInterface.kt | 33 +++++------ .../service/PlaybackServiceFragment.kt | 4 +- .../service/SystemPlaybackReceiver.kt | 1 + .../auxio/playback/state/PlaybackCommand.kt | 3 +- .../org/oxycblt/auxio/search/SearchEngine.kt | 2 +- .../java/org/oxycblt/auxio/tasker/Start.kt | 2 +- .../org/oxycblt/auxio/util/ContextUtil.kt | 1 - .../oxycblt/auxio/util/CopyleftNoticeTree.kt | 2 +- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 2 + .../java/org/oxycblt/auxio/util/LangUtil.kt | 1 - .../widgets/WidgetBitmapTransformation.kt | 2 +- app/src/main/res/color/overlay_stroke.xml | 4 -- .../res/layout-h480dp/item_playback_song.xml | 56 ------------------- .../res/layout-sw600dp/item_playback_song.xml | 56 ------------------- .../main/res/layout/item_playback_song.xml | 55 ------------------ 34 files changed, 43 insertions(+), 227 deletions(-) delete mode 100644 app/src/main/res/color/overlay_stroke.xml delete mode 100644 app/src/main/res/layout-h480dp/item_playback_song.xml delete mode 100644 app/src/main/res/layout-sw600dp/item_playback_song.xml delete mode 100644 app/src/main/res/layout/item_playback_song.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 91ca80093..7e3ff8019 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,7 +93,8 @@ android:foregroundServiceType="mediaPlayback" android:icon="@mipmap/ic_launcher" android:exported="true" - android:roundIcon="@mipmap/ic_launcher"> + android:roundIcon="@mipmap/ic_launcher" + tools:ignore="ExportedService"> diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index 7edcad1e9..cf8033f7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -36,7 +36,6 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.music.service.MusicServiceFragment import org.oxycblt.auxio.playback.service.PlaybackServiceFragment -import timber.log.Timber as T @AndroidEntryPoint class AuxioService : @@ -150,7 +149,6 @@ class AuxioService : } override fun invalidateMusic(mediaId: String) { - T.d(mediaId) notifyChildrenChanged(mediaId) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index cf86c72d8..01201e33d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -40,7 +40,6 @@ import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt index c28dce1f2..87aebf5df 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt @@ -38,7 +38,6 @@ import org.oxycblt.auxio.list.recycler.DividerViewHolder import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater -import timber.log.Timber as T /** * A [RecyclerView.Adapter] that implements shared behavior between lists of child items in the diff --git a/app/src/main/java/org/oxycblt/auxio/list/Data.kt b/app/src/main/java/org/oxycblt/auxio/list/Data.kt index 9c874b5b1..5507bb9fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.list import androidx.annotation.StringRes -import timber.log.Timber as T // TODO: Consider breaking this up into sealed classes for individual adapters /** A marker for something that is a RecyclerView item. Has no functionality on it's own. */ diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 49636b594..546b03a49 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -21,7 +21,6 @@ package org.oxycblt.auxio.list import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import org.oxycblt.auxio.music.Music -import timber.log.Timber as T /** * A Fragment containing a selectable list. diff --git a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt index 0b0b701d7..c7704e503 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt @@ -21,7 +21,6 @@ package org.oxycblt.auxio.list import android.view.MotionEvent import android.view.View import androidx.recyclerview.widget.RecyclerView -import timber.log.Timber as T /** * A basic listener for list interactions. diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt index 99bd112f3..34b219a64 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -25,7 +25,6 @@ import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import java.util.concurrent.Executor -import timber.log.Timber as T /** * A variant of ListDiffer with more flexible updates. diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt index 15e36be00..d494da0a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt @@ -21,7 +21,6 @@ package org.oxycblt.auxio.list.adapter import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import timber.log.Timber as T /** * A [RecyclerView.Adapter] that supports indicating the playback status of a particular item. diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt index 0bd0a80ed..65b9bcef1 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt @@ -22,7 +22,6 @@ import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.Music -import timber.log.Timber as T /** * A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt index 53105d975..d1fdc8a7a 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.list.adapter import androidx.recyclerview.widget.DiffUtil import org.oxycblt.auxio.list.Item -import timber.log.Timber as T /** * A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. Use this diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index d7f5e1d23..c99d87953 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.list.recycler +import android.annotation.SuppressLint import android.view.View import androidx.recyclerview.widget.RecyclerView import com.google.android.material.divider.MaterialDivider @@ -382,6 +383,7 @@ class DividerViewHolder private constructor(divider: MaterialDivider) : /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = object : SimpleDiffCallback() { + @SuppressLint("DiffUtilEquals") override fun areContentsTheSame(oldItem: PlainDivider, newItem: PlainDivider) = oldItem.anchor == newItem.anchor } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 384151936..e7916d02b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -22,9 +22,6 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import androidx.room.TypeConverter -import java.security.MessageDigest -import java.util.UUID -import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.image.extractor.Cover @@ -39,7 +36,9 @@ import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.toUuidOrNull -import timber.log.Timber as T +import java.security.MessageDigest +import java.util.UUID +import kotlin.math.max /** * Abstract music data. This contains universal information about all concrete music diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index bb8318241..3cfd30487 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -261,7 +261,7 @@ constructor( } commonIndex == components.size -> { // The working directory is deeper in the path, backtrack. - for (i in 0..workingDirectory.components.size - commonIndex - 1) { + for (i in 0.. { // The paths are siblings. Backtrack and append as needed. - for (i in 0..workingDirectory.components.size - commonIndex - 1) { + for (i in 0..) : Comparable { val year = tokens[0] - val month = tokens.getOrNull(1) - val day = tokens.getOrNull(2) + private val month = tokens.getOrNull(1) + private val day = tokens.getOrNull(2) private val hour = tokens.getOrNull(3) private val minute = tokens.getOrNull(4) private val second = tokens.getOrNull(5) diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt index 7494878e3..79dfe384a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt @@ -23,7 +23,6 @@ import android.os.Bundle import android.support.v4.media.MediaBrowserCompat.MediaItem import androidx.media.MediaBrowserServiceCompat.BrowserRoot import androidx.media.MediaBrowserServiceCompat.Result -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -31,12 +30,12 @@ import kotlinx.coroutines.launch import org.oxycblt.auxio.ForegroundListener import org.oxycblt.auxio.ForegroundServiceNotification import org.oxycblt.auxio.music.MusicRepository -import timber.log.Timber as T +import javax.inject.Inject class MusicServiceFragment @Inject constructor( - private val context: Context, + context: Context, foregroundListener: ForegroundListener, private val invalidator: Invalidator, indexerFactory: Indexer.Factory, diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt b/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt index b518e8f24..4bb23abd9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt @@ -33,7 +33,7 @@ import timber.log.Timber as T /** * A [ContentObserver] that observes the [MediaStore] music database for changes, a behavior known * to the user as automatic rescanning. The active (and not passive) nature of observing the - * database is what requires [IndexerServiceFragment] to stay foreground when this is enabled. + * database is what requires [MusicServiceFragment] to stay foreground when this is enabled. */ class SystemContentObserver @Inject diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt index 2c32f25e2..c70b14a7e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt @@ -31,7 +31,7 @@ import timber.log.Timber as T /** * A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to - * [MediaSessionServiceFragment]. + * [PlaybackServiceFragment]. * * @author Alexander Capehart (OxygenCobalt) */ diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt index d5b0f43b3..54e5774c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt @@ -53,7 +53,7 @@ import timber.log.Timber as T /** * A component that mirrors the current playback state into the [MediaSessionCompat] and - * [NotificationComponent]. + * [PlaybackNotification]. * * @author Alexander Capehart (OxygenCobalt) */ @@ -109,7 +109,7 @@ private constructor( /** * Release this instance, closing the [MediaSessionCompat] and preventing any further updates to - * the [NotificationComponent]. + * the [PlaybackNotification]. */ fun release() { bitmapProvider.release() @@ -202,7 +202,7 @@ private constructor( /** * Upload a new [MediaMetadataCompat] based on the current playback state to the - * [MediaSessionCompat] and [NotificationComponent]. + * [MediaSessionCompat] and [PlaybackNotification]. * * @param song The current [Song] to create the [MediaMetadataCompat] from, or null if no [Song] * is currently playing. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt index d9ec16787..13c57e97a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt @@ -59,25 +59,22 @@ constructor( ) : MediaSessionCompat.Callback() { private val jaroWinkler = JaroWinklerSimilarity() - override fun onPrepare() { - super.onPrepare() - // STUB, we already automatically prepare playback. - } + // STUBS: We already automatically prepare playback. + // override fun onPrepare() { + // super.onPrepare() + // } - override fun onPrepareFromMediaId(mediaId: String?, extras: Bundle?) { - super.onPrepareFromMediaId(mediaId, extras) - // STUB, can't tell when this is called - } - - override fun onPrepareFromUri(uri: Uri?, extras: Bundle?) { - super.onPrepareFromUri(uri, extras) - // STUB, can't tell when this is called - } - - override fun onPlayFromUri(uri: Uri?, extras: Bundle?) { - super.onPlayFromUri(uri, extras) - // STUB, can't tell when this is called - } + // override fun onPrepareFromMediaId(mediaId: String?, extras: Bundle?) { + // super.onPrepareFromMediaId(mediaId, extras) + // } + // + // override fun onPrepareFromUri(uri: Uri?, extras: Bundle?) { + // super.onPrepareFromUri(uri, extras) + // } + // + // override fun onPlayFromUri(uri: Uri?, extras: Bundle?) { + // super.onPlayFromUri(uri, extras) + // } override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { super.onPlayFromMediaId(mediaId, extras) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt index b21f43b97..f5025239f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt @@ -32,14 +32,14 @@ import timber.log.Timber as T class PlaybackServiceFragment private constructor( - private val context: Context, + context: Context, private val foregroundListener: ForegroundListener, private val playbackManager: PlaybackStateManager, exoHolderFactory: ExoPlaybackStateHolder.Factory, sessionHolderFactory: MediaSessionHolder.Factory, widgetComponentFactory: WidgetComponent.Factory, systemReceiverFactory: SystemPlaybackReceiver.Factory, -) : MediaSessionCompat.Callback(), PlaybackStateManager.Listener { +) : PlaybackStateManager.Listener { class Factory @Inject constructor( diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt index 956800067..59ffad0d5 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt @@ -54,6 +54,7 @@ private constructor( SystemPlaybackReceiver(context, playbackManager, playbackSettings, widgetComponent) } + @Suppress("WrongConstant") fun attach() { ContextCompat.registerReceiver( context, this, INTENT_FILTER, ContextCompat.RECEIVER_EXPORTED) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt index 61ff8087e..66ce48026 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.playback.state -import javax.inject.Inject import org.oxycblt.auxio.list.ListSettings import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album @@ -29,7 +28,7 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackSettings -import timber.log.Timber as T +import javax.inject.Inject /** * A playback command that can be passed to [PlaybackStateManager] to start new playback. diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt index 663f8accd..864e46785 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -61,7 +61,7 @@ interface SearchEngine { val artists: Collection? = null, val genres: Collection? = null, val playlists: Collection? = null - ) {} + ) } class SearchEngineImpl @Inject constructor(@ApplicationContext private val context: Context) : diff --git a/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt b/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt index 174ffa884..5381b3383 100644 --- a/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt +++ b/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt @@ -45,7 +45,7 @@ class StartActionHelper(config: TaskerPluginConfig) : } class ActivityConfigStartAction : Activity(), TaskerPluginConfigNoInput { - override val context + override val context: Context get() = applicationContext private val taskerHelper by lazy { StartActionHelper(this) } diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index c97ccf1c9..d2321ab5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -40,7 +40,6 @@ import kotlin.reflect.KClass import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.MainActivity import org.oxycblt.auxio.R -import timber.log.Timber as T /** * Get a [LayoutInflater] instance from this [Context]. diff --git a/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt b/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt index a191a4c14..e0b4cb588 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt @@ -3,7 +3,7 @@ package org.oxycblt.auxio.util import timber.log.Timber class CopyleftNoticeTree : Timber.DebugTree() { - // Feel free to remove this if you are forking the project in good faith. + // Feel free to remove this logger if you are forking the project in good faith. // // However, if you are stealing the source code to repackage it into a new closed-source app, // I will warn you that the One True, Living, Almighty God HATES thieves and WILL punish you diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index a23d33b8a..3dfb4f7d4 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.util +import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent @@ -117,6 +118,7 @@ val ViewBinding.context: Context * Override the behavior of a [MaterialToolbar]'s overflow menu to do something else. This is * extremely dumb, but required to hook overflow menus to bottom sheet menus. */ +@SuppressLint("RestrictedApi") fun Toolbar.overrideOnOverflowMenuClick(block: (View) -> Unit) { for (toolbarChild in children) { if (toolbarChild is ActionMenuView) { diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index e1e38784a..8b8d8c6a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -23,7 +23,6 @@ import java.util.UUID import kotlin.reflect.KClass import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.info.Date -import timber.log.Timber as T /** * Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull], diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt index ac44e418d..e705a3274 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt @@ -24,7 +24,7 @@ import coil.size.Size import coil.transform.Transformation import kotlin.math.sqrt -class WidgetBitmapTransformation(private val reduce: Float) : Transformation { +class WidgetBitmapTransformation(reduce: Float) : Transformation { private val metrics = Resources.getSystem().displayMetrics private val sw = metrics.widthPixels private val sh = metrics.heightPixels diff --git a/app/src/main/res/color/overlay_stroke.xml b/app/src/main/res/color/overlay_stroke.xml deleted file mode 100644 index 21337acc3..000000000 --- a/app/src/main/res/color/overlay_stroke.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-h480dp/item_playback_song.xml b/app/src/main/res/layout-h480dp/item_playback_song.xml deleted file mode 100644 index 9ce0bcf47..000000000 --- a/app/src/main/res/layout-h480dp/item_playback_song.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/item_playback_song.xml b/app/src/main/res/layout-sw600dp/item_playback_song.xml deleted file mode 100644 index 9ce0bcf47..000000000 --- a/app/src/main/res/layout-sw600dp/item_playback_song.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_playback_song.xml b/app/src/main/res/layout/item_playback_song.xml deleted file mode 100644 index 3e8c0c6a1..000000000 --- a/app/src/main/res/layout/item_playback_song.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file From 0f4702c4dd780ef0d1b55df71f966ecd8aa97268 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 16:09:32 -0600 Subject: [PATCH 096/550] all: fix logging & anim unification Can't bisect this without spending way too much time on it. --- .../java/org/oxycblt/auxio/AuxioService.kt | 3 +- .../java/org/oxycblt/auxio/MainActivity.kt | 16 ++-- .../java/org/oxycblt/auxio/MainFragment.kt | 40 ++++----- .../auxio/detail/AlbumDetailFragment.kt | 26 +++--- .../auxio/detail/ArtistDetailFragment.kt | 24 +++--- .../oxycblt/auxio/detail/DetailGenerator.kt | 4 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 34 ++++---- .../auxio/detail/GenreDetailFragment.kt | 22 ++--- .../auxio/detail/PlaylistDetailFragment.kt | 46 +++++----- .../oxycblt/auxio/detail/SongDetailDialog.kt | 4 +- .../decision/DetailDecisionViewModel.kt | 12 +-- .../auxio/detail/decision/ShowArtistDialog.kt | 4 +- .../detail/list/PlaylistDetailListAdapter.kt | 4 +- .../auxio/detail/sort/AlbumSongSortDialog.kt | 4 +- .../auxio/detail/sort/ArtistSongSortDialog.kt | 4 +- .../auxio/detail/sort/GenreSongSortDialog.kt | 4 +- .../detail/sort/PlaylistSongSortDialog.kt | 4 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 60 ++++++------- .../org/oxycblt/auxio/home/HomeGenerator.kt | 8 +- .../org/oxycblt/auxio/home/HomeSettings.kt | 12 +-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 6 +- .../oxycblt/auxio/home/ThemedSpeedDialView.kt | 15 +--- .../home/fastscroll/FastScrollRecyclerView.kt | 59 +++---------- .../java/org/oxycblt/auxio/home/tabs/Tab.kt | 8 +- .../org/oxycblt/auxio/home/tabs/TabAdapter.kt | 8 +- .../auxio/home/tabs/TabCustomizeDialog.kt | 6 +- .../org/oxycblt/auxio/image/ImageSettings.kt | 6 +- .../auxio/image/extractor/CoverExtractor.kt | 7 +- .../org/oxycblt/auxio/list/ListViewModel.kt | 26 +++--- .../auxio/list/adapter/FlexibleListAdapter.kt | 9 +- .../list/adapter/PlayingIndicatorAdapter.kt | 9 +- .../list/adapter/SelectionIndicatorAdapter.kt | 3 +- .../auxio/list/menu/MenuDialogFragment.kt | 4 +- .../oxycblt/auxio/list/menu/MenuViewModel.kt | 4 +- .../list/recycler/MaterialDragCallback.kt | 6 +- .../java/org/oxycblt/auxio/music/Music.kt | 6 +- .../oxycblt/auxio/music/MusicRepository.kt | 84 +++++++++--------- .../org/oxycblt/auxio/music/MusicSettings.kt | 6 +- .../org/oxycblt/auxio/music/MusicViewModel.kt | 42 ++++----- .../auxio/music/cache/CacheRepository.kt | 16 ++-- .../music/decision/AddToPlaylistDialog.kt | 4 +- .../music/decision/DeletePlaylistDialog.kt | 4 +- .../music/decision/ExportPlaylistDialog.kt | 10 +-- .../auxio/music/decision/NewPlaylistDialog.kt | 4 +- .../music/decision/PlaylistPickerViewModel.kt | 48 +++++------ .../music/decision/RenamePlaylistDialog.kt | 4 +- .../auxio/music/device/DeviceLibrary.kt | 4 +- .../auxio/music/device/DeviceMusicImpl.kt | 3 +- .../auxio/music/dirs/DirectoryAdapter.kt | 8 +- .../auxio/music/dirs/MusicDirsDialog.kt | 8 +- .../music/external/ExternalPlaylistManager.kt | 8 +- .../org/oxycblt/auxio/music/external/M3U.kt | 8 +- .../auxio/music/fs/MediaStoreExtractor.kt | 16 ++-- .../music/fs/MediaStorePathInterpreter.kt | 6 +- .../java/org/oxycblt/auxio/music/info/Date.kt | 4 +- .../java/org/oxycblt/auxio/music/info/Name.kt | 3 +- .../auxio/music/metadata/AudioProperties.kt | 14 +-- .../auxio/music/metadata/SeparatorsDialog.kt | 4 +- .../auxio/music/metadata/TagExtractor.kt | 16 ++-- .../auxio/music/metadata/TagInterpreter.kt | 66 ++++++++------ .../oxycblt/auxio/music/metadata/TagUtil.kt | 4 +- .../oxycblt/auxio/music/service/Indexer.kt | 14 +-- .../music/service/IndexerNotifications.kt | 6 +- .../auxio/music/service/MusicBrowser.kt | 3 +- .../music/service/MusicServiceFragment.kt | 11 +-- .../music/service/SystemContentObserver.kt | 4 +- .../oxycblt/auxio/music/user/UserLibrary.kt | 36 ++++---- .../auxio/playback/PlaybackBarFragment.kt | 8 +- .../auxio/playback/PlaybackPanelFragment.kt | 6 +- .../auxio/playback/PlaybackSettings.kt | 14 +-- .../auxio/playback/PlaybackViewModel.kt | 86 +++++++++---------- .../playback/decision/PlayFromArtistDialog.kt | 4 +- .../playback/decision/PlayFromGenreDialog.kt | 4 +- .../decision/PlaybackPickerViewModel.kt | 6 +- .../playback/persist/PersistenceRepository.kt | 18 ++-- .../auxio/playback/queue/QueueAdapter.kt | 8 +- .../auxio/playback/queue/QueueFragment.kt | 6 +- .../auxio/playback/queue/QueueViewModel.kt | 18 ++-- .../replaygain/PreAmpCustomizeDialog.kt | 4 +- .../replaygain/ReplayGainAudioProcessor.kt | 27 +++--- .../service/ExoPlaybackStateHolder.kt | 38 ++++---- .../playback/service/MediaButtonReceiver.kt | 4 +- .../playback/service/MediaSessionHolder.kt | 38 ++++---- .../playback/service/MediaSessionInterface.kt | 11 +-- .../service/PlaybackServiceFragment.kt | 6 +- .../service/SystemPlaybackReceiver.kt | 24 +++--- .../auxio/playback/state/PlaybackCommand.kt | 2 +- .../playback/state/PlaybackStateManager.kt | 60 ++++++------- .../playback/ui/AnimatedMaterialButton.kt | 27 ++---- .../auxio/playback/ui/StyledSeekBar.kt | 10 +-- .../org/oxycblt/auxio/search/SearchEngine.kt | 4 +- .../oxycblt/auxio/search/SearchFragment.kt | 48 +++++------ .../oxycblt/auxio/search/SearchViewModel.kt | 24 +++--- .../auxio/settings/BasePreferenceFragment.kt | 4 +- .../auxio/settings/RootPreferenceFragment.kt | 10 +-- .../org/oxycblt/auxio/settings/Settings.kt | 12 +-- .../categories/AudioPreferenceFragment.kt | 4 +- .../categories/MusicPreferenceFragment.kt | 8 +- .../PersonalizePreferenceFragment.kt | 4 +- .../categories/UIPreferenceFragment.kt | 14 +-- .../java/org/oxycblt/auxio/ui/Animations.kt | 83 ++++++++++++++++++ .../auxio/ui/BaseBottomSheetBehavior.kt | 4 +- .../auxio/ui/BottomSheetContentBehavior.kt | 6 +- .../auxio/ui/CoordinatorAppBarLayout.kt | 4 +- .../java/org/oxycblt/auxio/ui/MultiToolbar.kt | 63 ++++---------- .../java/org/oxycblt/auxio/ui/UISettings.kt | 6 +- .../ViewBindingBottomSheetDialogFragment.kt | 6 +- .../oxycblt/auxio/ui/ViewBindingFragment.kt | 6 +- .../ui/ViewBindingMaterialDialogFragment.kt | 6 +- .../org/oxycblt/auxio/ui/accent/Accent.kt | 4 +- .../auxio/ui/accent/AccentCustomizeDialog.kt | 4 +- .../oxycblt/auxio/util/CopyleftNoticeTree.kt | 27 +++++- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 20 ++--- .../java/org/oxycblt/auxio/util/StateUtil.kt | 10 +-- .../oxycblt/auxio/widgets/WidgetComponent.kt | 14 +-- .../oxycblt/auxio/widgets/WidgetProvider.kt | 14 +-- .../org/oxycblt/auxio/widgets/WidgetUtil.kt | 6 +- build.gradle | 2 +- 118 files changed, 937 insertions(+), 894 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/ui/Animations.kt diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index cf8033f7d..256e62e79 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -116,8 +116,7 @@ class AuxioService : private fun getRootChildrenLimit(): Int { return browserRootHints?.getInt( - MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, 4) - ?: 4 + MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, 4) ?: 4 } private fun Bundle.getPage(): MusicServiceFragment.Page? { diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index c745f8273..9430e6fd6 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -34,7 +34,7 @@ import org.oxycblt.auxio.playback.state.DeferredPlayback import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.systemBarInsetsCompat -import timber.log.Timber as T +import timber.log.Timber as L /** * Auxio's single [AppCompatActivity]. @@ -62,7 +62,7 @@ class MainActivity : AppCompatActivity() { val binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setupEdgeToEdge(binding.root) - T.d("Activity created") + L.d("Activity created") } override fun onResume() { @@ -90,10 +90,10 @@ class MainActivity : AppCompatActivity() { // Apply the color scheme. The black theme requires it's own set of themes since // it's not possible to modify the themes at run-time. if (isNight && uiSettings.useBlackTheme) { - T.d("Applying black theme [accent ${uiSettings.accent}]") + L.d("Applying black theme [accent ${uiSettings.accent}]") setTheme(uiSettings.accent.blackTheme) } else { - T.d("Applying normal theme [accent ${uiSettings.accent}]") + L.d("Applying normal theme [accent ${uiSettings.accent}]") setTheme(uiSettings.accent.theme) } } @@ -120,7 +120,7 @@ class MainActivity : AppCompatActivity() { private fun startIntentAction(intent: Intent?): Boolean { if (intent == null) { // Nothing to do. - T.d("No intent to handle") + L.d("No intent to handle") return false } @@ -129,7 +129,7 @@ class MainActivity : AppCompatActivity() { // This is because onStart can run multiple times, and thus we really don't // want to return false and override the original delayed action with a // RestoreState action. - T.d("Already used this intent") + L.d("Already used this intent") return true } intent.putExtra(KEY_INTENT_USED, true) @@ -139,11 +139,11 @@ class MainActivity : AppCompatActivity() { Intent.ACTION_VIEW -> DeferredPlayback.Open(intent.data ?: return false) Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> DeferredPlayback.ShuffleAll else -> { - T.w("Unexpected intent ${intent.action}") + L.w("Unexpected intent ${intent.action}") return false } } - T.d("Translated intent to $action") + L.d("Translated intent to $action") playbackModel.playDeferred(action) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 58fd9f109..3eade4d09 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -68,7 +68,7 @@ import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.lazyReflectedMethod import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A wrapper around the home fragment that shows the playback fragment and high-level navigation. @@ -145,13 +145,13 @@ class MainFragment : if (queueSheetBehavior != null) { // In portrait mode, set up click listeners on the stacked sheets. - T.d("Configuring stacked bottom sheets") + L.d("Configuring stacked bottom sheets") unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener { playbackModel.openQueue() } } else { // Dual-pane mode, manually style the static queue sheet. - T.d("Configuring dual-pane bottom sheet") + L.d("Configuring dual-pane bottom sheet") binding.queueSheet.apply { // Emulate the elevated bottom sheet style. background = @@ -367,11 +367,11 @@ class MainFragment : override fun onActionSelected(actionItem: SpeedDialActionItem): Boolean { when (actionItem.id) { R.id.action_new_playlist -> { - T.d("Creating playlist") + L.d("Creating playlist") musicModel.createPlaylist() } R.id.action_import_playlist -> { - T.d("Importing playlist") + L.d("Importing playlist") musicModel.importPlaylist() } else -> {} @@ -402,7 +402,7 @@ class MainFragment : // 1. Loading placeholder for item lists // 2. Rework the "No Music" case to not be an error and instead result in a placeholder if (state is IndexingState.Completed && state.error == null) { - T.d("Received ok response") + L.d("Received ok response") val binding = requireBinding() updateFabVisibility( binding, @@ -427,7 +427,7 @@ class MainFragment : // displaying the shuffle FAB makes no sense. We also don't want the fast scroll // popup to overlap with the FAB, so we hide the FAB when fast scrolling too. if (shouldHideAllFabs(binding, songs, isFastScrolling)) { - T.d("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]") + L.d("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]") forceHideAllFabs() } else { if (tabType != MusicType.PLAYLISTS) { @@ -436,7 +436,7 @@ class MainFragment : } if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { - T.d("Animating transition") + L.d("Animating transition") binding.homeNewPlaylistFab.hide( object : FloatingActionButton.OnVisibilityChangedListener() { override fun onHidden(fab: FloatingActionButton) { @@ -451,17 +451,17 @@ class MainFragment : } }) } else { - T.d("Showing immediately") + L.d("Showing immediately") binding.homeShuffleFab.show() } } else { - T.d("Showing playlist button") + L.d("Showing playlist button") if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) { return } if (binding.homeShuffleFab.isOrWillBeShown) { - T.d("Animating transition") + L.d("Animating transition") binding.homeShuffleFab.hide( object : FloatingActionButton.OnVisibilityChangedListener() { override fun onHidden(fab: FloatingActionButton) { @@ -476,7 +476,7 @@ class MainFragment : } }) } else { - T.d("Showing immediately") + L.d("Showing immediately") binding.homeNewPlaylistFab.show() } } @@ -551,7 +551,7 @@ class MainFragment : private fun handlePanel(panel: OpenPanel?) { if (panel == null) return - T.d("Trying to update panel to $panel") + L.d("Trying to update panel to $panel") when (panel) { OpenPanel.MAIN -> tryClosePlaybackPanel() OpenPanel.PLAYBACK -> tryOpenPlaybackPanel() @@ -567,7 +567,7 @@ class MainFragment : if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_COLLAPSED) { // Playback sheet is not expanded and not hidden, we can expand it. - T.d("Expanding playback sheet") + L.d("Expanding playback sheet") playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED return } @@ -578,7 +578,7 @@ class MainFragment : queueSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) { // Queue sheet and playback sheet is expanded, close the queue sheet so the // playback panel can shown. - T.d("Collapsing queue sheet") + L.d("Collapsing queue sheet") queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED } } @@ -589,7 +589,7 @@ class MainFragment : binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_EXPANDED) { // Playback sheet (and possibly queue) needs to be collapsed. - T.d("Collapsing playback and queue sheets") + L.d("Collapsing playback and queue sheets") val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED @@ -615,7 +615,7 @@ class MainFragment : val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_HIDDEN) { - T.d("Unhiding and enabling playback sheet") + L.d("Unhiding and enabling playback sheet") val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? // Queue sheet behavior is either collapsed or expanded, no hiding needed @@ -636,7 +636,7 @@ class MainFragment : val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? - T.d("Hiding and disabling playback and queue sheets") + L.d("Hiding and disabling playback and queue sheets") // Make both bottom sheets non-draggable so the user can't halt the hiding event. queueSheetBehavior?.apply { @@ -720,7 +720,7 @@ class MainFragment : OnBackPressedCallback(false) { override fun handleOnBackPressed() { if (detailModel.dropPlaylistEdit()) { - T.d("Dropped playlist edits") + L.d("Dropped playlist edits") } } @@ -733,7 +733,7 @@ class MainFragment : OnBackPressedCallback(false) { override fun handleOnBackPressed() { if (listModel.dropSelection()) { - T.d("Dropped selection") + L.d("Dropped selection") } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 36488ba68..dee23bc38 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ListFragment] that shows information about an [Album]. @@ -103,7 +103,7 @@ class AlbumDetailFragment : DetailFragment() { private fun updateAlbum(album: Album?) { if (album == null) { - T.d("No album to show, navigating away") + L.d("No album to show, navigating away") findNavController().navigateUp() return } @@ -153,7 +153,7 @@ class AlbumDetailFragment : DetailFragment() { val binding = requireBinding() when (show) { is Show.SongDetails -> { - T.d("Navigating to ${show.song}") + L.d("Navigating to ${show.song}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showSong(show.song.uid)) } @@ -162,11 +162,11 @@ class AlbumDetailFragment : DetailFragment() { // fragment should be launched otherwise. is Show.SongAlbumDetails -> { if (unlikelyToBeNull(detailModel.currentAlbum.value) == show.song.album) { - T.d("Navigating to a ${show.song} in this album") + L.d("Navigating to a ${show.song} in this album") scrollToAlbumSong(show.song) detailModel.toShow.consume() } else { - T.d("Navigating to the album of ${show.song}") + L.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showAlbum(show.song.album.uid)) } @@ -176,27 +176,27 @@ class AlbumDetailFragment : DetailFragment() { // detail fragment. is Show.AlbumDetails -> { if (unlikelyToBeNull(detailModel.currentAlbum.value) == show.album) { - T.d("Navigating to the top of this album") + L.d("Navigating to the top of this album") binding.detailRecycler.scrollToPosition(0) detailModel.toShow.consume() } else { - T.d("Navigating to ${show.album}") + L.d("Navigating to ${show.album}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showAlbum(show.album.uid)) } } is Show.ArtistDetails -> { - T.d("Navigating to ${show.artist}") + L.d("Navigating to ${show.artist}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - T.d("Navigating to artist choices for ${show.song}") + L.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - T.d("Navigating to artist choices for ${show.album}") + L.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(AlbumDetailFragmentDirections.showArtistChoices(show.album.uid)) } @@ -239,7 +239,7 @@ class AlbumDetailFragment : DetailFragment() { val directions = when (decision) { is PlaylistDecision.Add -> { - T.d("Adding ${decision.songs.size} songs to a playlist") + L.d("Adding ${decision.songs.size} songs to a playlist") AlbumDetailFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -268,11 +268,11 @@ class AlbumDetailFragment : DetailFragment() { val directions = when (decision) { is PlaybackDecision.PlayFromArtist -> { - T.d("Launching play from artist dialog for $decision") + L.d("Launching play from artist dialog for $decision") AlbumDetailFragmentDirections.playFromArtist(decision.song.uid) } is PlaybackDecision.PlayFromGenre -> { - T.d("Launching play from artist dialog for $decision") + L.d("Launching play from artist dialog for $decision") AlbumDetailFragmentDirections.playFromGenre(decision.song.uid) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 01201e33d..59afd9649 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ListFragment] that shows information about an [Artist]. @@ -111,7 +111,7 @@ class ArtistDetailFragment : DetailFragment() { private fun updateArtist(artist: Artist?) { if (artist == null) { - T.d("No artist to show, navigating away") + L.d("No artist to show, navigating away") findNavController().navigateUp() return } @@ -154,7 +154,7 @@ class ArtistDetailFragment : DetailFragment() { // The artist does not have any songs, so hide functionality that makes no sense. // ex. Play and Shuffle, Song Counts, and Genre Information. // Artists are always guaranteed to have albums however, so continue to show those. - T.d("Artist is empty, disabling genres and playback") + L.d("Artist is empty, disabling genres and playback") binding.detailSubhead.isVisible = false binding.detailPlayButton?.isEnabled = false binding.detailShuffleButton?.isEnabled = false @@ -176,14 +176,14 @@ class ArtistDetailFragment : DetailFragment() { val binding = requireBinding() when (show) { is Show.SongDetails -> { - T.d("Navigating to ${show.song}") + L.d("Navigating to ${show.song}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showSong(show.song.uid)) } // Songs should be shown in their album, not in their artist. is Show.SongAlbumDetails -> { - T.d("Navigating to the album of ${show.song}") + L.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showAlbum(show.song.album.uid)) } @@ -191,7 +191,7 @@ class ArtistDetailFragment : DetailFragment() { // Launch a new detail view for an album, even if it is part of // this artist. is Show.AlbumDetails -> { - T.d("Navigating to ${show.album}") + L.d("Navigating to ${show.album}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showAlbum(show.album.uid)) } @@ -200,22 +200,22 @@ class ArtistDetailFragment : DetailFragment() { // scroll back to the top. Otherwise launch a new detail view. is Show.ArtistDetails -> { if (show.artist == detailModel.currentArtist.value) { - T.d("Navigating to the top of this artist") + L.d("Navigating to the top of this artist") binding.detailRecycler.scrollToPosition(0) detailModel.toShow.consume() } else { - T.d("Navigating to ${show.artist}") + L.d("Navigating to ${show.artist}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showArtist(show.artist.uid)) } } is Show.SongArtistDecision -> { - T.d("Navigating to artist choices for ${show.song}") + L.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - T.d("Navigating to artist choices for ${show.album}") + L.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(ArtistDetailFragmentDirections.showArtistChoices(show.album.uid)) } @@ -259,7 +259,7 @@ class ArtistDetailFragment : DetailFragment() { val directions = when (decision) { is PlaylistDecision.Add -> { - T.d("Adding ${decision.songs.size} songs to a playlist") + L.d("Adding ${decision.songs.size} songs to a playlist") ArtistDetailFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -300,7 +300,7 @@ class ArtistDetailFragment : DetailFragment() { is PlaybackDecision.PlayFromArtist -> error("Unexpected playback decision $decision") is PlaybackDecision.PlayFromGenre -> { - T.d("Launching play from artist dialog for $decision") + L.d("Launching play from artist dialog for $decision") ArtistDetailFragmentDirections.playFromGenre(decision.song.uid) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt index f70ffe98e..826f0a517 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailGenerator.kt @@ -34,7 +34,7 @@ import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.ReleaseType -import timber.log.Timber as T +import timber.log.Timber as L interface DetailGenerator { fun any(uid: Music.UID): Detail? @@ -159,7 +159,7 @@ private class DetailGeneratorImpl( // groupByTo normally returns a mapping to a MutableList mapping. Since MutableList // inherits list, we can cast upwards and save a copy by directly inserting the // implicit album list into the mapping. - T.d("Implicit albums present, adding to list") + L.d("Implicit albums present, adding to list") @Suppress("UNCHECKED_CAST") (grouping as MutableMap>)[ DetailSection.Albums.Category.APPEARANCES] = artist.implicitAlbums diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 0cd42fa29..e122de45d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -55,7 +55,7 @@ import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the @@ -301,7 +301,7 @@ constructor( private fun showImpl(show: Show) { val existing = toShow.flow.value if (existing != null) { - T.d("Already have pending show command $existing, ignoring $show") + L.d("Already have pending show command $existing, ignoring $show") return } _toShow.put(show) @@ -314,10 +314,10 @@ constructor( * @param uid The UID of the [Song] to load. Must be valid. */ fun setSong(uid: Music.UID) { - T.d("Opening song $uid") + L.d("Opening song $uid") _currentSong.value = musicRepository.deviceLibrary?.findSong(uid)?.also(::refreshAudioInfo) if (_currentSong.value == null) { - T.w("Given song UID was invalid") + L.w("Given song UID was invalid") } } @@ -328,14 +328,14 @@ constructor( * @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid. */ fun setAlbum(uid: Music.UID) { - T.d("Opening album $uid") + L.d("Opening album $uid") if (uid === _currentAlbum.value?.uid) { return } val album = detailGenerator.album(uid) refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, null) if (_currentAlbum.value == null) { - T.w("Given album UID was invalid") + L.w("Given album UID was invalid") } } @@ -355,7 +355,7 @@ constructor( * @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid. */ fun setArtist(uid: Music.UID) { - T.d("Opening artist $uid") + L.d("Opening artist $uid") if (uid === _currentArtist.value?.uid) { return } @@ -379,7 +379,7 @@ constructor( * @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid. */ fun setGenre(uid: Music.UID) { - T.d("Opening genre $uid") + L.d("Opening genre $uid") if (uid === _currentGenre.value?.uid) { return } @@ -403,7 +403,7 @@ constructor( * @param uid The [Music.UID] of the [Playlist] to update [currentPlaylist] to. Must be valid. */ fun setPlaylist(uid: Music.UID) { - T.d("Opening playlist $uid") + L.d("Opening playlist $uid") if (uid === _currentPlaylist.value?.uid) { return } @@ -413,7 +413,7 @@ constructor( /** Start a playlist editing session. Does nothing if a playlist is not being shown. */ fun startPlaylistEdit() { val playlist = _currentPlaylist.value ?: return - T.d("Starting playlist edit") + L.d("Starting playlist edit") _editedPlaylist.value = playlist.songs refreshPlaylist(playlist.uid) } @@ -425,7 +425,7 @@ constructor( fun savePlaylistEdit() { val playlist = _currentPlaylist.value ?: return val editedPlaylist = _editedPlaylist.value ?: return - T.d("Committing playlist edits") + L.d("Committing playlist edits") viewModelScope.launch { musicRepository.rewritePlaylist(playlist, editedPlaylist) // TODO: The user could probably press some kind of button if they were fast enough. @@ -478,7 +478,7 @@ constructor( if (realFrom !in editedPlaylist.indices || realTo !in editedPlaylist.indices) { return false } - T.d("Moving playlist song from $realFrom [$from] to $realTo [$to]") + L.d("Moving playlist song from $realFrom [$from] to $realTo [$to]") editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo)) _editedPlaylist.value = editedPlaylist refreshPlaylist(playlist.uid, UpdateInstructions.Move(from, to)) @@ -497,7 +497,7 @@ constructor( if (realAt !in editedPlaylist.indices) { return } - T.d("Removing playlist song at $realAt [$at]") + L.d("Removing playlist song at $realAt [$at]") editedPlaylist.removeAt(realAt) _editedPlaylist.value = editedPlaylist refreshPlaylist( @@ -505,13 +505,13 @@ constructor( if (editedPlaylist.isNotEmpty()) { UpdateInstructions.Remove(at, 1) } else { - T.d("Playlist will be empty after removal, removing header") + L.d("Playlist will be empty after removal, removing header") UpdateInstructions.Remove(at - 1, 3) }) } private fun refreshAudioInfo(song: Song) { - T.d("Refreshing audio info") + L.d("Refreshing audio info") // Clear any previous job in order to avoid stale data from appearing in the UI. currentSongJob?.cancel() _songAudioProperties.value = null @@ -519,7 +519,7 @@ constructor( viewModelScope.launch(Dispatchers.IO) { val info = audioPropertiesFactory.extract(song) yield() - T.d("Updating audio info to $info") + L.d("Updating audio info to $info") _songAudioProperties.value = info } } @@ -586,7 +586,7 @@ constructor( uid: Music.UID, instructions: UpdateInstructions = UpdateInstructions.Diff ) { - T.d("Refreshing playlist list") + L.d("Refreshing playlist list") val edited = editedPlaylist.value if (edited == null) { val playlist = detailGenerator.playlist(uid) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 8173d781a..4c53debb6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ListFragment] that shows information for a particular [Genre]. @@ -110,7 +110,7 @@ class GenreDetailFragment : DetailFragment() { private fun updateGenre(genre: Genre?) { if (genre == null) { - T.d("No genre to show, navigating away") + L.d("No genre to show, navigating away") findNavController().navigateUp() return } @@ -144,7 +144,7 @@ class GenreDetailFragment : DetailFragment() { private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { - T.d("Navigating to ${show.song}") + L.d("Navigating to ${show.song}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showSong(show.song.uid)) } @@ -152,7 +152,7 @@ class GenreDetailFragment : DetailFragment() { // Songs should be scrolled to if the album matches, or a new detail // fragment should be launched otherwise. is Show.SongAlbumDetails -> { - T.d("Navigating to the album of ${show.song}") + L.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showAlbum(show.song.album.uid)) } @@ -160,29 +160,29 @@ class GenreDetailFragment : DetailFragment() { // If the album matches, no need to do anything. Otherwise launch a new // detail fragment. is Show.AlbumDetails -> { - T.d("Navigating to ${show.album}") + L.d("Navigating to ${show.album}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showAlbum(show.album.uid)) } // Always launch a new ArtistDetailFragment. is Show.ArtistDetails -> { - T.d("Navigating to ${show.artist}") + L.d("Navigating to ${show.artist}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - T.d("Navigating to artist choices for ${show.song}") + L.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - T.d("Navigating to artist choices for ${show.album}") + L.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(GenreDetailFragmentDirections.showArtistChoices(show.album.uid)) } is Show.GenreDetails -> { - T.d("Navigated to this genre") + L.d("Navigated to this genre") detailModel.toShow.consume() } is Show.PlaylistDetails -> { @@ -223,7 +223,7 @@ class GenreDetailFragment : DetailFragment() { val directions = when (decision) { is PlaylistDecision.Add -> { - T.d("Adding ${decision.songs.size} songs to a playlist") + L.d("Adding ${decision.songs.size} songs to a playlist") GenreDetailFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -262,7 +262,7 @@ class GenreDetailFragment : DetailFragment() { val directions = when (decision) { is PlaybackDecision.PlayFromArtist -> { - T.d("Launching play from artist dialog for $decision") + L.d("Launching play from artist dialog for $decision") GenreDetailFragmentDirections.playFromArtist(decision.song.uid) } is PlaybackDecision.PlayFromGenre -> error("Unexpected playback decision $decision") diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 1c8a0882d..b34327745 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -52,7 +52,7 @@ import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ListFragment] that shows information for a particular [Playlist]. @@ -81,11 +81,11 @@ class PlaylistDetailFragment : getContentLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) { - T.w("No URI returned from file picker") + L.w("No URI returned from file picker") return@registerForActivityResult } - T.d("Received playlist URI $uri") + L.d("Received playlist URI $uri") musicModel.importPlaylist(uri, pendingImportTarget) } @@ -193,7 +193,7 @@ class PlaylistDetailFragment : getString(R.string.fmt_editing, playlist.name.resolve(requireContext())) if (editedPlaylist != null) { - T.d("Binding edited playlist image") + L.d("Binding edited playlist image") binding.detailCover.bind( editedPlaylist, binding.context.getString(R.string.desc_playlist_image, playlist.name), @@ -222,7 +222,7 @@ class PlaylistDetailFragment : val playable = playlist.songs.isNotEmpty() && editedPlaylist == null if (!playable) { - T.d("Playlist is being edited or is empty, disabling playback options") + L.d("Playlist is being edited or is empty, disabling playback options") } binding.detailPlayButton?.apply { @@ -248,7 +248,7 @@ class PlaylistDetailFragment : listModel.dropSelection() if (editedPlaylist != null) { - T.d("Updating save button state") + L.d("Updating save button state") requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).apply { isEnabled = editedPlaylist != detailModel.currentPlaylist.value?.songs } @@ -260,38 +260,38 @@ class PlaylistDetailFragment : private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { - T.d("Navigating to ${show.song}") + L.d("Navigating to ${show.song}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showSong(show.song.uid)) } is Show.SongAlbumDetails -> { - T.d("Navigating to the album of ${show.song}") + L.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showAlbum(show.song.album.uid)) } is Show.AlbumDetails -> { - T.d("Navigating to ${show.album}") + L.d("Navigating to ${show.album}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showAlbum(show.album.uid)) } is Show.ArtistDetails -> { - T.d("Navigating to ${show.artist}") + L.d("Navigating to ${show.artist}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - T.d("Navigating to artist choices for ${show.song}") + L.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(PlaylistDetailFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - T.d("Navigating to artist choices for ${show.album}") + L.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe( PlaylistDetailFragmentDirections.showArtistChoices(show.album.uid)) } is Show.PlaylistDetails -> { - T.d("Navigated to this playlist") + L.d("Navigated to this playlist") detailModel.toShow.consume() } is Show.GenreDetails -> { @@ -332,7 +332,7 @@ class PlaylistDetailFragment : val directions = when (decision) { is PlaylistDecision.Import -> { - T.d("Importing playlist") + L.d("Importing playlist") pendingImportTarget = decision.target requireNotNull(getContentLauncher) { "Content picker launcher was not available" @@ -342,7 +342,7 @@ class PlaylistDetailFragment : return } is PlaylistDecision.Rename -> { - T.d("Renaming ${decision.playlist}") + L.d("Renaming ${decision.playlist}") PlaylistDetailFragmentDirections.renamePlaylist( decision.playlist.uid, decision.template, @@ -350,15 +350,15 @@ class PlaylistDetailFragment : decision.reason) } is PlaylistDecision.Export -> { - T.d("Exporting ${decision.playlist}") + L.d("Exporting ${decision.playlist}") PlaylistDetailFragmentDirections.exportPlaylist(decision.playlist.uid) } is PlaylistDecision.Delete -> { - T.d("Deleting ${decision.playlist}") + L.d("Deleting ${decision.playlist}") PlaylistDetailFragmentDirections.deletePlaylist(decision.playlist.uid) } is PlaylistDecision.Add -> { - T.d("Adding ${decision.songs.size} songs to a playlist") + L.d("Adding ${decision.songs.size} songs to a playlist") PlaylistDetailFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -384,11 +384,11 @@ class PlaylistDetailFragment : val directions = when (decision) { is PlaybackDecision.PlayFromArtist -> { - T.d("Launching play from artist dialog for $decision") + L.d("Launching play from artist dialog for $decision") PlaylistDetailFragmentDirections.playFromArtist(decision.song.uid) } is PlaybackDecision.PlayFromGenre -> { - T.d("Launching play from artist dialog for $decision") + L.d("Launching play from artist dialog for $decision") PlaylistDetailFragmentDirections.playFromGenre(decision.song.uid) } } @@ -399,15 +399,15 @@ class PlaylistDetailFragment : val id = when { detailModel.editedPlaylist.value != null -> { - T.d("Currently editing playlist, showing edit toolbar") + L.d("Currently editing playlist, showing edit toolbar") R.id.detail_edit_toolbar } listModel.selected.value.isNotEmpty() -> { - T.d("Currently selecting, showing selection toolbar") + L.d("Currently selecting, showing selection toolbar") R.id.detail_selection_toolbar } else -> { - T.d("Using normal toolbar") + L.d("Using normal toolbar") R.id.detail_normal_toolbar } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index b89e33292..f789b042a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -42,7 +42,7 @@ import org.oxycblt.auxio.playback.replaygain.formatDb import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.concatLocalized -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingMaterialDialogFragment] that shows information about a Song. @@ -76,7 +76,7 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment { - T.d("Creating navigation choices for song") + L.d("Creating navigation choices for song") ArtistShowChoices.FromSong(music) } is Album -> { - T.d("Creating navigation choices for album") + L.d("Creating navigation choices for album") ArtistShowChoices.FromAlbum(music) } else -> { - T.w("Given song/album UID was invalid") + L.w("Given song/album UID was invalid") null } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt index 7fddcd6b9..3879c677a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt @@ -35,7 +35,7 @@ import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately -import timber.log.Timber as T +import timber.log.Timber as L /** * A picker [ViewBindingMaterialDialogFragment] intended for when the [Artist] to show is ambiguous. @@ -85,7 +85,7 @@ class ShowArtistDialog : private fun updateChoices(choices: ArtistShowChoices?) { if (choices == null) { - T.d("No choices to show, navigating away") + L.d("No choices to show, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index 4a00f6fe8..dd9513335 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -46,7 +46,7 @@ import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.inflater -import timber.log.Timber as T +import timber.log.Timber as L /** * A [DetailListAdapter] implementing the header, sub-items, and editing state for the [Playlist] @@ -97,7 +97,7 @@ class PlaylistDetailListAdapter(private val listener: Listener) : // Nothing to do. return } - T.d("Updating editing state [old: $isEditing new: $editing]") + L.d("Updating editing state [old: $isEditing new: $editing]") this.isEditing = editing notifyItemRangeChanged(0, currentList.size, PAYLOAD_EDITING_CHANGED) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt index 19fdc2b36..2ded72ab9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.SortDialog import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.util.collectImmediately -import timber.log.Timber as T +import timber.log.Timber as L /** * A [SortDialog] that controls the [Sort] of [DetailViewModel.albumSongSort]. @@ -56,7 +56,7 @@ class AlbumSongSortDialog : SortDialog() { private fun updateAlbum(album: Album?) { if (album == null) { - T.d("No album to sort, navigating away") + L.d("No album to sort, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt index c7961a3d6..d132be42c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.SortDialog import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.util.collectImmediately -import timber.log.Timber as T +import timber.log.Timber as L /** * A [SortDialog] that controls the [Sort] of [DetailViewModel.artistSongSort]. @@ -57,7 +57,7 @@ class ArtistSongSortDialog : SortDialog() { private fun updateArtist(artist: Artist?) { if (artist == null) { - T.d("No artist to sort, navigating away") + L.d("No artist to sort, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt index e92bd867a..504d857cc 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.SortDialog import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.util.collectImmediately -import timber.log.Timber as T +import timber.log.Timber as L /** * A [SortDialog] that controls the [Sort] of [DetailViewModel.genreSongSort]. @@ -62,7 +62,7 @@ class GenreSongSortDialog : SortDialog() { private fun updateGenre(genre: Genre?) { if (genre == null) { - T.d("No genre to sort, navigating away") + L.d("No genre to sort, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt index 8c90759ba..86192da1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.list.sort.SortDialog import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.util.collectImmediately -import timber.log.Timber as T +import timber.log.Timber as L /** * A [SortDialog] that controls the [Sort] of [DetailViewModel.genreSongSort]. @@ -62,7 +62,7 @@ class PlaylistSongSortDialog : SortDialog() { private fun updatePlaylist(genre: Playlist?) { if (genre == null) { - T.d("No genre to sort, navigating away") + L.d("No genre to sort, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 850931796..6dd428f98 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -79,7 +79,7 @@ import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedMethod import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast -import timber.log.Timber as T +import timber.log.Timber as L /** * The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation @@ -125,11 +125,11 @@ class HomeFragment : getContentLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) { - T.w("No URI returned from file picker") + L.w("No URI returned from file picker") return@registerForActivityResult } - T.d("Received playlist URI $uri") + L.d("Received playlist URI $uri") musicModel.importPlaylist(uri, pendingImportTarget) } @@ -220,17 +220,17 @@ class HomeFragment : return when (item.itemId) { // Handle main actions (Search, Settings, About) R.id.action_search -> { - T.d("Navigating to search") + L.d("Navigating to search") findNavController().navigateSafe(HomeFragmentDirections.search()) true } R.id.action_settings -> { - T.d("Navigating to preferences") + L.d("Navigating to preferences") homeModel.showSettings() true } R.id.action_about -> { - T.d("Navigating to about") + L.d("Navigating to about") homeModel.showAbout() true } @@ -250,7 +250,7 @@ class HomeFragment : true } else -> { - T.w("Unexpected menu item selected") + L.w("Unexpected menu item selected") false } } @@ -264,7 +264,7 @@ class HomeFragment : if (homeModel.currentTabTypes.size == 1) { // A single tab makes the tab layout redundant, hide it and disable the collapsing // behavior. - T.d("Single tab shown, disabling TabLayout") + L.d("Single tab shown, disabling TabLayout") binding.homeTabs.isVisible = false binding.homeAppbar.setExpanded(true, false) toolbarParams.scrollFlags = 0 @@ -302,7 +302,7 @@ class HomeFragment : private fun handleRecreate(recreate: Unit?) { if (recreate == null) return val binding = requireBinding() - T.d("Recreating ViewPager") + L.d("Recreating ViewPager") // Move back to position zero, as there must be a tab there. binding.homePager.currentItem = 0 // Make sure tabs are set up to also follow the new ViewPager configuration. @@ -319,7 +319,7 @@ class HomeFragment : is IndexingState.Completed -> setupCompleteState(binding, state.error) is IndexingState.Indexing -> setupIndexingState(binding, state.progress) null -> { - T.d("Indexer is in indeterminate state") + L.d("Indexer is in indeterminate state") binding.homeIndexingContainer.visibility = View.INVISIBLE } } @@ -327,19 +327,19 @@ class HomeFragment : private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) { if (error == null) { - T.d("Received ok response") + L.d("Received ok response") binding.homeIndexingContainer.visibility = View.INVISIBLE return } - T.d("Received non-ok response") + L.d("Received non-ok response") val context = requireContext() binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingActions.visibility = View.VISIBLE when (error) { is NoAudioPermissionException -> { - T.d("Showing permission prompt") + L.d("Showing permission prompt") binding.homeIndexingStatus.setText(R.string.err_no_perms) // Configure the action to act as a permission launcher. binding.homeIndexingTry.apply { @@ -354,7 +354,7 @@ class HomeFragment : binding.homeIndexingMore.visibility = View.GONE } is NoMusicException -> { - T.d("Showing no music error") + L.d("Showing no music error") binding.homeIndexingStatus.setText(R.string.err_no_music) // Configure the action to act as a reload trigger. binding.homeIndexingTry.apply { @@ -365,7 +365,7 @@ class HomeFragment : binding.homeIndexingMore.visibility = View.GONE } else -> { - T.d("Showing generic error") + L.d("Showing generic error") binding.homeIndexingStatus.setText(R.string.err_index_failed) // Configure the action to act as a reload trigger. binding.homeIndexingTry.apply { @@ -411,14 +411,14 @@ class HomeFragment : val directions = when (decision) { is PlaylistDecision.New -> { - T.d("Creating new playlist") + L.d("Creating new playlist") HomeFragmentDirections.newPlaylist( decision.songs.map { it.uid }.toTypedArray(), decision.template, decision.reason) } is PlaylistDecision.Import -> { - T.d("Importing playlist") + L.d("Importing playlist") pendingImportTarget = decision.target requireNotNull(getContentLauncher) { "Content picker launcher was not available" @@ -428,7 +428,7 @@ class HomeFragment : return } is PlaylistDecision.Rename -> { - T.d("Renaming ${decision.playlist}") + L.d("Renaming ${decision.playlist}") HomeFragmentDirections.renamePlaylist( decision.playlist.uid, decision.template, @@ -436,15 +436,15 @@ class HomeFragment : decision.reason) } is PlaylistDecision.Export -> { - T.d("Exporting ${decision.playlist}") + L.d("Exporting ${decision.playlist}") HomeFragmentDirections.exportPlaylist(decision.playlist.uid) } is PlaylistDecision.Delete -> { - T.d("Deleting ${decision.playlist}") + L.d("Deleting ${decision.playlist}") HomeFragmentDirections.deletePlaylist(decision.playlist.uid) } is PlaylistDecision.Add -> { - T.d("Adding ${decision.songs.size} to a playlist") + L.d("Adding ${decision.songs.size} to a playlist") HomeFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -475,38 +475,38 @@ class HomeFragment : private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { - T.d("Navigating to ${show.song}") + L.d("Navigating to ${show.song}") findNavController().navigateSafe(HomeFragmentDirections.showSong(show.song.uid)) } is Show.SongAlbumDetails -> { - T.d("Navigating to the album of ${show.song}") + L.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(HomeFragmentDirections.showAlbum(show.song.album.uid)) } is Show.AlbumDetails -> { - T.d("Navigating to ${show.album}") + L.d("Navigating to ${show.album}") findNavController().navigateSafe(HomeFragmentDirections.showAlbum(show.album.uid)) } is Show.ArtistDetails -> { - T.d("Navigating to ${show.artist}") + L.d("Navigating to ${show.artist}") findNavController().navigateSafe(HomeFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - T.d("Navigating to artist choices for ${show.song}") + L.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(HomeFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - T.d("Navigating to artist choices for ${show.album}") + L.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(HomeFragmentDirections.showArtistChoices(show.album.uid)) } is Show.GenreDetails -> { - T.d("Navigating to ${show.genre}") + L.d("Navigating to ${show.genre}") findNavController().navigateSafe(HomeFragmentDirections.showGenre(show.genre.uid)) } is Show.PlaylistDetails -> { - T.d("Navigating to ${show.playlist}") + L.d("Navigating to ${show.playlist}") findNavController() .navigateSafe(HomeFragmentDirections.showPlaylist(show.playlist.uid)) } @@ -534,7 +534,7 @@ class HomeFragment : binding.homeSelectionToolbar.title = getString(R.string.fmt_selected, selected.size) if (binding.homeToolbar.setVisible(R.id.home_selection_toolbar)) { // New selection started, show the AppBarLayout to indicate the new state. - T.d("Significant selection occurred, expanding AppBar") + L.d("Significant selection occurred, expanding AppBar") binding.homeAppbar.expandWithScrollingRecycler() } } else { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt index 140b5c573..2d3f0d1ab 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeGenerator.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import timber.log.Timber as T +import timber.log.Timber as L interface HomeGenerator { fun attach() @@ -89,7 +89,7 @@ private class HomeGeneratorImpl( override fun onHideCollaboratorsChanged() { // Changes in the hide collaborator setting will change the artist contents // of the library, consider it a library update. - T.d("Collaborator setting changed, forwarding update") + L.d("Collaborator setting changed, forwarding update") onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = false)) } @@ -121,7 +121,7 @@ private class HomeGeneratorImpl( override fun onMusicChanges(changes: MusicRepository.Changes) { val deviceLibrary = musicRepository.deviceLibrary if (changes.deviceLibrary && deviceLibrary != null) { - T.d("Refreshing library") + L.d("Refreshing library") // Get the each list of items in the library to use as our list data. // Applying the preferred sorting to them. invalidator.invalidateMusic(MusicType.SONGS, UpdateInstructions.Diff) @@ -132,7 +132,7 @@ private class HomeGeneratorImpl( val userLibrary = musicRepository.userLibrary if (changes.userLibrary && userLibrary != null) { - T.d("Refreshing playlists") + L.d("Refreshing playlists") invalidator.invalidateMusic(MusicType.PLAYLISTS, UpdateInstructions.Diff) } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt index fe88eee89..aadacc21d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * User configuration specific to the home UI. @@ -68,17 +68,17 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) override fun migrate() { if (sharedPreferences.contains(OLD_KEY_LIB_TABS)) { - T.d("Migrating tab setting") + L.d("Migrating tab setting") val oldTabs = Tab.fromIntCode(sharedPreferences.getInt(OLD_KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) ?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT)) - T.d("Old tabs: $oldTabs") + L.d("Old tabs: $oldTabs") // The playlist tab is now parsed, but it needs to be made visible. val playlistIndex = oldTabs.indexOfFirst { it.type == MusicType.PLAYLISTS } check(playlistIndex > -1) // This should exist, otherwise we are in big trouble oldTabs[playlistIndex] = Tab.Visible(MusicType.PLAYLISTS) - T.d("New tabs: $oldTabs") + L.d("New tabs: $oldTabs") sharedPreferences.edit { putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(oldTabs)) @@ -90,11 +90,11 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) override fun onSettingChanged(key: String, listener: HomeSettings.Listener) { when (key) { getString(R.string.set_key_home_tabs) -> { - T.d("Dispatching tab setting change") + L.d("Dispatching tab setting change") listener.onTabsChanged() } getString(R.string.set_key_hide_collaborators) -> { - T.d("Dispatching collaborator setting change") + L.d("Dispatching collaborator setting change") listener.onHideCollaboratorsChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index e90112c26..4ebc98baf 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import timber.log.Timber as T +import timber.log.Timber as L /** * The ViewModel for managing the tab data and lists of the home view. @@ -249,7 +249,7 @@ constructor( * @param pagerPos The new position of the ViewPager2 instance. */ fun synchronizeTabPosition(pagerPos: Int) { - T.d("Updating current tab to ${currentTabTypes[pagerPos]}") + L.d("Updating current tab to ${currentTabTypes[pagerPos]}") _currentTabType.value = currentTabTypes[pagerPos] } @@ -259,7 +259,7 @@ constructor( * @param isFastScrolling true if the user is currently fast scrolling, false otherwise. */ fun setFastScrolling(isFastScrolling: Boolean) { - T.d("Updating fast scrolling state: $isFastScrolling") + L.d("Updating fast scrolling state: $isFastScrolling") _isFastScrolling.value = isFastScrolling } diff --git a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt index 2b1d0c862..2637fb331 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt @@ -39,9 +39,6 @@ import androidx.core.os.BundleCompat import androidx.core.view.setMargins import androidx.core.view.updateLayoutParams import androidx.core.widget.TextViewCompat -import androidx.interpolator.view.animation.FastOutSlowInInterpolator -import com.google.android.material.R as MR -import com.google.android.material.motion.MotionUtils import com.google.android.material.shape.MaterialShapeDrawable import com.leinardi.android.speeddial.FabWithLabelView import com.leinardi.android.speeddial.SpeedDialActionItem @@ -49,6 +46,7 @@ import com.leinardi.android.speeddial.SpeedDialView import kotlin.math.roundToInt import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.R +import org.oxycblt.auxio.ui.StationaryAnim import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimenPixels @@ -80,12 +78,7 @@ class ThemedSpeedDialView : SpeedDialView { @AttrRes defStyleAttr: Int ) : super(context, attrs, defStyleAttr) - private val matInterpolator = - MotionUtils.resolveThemeInterpolator( - context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()) - - private val matDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 350) + private val inAnim = StationaryAnim.forMediumComponent(context) init { // Work around ripple bug on Android 12 when useCompatPadding = true. @@ -149,7 +142,7 @@ class ThemedSpeedDialView : SpeedDialView { } private fun createMainFabAnimator(isOpen: Boolean): Animator { - val totalDuration = matDuration.toLong() + val totalDuration = inAnim.duration val partialDuration = totalDuration / 2 // This is half of the total duration val delay = totalDuration / 4 // This is one fourth of the total duration @@ -181,7 +174,7 @@ class ThemedSpeedDialView : SpeedDialView { val animatorSet = AnimatorSet().apply { playTogether(backgroundTintAnimator, imageTintAnimator, levelAnimator) - interpolator = matInterpolator + interpolator = inAnim.interpolator } animatorSet.start() return animatorSet diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index c973a8cfb..4631750e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -31,15 +31,14 @@ import android.view.WindowInsets import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.core.view.isInvisible -import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.R as MR -import com.google.android.material.motion.MotionUtils import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.list.recycler.AuxioRecyclerView +import org.oxycblt.auxio.ui.InAnim +import org.oxycblt.auxio.ui.OutAnim import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.isRtl @@ -85,23 +84,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr background = context.getDrawableCompat(R.drawable.ui_scroll_thumb) } - private val thumbEnterInterpolator = - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedDecelerateInterpolator, - FastOutSlowInInterpolator()) - - private val thumbEnterDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 300) - - private val thumbExitInterpolator = - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedAccelerateInterpolator, - FastOutSlowInInterpolator()) - - private val thumbExitDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100) + private val thumbEnter = InAnim.forSmallComponent(context) + private val thumbExit = OutAnim.forSmallComponent(context) private val thumbWidth = thumbView.background.intrinsicWidth private val thumbHeight = thumbView.background.intrinsicHeight @@ -130,23 +114,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } - private val popupEnterInterpolator = - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedDecelerateInterpolator, - FastOutSlowInInterpolator()) - - private val popupEnterDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium1, 300) - - private val popupExitInterpolator = - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedAccelerateInterpolator, - FastOutSlowInInterpolator()) - - private val popupExitDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100) + private val popupEnter = InAnim.forSmallComponent(context) + private val popupExit = OutAnim.forSmallComponent(context) private var showingPopup = false @@ -460,8 +429,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr thumbView .animate() .scaleX(1f) - .setInterpolator(thumbEnterInterpolator) - .setDuration(thumbEnterDuration.toLong()) + .setInterpolator(thumbEnter.interpolator) + .setDuration(thumbEnter.duration) .start() } @@ -474,8 +443,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr thumbView .animate() .scaleX(0f) - .setInterpolator(thumbExitInterpolator) - .setDuration(thumbExitDuration.toLong()) + .setInterpolator(thumbExit.interpolator) + .setDuration(thumbExit.duration) .start() } @@ -493,8 +462,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr .animate() .scaleX(1f) .scaleY(1f) - .setInterpolator(popupEnterInterpolator) - .setDuration(popupEnterDuration.toLong()) + .setInterpolator(popupEnter.interpolator) + .setDuration(popupEnter.duration) .start() } @@ -509,8 +478,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr .alpha(0f) .scaleX(0.75f) .scaleY(0.75f) - .setInterpolator(popupExitInterpolator) - .setDuration(popupExitDuration.toLong()) + .setInterpolator(popupExit.interpolator) + .setDuration(popupExit.duration) .start() } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index 1c1ba6fe8..5f741bad2 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -19,7 +19,7 @@ package org.oxycblt.auxio.home.tabs import org.oxycblt.auxio.music.MusicType -import timber.log.Timber as T +import timber.log.Timber as L /** * A representation of a library tab suitable for configuration. @@ -85,7 +85,7 @@ sealed class Tab(open val type: MusicType) { // Like when deserializing, make sure there are no duplicate tabs for whatever reason. val distinct = tabs.distinctBy { it.type } if (tabs.size != distinct.size) { - T.w( + L.w( "Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]") } @@ -132,13 +132,13 @@ sealed class Tab(open val type: MusicType) { // Make sure there are no duplicate tabs val distinct = tabs.distinctBy { it.type } if (tabs.size != distinct.size) { - T.w( + L.w( "Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]") } // For safety, return null if we have an empty or larger-than-expected tab array. if (distinct.isEmpty() || distinct.size < MAX_SEQUENCE_IDX) { - T.e("Sequence size was ${distinct.size}, which is invalid") + L.e("Sequence size was ${distinct.size}, which is invalid") return null } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index cb5c56384..592b808b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.util.inflater -import timber.log.Timber as T +import timber.log.Timber as L /** * A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration. @@ -55,7 +55,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param newTabs The new array of tabs to show. */ fun submitTabs(newTabs: Array) { - T.d("Force-updating tab information") + L.d("Force-updating tab information") tabs = newTabs @Suppress("NotifyDatasetChanged") notifyDataSetChanged() } @@ -67,7 +67,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param tab The new tab. */ fun setTab(at: Int, tab: Tab) { - T.d("Updating tab [at: $at, tab: $tab]") + L.d("Updating tab [at: $at, tab: $tab]") tabs[at] = tab // Use a payload to avoid an item change animation. notifyItemChanged(at, PAYLOAD_TAB_CHANGED) @@ -80,7 +80,7 @@ class TabAdapter(private val listener: EditClickListListener) : * @param b The position of the second tab to swap. */ fun swapTabs(a: Int, b: Int) { - T.d("Swapping tabs [a: $a, b: $b]") + L.d("Swapping tabs [a: $a, b: $b]") val tmp = tabs[b] tabs[b] = tabs[a] tabs[a] = tmp diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index e550fe297..ad7dd3fd5 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -31,7 +31,7 @@ import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.home.HomeSettings import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingMaterialDialogFragment] that allows the user to modify the home [Tab] @@ -52,7 +52,7 @@ class TabCustomizeDialog : builder .setTitle(R.string.set_lib_tabs) .setPositiveButton(R.string.lbl_ok) { _, _ -> - T.d("Committing tab changes") + L.d("Committing tab changes") homeSettings.homeTabs = tabAdapter.tabs } .setNegativeButton(R.string.lbl_cancel, null) @@ -99,7 +99,7 @@ class TabCustomizeDialog : is Tab.Visible -> Tab.Invisible(old.type) is Tab.Invisible -> Tab.Visible(old.type) } - T.d("Flipping tab visibility [from: $old to: $new]") + L.d("Flipping tab visibility [from: $old to: $new]") tabAdapter.setTab(index, new) // Prevent the user from saving if all the tabs are Invisible, as that's an invalid state. diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt index d94b0faf4..92195625e 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt @@ -24,7 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.Settings -import timber.log.Timber as T +import timber.log.Timber as L /** * User configuration specific to image loading. @@ -58,7 +58,7 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context // Show album covers and Ignore MediaStore covers were unified in 3.0.0 if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) || sharedPreferences.contains(OLD_KEY_QUALITY_COVERS)) { - T.d("Migrating cover settings") + L.d("Migrating cover settings") val mode = when { @@ -79,7 +79,7 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context override fun onSettingChanged(key: String, listener: ImageSettings.Listener) { if (key == getString(R.string.set_key_cover_mode) || key == getString(R.string.set_key_square_covers)) { - T.d("Dispatching image setting change") + L.d("Dispatching image setting change") listener.onImageSettingsChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index ca0f82bcf..0b95bb2f6 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -53,7 +53,7 @@ import okio.source import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.music.Song -import timber.log.Timber as T +import timber.log.Timber as L /** * Provides functionality for extracting album cover information. Meant for internal use only. @@ -153,13 +153,14 @@ constructor( } } } catch (e: Exception) { - T.e("Unable to extract album cover due to an error: $e") + L.e("Unable to extract album cover due to an error: $e") null } private suspend fun extractQualityCover(cover: Cover.Embedded) = extractExoplayerCover(cover) - ?: extractAospMetadataCover(cover) ?: extractMediaStoreCover(cover) + ?: extractAospMetadataCover(cover) + ?: extractMediaStoreCover(cover) private fun extractAospMetadataCover(cover: Cover.Embedded): InputStream? = MediaMetadataRetriever().run { diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt index a648019f9..0de81e3c1 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt @@ -36,7 +36,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewModel] that orchestrates menu dialogs and selection state. @@ -93,16 +93,16 @@ constructor(private val listSettings: ListSettings, private val musicRepository: */ fun select(music: Music) { if (music is MusicParent && music.songs.isEmpty()) { - T.d("Cannot select empty parent, ignoring operation") + L.d("Cannot select empty parent, ignoring operation") return } val selected = _selected.value.toMutableList() if (!selected.remove(music)) { - T.d("Adding $music to selection") + L.d("Adding $music to selection") selected.add(music) } else { - T.d("Removed $music from selection") + L.d("Removed $music from selection") } _selected.value = selected @@ -130,7 +130,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @return A list of [Song]s collated from each item selected. */ fun takeSelection(): List { - T.d("Taking selection") + L.d("Taking selection") return peekSelection().also { _selected.value = listOf() } } @@ -140,7 +140,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @return true if the prior selection was non-empty, false otherwise. */ fun dropSelection(): Boolean { - T.d("Dropping selection [empty=${_selected.value.isEmpty()}]") + L.d("Dropping selection [empty=${_selected.value.isEmpty()}]") return _selected.value.isNotEmpty().also { _selected.value = listOf() } } @@ -154,7 +154,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * should do. */ fun openMenu(@MenuRes menuRes: Int, song: Song, playWith: PlaySong) { - T.d("Opening menu for $song") + L.d("Opening menu for $song") openImpl(Menu.ForSong(menuRes, song, playWith)) } @@ -166,7 +166,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param album The [Album] to show. */ fun openMenu(@MenuRes menuRes: Int, album: Album) { - T.d("Opening menu for $album") + L.d("Opening menu for $album") openImpl(Menu.ForAlbum(menuRes, album)) } @@ -178,7 +178,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param artist The [Artist] to show. */ fun openMenu(@MenuRes menuRes: Int, artist: Artist) { - T.d("Opening menu for $artist") + L.d("Opening menu for $artist") openImpl(Menu.ForArtist(menuRes, artist)) } @@ -190,7 +190,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param genre The [Genre] to show. */ fun openMenu(@MenuRes menuRes: Int, genre: Genre) { - T.d("Opening menu for $genre") + L.d("Opening menu for $genre") openImpl(Menu.ForGenre(menuRes, genre)) } @@ -202,7 +202,7 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param playlist The [Playlist] to show. */ fun openMenu(@MenuRes menuRes: Int, playlist: Playlist) { - T.d("Opening menu for $playlist") + L.d("Opening menu for $playlist") openImpl(Menu.ForPlaylist(menuRes, playlist)) } @@ -214,14 +214,14 @@ constructor(private val listSettings: ListSettings, private val musicRepository: * @param songs The [Song] selection to show. */ fun openMenu(@MenuRes menuRes: Int, songs: List) { - T.d("Opening menu for ${songs.size} songs") + L.d("Opening menu for ${songs.size} songs") openImpl(Menu.ForSelection(menuRes, songs)) } private fun openImpl(menu: Menu) { val existing = _menu.flow.value if (existing != null) { - T.w("Already opening $existing, ignoring $menu") + L.w("Already opening $existing, ignoring $menu") return } _menu.put(menu) diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt index 34b219a64..4a5da90ca 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import java.util.concurrent.Executor +import timber.log.Timber as L /** * A variant of ListDiffer with more flexible updates. @@ -56,7 +57,7 @@ abstract class FlexibleListAdapter( instructions: UpdateInstructions?, callback: (() -> Unit)? = null ) { - T.d("Updating list to ${newList.size} items with $instructions") + L.d("Updating list to ${newList.size} items with $instructions") differ.update(newList, instructions, callback) } } @@ -170,7 +171,7 @@ private class FlexibleListDiffer( ) { // fast simple remove all if (newList.isEmpty()) { - T.d("Short-circuiting diff to remove all") + L.d("Short-circuiting diff to remove all") val countRemoved = oldList.size currentList = emptyList() // notify last, after list is updated @@ -181,7 +182,7 @@ private class FlexibleListDiffer( // fast simple first insert if (oldList.isEmpty()) { - T.d("Short-circuiting diff to insert all") + L.d("Short-circuiting diff to insert all") currentList = newList // notify last, after list is updated updateCallback.onInserted(0, newList.size) @@ -243,7 +244,7 @@ private class FlexibleListDiffer( mainThreadExecutor.execute { if (maxScheduledGeneration == runGeneration) { - T.d("Applying calculated diff") + L.d("Applying calculated diff") currentList = newList result.dispatchUpdatesTo(updateCallback) callback?.invoke() diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt index d494da0a1..8d1721b46 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt @@ -21,6 +21,7 @@ package org.oxycblt.auxio.list.adapter import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import timber.log.Timber as L /** * A [RecyclerView.Adapter] that supports indicating the playback status of a particular item. @@ -57,7 +58,7 @@ abstract class PlayingIndicatorAdapter( * @param isPlaying Whether playback is ongoing or paused. */ fun setPlaying(item: T?, isPlaying: Boolean) { - T.d("Updating playing item [old: $currentItem new: $item]") + L.d("Updating playing item [old: $currentItem new: $item]") var updatedItem = false if (currentItem != item) { @@ -70,7 +71,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - T.w("oldItem was not in adapter data") + L.w("oldItem was not in adapter data") } } @@ -80,7 +81,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - T.w("newItem was not in adapter data") + L.w("newItem was not in adapter data") } } @@ -98,7 +99,7 @@ abstract class PlayingIndicatorAdapter( if (pos > -1) { notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED) } else { - T.w("newItem was not in adapter data") + L.w("newItem was not in adapter data") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt index 65b9bcef1..1c43ca72b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt @@ -22,6 +22,7 @@ import android.view.View import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.Music +import timber.log.Timber as L /** * A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of @@ -54,7 +55,7 @@ abstract class SelectionIndicatorAdapter( // Nothing to do. return } - T.d("Updating selection [old=${oldSelectedItems.size} new=${newSelectedItems.size}") + L.d("Updating selection [old=${oldSelectedItems.size} new=${newSelectedItems.size}") selectedItems = newSelectedItems for (i in currentList.indices) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt index ae8e49aaa..62a846145 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.ui.ViewBindingBottomSheetDialogFragment import org.oxycblt.auxio.util.collectImmediately -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingBottomSheetDialogFragment] that displays basic music information and a series of @@ -102,7 +102,7 @@ abstract class MenuDialogFragment : private fun updateMenu(menu: Menu?) { if (menu == null) { - T.d("No menu to show, navigating away") + L.d("No menu to show, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt index 220022b7b..387f8dc80 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.playback.PlaySong -import timber.log.Timber as T +import timber.log.Timber as L /** * Manages the state information for [MenuDialogFragment] implementations. @@ -55,7 +55,7 @@ class MenuViewModel @Inject constructor(private val musicRepository: MusicReposi fun setMenu(parcel: Menu.Parcel) { _currentMenu.value = unpackParcel(parcel) if (_currentMenu.value == null) { - T.w("Given menu parcel $parcel was invalid") + L.w("Given menu parcel $parcel was invalid") } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt index daa032a59..9b94234c7 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt @@ -34,7 +34,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.list.recycler.MaterialDragCallback.ViewHolder import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getInteger -import timber.log.Timber as T +import timber.log.Timber as L /** * A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in editable UIs, @@ -94,7 +94,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { // this is only done once when the item is initially picked up. // TODO: I think this is possible to improve with a raw ValueAnimator. if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) { - T.d("Lifting ViewHolder") + L.d("Lifting ViewHolder") val bg = holder.background val elevation = recyclerView.context.getDimen(MR.dimen.m3_sys_elevation_level4) @@ -136,7 +136,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { // This function can be called multiple times, so only start the animation when the view's // translationZ is already non-zero. if (holder.root.translationZ != 0f) { - T.d("Lifting ViewHolder") + L.d("Lifting ViewHolder") val bg = holder.background val elevation = recyclerView.context.getDimen(MR.dimen.m3_sys_elevation_level4) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index e7916d02b..359afd9c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -22,6 +22,9 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import androidx.room.TypeConverter +import java.security.MessageDigest +import java.util.UUID +import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.image.extractor.Cover @@ -36,9 +39,6 @@ import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.toUuidOrNull -import java.security.MessageDigest -import java.util.UUID -import kotlin.math.max /** * Abstract music data. This contains universal information about all concrete music diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 89c61db05..1de4de180 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -44,7 +44,7 @@ import org.oxycblt.auxio.music.user.MutableUserLibrary import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.util.DEFAULT_TIMEOUT import org.oxycblt.auxio.util.forEachWithTimeout -import timber.log.Timber as T +import timber.log.Timber as L /** * Primary manager of music information and loading. @@ -242,51 +242,51 @@ constructor( @Synchronized override fun addUpdateListener(listener: MusicRepository.UpdateListener) { - T.d("Adding $listener to update listeners") + L.d("Adding $listener to update listeners") updateListeners.add(listener) listener.onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = true)) } @Synchronized override fun removeUpdateListener(listener: MusicRepository.UpdateListener) { - T.d("Removing $listener to update listeners") + L.d("Removing $listener to update listeners") if (!updateListeners.remove(listener)) { - T.w("Update listener $listener was not added prior, cannot remove") + L.w("Update listener $listener was not added prior, cannot remove") } } @Synchronized override fun addIndexingListener(listener: MusicRepository.IndexingListener) { - T.d("Adding $listener to indexing listeners") + L.d("Adding $listener to indexing listeners") indexingListeners.add(listener) listener.onIndexingStateChanged() } @Synchronized override fun removeIndexingListener(listener: MusicRepository.IndexingListener) { - T.d("Removing $listener from indexing listeners") + L.d("Removing $listener from indexing listeners") if (!indexingListeners.remove(listener)) { - T.w("Indexing listener $listener was not added prior, cannot remove") + L.w("Indexing listener $listener was not added prior, cannot remove") } } @Synchronized override fun registerWorker(worker: MusicRepository.IndexingWorker) { if (indexingWorker != null) { - T.w("Worker is already registered") + L.w("Worker is already registered") return } - T.d("Registering worker $worker") + L.d("Registering worker $worker") indexingWorker = worker } @Synchronized override fun unregisterWorker(worker: MusicRepository.IndexingWorker) { if (indexingWorker !== worker) { - T.w("Given worker did not match current worker") + L.w("Given worker did not match current worker") return } - T.d("Unregistering worker $worker") + L.d("Unregistering worker $worker") indexingWorker = null currentIndexingState = null } @@ -298,42 +298,42 @@ constructor( override suspend fun createPlaylist(name: String, songs: List) { val userLibrary = synchronized(this) { userLibrary ?: return } - T.d("Creating playlist $name with ${songs.size} songs") + L.d("Creating playlist $name with ${songs.size} songs") userLibrary.createPlaylist(name, songs) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun renamePlaylist(playlist: Playlist, name: String) { val userLibrary = synchronized(this) { userLibrary ?: return } - T.d("Renaming $playlist to $name") + L.d("Renaming $playlist to $name") userLibrary.renamePlaylist(playlist, name) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun deletePlaylist(playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } - T.d("Deleting $playlist") + L.d("Deleting $playlist") userLibrary.deletePlaylist(playlist) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun addToPlaylist(songs: List, playlist: Playlist) { val userLibrary = synchronized(this) { userLibrary ?: return } - T.d("Adding ${songs.size} songs to $playlist") + L.d("Adding ${songs.size} songs to $playlist") userLibrary.addToPlaylist(playlist, songs) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { val userLibrary = synchronized(this) { userLibrary ?: return } - T.d("Rewriting $playlist with ${songs.size} songs") + L.d("Rewriting $playlist with ${songs.size} songs") userLibrary.rewritePlaylist(playlist, songs) withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } @Synchronized override fun requestIndex(withCache: Boolean) { - T.d("Requesting index operation [cache=$withCache]") + L.d("Requesting index operation [cache=$withCache]") indexingWorker?.requestIndex(withCache) } @@ -345,13 +345,13 @@ constructor( indexImpl(context, scope, withCache) } catch (e: CancellationException) { // Got cancelled, propagate upwards to top-level co-routine. - T.d("Loading routine was cancelled") + L.d("Loading routine was cancelled") throw e } catch (e: Exception) { // Music loading process failed due to something we have not handled. // TODO: Still want to display this error eventually - T.e("Music indexing failed") - T.e(e.stackTraceToString()) + L.e("Music indexing failed") + L.e(e.stackTraceToString()) emitIndexingCompletion(e) } } @@ -364,7 +364,7 @@ constructor( // done at the UI level, but that intertwines logic and display too much. if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == PackageManager.PERMISSION_DENIED) { - T.e("Permissions were not granted") + L.e("Permissions were not granted") throw NoAudioPermissionException() } @@ -383,7 +383,7 @@ constructor( // to figure out what songs are (probably) on the device, and the latter will be needed // for discovery (described later). These have no shared state, so they are done in // parallel. - T.d("Starting MediaStore query") + L.d("Starting MediaStore query") emitIndexingProgress(IndexingProgress.Indeterminate) val mediaStoreQueryJob = @@ -404,18 +404,18 @@ constructor( // identical to calling async. val cache = if (withCache) { - T.d("Reading cache") + L.d("Reading cache") cacheRepository.readCache() } else { null } - T.d("Awaiting MediaStore query") + L.d("Awaiting MediaStore query") val query = mediaStoreQueryJob.await().getOrThrow() // We now have all the information required to start the "discovery" process. This // is the point at which Auxio starts scanning each file given from MediaStore and // transforming it into a music library. MediaStore normally - T.d("Starting discovery") + L.d("Starting discovery") val incompleteSongs = Channel(Channel.UNLIMITED) // Not fully populated w/metadata val completeSongs = Channel(Channel.UNLIMITED) // Populated with quality metadata val processedSongs = Channel(Channel.UNLIMITED) // Transformed into SongImpl @@ -423,7 +423,7 @@ constructor( // MediaStoreExtractor discovers all music on the device, and forwards them to either // DeviceLibrary if cached metadata exists for it, or TagExtractor if cached metadata // does not exist. In the latter situation, it also applies it's own (inferior) metadata. - T.d("Starting MediaStore discovery") + L.d("Starting MediaStore discovery") val mediaStoreJob = scope.async { try { @@ -432,7 +432,7 @@ constructor( // To prevent a deadlock, we want to close the channel with an exception // to cascade to and cancel all other routines before finally bubbling up // to the main extractor loop. - T.e("MediaStore extraction failed: $e") + L.e("MediaStore extraction failed: $e") incompleteSongs.close( Exception("MediaStore extraction failed: ${e.stackTraceToString()}")) return@async @@ -442,13 +442,13 @@ constructor( // TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date // metadata for them, and then forwards it to DeviceLibrary. - T.d("Starting tag extraction") + L.d("Starting tag extraction") val tagJob = scope.async { try { tagExtractor.consume(incompleteSongs, completeSongs) } catch (e: Exception) { - T.e("Tag extraction failed: $e") + L.e("Tag extraction failed: $e") completeSongs.close( Exception("Tag extraction failed: ${e.stackTraceToString()}")) return@async @@ -458,7 +458,7 @@ constructor( // DeviceLibrary constructs music parent instances as song information is provided, // and then forwards them to the primary loading loop. - T.d("Starting DeviceLibrary creation") + L.d("Starting DeviceLibrary creation") val deviceLibraryJob = scope.async(Dispatchers.Default) { val deviceLibrary = @@ -466,7 +466,7 @@ constructor( deviceLibraryFactory.create( completeSongs, processedSongs, separators, nameFactory) } catch (e: Exception) { - T.e("DeviceLibrary creation failed: $e") + L.e("DeviceLibrary creation failed: $e") processedSongs.close( Exception("DeviceLibrary creation failed: ${e.stackTraceToString()}")) return@async Result.failure(e) @@ -497,14 +497,14 @@ constructor( // that the short-circuit occurs so quickly as to break the UI. // TODO: Do not error, instead just wipe the entire library. if (rawSongs.isEmpty()) { - T.e("Music library was empty") + L.e("Music library was empty") throw NoMusicException() } // Now that the library is effectively loaded, we can start the finalization step, which // involves writing new cache information and creating more music data that is derived // from the library (e.g playlists) - T.d("Discovered ${rawSongs.size} songs, starting finalization") + L.d("Discovered ${rawSongs.size} songs, starting finalization") // We have no idea how long the cache will take, and the playlist construction // will be too fast to indicate, so switch back to an indeterminate state. @@ -513,7 +513,7 @@ constructor( // The UserLibrary job is split into a query and construction step, a la MediaStore. // This way, we can start working on playlists even as DeviceLibrary might still be // working on parent information. - T.d("Starting UserLibrary query") + L.d("Starting UserLibrary query") val userLibraryQueryJob = scope.async { val rawPlaylists = @@ -530,20 +530,20 @@ constructor( // since the playlist read will probably take some time. // TODO: Read/write from the cache incrementally instead of in bulk? if (cache == null || cache.invalidated) { - T.d("Writing cache [why=${cache?.invalidated}]") + L.d("Writing cache [why=${cache?.invalidated}]") cacheRepository.writeCache(rawSongs) } // Create UserLibrary once we finally get the required components for it. - T.d("Awaiting UserLibrary query") + L.d("Awaiting UserLibrary query") val rawPlaylists = userLibraryQueryJob.await().getOrThrow() - T.d("Awaiting DeviceLibrary creation") + L.d("Awaiting DeviceLibrary creation") val deviceLibrary = deviceLibraryJob.await().getOrThrow() - T.d("Starting UserLibrary creation") + L.d("Starting UserLibrary creation") val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary, nameFactory) // Loading process is functionally done, indicate such - T.d( + L.d( "Successfully indexed music library [device=$deviceLibrary " + "user=$userLibrary time=${System.currentTimeMillis() - start}]") emitIndexingCompletion(null) @@ -559,7 +559,7 @@ constructor( deviceLibraryChanged = this.deviceLibrary != deviceLibrary userLibraryChanged = this.userLibrary != userLibrary if (!deviceLibraryChanged && !userLibraryChanged) { - T.d("Library has not changed, skipping update") + L.d("Library has not changed, skipping update") return } @@ -589,7 +589,7 @@ constructor( synchronized(this) { previousCompletedState = IndexingState.Completed(error) currentIndexingState = null - T.d("Dispatching completion state [error=$error]") + L.d("Dispatching completion state [error=$error]") for (listener in indexingListeners) { listener.onIndexingStateChanged() } @@ -599,7 +599,7 @@ constructor( @Synchronized private fun dispatchLibraryChange(device: Boolean, user: Boolean) { val changes = MusicRepository.Changes(device, user) - T.d("Dispatching library change [changes=$changes]") + L.d("Dispatching library change [changes=$changes]") for (listener in updateListeners) { listener.onMusicChanges(changes) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index c937abfe0..2b11b885a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.dirs.MusicDirectories import org.oxycblt.auxio.music.fs.DocumentPathFactory import org.oxycblt.auxio.settings.Settings -import timber.log.Timber as T +import timber.log.Timber as L /** * User configuration specific to music system. @@ -108,11 +108,11 @@ constructor( getString(R.string.set_key_music_dirs_include), getString(R.string.set_key_separators), getString(R.string.set_key_auto_sort_names) -> { - T.d("Dispatching indexing setting change for $key") + L.d("Dispatching indexing setting change for $key") listener.onIndexingSettingChanged() } getString(R.string.set_key_observing) -> { - T.d("Dispatching observing setting change") + L.d("Dispatching observing setting change") listener.onObservingChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index b81a766b3..63f56fff7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.music.external.ExportConfig import org.oxycblt.auxio.music.external.ExternalPlaylistManager import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewModel] providing data specific to the music loading process. @@ -93,7 +93,7 @@ constructor( deviceLibrary.artists.size, deviceLibrary.genres.size, deviceLibrary.songs.sumOf { it.durationMs }) - T.d("Updated statistics: ${_statistics.value}") + L.d("Updated statistics: ${_statistics.value}") } override fun onIndexingStateChanged() { @@ -102,13 +102,13 @@ constructor( /** Requests that the music library should be re-loaded while leveraging the cache. */ fun refresh() { - T.d("Refreshing library") + L.d("Refreshing library") musicRepository.requestIndex(true) } /** Requests that the music library be re-loaded without the cache. */ fun rescan() { - T.d("Rescanning library") + L.d("Rescanning library") musicRepository.requestIndex(false) } @@ -126,7 +126,7 @@ constructor( reason: PlaylistDecision.New.Reason = PlaylistDecision.New.Reason.NEW ) { if (name != null) { - T.d("Creating $name with ${songs.size} songs]") + L.d("Creating $name with ${songs.size} songs]") viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) val message = @@ -138,7 +138,7 @@ constructor( _playlistMessage.put(message) } } else { - T.d("Launching creation dialog for ${songs.size} songs") + L.d("Launching creation dialog for ${songs.size} songs") _playlistDecision.put(PlaylistDecision.New(songs, null, reason)) } } @@ -157,7 +157,7 @@ constructor( viewModelScope.launch(Dispatchers.IO) { val importedPlaylist = externalPlaylistManager.import(uri) if (importedPlaylist == null) { - T.e("Could not import playlist") + L.e("Could not import playlist") _playlistMessage.put(PlaylistMessage.ImportFailed) return@launch } @@ -169,7 +169,7 @@ constructor( } if (songs.isEmpty()) { - T.e("No songs found") + L.e("No songs found") _playlistMessage.put(PlaylistMessage.ImportFailed) return@launch } @@ -193,7 +193,7 @@ constructor( } } } else { - T.d("Launching import picker") + L.d("Launching import picker") _playlistDecision.put(PlaylistDecision.Import(target)) } } @@ -206,7 +206,7 @@ constructor( */ fun exportPlaylist(playlist: Playlist, uri: Uri? = null, config: ExportConfig? = null) { if (uri != null && config != null) { - T.d("Exporting playlist to $uri") + L.d("Exporting playlist to $uri") viewModelScope.launch(Dispatchers.IO) { if (externalPlaylistManager.export(playlist, uri, config)) { _playlistMessage.put(PlaylistMessage.ExportSuccess) @@ -215,7 +215,7 @@ constructor( } } } else { - T.d("Launching export dialog") + L.d("Launching export dialog") _playlistDecision.put(PlaylistDecision.Export(playlist)) } } @@ -237,7 +237,7 @@ constructor( reason: PlaylistDecision.Rename.Reason = PlaylistDecision.Rename.Reason.ACTION ) { if (name != null) { - T.d("Renaming $playlist to $name") + L.d("Renaming $playlist to $name") viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) if (applySongs.isNotEmpty()) { @@ -251,7 +251,7 @@ constructor( _playlistMessage.put(message) } } else { - T.d("Launching rename dialog for $playlist") + L.d("Launching rename dialog for $playlist") _playlistDecision.put(PlaylistDecision.Rename(playlist, null, applySongs, reason)) } } @@ -266,13 +266,13 @@ constructor( */ fun deletePlaylist(playlist: Playlist, rude: Boolean = false) { if (rude) { - T.d("Deleting $playlist") + L.d("Deleting $playlist") viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) _playlistMessage.put(PlaylistMessage.DeleteSuccess) } } else { - T.d("Launching deletion dialog for $playlist") + L.d("Launching deletion dialog for $playlist") _playlistDecision.put(PlaylistDecision.Delete(playlist)) } } @@ -284,7 +284,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(song: Song, playlist: Playlist? = null) { - T.d("Adding $song to playlist") + L.d("Adding $song to playlist") addToPlaylist(listOf(song), playlist) } @@ -295,7 +295,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(album: Album, playlist: Playlist? = null) { - T.d("Adding $album to playlist") + L.d("Adding $album to playlist") addToPlaylist(listSettings.albumSongSort.songs(album.songs), playlist) } @@ -306,7 +306,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(artist: Artist, playlist: Playlist? = null) { - T.d("Adding $artist to playlist") + L.d("Adding $artist to playlist") addToPlaylist(listSettings.artistSongSort.songs(artist.songs), playlist) } @@ -317,7 +317,7 @@ constructor( * @param playlist The [Playlist] to add to. If null, the user will be prompted for one. */ fun addToPlaylist(genre: Genre, playlist: Playlist? = null) { - T.d("Adding $genre to playlist") + L.d("Adding $genre to playlist") addToPlaylist(listSettings.genreSongSort.songs(genre.songs), playlist) } @@ -329,13 +329,13 @@ constructor( */ fun addToPlaylist(songs: List, playlist: Playlist? = null) { if (playlist != null) { - T.d("Adding ${songs.size} songs to $playlist") + L.d("Adding ${songs.size} songs to $playlist") viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) _playlistMessage.put(PlaylistMessage.AddSuccess) } } else { - T.d("Launching addition dialog for songs=${songs.size}") + L.d("Launching addition dialog for songs=${songs.size}") _playlistDecision.put(PlaylistDecision.Add(songs)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt index d99c61ae5..0621b6ee7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt @@ -20,7 +20,7 @@ package org.oxycblt.auxio.music.cache import javax.inject.Inject import org.oxycblt.auxio.music.device.RawSong -import timber.log.Timber as T +import timber.log.Timber as L /** * A repository allowing access to cached metadata obtained in prior music loading operations. @@ -50,11 +50,11 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached // Faster to load the whole database into memory than do a query on each // populate call. val songs = cachedSongsDao.readSongs() - T.d("Successfully read ${songs.size} songs from cache") + L.d("Successfully read ${songs.size} songs from cache") CacheImpl(songs) } catch (e: Exception) { - T.e("Unable to load cache database.") - T.e(e.stackTraceToString()) + L.e("Unable to load cache database.") + L.e(e.stackTraceToString()) null } @@ -62,12 +62,12 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached try { // Still write out whatever data was extracted. cachedSongsDao.nukeSongs() - T.d("Successfully deleted old cache") + L.d("Successfully deleted old cache") cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw)) - T.d("Successfully wrote ${rawSongs.size} songs to cache") + L.d("Successfully wrote ${rawSongs.size} songs to cache") } catch (e: Exception) { - T.e("Unable to save cache database.") - T.e(e.stackTraceToString()) + L.e("Unable to save cache database.") + L.e(e.stackTraceToString()) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/AddToPlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/AddToPlaylistDialog.kt index 22c9688e0..0ac15185d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/AddToPlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/AddToPlaylistDialog.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.navigateSafe -import timber.log.Timber as T +import timber.log.Timber as L /** * A dialog that allows the user to pick a specific playlist to add song(s) to. @@ -105,7 +105,7 @@ class AddToPlaylistDialog : private fun updatePendingSongs(songs: List?) { if (songs == null) { - T.d("No songs to show choices for, navigating away") + L.d("No songs to show choices for, navigating away") findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt index b7e20adb7..4b0d04af4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/DeletePlaylistDialog.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingMaterialDialogFragment] that asks the user to confirm the deletion of a [Playlist]. @@ -76,7 +76,7 @@ class DeletePlaylistDialog : ViewBindingMaterialDialogFragment if (uri == null) { - T.w("No URI returned from file picker") + L.w("No URI returned from file picker") return@registerForActivityResult } val playlist = pickerModel.currentPlaylistToExport.value if (playlist == null) { - T.w("No playlist to export") + L.w("No playlist to export") findNavController().navigateUp() return@registerForActivityResult } - T.d("Received playlist URI $uri") + L.d("Received playlist URI $uri") musicModel.exportPlaylist(playlist, uri, pickerModel.currentExportConfig.value) findNavController().navigateUp() } @@ -128,7 +128,7 @@ class ExportPlaylistDialog : ViewBindingMaterialDialogFragment @@ -108,7 +108,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M .ifEmpty { null } .also { refreshChoicesWith = it } } - T.d("Updated songs to add: ${_currentSongsToAdd.value?.size} songs") + L.d("Updated songs to add: ${_currentSongsToAdd.value?.size} songs") } val chosenName = _chosenName.value @@ -120,7 +120,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M // Nothing to do. } } - T.d("Updated chosen name to $chosenName") + L.d("Updated chosen name to $chosenName") refreshChoicesWith = refreshChoicesWith ?: _currentSongsToAdd.value // TODO: Add music syncing for other playlist states here @@ -129,7 +129,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M _currentPlaylistToExport.value?.let { playlist -> musicRepository.userLibrary?.findPlaylist(playlist.uid) } - T.d("Updated playlist to export to ${_currentPlaylistToExport.value}") + L.d("Updated playlist to export to ${_currentPlaylistToExport.value}") } refreshChoicesWith?.let(::refreshPlaylistChoices) @@ -152,7 +152,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M template: String?, reason: PlaylistDecision.New.Reason ) { - T.d("Opening ${songUids.size} songs to create a playlist from") + L.d("Opening ${songUids.size} songs to create a playlist from") val userLibrary = musicRepository.userLibrary ?: return val songs = musicRepository.deviceLibrary @@ -166,10 +166,10 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M var possibleName: String do { possibleName = context.getString(R.string.fmt_def_playlist, i) - T.d("Trying $possibleName as a playlist name") + L.d("Trying $possibleName as a playlist name") ++i } while (userLibrary.playlists.any { it.name.resolve(context) == possibleName }) - T.d("$possibleName is unique, using it as the playlist name") + L.d("$possibleName is unique, using it as the playlist name") possibleName } @@ -177,7 +177,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M if (possibleName != null && songs != null) { PendingNewPlaylist(possibleName, songs, template, reason) } else { - T.w("Given song UIDs to create were invalid") + L.w("Given song UIDs to create were invalid") null } } @@ -193,7 +193,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M template: String?, reason: PlaylistDecision.Rename.Reason ) { - T.d("Opening playlist $playlistUid to rename") + L.d("Opening playlist $playlistUid to rename") val playlist = musicRepository.userLibrary?.findPlaylist(playlistUid) val applySongs = musicRepository.deviceLibrary?.let { applySongUids.mapNotNull(it::findSong) } @@ -202,7 +202,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M if (playlist != null && applySongs != null) { PendingRenamePlaylist(playlist, applySongs, template, reason) } else { - T.w("Given playlist UID to rename was invalid") + L.w("Given playlist UID to rename was invalid") null } } @@ -213,12 +213,12 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param playlistUid The [Music.UID] of the [Playlist] to export. */ fun setPlaylistToExport(playlistUid: Music.UID) { - T.d("Opening playlist $playlistUid to export") + L.d("Opening playlist $playlistUid to export") // TODO: Add this guard to the rest of the methods here if (_currentPlaylistToExport.value?.uid == playlistUid) return _currentPlaylistToExport.value = musicRepository.userLibrary?.findPlaylist(playlistUid) if (_currentPlaylistToExport.value == null) { - T.w("Given playlist UID to export was invalid") + L.w("Given playlist UID to export was invalid") } else { _currentExportConfig.value = DEFAULT_EXPORT_CONFIG } @@ -230,7 +230,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param exportConfig The new [ExportConfig] to use. */ fun setExportConfig(exportConfig: ExportConfig) { - T.d("Setting export config to $exportConfig") + L.d("Setting export config to $exportConfig") _currentExportConfig.value = exportConfig } @@ -240,10 +240,10 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param playlistUid The [Music.UID] of the [Playlist] to delete. */ fun setPlaylistToDelete(playlistUid: Music.UID) { - T.d("Opening playlist $playlistUid to delete") + L.d("Opening playlist $playlistUid to delete") _currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid) if (_currentPlaylistToDelete.value == null) { - T.w("Given playlist UID to delete was invalid") + L.w("Given playlist UID to delete was invalid") } } @@ -253,25 +253,25 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param name The new user-inputted name, or null if not present. */ fun updateChosenName(name: String?) { - T.d("Updating chosen name to $name") + L.d("Updating chosen name to $name") _chosenName.value = when { name.isNullOrEmpty() -> { - T.e("Chosen name is empty") + L.e("Chosen name is empty") ChosenName.Empty } name.isBlank() -> { - T.e("Chosen name is blank") + L.e("Chosen name is blank") ChosenName.Blank } else -> { val trimmed = name.trim() val userLibrary = musicRepository.userLibrary if (userLibrary != null && userLibrary.findPlaylist(trimmed) == null) { - T.d("Chosen name is valid") + L.d("Chosen name is valid") ChosenName.Valid(trimmed) } else { - T.d("Chosen name already exists in library") + L.d("Chosen name already exists in library") ChosenName.AlreadyExists(trimmed) } } @@ -284,19 +284,19 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M * @param songUids The [Music.UID]s of songs to add to a playlist. */ fun setSongsToAdd(songUids: Array) { - T.d("Opening ${songUids.size} songs to add to a playlist") + L.d("Opening ${songUids.size} songs to add to a playlist") _currentSongsToAdd.value = musicRepository.deviceLibrary ?.let { songUids.mapNotNull(it::findSong).ifEmpty { null } } ?.also(::refreshPlaylistChoices) if (_currentSongsToAdd.value == null || songUids.size != _currentSongsToAdd.value?.size) { - T.w("Given song UIDs to add were (partially) invalid") + L.w("Given song UIDs to add were (partially) invalid") } } private fun refreshPlaylistChoices(songs: List) { val userLibrary = musicRepository.userLibrary ?: return - T.d("Refreshing playlist choices") + L.d("Refreshing playlist choices") _playlistAddChoices.value = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).playlists(userLibrary.playlists).map { val songSet = it.songs.toSet() diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt index f3ef3f9b1..8e103671b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/RenamePlaylistDialog.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A dialog allowing the name of a new playlist to be chosen before committing it to the database. @@ -94,7 +94,7 @@ class RenamePlaylistDialog : ViewBindingMaterialDialogFragment) { - T.d("Adding ${path.size} directories") + L.d("Adding ${path.size} directories") val oldLastIndex = path.lastIndex _dirs.addAll(path) notifyItemRangeInserted(oldLastIndex, path.size) @@ -78,7 +78,7 @@ class DirectoryAdapter(private val listener: Listener) : * @param path The [Path] to remove. Must exist in the list. */ fun remove(path: Path) { - T.d("Removing $path") + L.d("Removing $path") val idx = _dirs.indexOf(path) _dirs.removeAt(idx) notifyItemRemoved(idx) diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt index 85ef0a682..fd433ba57 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.fs.DocumentPathFactory import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.showToast -import timber.log.Timber as T +import timber.log.Timber as L /** * Dialog that manages the music dirs setting. @@ -62,7 +62,7 @@ class MusicDirsDialog : .setPositiveButton(R.string.lbl_save) { _, _ -> val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) if (musicSettings.musicDirs != newDirs) { - T.d("Committing changes") + L.d("Committing changes") musicSettings.musicDirs = newDirs } } @@ -76,7 +76,7 @@ class MusicDirsDialog : binding.dirsAdd.apply { ViewCompat.setTooltipText(this, contentDescription) setOnClickListener { - T.d("Opening launcher") + L.d("Opening launcher") val launcher = requireNotNull(openDocumentTreeLauncher) { "Document tree launcher was not available" @@ -150,7 +150,7 @@ class MusicDirsDialog : private fun addDocumentTreeUriToDirs(uri: Uri?) { if (uri == null) { // A null URI means that the user left the file picker without picking a directory - T.d("No URI given (user closed the dialog)") + L.d("No URI given (user closed the dialog)") return } diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt index a77676bbb..1ba167f47 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.music.fs.Components import org.oxycblt.auxio.music.fs.DocumentPathFactory import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.contentResolverSafe -import timber.log.Timber as T +import timber.log.Timber as L /** * Generic playlist file importing abstraction. @@ -108,7 +108,7 @@ constructor( return ImportedPlaylist(newName, imported.paths) } } catch (e: Exception) { - T.e("Failed to import playlist: $e") + L.e("Failed to import playlist: $e") null } } @@ -124,7 +124,7 @@ constructor( return try { val outputStream = context.contentResolverSafe.openOutputStream(uri) if (outputStream == null) { - T.e("Failed to export playlist: Could not open output stream") + L.e("Failed to export playlist: Could not open output stream") return false } outputStream.use { @@ -132,7 +132,7 @@ constructor( true } } catch (e: Exception) { - T.e("Failed to export playlist: $e") + L.e("Failed to export playlist: $e") false } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt index 3cfd30487..717f43619 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt @@ -34,7 +34,7 @@ import org.oxycblt.auxio.music.fs.VolumeManager import org.oxycblt.auxio.music.metadata.correctWhitespace import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * Minimal M3U file format implementation. @@ -116,7 +116,7 @@ constructor( } if (path == null) { - T.e("Expected a path, instead got an EOF") + L.e("Expected a path, instead got an EOF") break@consumeFile } @@ -261,7 +261,7 @@ constructor( } commonIndex == components.size -> { // The working directory is deeper in the path, backtrack. - for (i in 0.. { // The paths are siblings. Backtrack and append as needed. - for (i in 0..() @@ -194,8 +194,8 @@ private class MediaStoreExtractorImpl( } } - T.d("Read ${genreNamesMap.values.distinct().size} genres from MediaStore") - T.d("Finished initialization in ${System.currentTimeMillis() - start}ms") + L.d("Read ${genreNamesMap.values.distinct().size} genres from MediaStore") + L.d("Finished initialization in ${System.currentTimeMillis() - start}ms") return QueryImpl( cursor, mediaStorePathInterpreterFactory.wrap(cursor), diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt index 9b57a9fab..50793e8e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt @@ -21,7 +21,7 @@ package org.oxycblt.auxio.music.fs import android.database.Cursor import android.os.Build import android.provider.MediaStore -import timber.log.Timber as T +import timber.log.Timber as L /** * Wrapper around a [Cursor] that interprets path information on a per-API/manufacturer basis. @@ -112,7 +112,7 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) : } } - T.e("Could not find volume for $data [tried: ${volumes.map { it.components }}]") + L.e("Could not find volume for $data [tried: ${volumes.map { it.components }}]") return null } @@ -181,7 +181,7 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) : val displayName = cursor.getString(displayNameIndex) val volume = volumes.find { it.mediaStoreName == volumeName } if (volume == null) { - T.e( + L.e( "Could not find volume for $volumeName:$relativePath/$displayName [tried: ${volumes.map { it.mediaStoreName }}]") return null } diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt index 7b60248df..a42ebc16b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt @@ -25,7 +25,7 @@ import kotlin.math.max import org.oxycblt.auxio.R import org.oxycblt.auxio.util.inRangeOrNull import org.oxycblt.auxio.util.positiveOrNull -import timber.log.Timber as T +import timber.log.Timber as L /** * An ISO-8601/RFC 3339 Date. @@ -64,7 +64,7 @@ class Date private constructor(private val tokens: List) : Comparable try { format.parse("$year-$month") } catch (e: ParseException) { - T.e("Unable to parse fine-grained date: $e") + L.e("Unable to parse fine-grained date: $e") return null } diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index 30f4564c8..289e4e59f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -62,8 +62,7 @@ sealed interface Name : Comparable { sortTokens .firstOrNull() ?.run { collationKey.sourceString.firstOrNull() } - ?.let { if (it.isDigit()) "#" else it.uppercase() } - ?: "?" + ?.let { if (it.isDigit()) "#" else it.uppercase() } ?: "?" final override fun resolve(context: Context) = raw diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt index c18db8d0c..6dec4e22d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioProperties.kt @@ -25,7 +25,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.MimeType -import timber.log.Timber as T +import timber.log.Timber as L /** * The properties of a [Song]'s file. @@ -73,8 +73,8 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. // Can feasibly fail with invalid file formats. Note that this isn't considered // an error condition in the UI, as there is still plenty of other song information // that we can show. - T.w("Unable to extract song attributes.") - T.w(e.stackTraceToString()) + L.w("Unable to extract song attributes.") + L.w(e.stackTraceToString()) return AudioProperties(null, null, song.mimeType) } @@ -90,7 +90,7 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. // Convert bytes-per-second to kilobytes-per-second. format.getInteger(MediaFormat.KEY_BIT_RATE) / 1000 } catch (e: NullPointerException) { - T.d("Unable to extract bit rate field") + L.d("Unable to extract bit rate field") null } @@ -98,7 +98,7 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. try { format.getInteger(MediaFormat.KEY_SAMPLE_RATE) } catch (e: NullPointerException) { - T.e("Unable to extract sample rate field") + L.e("Unable to extract sample rate field") null } @@ -108,13 +108,13 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties. try { format.getString(MediaFormat.KEY_MIME) } catch (e: NullPointerException) { - T.e("Unable to extract mime type field") + L.e("Unable to extract mime type field") null } extractor.release() - T.d("Finished extracting audio properties") + L.d("Finished extracting audio properties") return AudioProperties( bitrate, diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt index 1a4bfb0c7..49cbca524 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingMaterialDialogFragment] that allows the user to configure the separator characters @@ -76,7 +76,7 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment binding.separatorSlash.isChecked = true Separators.PLUS -> binding.separatorPlus.isChecked = true Separators.AND -> binding.separatorAnd.isChecked = true - else -> T.w("Unexpected separator in settings data") + else -> L.w("Unexpected separator in settings data") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt index bd6b9c725..1d294e04f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt @@ -42,7 +42,7 @@ import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.util.forEachWithTimeout import org.oxycblt.auxio.util.sendWithTimeout -import timber.log.Timber as T +import timber.log.Timber as L class TagExtractor @Inject @@ -66,7 +66,7 @@ constructor(private val mediaSourceFactory: Factory, private val tagInterpreter: songsIn++ } - T.d("All incomplete songs exhausted, starting cleanup loop") + L.d("All incomplete songs exhausted, starting cleanup loop") while (!worker.idle()) { val completeRawSong = worker.pull() if (completeRawSong != null) { @@ -152,8 +152,8 @@ private class MetadataWorker( try { tagInterpreter.interpret(job.rawSong, job.future.get()) } catch (e: Exception) { - T.e("Failed to extract metadata") - T.e(e.stackTraceToString()) + L.e("Failed to extract metadata") + L.e(e.stackTraceToString()) } jobs[i] = null return job.rawSong @@ -176,7 +176,7 @@ private class MetadataWorker( mediaSource = currentMediaSource mediaSourceCaller = currentMediaSourceCaller } else { - T.d("new media source yahoo") + L.d("new media source yahoo") mediaSource = mediaSourceFactory.createMediaSource(job.mediaItem) mediaSourceCaller = MediaSourceCaller(job) mediaSource.prepareSource( @@ -193,8 +193,8 @@ private class MetadataWorker( mediaPeriod.maybeThrowPrepareError() } } catch (e: Exception) { - T.e("Failed to extract MediaSource") - T.e(e.stackTraceToString()) + L.e("Failed to extract MediaSource") + L.e(e.stackTraceToString()) job.mediaPeriod?.let(mediaSource::releasePeriod) mediaSource.releaseSource(mediaSourceCaller) job.future.setException(e) @@ -247,7 +247,7 @@ private class MetadataWorker( // Ignore dynamic updates. return } - T.d("yay source created") + L.d("yay source created") mediaPeriodCreated = true val mediaPeriod = source.createPeriod( diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt index 04b23812c..ffe333a76 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagInterpreter.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.image.extractor.CoverExtractor import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.util.nonZeroOrNull -import timber.log.Timber as T +import timber.log.Timber as L /** * An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on @@ -82,19 +82,19 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx // val gain = // (((header[16]).toInt() and 0xFF) or ((header[17].toInt() shl 8))) // .R128ToLUFS18() - // T.d("Obtained opus base gain: $gain dB") + // L.d("Obtained opus base gain: $gain dB") // if (gain != 0f) { - // T.d("Applying opus base gain") + // L.d("Applying opus base gain") // rawSong.replayGainTrackAdjustment = // (rawSong.replayGainTrackAdjustment ?: 0f) + gain // rawSong.replayGainAlbumAdjustment = // (rawSong.replayGainAlbumAdjustment ?: 0f) + gain // } else { - // T.d("Ignoring opus base gain") + // L.d("Ignoring opus base gain") // } // } } else { - T.d("No metadata could be extracted for ${rawSong.name}") + L.d("No metadata could be extracted for ${rawSong.name}") } } @@ -127,8 +127,8 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx // isn't known? (textFrames["TDOR"]?.run { Date.from(first()) } ?: textFrames["TDRC"]?.run { Date.from(first()) } - ?: textFrames["TDRL"]?.run { Date.from(first()) } - ?: parseId3v23Date(textFrames)) + ?: textFrames["TDRL"]?.run { Date.from(first()) } + ?: parseId3v23Date(textFrames)) ?.let { rawSong.date = it } // Album @@ -138,7 +138,8 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx textFrames["TALB"]?.let { rawSong.albumName = it.first() } textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } (textFrames["TXXX:musicbrainz album type"] - ?: textFrames["TXXX:releasetype"] ?: + ?: textFrames["TXXX:releasetype"] + ?: // This is a non-standard iTunes extension textFrames["GRP1"]) ?.let { rawSong.releaseTypes = it } @@ -151,9 +152,11 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx rawSong.artistNames = it } (textFrames["TXXX:artistssort"] - ?: textFrames["TXXX:artists_sort"] ?: textFrames["TXXX:artists sort"] - ?: textFrames["TSOP"] ?: textFrames["artistsort"] - ?: textFrames["TXXX:artist sort"]) + ?: textFrames["TXXX:artists_sort"] + ?: textFrames["TXXX:artists sort"] + ?: textFrames["TSOP"] + ?: textFrames["artistsort"] + ?: textFrames["TXXX:artist sort"]) ?.let { rawSong.artistSortNames = it } // Album artist @@ -161,15 +164,19 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx ?: textFrames["TXXX:musicbrainz_albumartistid"]) ?.let { rawSong.albumArtistMusicBrainzIds = it } (textFrames["TXXX:albumartists"] - ?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"] - ?: textFrames["TPE2"] ?: textFrames["TXXX:albumartist"] - ?: textFrames["TXXX:album artist"]) + ?: textFrames["TXXX:album_artists"] + ?: textFrames["TXXX:album artists"] + ?: textFrames["TPE2"] + ?: textFrames["TXXX:albumartist"] + ?: textFrames["TXXX:album artist"]) ?.let { rawSong.albumArtistNames = it } (textFrames["TXXX:albumartistssort"] - ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartists sort"] - ?: textFrames["TXXX:albumartistsort"] + ?: textFrames["TXXX:albumartists_sort"] + ?: textFrames["TXXX:albumartists sort"] + ?: textFrames["TXXX:albumartistsort"] // This is a non-standard iTunes extension - ?: textFrames["TSO2"] ?: textFrames["TXXX:album artist sort"]) + ?: textFrames["TSO2"] + ?: textFrames["TXXX:album artist sort"]) ?.let { rawSong.albumArtistSortNames = it } // Genre @@ -177,7 +184,7 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx // Compilation Flag (textFrames["TCMP"] // This is a non-standard itunes extension - ?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"]) + ?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"]) ?.let { // Ignore invalid instances of this tag if (it.size != 1 || it[0] != "1") return@let @@ -201,7 +208,8 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx // is present. val year = textFrames["TORY"]?.run { first().toIntOrNull() } - ?: textFrames["TYER"]?.run { first().toIntOrNull() } ?: return null + ?: textFrames["TYER"]?.run { first().toIntOrNull() } + ?: return null val tdat = textFrames["TDAT"] return if (tdat != null && tdat.first().length == 4 && tdat.first().isDigitsOnly()) { @@ -258,7 +266,7 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx // date tag that android supports, so it must be 15 years old or more!) (comments["originaldate"]?.run { Date.from(first()) } ?: comments["date"]?.run { Date.from(first()) } - ?: comments["year"]?.run { Date.from(first()) }) + ?: comments["year"]?.run { Date.from(first()) }) ?.let { rawSong.date = it } // Album @@ -277,8 +285,10 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx } (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } (comments["artistssort"] - ?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"] - ?: comments["artist sort"]) + ?: comments["artists_sort"] + ?: comments["artists sort"] + ?: comments["artistsort"] + ?: comments["artist sort"]) ?.let { rawSong.artistSortNames = it } // Album artist @@ -286,12 +296,16 @@ class TagInterpreterImpl @Inject constructor(private val coverExtractor: CoverEx rawSong.albumArtistMusicBrainzIds = it } (comments["albumartists"] - ?: comments["album_artists"] ?: comments["album artists"] ?: comments["albumartist"] - ?: comments["album artist"]) + ?: comments["album_artists"] + ?: comments["album artists"] + ?: comments["albumartist"] + ?: comments["album artist"]) ?.let { rawSong.albumArtistNames = it } (comments["albumartistssort"] - ?: comments["albumartists_sort"] ?: comments["albumartists sort"] - ?: comments["albumartistsort"] ?: comments["album artist sort"]) + ?: comments["albumartists_sort"] + ?: comments["albumartists sort"] + ?: comments["albumartistsort"] + ?: comments["album artist sort"]) ?.let { rawSong.albumArtistSortNames = it } // Genre diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt index f7465c73c..490f72185 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt @@ -170,8 +170,8 @@ private fun String.parseId3v1Genre(): String? { // try to index the genre table with such. val numeric = toIntOrNull() - // Not a numeric value, try some other fixed values. - ?: return when (this) { + // Not a numeric value, try some other fixed values. + ?: return when (this) { // CR and RX are not technically ID3v1, but are formatted similarly to a plain // number. "CR" -> "Cover" diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt index 661305f49..495cbe8ce 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.getSystemServiceCompat -import timber.log.Timber as T +import timber.log.Timber as L class Indexer private constructor( @@ -117,7 +117,7 @@ private constructor( } } else if (musicSettings.shouldBeObserving) { // Not observing and done loading, exit foreground. - T.d("Exiting foreground") + L.d("Exiting foreground") post(observingNotification) } else { post(null) @@ -125,7 +125,7 @@ private constructor( } override fun requestIndex(withCache: Boolean) { - T.d("Starting new indexing job (previous=${currentIndexJob?.hashCode()})") + L.d("Starting new indexing job (previous=${currentIndexJob?.hashCode()})") // Cancel the previous music loading job. currentIndexJob?.cancel() // Start a new music loading job on a co-routine. @@ -146,7 +146,7 @@ private constructor( override fun onMusicChanges(changes: MusicRepository.Changes) { val deviceLibrary = musicRepository.deviceLibrary ?: return - T.d("Music changed, updating shared objects") + L.d("Music changed, updating shared objects") // Wipe possibly-invalidated outdated covers imageLoader.memoryCache?.clear() // Clear invalid models from PlaybackStateManager. This is not connected @@ -175,7 +175,7 @@ private constructor( // setting changed. In such a case, the state will still be updated when // the music loading process ends. if (musicRepository.indexingState == null) { - T.d("Not loading, updating idle session") + L.d("Not loading, updating idle session") foregroundListener.updateForeground(ForegroundListener.Change.INDEXER) } } @@ -184,7 +184,7 @@ private constructor( private fun PowerManager.WakeLock.acquireSafe() { // Avoid unnecessary acquire calls. if (!wakeLock.isHeld) { - T.d("Acquiring wake lock") + L.d("Acquiring wake lock") // Time out after a minute, which is the average music loading time for a medium-sized // library. If this runs out, we will re-request the lock, and if music loading is // shorter than the timeout, it will be released early. @@ -196,7 +196,7 @@ private constructor( private fun PowerManager.WakeLock.releaseSafe() { // Avoid unnecessary release calls. if (wakeLock.isHeld) { - T.d("Releasing wake lock") + L.d("Releasing wake lock") release() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt index 782109ca6..aafe8ab8b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.music.IndexingProgress import org.oxycblt.auxio.util.newMainPendingIntent -import timber.log.Timber as T +import timber.log.Timber as L /** * A dynamic [ForegroundServiceNotification] that shows the current music loading state. @@ -66,7 +66,7 @@ class IndexingNotification(private val context: Context) : // Indeterminate state, use a vaguer description and in-determinate progress. // These events are not very frequent, and thus we don't need to safeguard // against rate limiting. - T.d("Updating state to $progress") + L.d("Updating state to $progress") lastUpdateTime = -1 setContentText(context.getString(R.string.lng_indexing)) setProgress(0, 0, true) @@ -81,7 +81,7 @@ class IndexingNotification(private val context: Context) : return false } lastUpdateTime = SystemClock.elapsedRealtime() - T.d("Updating state to $progress") + L.d("Updating state to $progress") setContentText( context.getString(R.string.fmt_indexing, progress.current, progress.total)) setProgress(progress.total, progress.current, false) diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt index 35a43c7b5..26751d587 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MusicBrowser.kt @@ -119,8 +119,7 @@ private constructor( is MediaSessionUID.SingleItem -> musicRepository.find(uid.uid)?.let { musicRepository.find(it.uid) } null -> null - } - ?: return null + } ?: return null return when (music) { is Album -> music.toMediaItem(context) diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt index 79dfe384a..05a2b60e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MusicServiceFragment.kt @@ -23,6 +23,7 @@ import android.os.Bundle import android.support.v4.media.MediaBrowserCompat.MediaItem import androidx.media.MediaBrowserServiceCompat.BrowserRoot import androidx.media.MediaBrowserServiceCompat.Result +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -30,7 +31,7 @@ import kotlinx.coroutines.launch import org.oxycblt.auxio.ForegroundListener import org.oxycblt.auxio.ForegroundServiceNotification import org.oxycblt.auxio.music.MusicRepository -import javax.inject.Inject +import timber.log.Timber as L class MusicServiceFragment @Inject @@ -122,11 +123,11 @@ constructor( try { val result = body() if (result == null) { - T.w("Result is null") + L.w("Result is null") } sendResult(result) } catch (e: Exception) { - T.d("Error while dispatching: $e") + L.d("Error while dispatching: $e") sendResult(null) } } @@ -137,11 +138,11 @@ constructor( try { val result = body() if (result == null) { - T.w("Result is null") + L.w("Result is null") } sendResult(result) } catch (e: Exception) { - T.d("Error while dispatching: $e") + L.d("Error while dispatching: $e") sendResult(null) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt b/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt index 4bb23abd9..a07df70d9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/SystemContentObserver.kt @@ -28,7 +28,7 @@ import javax.inject.Inject import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.fs.contentResolverSafe -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ContentObserver] that observes the [MediaStore] music database for changes, a behavior known @@ -68,7 +68,7 @@ constructor( // Check here if we should even start a reindex. This is much less bug-prone than // registering and de-registering this component as this setting changes. if (musicSettings.shouldBeObserving) { - T.d("MediaStore changed, starting re-index") + L.d("MediaStore changed, starting re-index") musicRepository.requestIndex(true) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index bba60957c..c2dc32b5f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.info.Name -import timber.log.Timber as T +import timber.log.Timber as L /** * Organized library information controlled by the user. @@ -143,10 +143,10 @@ class UserLibraryFactoryImpl @Inject constructor(private val playlistDao: Playli override suspend fun query() = try { val rawPlaylists = playlistDao.readRawPlaylists() - T.d("Successfully read ${rawPlaylists.size} playlists") + L.d("Successfully read ${rawPlaylists.size} playlists") rawPlaylists } catch (e: Exception) { - T.e("Unable to read playlists: $e") + L.e("Unable to read playlists: $e") listOf() } @@ -192,11 +192,11 @@ private class UserLibraryImpl( return try { playlistDao.insertPlaylist(rawPlaylist) - T.d("Successfully created playlist $name with ${songs.size} songs") + L.d("Successfully created playlist $name with ${songs.size} songs") playlistImpl } catch (e: Exception) { - T.e("Unable to create playlist $name with ${songs.size} songs") - T.e(e.stackTraceToString()) + L.e("Unable to create playlist $name with ${songs.size} songs") + L.e(e.stackTraceToString()) synchronized(this) { playlistMap.remove(playlistImpl.uid) } null } @@ -211,11 +211,11 @@ private class UserLibraryImpl( return try { playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name)) - T.d("Successfully renamed $playlist to $name") + L.d("Successfully renamed $playlist to $name") true } catch (e: Exception) { - T.e("Unable to rename $playlist to $name: $e") - T.e(e.stackTraceToString()) + L.e("Unable to rename $playlist to $name: $e") + L.e(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } false } @@ -230,11 +230,11 @@ private class UserLibraryImpl( return try { playlistDao.deletePlaylist(playlist.uid) - T.d("Successfully deleted $playlist") + L.d("Successfully deleted $playlist") true } catch (e: Exception) { - T.e("Unable to delete $playlist: $e") - T.e(e.stackTraceToString()) + L.e("Unable to delete $playlist: $e") + L.e(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } false } @@ -249,11 +249,11 @@ private class UserLibraryImpl( return try { playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) - T.d("Successfully added ${songs.size} songs to $playlist") + L.d("Successfully added ${songs.size} songs to $playlist") true } catch (e: Exception) { - T.e("Unable to add ${songs.size} songs to $playlist: $e") - T.e(e.stackTraceToString()) + L.e("Unable to add ${songs.size} songs to $playlist: $e") + L.e(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } false } @@ -268,11 +268,11 @@ private class UserLibraryImpl( return try { playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) }) - T.d("Successfully rewrote $playlist with ${songs.size} songs") + L.d("Successfully rewrote $playlist with ${songs.size} songs") true } catch (e: Exception) { - T.e("Unable to rewrite $playlist with ${songs.size} songs: $e") - T.e(e.stackTraceToString()) + L.e("Unable to rewrite $playlist with ${songs.size} songs: $e") + L.e(e.stackTraceToString()) synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } false } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index 283b2047c..c40b17a9f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getColorCompat -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingFragment] that shows the current playback state in a compact manner. @@ -128,7 +128,7 @@ class PlaybackBarFragment : ViewBindingFragment() { val binding = requireBinding() when (actionMode) { ActionMode.NEXT -> { - T.d("Using skip next action") + L.d("Using skip next action") binding.playbackSecondaryAction.apply { if (tag != actionMode) { setIconResource(R.drawable.ic_skip_next_24) @@ -140,7 +140,7 @@ class PlaybackBarFragment : ViewBindingFragment() { } } ActionMode.REPEAT -> { - T.d("Using repeat mode action") + L.d("Using repeat mode action") binding.playbackSecondaryAction.apply { if (tag != actionMode) { contentDescription = getString(R.string.desc_change_repeat) @@ -153,7 +153,7 @@ class PlaybackBarFragment : ViewBindingFragment() { } } ActionMode.SHUFFLE -> { - T.d("Using shuffle action") + L.d("Using shuffle action") binding.playbackSecondaryAction.apply { if (tag != actionMode) { setIconResource(R.drawable.sel_shuffle_state_24) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index b81ec6b79..361fdc8b4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -45,7 +45,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.systemBarInsetsCompat -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingFragment] more information about the currently playing song, alongside all @@ -179,7 +179,7 @@ class PlaybackPanelFragment : override fun onMenuItemClick(item: MenuItem): Boolean { if (item.itemId == R.id.action_open_equalizer) { // Launch the system equalizer app, if possible. - T.d("Launching equalizer") + L.d("Launching equalizer") val equalizerIntent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) // Provide audio session ID so the equalizer can show options for this app @@ -220,7 +220,7 @@ class PlaybackPanelFragment : val binding = requireBinding() val context = requireContext() - T.d("Updating song display: $song") + L.d("Updating song display: $song") binding.playbackCover.bind(song) binding.playbackSong.text = song.name.resolve(context) binding.playbackArtist.text = song.artists.resolveNames(context) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt index b9dd5c73f..a86fbee47 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.playback.replaygain.ReplayGainMode import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp import org.oxycblt.auxio.settings.Settings -import timber.log.Timber as T +import timber.log.Timber as L /** * User configuration specific to the playback system. @@ -146,7 +146,7 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont } if (sharedPreferences.contains(OLD_KEY_LIB_MUSIC_PLAYBACK_MODE)) { - T.d("Migrating $OLD_KEY_LIB_MUSIC_PLAYBACK_MODE") + L.d("Migrating $OLD_KEY_LIB_MUSIC_PLAYBACK_MODE") val mode = sharedPreferences @@ -162,7 +162,7 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont } if (sharedPreferences.contains(OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE)) { - T.d("Migrating $OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE") + L.d("Migrating $OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE") val mode = sharedPreferences @@ -183,19 +183,19 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont getString(R.string.set_key_replay_gain), getString(R.string.set_key_pre_amp_with), getString(R.string.set_key_pre_amp_without) -> { - T.d("Dispatching ReplayGain setting change") + L.d("Dispatching ReplayGain setting change") listener.onReplayGainSettingsChanged() } getString(R.string.set_key_notif_action) -> { - T.d("Dispatching notification setting change") + L.d("Dispatching notification setting change") listener.onNotificationActionChanged() } getString(R.string.set_key_bar_action) -> { - T.d("Dispatching bar action change") + L.d("Dispatching bar action change") listener.onBarActionChanged() } getString(R.string.set_key_repeat_pause) -> { - T.d("Dispatching pause on repeat change") + L.d("Dispatching pause on repeat change") listener.onPauseOnRepeatChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index ffbb96319..944b7bae7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -43,7 +43,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import timber.log.Timber as T +import timber.log.Timber as L /** * An [ViewModel] that provides a safe UI frontend for the current playback state. @@ -129,20 +129,20 @@ constructor( } override fun onIndexMoved(index: Int) { - T.d("Index moved, updating current song") + L.d("Index moved, updating current song") _song.value = playbackManager.currentSong } override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { // Other types of queue changes preserve the current song. if (change.type == QueueChange.Type.SONG) { - T.d("Queue changed, updating current song") + L.d("Queue changed, updating current song") _song.value = playbackManager.currentSong } } override fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) { - T.d("Queue completely changed, updating current song") + L.d("Queue completely changed, updating current song") _isShuffled.value = isShuffled } @@ -152,14 +152,14 @@ constructor( index: Int, isShuffled: Boolean ) { - T.d("New playback started, updating playback information") + L.d("New playback started, updating playback information") _song.value = playbackManager.currentSong _parent.value = parent _isShuffled.value = isShuffled } override fun onProgressionChanged(progression: Progression) { - T.d("Player state changed, starting new position polling") + L.d("Player state changed, starting new position polling") _isPlaying.value = progression.isPlaying // Still need to update the position now due to co-routine launch delays _positionDs.value = progression.calculateElapsedPositionMs().msToDs() @@ -187,7 +187,7 @@ constructor( // --- PLAYING FUNCTIONS --- fun play(song: Song, with: PlaySong) { - T.d("Playing $song with $with") + L.d("Playing $song with $with") playWithImpl(song, with, ShuffleMode.IMPLICIT) } @@ -201,7 +201,7 @@ constructor( /** Shuffle all songs in the music library. */ fun shuffleAll() { - T.d("Shuffling all songs") + L.d("Shuffling all songs") playFromAllImpl(null, ShuffleMode.ON) } @@ -257,7 +257,7 @@ constructor( } private fun playFromAlbumImpl(song: Song, shuffle: ShuffleMode) { - T.d("Playing $song from album") + L.d("Playing $song from album") playImpl(commandFactory.songFromAlbum(song, shuffle)) } @@ -267,7 +267,7 @@ constructor( playbackManager.play(params) return } - T.d( + L.d( "Cannot use given artist parameter for $song [$artist from ${song.artists}], showing choice dialog") startPlaybackDecision(PlaybackDecision.PlayFromArtist(song)) } @@ -278,20 +278,20 @@ constructor( playbackManager.play(params) return } - T.d( + L.d( "Cannot use given genre parameter for $song [$genre from ${song.genres}], showing choice dialog") startPlaybackDecision(PlaybackDecision.PlayFromArtist(song)) } private fun playFromPlaylistImpl(song: Song, playlist: Playlist, shuffle: ShuffleMode) { - T.d("Playing $song from $playlist") + L.d("Playing $song from $playlist") playImpl(commandFactory.songFromPlaylist(song, playlist, shuffle)) } private fun startPlaybackDecision(decision: PlaybackDecision) { val existing = _playbackDecision.flow.value if (existing != null) { - T.d("Already handling decision $existing, ignoring $decision") + L.d("Already handling decision $existing, ignoring $decision") return } _playbackDecision.put(decision) @@ -303,7 +303,7 @@ constructor( * @param album The [Album] to play. */ fun play(album: Album) { - T.d("Playing $album") + L.d("Playing $album") playImpl(commandFactory.album(album, ShuffleMode.OFF)) } @@ -313,7 +313,7 @@ constructor( * @param album The [Album] to shuffle. */ fun shuffle(album: Album) { - T.d("Shuffling $album") + L.d("Shuffling $album") playImpl(commandFactory.album(album, ShuffleMode.ON)) } @@ -323,7 +323,7 @@ constructor( * @param artist The [Artist] to play. */ fun play(artist: Artist) { - T.d("Playing $artist") + L.d("Playing $artist") playImpl(commandFactory.artist(artist, ShuffleMode.OFF)) } @@ -333,7 +333,7 @@ constructor( * @param artist The [Artist] to shuffle. */ fun shuffle(artist: Artist) { - T.d("Shuffling $artist") + L.d("Shuffling $artist") playImpl(commandFactory.artist(artist, ShuffleMode.ON)) } @@ -343,7 +343,7 @@ constructor( * @param genre The [Genre] to play. */ fun play(genre: Genre) { - T.d("Playing $genre") + L.d("Playing $genre") playImpl(commandFactory.genre(genre, ShuffleMode.OFF)) } @@ -353,7 +353,7 @@ constructor( * @param genre The [Genre] to shuffle. */ fun shuffle(genre: Genre) { - T.d("Shuffling $genre") + L.d("Shuffling $genre") playImpl(commandFactory.genre(genre, ShuffleMode.ON)) } @@ -363,7 +363,7 @@ constructor( * @param playlist The [Playlist] to play. */ fun play(playlist: Playlist) { - T.d("Playing $playlist") + L.d("Playing $playlist") playImpl(commandFactory.playlist(playlist, ShuffleMode.OFF)) } @@ -373,7 +373,7 @@ constructor( * @param playlist The [Playlist] to shuffle. */ fun shuffle(playlist: Playlist) { - T.d("Shuffling $playlist") + L.d("Shuffling $playlist") playImpl(commandFactory.playlist(playlist, ShuffleMode.ON)) } @@ -383,7 +383,7 @@ constructor( * @param songs The [Song]s to play. */ fun play(songs: List) { - T.d("Playing ${songs.size} songs") + L.d("Playing ${songs.size} songs") playImpl(commandFactory.songs(songs, ShuffleMode.OFF)) } @@ -393,7 +393,7 @@ constructor( * @param songs The [Song]s to shuffle. */ fun shuffle(songs: List) { - T.d("Shuffling ${songs.size} songs") + L.d("Shuffling ${songs.size} songs") playImpl(commandFactory.songs(songs, ShuffleMode.ON)) } @@ -408,7 +408,7 @@ constructor( * @param action The [DeferredPlayback] to perform eventually. */ fun playDeferred(action: DeferredPlayback) { - T.d("Starting action $action") + L.d("Starting action $action") playbackManager.playDeferred(action) } @@ -420,7 +420,7 @@ constructor( * @param positionDs The position to seek to, in deci-seconds (1/10th of a second). */ fun seekTo(positionDs: Long) { - T.d("Seeking to ${positionDs}ds") + L.d("Seeking to ${positionDs}ds") playbackManager.seekTo(positionDs.dsToMs()) } @@ -428,13 +428,13 @@ constructor( /** Skip to the next [Song]. */ fun next() { - T.d("Skipping to next song") + L.d("Skipping to next song") playbackManager.next() } /** Skip to the previous [Song]. */ fun prev() { - T.d("Skipping to previous song") + L.d("Skipping to previous song") playbackManager.prev() } @@ -444,7 +444,7 @@ constructor( * @param song The [Song] to add. */ fun playNext(song: Song) { - T.d("Playing $song next") + L.d("Playing $song next") playbackManager.playNext(song) } @@ -454,7 +454,7 @@ constructor( * @param album The [Album] to add. */ fun playNext(album: Album) { - T.d("Playing $album next") + L.d("Playing $album next") playbackManager.playNext(listSettings.albumSongSort.songs(album.songs)) } @@ -464,7 +464,7 @@ constructor( * @param artist The [Artist] to add. */ fun playNext(artist: Artist) { - T.d("Playing $artist next") + L.d("Playing $artist next") playbackManager.playNext(listSettings.artistSongSort.songs(artist.songs)) } @@ -474,7 +474,7 @@ constructor( * @param genre The [Genre] to add. */ fun playNext(genre: Genre) { - T.d("Playing $genre next") + L.d("Playing $genre next") playbackManager.playNext(listSettings.genreSongSort.songs(genre.songs)) } @@ -484,7 +484,7 @@ constructor( * @param playlist The [Playlist] to add. */ fun playNext(playlist: Playlist) { - T.d("Playing $playlist next") + L.d("Playing $playlist next") playbackManager.playNext(playlist.songs) } @@ -494,7 +494,7 @@ constructor( * @param songs The [Song]s to add. */ fun playNext(songs: List) { - T.d("Playing ${songs.size} songs next") + L.d("Playing ${songs.size} songs next") playbackManager.playNext(songs) } @@ -504,7 +504,7 @@ constructor( * @param song The [Song] to add. */ fun addToQueue(song: Song) { - T.d("Adding $song to queue") + L.d("Adding $song to queue") playbackManager.addToQueue(song) } @@ -514,7 +514,7 @@ constructor( * @param album The [Album] to add. */ fun addToQueue(album: Album) { - T.d("Adding $album to queue") + L.d("Adding $album to queue") playbackManager.addToQueue(listSettings.albumSongSort.songs(album.songs)) } @@ -524,7 +524,7 @@ constructor( * @param artist The [Artist] to add. */ fun addToQueue(artist: Artist) { - T.d("Adding $artist to queue") + L.d("Adding $artist to queue") playbackManager.addToQueue(listSettings.artistSongSort.songs(artist.songs)) } @@ -534,7 +534,7 @@ constructor( * @param genre The [Genre] to add. */ fun addToQueue(genre: Genre) { - T.d("Adding $genre to queue") + L.d("Adding $genre to queue") playbackManager.addToQueue(listSettings.genreSongSort.songs(genre.songs)) } @@ -544,7 +544,7 @@ constructor( * @param playlist The [Playlist] to add. */ fun addToQueue(playlist: Playlist) { - T.d("Adding $playlist to queue") + L.d("Adding $playlist to queue") playbackManager.addToQueue(playlist.songs) } @@ -554,7 +554,7 @@ constructor( * @param songs The [Song]s to add. */ fun addToQueue(songs: List) { - T.d("Adding ${songs.size} songs to queue") + L.d("Adding ${songs.size} songs to queue") playbackManager.addToQueue(songs) } @@ -562,13 +562,13 @@ constructor( /** Toggle [isPlaying] (i.e from playing to paused) */ fun togglePlaying() { - T.d("Toggling playing state") + L.d("Toggling playing state") playbackManager.playing(!playbackManager.progression.isPlaying) } /** Toggle [isShuffled] (ex. from on to off) */ fun toggleShuffled() { - T.d("Toggling shuffled state") + L.d("Toggling shuffled state") playbackManager.shuffled(!playbackManager.isShuffled) } @@ -578,7 +578,7 @@ constructor( * @see RepeatMode.increment */ fun toggleRepeatMode() { - T.d("Toggling repeat mode") + L.d("Toggling repeat mode") playbackManager.repeatMode(playbackManager.repeatMode.increment()) } @@ -599,7 +599,7 @@ constructor( private fun openImpl(panel: OpenPanel) { val existing = openPanel.flow.value if (existing != null) { - T.d("Already opening $existing, ignoring opening $panel") + L.d("Already opening $existing, ignoring opening $panel") return } _openPanel.put(panel) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromArtistDialog.kt index 7ecea8b7b..7139ff8af 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromArtistDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromArtistDialog.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A picker [ViewBindingMaterialDialogFragment] intended for when [Artist] playback is ambiguous. @@ -88,7 +88,7 @@ class PlayFromArtistDialog : private fun updateSong(song: Song?) { if (song == null) { - T.d("No song to show choices for, navigating away") + L.d("No song to show choices for, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromGenreDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromGenreDialog.kt index 2679479c0..c92d24e8c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromGenreDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlayFromGenreDialog.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A picker [ViewBindingMaterialDialogFragment] intended for when [Genre] playback is ambiguous. @@ -88,7 +88,7 @@ class PlayFromGenreDialog : private fun updateSong(song: Song?) { if (song == null) { - T.d("No song to show choices for, navigating away") + L.d("No song to show choices for, navigating away") findNavController().navigateUp() return } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt index 2c6f990d2..21af44a86 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/decision/PlaybackPickerViewModel.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewModel] that stores the choices shown in the playback picker dialogs. @@ -63,10 +63,10 @@ class PlaybackPickerViewModel @Inject constructor(private val musicRepository: M * @param uid The [Music.UID] of the item to show. Must be a [Song]. */ fun setPickerSongUid(uid: Music.UID) { - T.d("Opening picker for song $uid") + L.d("Opening picker for song $uid") _currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid) if (_currentPickerSong.value != null) { - T.w("Given song UID was invalid") + L.w("Given song UID was invalid") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt index a64e49853..6fbbbdb71 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt @@ -22,7 +22,7 @@ import javax.inject.Inject import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.playback.state.PlaybackStateManager -import timber.log.Timber as T +import timber.log.Timber as L /** * Manages the persisted playback state in a structured manner. @@ -59,8 +59,8 @@ constructor( heapItems = queueDao.getHeap() mappingItems = queueDao.getShuffledMapping() } catch (e: Exception) { - T.e("Unable read playback state") - T.e(e.stackTraceToString()) + L.e("Unable read playback state") + L.e(e.stackTraceToString()) return null } @@ -84,12 +84,12 @@ constructor( queueDao.nukeHeap() queueDao.nukeShuffledMapping() } catch (e: Exception) { - T.e("Unable to clear previous state") - T.e(e.stackTraceToString()) + L.e("Unable to clear previous state") + L.e(e.stackTraceToString()) return false } - T.d("Successfully cleared previous state") + L.d("Successfully cleared previous state") if (state != null) { // Transform saved state into raw state, which can then be written to the database. val playbackState = @@ -113,12 +113,12 @@ constructor( queueDao.insertHeap(heap) queueDao.insertShuffledMapping(shuffledMapping) } catch (e: Exception) { - T.e("Unable to write new state") - T.e(e.stackTraceToString()) + L.e("Unable to write new state") + L.e(e.stackTraceToString()) return false } - T.d("Successfully wrote new state") + L.d("Successfully wrote new state") } return true diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 1fe39292a..05d0d5927 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.inflater -import timber.log.Timber as T +import timber.log.Timber as L /** * A [RecyclerView.Adapter] that shows an editable list of queue items. @@ -80,7 +80,7 @@ class QueueAdapter(private val listener: EditClickListListener) : * @param isPlaying Whether playback is ongoing or paused. */ fun setPosition(index: Int, isPlaying: Boolean) { - T.d("Updating index") + L.d("Updating index") val lastIndex = currentIndex currentIndex = index @@ -89,10 +89,10 @@ class QueueAdapter(private val listener: EditClickListListener) : // TODO: Optimize this by only updating the range between old and new indices? // TODO: Don't update when the index has not moved. if (currentIndex < lastIndex) { - T.d("Moved backwards, must update items above last index") + L.d("Moved backwards, must update items above last index") notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_POSITION) } else { - T.d("Moved forwards, update items after index") + L.d("Moved forwards, update items after index") notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_POSITION) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 02b691e81..2654fe9b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -34,7 +34,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingFragment] that displays an editable queue. @@ -122,14 +122,14 @@ class QueueFragment : ViewBindingFragment(), EditClickList // dependent on where we have to scroll to get to the currently playing song. if (notInitialized || scrollTo < start) { // We need to scroll upwards, or initialize the scroll, no need to offset - T.d("Not scrolling downwards, no offset needed") + L.d("Not scrolling downwards, no offset needed") binding.queueRecycler.scrollToPosition(scrollTo) } else if (scrollTo > end) { // We need to scroll downwards, we need to offset by a screen of songs. // This does have some error due to how many completely visible items on-screen // can vary. This is considered okay. val offset = scrollTo + (end - start) - T.d("Scrolling downwards, offsetting by $offset") + L.d("Scrolling downwards, offsetting by $offset") binding.queueRecycler.scrollToPosition(min(queue.lastIndex, offset)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 85d37b366..97ed2493f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewModel] that manages the current queue state and allows navigation through the queue. @@ -62,26 +62,26 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt } override fun onIndexMoved(index: Int) { - T.d("Index moved, synchronizing and scrolling to new position") + L.d("Index moved, synchronizing and scrolling to new position") _scrollTo.put(index) _index.value = index } override fun onQueueChanged(queue: List, index: Int, change: QueueChange) { // Queue changed trivially due to item mo -> Diff queue, stay at current index. - T.d("Updating queue display") + L.d("Updating queue display") _queueInstructions.put(change.instructions) _queue.value = queue if (change.type != QueueChange.Type.MAPPING) { // Index changed, make sure it remains updated without actually scrolling to it. - T.d("Index changed with queue, synchronizing new position") + L.d("Index changed with queue, synchronizing new position") _index.value = index } } override fun onQueueReordered(queue: List, index: Int, isShuffled: Boolean) { // Queue changed completely -> Replace queue, update index - T.d("Queue changed completely, replacing queue and position") + L.d("Queue changed completely, replacing queue and position") _queueInstructions.put(UpdateInstructions.Replace(0)) _scrollTo.put(index) _queue.value = queue @@ -95,7 +95,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt isShuffled: Boolean ) { // Entirely new queue -> Replace queue, update index - T.d("New playback, replacing queue and position") + L.d("New playback, replacing queue and position") _queueInstructions.put(UpdateInstructions.Replace(0)) _scrollTo.put(index) _queue.value = queue @@ -117,7 +117,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt if (adapterIndex !in queue.value.indices) { return } - T.d("Going to position $adapterIndex in queue") + L.d("Going to position $adapterIndex in queue") playbackManager.goto(adapterIndex) } @@ -131,7 +131,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt if (adapterIndex !in queue.value.indices) { return } - T.d("Removing item $adapterIndex in queue") + L.d("Removing item $adapterIndex in queue") playbackManager.removeQueueItem(adapterIndex) } @@ -146,7 +146,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt if (adapterFrom !in queue.value.indices || adapterTo !in queue.value.indices) { return false } - T.d("Moving $adapterFrom to $adapterFrom in queue") + L.d("Moving $adapterFrom to $adapterFrom in queue") playbackManager.moveQueueItem(adapterFrom, adapterTo) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 9e52d1521..0ef752a1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import timber.log.Timber as T +import timber.log.Timber as L /** * aa [ViewBindingMaterialDialogFragment] that allows user configuration of the current @@ -62,7 +62,7 @@ class PreAmpCustomizeDialog : ViewBindingMaterialDialogFragment { - T.d("ReplayGain is off") + L.d("ReplayGain is off") null } // User wants track gain to be preferred. Default to album gain only if // there is no track gain. ReplayGainMode.TRACK -> { - T.d("Using track strategy") + L.d("Using track strategy") gain.track ?: gain.album } // User wants album gain to be preferred. Default to track gain only if // here is no album gain. ReplayGainMode.ALBUM -> { - T.d("Using album strategy") + L.d("Using album strategy") gain.album ?: gain.track } // User wants album gain to be used when in an album, track gain otherwise. ReplayGainMode.DYNAMIC -> { - T.d("Using dynamic strategy") + L.d("Using dynamic strategy") gain.album?.takeIf { playbackManager.parent is Album && playbackManager.currentSong?.album == playbackManager.parent - } - ?: gain.track + } ?: gain.track } } val amplifiedAdjustment = if (resolvedAdjustment != null) { // Successfully resolved an adjustment, apply the corresponding pre-amp - T.d("Applying with pre-amp") + L.d("Applying with pre-amp") resolvedAdjustment + preAmp.with } else { // No adjustment found, use the corresponding user-defined pre-amp - T.d("Applying without pre-amp") + L.d("Applying without pre-amp") preAmp.without } - T.d("Applying ReplayGain adjustment ${amplifiedAdjustment}db") + L.d("Applying ReplayGain adjustment ${amplifiedAdjustment}db") // Final adjustment along the volume curve. volume = 10f.pow(amplifiedAdjustment / 20f) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 487bde8d4..b409d6bb0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -59,7 +59,7 @@ import org.oxycblt.auxio.playback.state.RawQueue import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.playback.state.StateAck -import timber.log.Timber as T +import timber.log.Timber as L class ExoPlaybackStateHolder( private val context: Context, @@ -130,8 +130,8 @@ class ExoPlaybackStateHolder( override fun resolveQueue(): RawQueue { val deviceLibrary = musicRepository.deviceLibrary - // No library, cannot do anything. - ?: return RawQueue(emptyList(), emptyList(), 0) + // No library, cannot do anything. + ?: return RawQueue(emptyList(), emptyList(), 0) val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it) } val shuffledMapping = if (player.shuffleModeEnabled) { @@ -145,13 +145,13 @@ class ExoPlaybackStateHolder( override fun handleDeferred(action: DeferredPlayback): Boolean { val deviceLibrary = musicRepository.deviceLibrary - // No library, cannot do anything. - ?: return false + // No library, cannot do anything. + ?: return false when (action) { // Restore state -> Start a new restoreState job is DeferredPlayback.RestoreState -> { - T.d("Restoring playback state") + L.d("Restoring playback state") restoreScope.launch { val state = persistenceRepository.readState() withContext(Dispatchers.Main) { @@ -170,7 +170,7 @@ class ExoPlaybackStateHolder( } // Shuffle all -> Start new playback from all songs is DeferredPlayback.ShuffleAll -> { - T.d("Shuffling all tracks") + L.d("Shuffling all tracks") playbackManager.play( requireNotNull(commandFactory.all(ShuffleMode.ON)) { "Invalid playback parameters" @@ -178,7 +178,7 @@ class ExoPlaybackStateHolder( } // Open -> Try to find the Song for the given file and then play it from all songs is DeferredPlayback.Open -> { - T.d("Opening specified file") + L.d("Opening specified file") deviceLibrary.findSongForUri(context, action.uri)?.let { song -> playbackManager.play( requireNotNull(commandFactory.song(song, ShuffleMode.IMPLICIT)) { @@ -427,18 +427,18 @@ class ExoPlaybackStateHolder( if (player.playWhenReady) { // Mark that we have started playing so that the notification can now be posted. - T.d("Player has started playing") + L.d("Player has started playing") sessionOngoing = true if (!openAudioEffectSession) { // Convention to start an audioeffect session on play/pause rather than // start/stop - T.d("Opening audio effect session") + L.d("Opening audio effect session") broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) openAudioEffectSession = true } } else if (openAudioEffectSession) { // Make sure to close the audio session when we stop playback. - T.d("Closing audio effect session") + L.d("Closing audio effect session") broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) openAudioEffectSession = false } @@ -470,7 +470,7 @@ class ExoPlaybackStateHolder( Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_POSITION_DISCONTINUITY)) { - T.d("Player state changed, must synchronize state") + L.d("Player state changed, must synchronize state") playbackManager.ack(this, StateAck.ProgressionChanged) } } @@ -478,13 +478,13 @@ class ExoPlaybackStateHolder( override fun onPlayerError(error: PlaybackException) { // TODO: Replace with no skipping and a notification instead // If there's any issue, just go to the next song. - T.e("Player error occurred") - T.e(error.stackTraceToString()) + L.e("Player error occurred") + L.e(error.stackTraceToString()) playbackManager.next() } private fun broadcastAudioEffectAction(event: String) { - T.d("Broadcasting AudioEffect event: $event") + L.d("Broadcasting AudioEffect event: $event") context.sendBroadcast( Intent(event) .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) @@ -497,7 +497,7 @@ class ExoPlaybackStateHolder( override fun onMusicChanges(changes: MusicRepository.Changes) { if (changes.deviceLibrary && musicRepository.deviceLibrary != null) { // We now have a library, see if we have anything we need to do. - T.d("Library obtained, requesting action") + L.d("Library obtained, requesting action") playbackManager.requestAction(this) } } @@ -523,17 +523,17 @@ class ExoPlaybackStateHolder( private fun deferSave() { saveJob { - T.d("Waiting for save buffer") + L.d("Waiting for save buffer") delay(SAVE_BUFFER) yield() - T.d("Committing saved state") + L.d("Committing saved state") persistenceRepository.saveState(playbackManager.toSavedState()) } } private fun saveJob(block: suspend () -> Unit) { currentSaveJob?.let { - T.d("Discarding prior save job") + L.d("Discarding prior save job") it.cancel() } currentSaveJob = saveScope.launch { block() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt index c70b14a7e..22ad735a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt @@ -27,7 +27,7 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.AuxioService import org.oxycblt.auxio.playback.state.PlaybackStateManager -import timber.log.Timber as T +import timber.log.Timber as L /** * A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to @@ -47,7 +47,7 @@ class MediaButtonReceiver : BroadcastReceiver() { // stupid this is with the state of foreground services on modern android. One // wrong action at the wrong time will result in the app crashing, and there is // nothing I can do about it. - T.d("Delivering media button intent $intent") + L.d("Delivering media button intent $intent") intent.component = ComponentName(context, AuxioService::class.java) ContextCompat.startForegroundService(context, intent) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt index 54e5774c2..f190f657f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionHolder.kt @@ -49,7 +49,7 @@ import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.util.newBroadcastPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent -import timber.log.Timber as T +import timber.log.Timber as L /** * A component that mirrors the current playback state into the [MediaSessionCompat] and @@ -210,10 +210,10 @@ private constructor( * playback is currently occuring from all songs. */ private fun updateMediaMetadata(song: Song?, parent: MusicParent?) { - T.d("Updating media metadata to $song with $parent") + L.d("Updating media metadata to $song with $parent") if (song == null) { // Nothing playing, reset the MediaSession and close the notification. - T.d("Nothing playing, resetting media session") + L.d("Nothing playing, resetting media session") mediaSession.setMetadata(emptyMetadata) return } @@ -252,15 +252,15 @@ private constructor( MediaSessionUID.SingleItem(song.album.uid).toString()) // These fields are nullable and so we must check first before adding them to the fields. song.track?.let { - T.d("Adding track information") + L.d("Adding track information") builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, it.toLong()) } song.disc?.let { - T.d("Adding disc information") + L.d("Adding disc information") builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.number.toLong()) } song.date?.let { - T.d("Adding date information") + L.d("Adding date information") builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) builder.putLong(MediaMetadataCompat.METADATA_KEY_YEAR, it.year.toLong()) } @@ -272,7 +272,7 @@ private constructor( song, object : BitmapProvider.Target { override fun onCompleted(bitmap: Bitmap?) { - T.d("Bitmap loaded, applying media session and posting notification") + L.d("Bitmap loaded, applying media session and posting notification") if (bitmap != null) { builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap) builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) @@ -300,13 +300,13 @@ private constructor( // playback state. MediaSessionCompat.QueueItem(description, i.toLong()) } - T.d("Uploading ${queueItems.size} songs to MediaSession queue") + L.d("Uploading ${queueItems.size} songs to MediaSession queue") mediaSession.setQueue(queueItems) } /** Invalidate the current [MediaSessionCompat]'s [PlaybackStateCompat]. */ private fun invalidateSessionState() { - T.d("Updating media session playback state") + L.d("Updating media session playback state") val state = // InternalPlayer.State handles position/state information. @@ -322,7 +322,7 @@ private constructor( val secondaryAction = when (playbackSettings.notificationAction) { ActionMode.SHUFFLE -> { - T.d("Using shuffle MediaSession action") + L.d("Using shuffle MediaSession action") PlaybackStateCompat.CustomAction.Builder( PlaybackActions.ACTION_INVERT_SHUFFLE, context.getString(R.string.desc_shuffle), @@ -333,7 +333,7 @@ private constructor( }) } else -> { - T.d("Using repeat mode MediaSession action") + L.d("Using repeat mode MediaSession action") PlaybackStateCompat.CustomAction.Builder( PlaybackActions.ACTION_INC_REPEAT_MODE, context.getString(R.string.desc_change_repeat), @@ -356,22 +356,22 @@ private constructor( /** Invalidate the "secondary" action (i.e shuffle/repeat mode). */ private fun invalidateSecondaryAction() { - T.d("Invalidating secondary action") + L.d("Invalidating secondary action") invalidateSessionState() when (playbackSettings.notificationAction) { ActionMode.SHUFFLE -> { - T.d("Using shuffle notification action") + L.d("Using shuffle notification action") _notification.updateShuffled(playbackManager.isShuffled) } else -> { - T.d("Using repeat mode notification action") + L.d("Using repeat mode notification action") _notification.updateRepeatMode(playbackManager.repeatMode) } } if (!bitmapProvider.isBusy) { - T.d("Not loading a bitmap, post the notification") + L.d("Not loading a bitmap, post the notification") foregroundListener.updateForeground(ForegroundListener.Change.MEDIA_SESSION) } } @@ -423,7 +423,7 @@ private class PlaybackNotification( * @param metadata The [MediaMetadataCompat] to display in this notification. */ fun updateMetadata(metadata: MediaMetadataCompat) { - T.d("Updating shown metadata") + L.d("Updating shown metadata") setLargeIcon(metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)) setContentTitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)) setContentText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ARTIST)) @@ -436,7 +436,7 @@ private class PlaybackNotification( * @param isPlaying Whether playback should be indicated as ongoing or paused. */ fun updatePlaying(isPlaying: Boolean) { - T.d("Updating playing state: $isPlaying") + L.d("Updating playing state: $isPlaying") mActions[2] = buildPlayPauseAction(context, isPlaying) } @@ -446,7 +446,7 @@ private class PlaybackNotification( * @param repeatMode The current [RepeatMode]. */ fun updateRepeatMode(repeatMode: RepeatMode) { - T.d("Applying repeat mode action: $repeatMode") + L.d("Applying repeat mode action: $repeatMode") mActions[0] = buildRepeatAction(context, repeatMode) } @@ -456,7 +456,7 @@ private class PlaybackNotification( * @param isShuffled Whether the queue is currently shuffled or not. */ fun updateShuffled(isShuffled: Boolean) { - T.d("Applying shuffle action: $isShuffled") + L.d("Applying shuffle action: $isShuffled") mActions[0] = buildShuffleAction(context, isShuffled) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt index 13c57e97a..62c1ad914 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback.service import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Bundle import android.provider.MediaStore import android.support.v4.media.MediaDescriptionCompat @@ -47,7 +46,7 @@ import org.oxycblt.auxio.playback.state.PlaybackCommand import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.ShuffleMode -import timber.log.Timber as T +import timber.log.Timber as L class MediaSessionInterface @Inject @@ -82,7 +81,7 @@ constructor( val parentUid = extras?.getString(MusicBrowser.KEY_CHILD_OF)?.let { MediaSessionUID.fromString(it) } val command = expandUidIntoCommand(uid, parentUid) - T.d(extras?.getString(MusicBrowser.KEY_CHILD_OF)) + L.d(extras?.getString(MusicBrowser.KEY_CHILD_OF)) playbackManager.play(requireNotNull(command) { "Invalid playback configuration" }) } @@ -295,9 +294,11 @@ constructor( private fun expandSongIntoCommand(music: Song, parent: MusicParent?) = when (parent) { is Album -> commandFactory.songFromAlbum(music, ShuffleMode.IMPLICIT) - is Artist -> commandFactory.songFromArtist(music, parent, ShuffleMode.IMPLICIT) + is Artist -> + commandFactory.songFromArtist(music, parent, ShuffleMode.IMPLICIT) ?: commandFactory.songFromArtist(music, music.artists[0], ShuffleMode.IMPLICIT) - is Genre -> commandFactory.songFromGenre(music, parent, ShuffleMode.IMPLICIT) + is Genre -> + commandFactory.songFromGenre(music, parent, ShuffleMode.IMPLICIT) ?: commandFactory.songFromGenre(music, music.genres[0], ShuffleMode.IMPLICIT) is Playlist -> commandFactory.songFromPlaylist(music, parent, ShuffleMode.IMPLICIT) null -> commandFactory.songFromAll(music, ShuffleMode.IMPLICIT) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt index f5025239f..a49438472 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.playback.state.DeferredPlayback import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.widgets.WidgetComponent -import timber.log.Timber as T +import timber.log.Timber as L class PlaybackServiceFragment private constructor( @@ -86,7 +86,7 @@ private constructor( fun start(startedBy: Int) { // At minimum we want to ensure an active playback state. // TODO: Possibly also force to go foreground? - T.d("Handling non-native start.") + L.d("Handling non-native start.") val action = when (startedBy) { IntegerTable.START_ID_ACTIVITY -> null @@ -97,7 +97,7 @@ private constructor( else -> DeferredPlayback.RestoreState(play = false) } if (action != null) { - T.d("Initing service fragment using action $action") + L.d("Initing service fragment using action $action") playbackManager.playDeferred(action) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt index 59ffad0d5..0ff4b83e3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemPlaybackReceiver.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetProvider -import timber.log.Timber as T +import timber.log.Timber as L /** * A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require an @@ -76,7 +76,7 @@ private constructor( // 3. Some internal framework thing that also handles bluetooth headsets // Just use ACTION_HEADSET_PLUG. AudioManager.ACTION_HEADSET_PLUG -> { - T.d("Received headset plug event") + L.d("Received headset plug event") when (intent.getIntExtra("state", -1)) { 0 -> pauseFromHeadsetPlug() 1 -> playFromHeadsetPlug() @@ -85,37 +85,37 @@ private constructor( initialHeadsetPlugEventHandled = true } AudioManager.ACTION_AUDIO_BECOMING_NOISY -> { - T.d("Received Headset noise event") + L.d("Received Headset noise event") pauseFromHeadsetPlug() } // --- AUXIO EVENTS --- PlaybackActions.ACTION_PLAY_PAUSE -> { - T.d("Received play event") + L.d("Received play event") playbackManager.playing(!playbackManager.progression.isPlaying) } PlaybackActions.ACTION_INC_REPEAT_MODE -> { - T.d("Received repeat mode event") + L.d("Received repeat mode event") playbackManager.repeatMode(playbackManager.repeatMode.increment()) } PlaybackActions.ACTION_INVERT_SHUFFLE -> { - T.d("Received shuffle event") + L.d("Received shuffle event") playbackManager.shuffled(!playbackManager.isShuffled) } PlaybackActions.ACTION_SKIP_PREV -> { - T.d("Received skip previous event") + L.d("Received skip previous event") playbackManager.prev() } PlaybackActions.ACTION_SKIP_NEXT -> { - T.d("Received skip next event") + L.d("Received skip next event") playbackManager.next() } PlaybackActions.ACTION_EXIT -> { - T.d("Received exit event") + L.d("Received exit event") playbackManager.endSession() } WidgetProvider.ACTION_WIDGET_UPDATE -> { - T.d("Received widget update event") + L.d("Received widget update event") widgetComponent.update() } } @@ -128,14 +128,14 @@ private constructor( if (playbackSettings.headsetAutoplay && playbackManager.currentSong != null && initialHeadsetPlugEventHandled) { - T.d("Device connected, resuming") + L.d("Device connected, resuming") playbackManager.playing(true) } } private fun pauseFromHeadsetPlug() { if (playbackManager.currentSong != null) { - T.d("Device disconnected, pausing") + L.d("Device disconnected, pausing") playbackManager.playing(false) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt index 66ce48026..14b87c1dd 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackCommand.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.playback.state +import javax.inject.Inject import org.oxycblt.auxio.list.ListSettings import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album @@ -28,7 +29,6 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackSettings -import javax.inject.Inject /** * A playback command that can be passed to [PlaybackStateManager] to start new playback. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 4660d3210..b74b53d5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -25,7 +25,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener -import timber.log.Timber as T +import timber.log.Timber as L /** * Core playback state controller class. @@ -387,11 +387,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun addListener(listener: Listener) { - T.d("Adding $listener to listeners") + L.d("Adding $listener to listeners") listeners.add(listener) if (isInitialized) { - T.d("Sending initial state to $listener") + L.d("Sending initial state to $listener") listener.onNewPlayback( stateMirror.parent, stateMirror.queue, stateMirror.index, stateMirror.isShuffled) listener.onProgressionChanged(stateMirror.progression) @@ -401,16 +401,16 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun removeListener(listener: Listener) { - T.d("Removing $listener from listeners") + L.d("Removing $listener from listeners") if (!listeners.remove(listener)) { - T.w("Listener $listener was not added prior, cannot remove") + L.w("Listener $listener was not added prior, cannot remove") } } @Synchronized override fun registerStateHolder(stateHolder: PlaybackStateHolder) { if (this.stateHolder != null) { - T.w("Internal player is already registered") + L.w("Internal player is already registered") return } @@ -429,11 +429,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun unregisterStateHolder(stateHolder: PlaybackStateHolder) { if (this.stateHolder !== stateHolder) { - T.w("Given internal player did not match current internal player") + L.w("Given internal player did not match current internal player") return } - T.d("Unregistering internal player $stateHolder") + L.d("Unregistering internal player $stateHolder") this.stateHolder = null } @@ -443,7 +443,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun play(command: PlaybackCommand) { val stateHolder = stateHolder ?: return - T.d("Playing $command") + L.d("Playing $command") // Played something, so we are initialized now isInitialized = true stateHolder.newPlayback(command) @@ -454,32 +454,32 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun next() { val stateHolder = stateHolder ?: return - T.d("Going to next song") + L.d("Going to next song") stateHolder.next() } @Synchronized override fun prev() { val stateHolder = stateHolder ?: return - T.d("Going to previous song") + L.d("Going to previous song") stateHolder.prev() } @Synchronized override fun goto(index: Int) { val stateHolder = stateHolder ?: return - T.d("Going to index $index") + L.d("Going to index $index") stateHolder.goto(index) } @Synchronized override fun playNext(songs: List) { if (currentSong == null) { - T.d("Nothing playing, short-circuiting to new playback") + L.d("Nothing playing, short-circuiting to new playback") play(QueueCommand(songs)) } else { val stateHolder = stateHolder ?: return - T.d("Adding ${songs.size} songs to start of queue") + L.d("Adding ${songs.size} songs to start of queue") stateHolder.playNext(songs, StateAck.PlayNext(stateMirror.index + 1, songs.size)) } } @@ -487,11 +487,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun addToQueue(songs: List) { if (currentSong == null) { - T.d("Nothing playing, short-circuiting to new playback") + L.d("Nothing playing, short-circuiting to new playback") play(QueueCommand(songs)) } else { val stateHolder = stateHolder ?: return - T.d("Adding ${songs.size} songs to end of queue") + L.d("Adding ${songs.size} songs to end of queue") stateHolder.addToQueue(songs, StateAck.AddToQueue(queue.size, songs.size)) } } @@ -505,21 +505,21 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun moveQueueItem(src: Int, dst: Int) { val stateHolder = stateHolder ?: return - T.d("Moving item $src to position $dst") + L.d("Moving item $src to position $dst") stateHolder.move(src, dst, StateAck.Move(src, dst)) } @Synchronized override fun removeQueueItem(at: Int) { val stateHolder = stateHolder ?: return - T.d("Removing item at $at") + L.d("Removing item at $at") stateHolder.remove(at, StateAck.Remove(at)) } @Synchronized override fun shuffled(shuffled: Boolean) { val stateHolder = stateHolder ?: return - T.d("Reordering queue [shuffled=$shuffled]") + L.d("Reordering queue [shuffled=$shuffled]") stateHolder.shuffled(shuffled) } @@ -529,7 +529,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override fun playDeferred(action: DeferredPlayback) { val stateHolder = stateHolder if (stateHolder == null || !stateHolder.handleDeferred(action)) { - T.d("Internal player not present or did not consume action, waiting") + L.d("Internal player not present or did not consume action, waiting") pendingDeferredPlayback = action } } @@ -537,12 +537,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun requestAction(stateHolder: PlaybackStateHolder) { if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) { - T.w("Given internal player did not match current internal player") + L.w("Given internal player did not match current internal player") return } if (pendingDeferredPlayback?.let(stateHolder::handleDeferred) == true) { - T.d("Pending action consumed") + L.d("Pending action consumed") pendingDeferredPlayback = null } } @@ -550,35 +550,35 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun playing(isPlaying: Boolean) { val stateHolder = stateHolder ?: return - T.d("Updating playing state to $isPlaying") + L.d("Updating playing state to $isPlaying") stateHolder.playing(isPlaying) } @Synchronized override fun repeatMode(repeatMode: RepeatMode) { val stateHolder = stateHolder ?: return - T.d("Updating repeat mode to $repeatMode") + L.d("Updating repeat mode to $repeatMode") stateHolder.repeatMode(repeatMode) } @Synchronized override fun seekTo(positionMs: Long) { val stateHolder = stateHolder ?: return - T.d("Seeking to ${positionMs}ms") + L.d("Seeking to ${positionMs}ms") stateHolder.seekTo(positionMs) } @Synchronized override fun endSession() { val stateHolder = stateHolder ?: return - T.d("Ending session") + L.d("Ending session") stateHolder.endSession() } @Synchronized override fun ack(stateHolder: PlaybackStateHolder, ack: StateAck) { if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) { - T.w("Given internal player did not match current internal player") + L.w("Given internal player did not match current internal player") return } @@ -729,7 +729,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { destructive: Boolean ) { if (isInitialized && !destructive) { - T.w("Already initialized, cannot apply saved state") + L.w("Already initialized, cannot apply saved state") return } @@ -751,7 +751,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { } } - T.d("Created adjustment mapping [max shift=$currentShift]") + L.d("Created adjustment mapping [max shift=$currentShift]") val shuffledMapping = savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { index -> @@ -775,7 +775,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { index-- } - T.d("Corrected index: ${savedState.index} -> $index") + L.d("Corrected index: ${savedState.index} -> $index") check(shuffledMapping.all { it in heap.indices }) { "Queue inconsistency detected: Shuffled mapping indices out of heap bounds" diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index 48b8c209b..042fcede9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -21,12 +21,10 @@ package org.oxycblt.auxio.playback.ui import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet -import androidx.interpolator.view.animation.FastOutSlowInInterpolator -import com.google.android.material.R as MR import com.google.android.material.button.MaterialButton -import com.google.android.material.motion.MotionUtils import org.oxycblt.auxio.ui.RippleFixMaterialButton -import timber.log.Timber as T +import org.oxycblt.auxio.ui.StationaryAnim +import timber.log.Timber as L /** * A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when @@ -47,13 +45,7 @@ class AnimatedMaterialButton : RippleFixMaterialButton { private var currentCornerRadiusRatio = 0f private var animator: ValueAnimator? = null - - private val matInterpolator = - MotionUtils.resolveThemeInterpolator( - context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()) - - private val matDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300) + private val anim = StationaryAnim.forMediumComponent(context) override fun setActivated(activated: Boolean) { super.setActivated(activated) @@ -62,20 +54,17 @@ class AnimatedMaterialButton : RippleFixMaterialButton { val targetRadius = if (activated) 0.3f else 0.5f if (!isLaidOut) { // Not laid out, initialize it without animation before drawing. - T.d("Not laid out, immediately updating corner radius") + L.d("Not laid out, immediately updating corner radius") updateCornerRadiusRatio(targetRadius) return } - T.d("Starting corner radius animation") + L.d("Starting corner radius animation") animator?.cancel() animator = - ValueAnimator.ofFloat(currentCornerRadiusRatio, targetRadius).apply { - duration = matDuration.toLong() - interpolator = matInterpolator - addUpdateListener { updateCornerRadiusRatio(animatedValue as Float) } - start() - } + anim + .genericFloat(currentCornerRadiusRatio, targetRadius, ::updateCornerRadiusRatio) + .also { it.start() } } private fun updateCornerRadiusRatio(ratio: Float) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt index e7201ce97..551029e9f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt @@ -25,7 +25,7 @@ import kotlin.math.max import org.oxycblt.auxio.databinding.ViewSeekBarBinding import org.oxycblt.auxio.playback.formatDurationDs import org.oxycblt.auxio.util.inflater -import timber.log.Timber as T +import timber.log.Timber as L /** * A wrapper around [Slider] that shows position and duration values and sanitizes input to reduce @@ -81,11 +81,11 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 // zero, use 1 instead and disable the SeekBar. val to = max(value, 1) isEnabled = value > 0 - T.d("Value sanitization finished [to=$to, enabled=$isEnabled]") + L.d("Value sanitization finished [to=$to, enabled=$isEnabled]") // Sanity check 2: If the current value exceeds the new duration value, clamp it // down so that we don't crash and instead have an annoying visual flicker. if (positionDs > to) { - T.d("Clamping invalid position [current: $positionDs new max: $to]") + L.d("Clamping invalid position [current: $positionDs new max: $to]") binding.seekBarSlider.value = to.toFloat() } binding.seekBarSlider.valueTo = to.toFloat() @@ -93,14 +93,14 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 } override fun onStartTrackingTouch(slider: Slider) { - T.d("Starting seek mode") + L.d("Starting seek mode") // User has begun seeking, place the SeekBar into a "Suspended" mode in which no // position updates are sent and is indicated by the position value turning accented. isActivated = true } override fun onStopTrackingTouch(slider: Slider) { - T.d("Confirming seek") + L.d("Confirming seek") // End of seek event, send off new value to listener. isActivated = false listener?.onSeekConfirmed(slider.value.toLong()) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt index 864e46785..8af33ebbc 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Name -import timber.log.Timber as T +import timber.log.Timber as L /** * Implements the fuzzy-ish searching algorithm used in the search view. @@ -67,7 +67,7 @@ interface SearchEngine { class SearchEngineImpl @Inject constructor(@ApplicationContext private val context: Context) : SearchEngine { override suspend fun search(items: SearchEngine.Items, query: String): SearchEngine.Items { - T.d("Launching search for $query") + L.d("Launching search for $query") return SearchEngine.Items( songs = items.songs?.searchListImpl(query) { q, song -> diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 148c71893..b9515b4db 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -64,7 +64,7 @@ import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.setFullWidthLookup import org.oxycblt.auxio.util.showToast -import timber.log.Timber as T +import timber.log.Timber as L /** * The [ListFragment] providing search functionality for the music library. @@ -108,11 +108,11 @@ class SearchFragment : ListFragment() { getContentLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri == null) { - T.w("No URI returned from file picker") + L.w("No URI returned from file picker") return@registerForActivityResult } - T.d("Received playlist URI $uri") + L.d("Received playlist URI $uri") musicModel.importPlaylist(uri, pendingImportTarget) } @@ -139,7 +139,7 @@ class SearchFragment : ListFragment() { if (!launchedKeyboard) { // Auto-open the keyboard when this view is shown - T.d("Keyboard is not shown yet") + L.d("Keyboard is not shown yet") showKeyboard(this) launchedKeyboard = true } @@ -184,7 +184,7 @@ class SearchFragment : ListFragment() { if (item.itemId != R.id.submenu_filtering) { // Is a change in filter mode and not just a junk submenu click, update // the filtering within SearchViewModel. - T.d("Filter mode selected") + L.d("Filter mode selected") item.isChecked = true searchModel.setFilterOptionId(item.itemId) return true @@ -222,7 +222,7 @@ class SearchFragment : ListFragment() { // I would make it so that the position is only scrolled back to the top when // the query actually changes instead of once every re-creation event, but sadly // that doesn't seem possible. - T.d("Update finished, scrolling to top") + L.d("Update finished, scrolling to top") binding.searchRecycler.scrollToPosition(0) } } @@ -230,39 +230,39 @@ class SearchFragment : ListFragment() { private fun handleShow(show: Show?) { when (show) { is Show.SongDetails -> { - T.d("Navigating to ${show.song}") + L.d("Navigating to ${show.song}") findNavController().navigateSafe(SearchFragmentDirections.showSong(show.song.uid)) } is Show.SongAlbumDetails -> { - T.d("Navigating to the album of ${show.song}") + L.d("Navigating to the album of ${show.song}") findNavController() .navigateSafe(SearchFragmentDirections.showAlbum(show.song.album.uid)) } is Show.AlbumDetails -> { - T.d("Navigating to ${show.album}") + L.d("Navigating to ${show.album}") findNavController().navigateSafe(SearchFragmentDirections.showAlbum(show.album.uid)) } is Show.ArtistDetails -> { - T.d("Navigating to ${show.artist}") + L.d("Navigating to ${show.artist}") findNavController() .navigateSafe(SearchFragmentDirections.showArtist(show.artist.uid)) } is Show.SongArtistDecision -> { - T.d("Navigating to artist choices for ${show.song}") + L.d("Navigating to artist choices for ${show.song}") findNavController() .navigateSafe(SearchFragmentDirections.showArtistChoices(show.song.uid)) } is Show.AlbumArtistDecision -> { - T.d("Navigating to artist choices for ${show.album}") + L.d("Navigating to artist choices for ${show.album}") findNavController() .navigateSafe(SearchFragmentDirections.showArtistChoices(show.album.uid)) } is Show.GenreDetails -> { - T.d("Navigating to ${show.genre}") + L.d("Navigating to ${show.genre}") findNavController().navigateSafe(SearchFragmentDirections.showGenre(show.genre.uid)) } is Show.PlaylistDetails -> { - T.d("Navigating to ${show.playlist}") + L.d("Navigating to ${show.playlist}") findNavController() .navigateSafe(SearchFragmentDirections.showPlaylist(show.playlist.uid)) } @@ -296,7 +296,7 @@ class SearchFragment : ListFragment() { binding.searchSelectionToolbar.title = getString(R.string.fmt_selected, selected.size) if (binding.searchToolbar.setVisible(R.id.search_selection_toolbar)) { // New selection started, show the keyboard to make selection easier. - T.d("Significant selection occurred, hiding keyboard") + L.d("Significant selection occurred, hiding keyboard") hideKeyboard() } } else { @@ -309,7 +309,7 @@ class SearchFragment : ListFragment() { val directions = when (decision) { is PlaylistDecision.Import -> { - T.d("Importing playlist") + L.d("Importing playlist") pendingImportTarget = decision.target requireNotNull(getContentLauncher) { "Content picker launcher was not available" @@ -319,7 +319,7 @@ class SearchFragment : ListFragment() { return } is PlaylistDecision.Rename -> { - T.d("Renaming ${decision.playlist}") + L.d("Renaming ${decision.playlist}") SearchFragmentDirections.renamePlaylist( decision.playlist.uid, decision.template, @@ -327,15 +327,15 @@ class SearchFragment : ListFragment() { decision.reason) } is PlaylistDecision.Delete -> { - T.d("Deleting ${decision.playlist}") + L.d("Deleting ${decision.playlist}") SearchFragmentDirections.deletePlaylist(decision.playlist.uid) } is PlaylistDecision.Export -> { - T.d("Exporting ${decision.playlist}") + L.d("Exporting ${decision.playlist}") SearchFragmentDirections.exportPlaylist(decision.playlist.uid) } is PlaylistDecision.Add -> { - T.d("Adding ${decision.songs.size} to a playlist") + L.d("Adding ${decision.songs.size} to a playlist") SearchFragmentDirections.addToPlaylist( decision.songs.map { it.uid }.toTypedArray()) } @@ -361,11 +361,11 @@ class SearchFragment : ListFragment() { val directions = when (decision) { is PlaybackDecision.PlayFromArtist -> { - T.d("Launching play from artist dialog for $decision") + L.d("Launching play from artist dialog for $decision") SearchFragmentDirections.playFromArtist(decision.song.uid) } is PlaybackDecision.PlayFromGenre -> { - T.d("Launching play from artist dialog for $decision") + L.d("Launching play from artist dialog for $decision") SearchFragmentDirections.playFromGenre(decision.song.uid) } } @@ -378,7 +378,7 @@ class SearchFragment : ListFragment() { * @param view The [View] to focus the keyboard on. */ private fun showKeyboard(view: View) { - T.d("Launching keyboard") + L.d("Launching keyboard") view.apply { requestFocus() postDelayed(200) { @@ -390,7 +390,7 @@ class SearchFragment : ListFragment() { /** Safely hide the keyboard from this view. */ private fun hideKeyboard() { - T.d("Hiding keyboard") + L.d("Hiding keyboard") requireNotNull(imm) { "InputMethodManager was not available" } .hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 08cdb23b8..4f2c6070b 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -40,7 +40,7 @@ import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings -import timber.log.Timber as T +import timber.log.Timber as L /** * An [ViewModel] that keeps performs search operations and tracks their results. @@ -79,7 +79,7 @@ constructor( override fun onMusicChanges(changes: MusicRepository.Changes) { if (changes.deviceLibrary || changes.userLibrary) { - T.d("Music changed, re-searching library") + L.d("Music changed, re-searching library") search(lastQuery) } } @@ -98,13 +98,13 @@ constructor( val deviceLibrary = musicRepository.deviceLibrary val userLibrary = musicRepository.userLibrary if (query.isNullOrEmpty() || deviceLibrary == null || userLibrary == null) { - T.d("Cannot search for the current query, aborting") + L.d("Cannot search for the current query, aborting") _searchResults.value = listOf() return } // Searching is time-consuming, so do it in the background. - T.d("Searching music library for $query") + L.d("Searching music library for $query") currentSearchJob = viewModelScope.launch { _searchResults.value = @@ -122,7 +122,7 @@ constructor( val items = if (filter == null) { // A nulled filter type means to not filter anything. - T.d("No filter specified, using entire library") + L.d("No filter specified, using entire library") SearchEngine.Items( deviceLibrary.songs, deviceLibrary.albums, @@ -130,7 +130,7 @@ constructor( deviceLibrary.genres, userLibrary.playlists) } else { - T.d("Filter specified, reducing library") + L.d("Filter specified, reducing library") SearchEngine.Items( songs = if (filter == MusicType.SONGS) deviceLibrary.songs else null, albums = if (filter == MusicType.ALBUMS) deviceLibrary.albums else null, @@ -143,13 +143,13 @@ constructor( return buildList { results.artists?.let { - T.d("Adding ${it.size} artists to search results") + L.d("Adding ${it.size} artists to search results") val header = BasicHeader(R.string.lbl_artists) add(header) addAll(SORT.artists(it)) } results.albums?.let { - T.d("Adding ${it.size} albums to search results") + L.d("Adding ${it.size} albums to search results") val header = BasicHeader(R.string.lbl_albums) if (isNotEmpty()) { add(PlainDivider(header)) @@ -159,7 +159,7 @@ constructor( addAll(SORT.albums(it)) } results.playlists?.let { - T.d("Adding ${it.size} playlists to search results") + L.d("Adding ${it.size} playlists to search results") val header = BasicHeader(R.string.lbl_playlists) if (isNotEmpty()) { add(PlainDivider(header)) @@ -169,7 +169,7 @@ constructor( addAll(SORT.playlists(it)) } results.genres?.let { - T.d("Adding ${it.size} genres to search results") + L.d("Adding ${it.size} genres to search results") val header = BasicHeader(R.string.lbl_genres) if (isNotEmpty()) { add(PlainDivider(header)) @@ -179,7 +179,7 @@ constructor( addAll(SORT.genres(it)) } results.songs?.let { - T.d("Adding ${it.size} songs to search results") + L.d("Adding ${it.size} songs to search results") val header = BasicHeader(R.string.lbl_songs) if (isNotEmpty()) { add(PlainDivider(header)) @@ -225,7 +225,7 @@ constructor( R.id.option_filter_all -> null else -> error("Invalid option ID provided") } - T.d("Updating filter type to $newFilter") + L.d("Updating filter type to $newFilter") searchSettings.filterTo = newFilter search(lastQuery) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt index 4d300d25b..b8cd75c7f 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt @@ -38,7 +38,7 @@ import org.oxycblt.auxio.settings.ui.IntListPreferenceDialog import org.oxycblt.auxio.settings.ui.PreferenceHeaderItemDecoration import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.util.systemBarInsetsCompat -import timber.log.Timber as T +import timber.log.Timber as L /** * Shared [PreferenceFragmentCompat] used across all preference screens. @@ -82,7 +82,7 @@ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) : preferenceManager.onDisplayPreferenceDialogListener = this preferenceScreen.children.forEach(::setupPreference) - T.d("Fragment created") + L.d("Fragment created") } override fun onCreateRecyclerView( diff --git a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt index 52b720927..689bf9a05 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.util.navigateSafe -import timber.log.Timber as T +import timber.log.Timber as L /** * The [PreferenceFragmentCompat] that displays the root settings list. @@ -62,21 +62,21 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { // do one. when (preference.key) { getString(R.string.set_key_ui) -> { - T.d("Navigating to UI preferences") + L.d("Navigating to UI preferences") findNavController().navigateSafe(RootPreferenceFragmentDirections.uiPreferences()) } getString(R.string.set_key_personalize) -> { - T.d("Navigating to personalization preferences") + L.d("Navigating to personalization preferences") findNavController() .navigateSafe(RootPreferenceFragmentDirections.personalizePreferences()) } getString(R.string.set_key_music) -> { - T.d("Navigating to music preferences") + L.d("Navigating to music preferences") findNavController() .navigateSafe(RootPreferenceFragmentDirections.musicPreferences()) } getString(R.string.set_key_audio) -> { - T.d("Navigating to audio preferences") + L.d("Navigating to audio preferences") findNavController().navigateSafe(RootPreferenceFragmentDirections.audioPeferences()) } getString(R.string.set_key_reindex) -> musicModel.refresh() diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 95cc281e9..ebf93bca3 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -23,7 +23,7 @@ import android.content.SharedPreferences import androidx.annotation.StringRes import androidx.preference.PreferenceManager import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * Abstract user configuration information. This interface has no functionality whatsoever. Concrete @@ -73,19 +73,19 @@ interface Settings { override fun registerListener(listener: L) { if (this.listener == null) { // Registering a listener when it was null prior, attach the callback. - T.d("Registering shared preference listener") + L.d("Registering shared preference listener") sharedPreferences.registerOnSharedPreferenceChangeListener(this) } - T.d("Registering listener $listener") + L.d("Registering listener $listener") this.listener = listener } override fun unregisterListener(listener: L) { if (this.listener !== listener) { - T.w("Given listener was not the current listener.") + L.w("Given listener was not the current listener.") return } - T.d("Unregistering listener $listener") + L.d("Unregistering listener $listener") this.listener = null // No longer have a listener, detach from the preferences instance. sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) @@ -96,7 +96,7 @@ interface Settings { key: String? ) { // FIXME: Settings initialization firing the listener. - T.d("Dispatching settings change $key") + L.d("Dispatching settings change $key") onSettingChanged(unlikelyToBeNull(key), unlikelyToBeNull(listener)) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt index 7352c9c26..ff20bed7f 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt @@ -23,7 +23,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.util.navigateSafe -import timber.log.Timber as T +import timber.log.Timber as L /** * Audio settings interface. @@ -34,7 +34,7 @@ class AudioPreferenceFragment : BasePreferenceFragment(R.xml.preferences_audio) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_pre_amp)) { - T.d("Navigating to pre-amp dialog") + L.d("Navigating to pre-amp dialog") findNavController().navigateSafe(AudioPreferenceFragmentDirections.preAmpSettings()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt index 5f282a68d..efd62a347 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.util.navigateSafe -import timber.log.Timber as T +import timber.log.Timber as L /** * "Content" settings. @@ -40,7 +40,7 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_separators)) { - T.d("Navigating to separator dialog") + L.d("Navigating to separator dialog") findNavController().navigateSafe(MusicPreferenceFragmentDirections.separatorsSettings()) } } @@ -48,10 +48,10 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) override fun onSetupPreference(preference: Preference) { if (preference.key == getString(R.string.set_key_cover_mode) || preference.key == getString(R.string.set_key_square_covers)) { - T.d("Configuring cover mode setting") + L.d("Configuring cover mode setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> - T.d("Cover mode changed, resetting image memory cache") + L.d("Cover mode changed, resetting image memory cache") imageLoader.memoryCache?.clear() true } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt index cd576ad81..4d0e12215 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt @@ -23,7 +23,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.util.navigateSafe -import timber.log.Timber as T +import timber.log.Timber as L /** * Personalization settings interface. @@ -33,7 +33,7 @@ import timber.log.Timber as T class PersonalizePreferenceFragment : BasePreferenceFragment(R.xml.preferences_personalize) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_home_tabs)) { - T.d("Navigating to home tab dialog") + L.d("Navigating to home tab dialog") findNavController().navigateSafe(PersonalizePreferenceFragmentDirections.tabSettings()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt index a52df3bdf..ff5be6b60 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.navigateSafe -import timber.log.Timber as T +import timber.log.Timber as L /** * Display preferences. @@ -42,7 +42,7 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_accent)) { - T.d("Navigating to accent dialog") + L.d("Navigating to accent dialog") findNavController().navigateSafe(UIPreferenceFragmentDirections.accentSettings()) } } @@ -50,25 +50,25 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { override fun onSetupPreference(preference: Preference) { when (preference.key) { getString(R.string.set_key_theme) -> { - T.d("Configuring theme setting") + L.d("Configuring theme setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value -> - T.d("Theme changed, recreating") + L.d("Theme changed, recreating") AppCompatDelegate.setDefaultNightMode(value as Int) true } } getString(R.string.set_key_accent) -> { - T.d("Configuring accent setting") + L.d("Configuring accent setting") preference.summary = getString(uiSettings.accent.name) } getString(R.string.set_key_black_theme) -> { - T.d("Configuring black theme setting") + L.d("Configuring black theme setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> val activity = requireActivity() if (activity.isNight) { - T.d("Black theme changed in night mode, recreating") + L.d("Black theme changed in night mode, recreating") activity.recreate() } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt new file mode 100644 index 000000000..9a5b1a898 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 Auxio Project + * Animations.kt is part of Auxio. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.oxycblt.auxio.ui + +import android.animation.TimeInterpolator +import android.animation.ValueAnimator +import android.content.Context +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import com.google.android.material.R as MR +import com.google.android.material.motion.MotionUtils + +data class Anim(val interpolator: TimeInterpolator, val duration: Long) { + inline fun genericFloat( + from: Float, + to: Float, + crossinline update: (Float) -> Unit + ): ValueAnimator = + ValueAnimator.ofFloat(from, to).apply { + duration = duration + interpolator = interpolator + addUpdateListener { update(animatedValue as Float) } + } +} + +object StationaryAnim { + fun forMediumComponent(context: Context) = + Anim( + MotionUtils.resolveThemeInterpolator( + context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()), + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300).toLong()) +} + +object InAnim { + fun forSmallComponent(context: Context) = + Anim( + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingStandardDecelerateInterpolator, + FastOutSlowInInterpolator()), + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium1, 300).toLong()) + + fun forMediumComponent(context: Context) = + Anim( + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingEmphasizedDecelerateInterpolator, + FastOutSlowInInterpolator()), + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300).toLong()) +} + +object OutAnim { + fun forSmallComponent(context: Context) = + Anim( + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingStandardAccelerateInterpolator, + FastOutSlowInInterpolator()), + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100).toLong()) + + fun forMediumComponent(context: Context) = + Anim( + MotionUtils.resolveThemeInterpolator( + context, + MR.attr.motionEasingEmphasizedAccelerateInterpolator, + FastOutSlowInInterpolator()), + MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 250).toLong()) +} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt index 015305e0f..2d64c17d4 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -31,7 +31,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.systemGestureInsetsCompat -import timber.log.Timber as T +import timber.log.Timber as L /** * A BottomSheetBehavior that resolves several issues with the default implementation, including: @@ -96,7 +96,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: val layout = super.onLayoutChild(parent, child, layoutDirection) // Don't repeat redundant initialization. if (!initalized) { - T.d("Not initialized, setting up child") + L.d("Not initialized, setting up child") child.apply { // Set up compat elevation attributes. These are only shown below API 28. translationZ = context.getDimen(MR.dimen.m3_sys_elevation_level1) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt index 11440892b..50c266ee0 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt @@ -28,7 +28,7 @@ import kotlin.math.abs import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat -import timber.log.Timber as T +import timber.log.Timber as L /** * A behavior that automatically re-layouts and re-insets content to align with the parent layout's @@ -61,12 +61,12 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri val behavior = dependency.coordinatorLayoutBehavior as BackportBottomSheetBehavior val consumed = behavior.calculateConsumedByBar() if (consumed == Int.MIN_VALUE) { - T.d("Not laid out yet, cannot update dependent view") + L.d("Not laid out yet, cannot update dependent view") return false } if (consumed != lastConsumed) { - T.d("Consumed amount changed, re-applying insets") + L.d("Consumed amount changed, re-applying insets") lastConsumed = consumed lastInsets?.let(child::dispatchApplyWindowInsets) measureContent(parent, child, consumed) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt index bc43c1cc8..82d5fbbad 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt @@ -30,7 +30,7 @@ import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout import org.oxycblt.auxio.util.coordinatorLayoutBehavior -import timber.log.Timber as T +import timber.log.Timber as L /** * An [AppBarLayout] that resolves two issues with the default implementation: @@ -76,7 +76,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr fun expandWithScrollingRecycler() { setExpanded(true) (findScrollingChild() as? RecyclerView)?.let { - T.d("Found RecyclerView, expanding with it") + L.d("Found RecyclerView, expanding with it") addOnOffsetChangedListener(ExpansionHackListener(it)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt index 3eec71f1e..931bf358d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.ui import android.animation.AnimatorSet -import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout @@ -28,10 +27,7 @@ import androidx.annotation.IdRes import androidx.appcompat.widget.Toolbar import androidx.core.view.children import androidx.core.view.isInvisible -import androidx.interpolator.view.animation.FastOutSlowInInterpolator -import com.google.android.material.R as MR -import com.google.android.material.motion.MotionUtils -import timber.log.Timber as T +import timber.log.Timber as L class MultiToolbar @JvmOverloads @@ -39,20 +35,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr FrameLayout(context, attrs, defStyleAttr) { private var animator: AnimatorSet? = null private var currentlyVisible = 0 - private val outInterpolator = - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedAccelerateInterpolator, - FastOutSlowInInterpolator()) - private val outDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 250).toLong() - private val inInterpolator = - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedDecelerateInterpolator, - FastOutSlowInInterpolator()) - private val inDuration = - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium1, 300).toLong() + private val outAnim = OutAnim.forMediumComponent(context) + private val inAnim = InAnim.forMediumComponent(context) override fun onFinishInflate() { super.onFinishInflate() @@ -67,7 +51,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr fun setVisible(@IdRes viewId: Int): Boolean { val index = children.indexOfFirst { it.id == viewId } if (index == currentlyVisible) return false - T.d("Switching toolbar visibility from $currentlyVisible -> $index") + L.d("Switching toolbar visibility from $currentlyVisible -> $index") return animateToolbarsVisibility(currentlyVisible, index).also { currentlyVisible = index } } @@ -88,7 +72,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (!isLaidOut) { // Not laid out, just change it immediately while are not shown to the user. // This is an initialization, so we return false despite changing. - T.d("Not laid out, immediately updating visibility") + L.d("Not laid out, immediately updating visibility") fromView.apply { alpha = 0f isInvisible = true @@ -100,35 +84,24 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr return false } - T.d("Changing toolbar visibility $from -> 0f, $to -> 1f") + L.d("Changing toolbar visibility $from -> 0f, $to -> 1f") animator?.cancel() val outAnimator = - ValueAnimator.ofFloat(fromView.alpha, 0f).apply { - duration = outDuration - interpolator = outInterpolator - addUpdateListener { - val ratio = it.animatedValue as Float - fromView.apply { - scaleX = 1 - 0.05f * (1 - ratio) - scaleY = 1 - 0.05f * (1 - ratio) - alpha = ratio - isInvisible = alpha == 0f - } + outAnim.genericFloat(fromView.alpha, 0f) { + fromView.apply { + scaleX = 1 - 0.05f * (1 - it) + scaleY = 1 - 0.05f * (1 - it) + alpha = it + isInvisible = alpha == 0f } } val inAnimator = - ValueAnimator.ofFloat(toView.alpha, 1f).apply { - duration = inDuration - interpolator = inInterpolator - startDelay = outDuration - addUpdateListener { - val ratio = it.animatedValue as Float - toView.apply { - scaleX = 1 - 0.05f * (1 - ratio) - scaleY = 1 - 0.05f * (1 - ratio) - alpha = ratio - isInvisible = alpha == 0f - } + inAnim.genericFloat(toView.alpha, 1f) { + toView.apply { + scaleX = 1 - 0.05f * (1 - it) + scaleY = 1 - 0.05f * (1 - it) + alpha = it + isInvisible = alpha == 0f } } animator = diff --git a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt index 219f070e4..8bda28298 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt @@ -27,7 +27,7 @@ import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.accent.Accent -import timber.log.Timber as T +import timber.log.Timber as L /** * User configuration for the general app UI. @@ -76,7 +76,7 @@ class UISettingsImpl @Inject constructor(@ApplicationContext context: Context) : override fun migrate() { if (sharedPreferences.contains(OLD_KEY_ACCENT3)) { - T.d("Migrating $OLD_KEY_ACCENT3") + L.d("Migrating $OLD_KEY_ACCENT3") var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -96,7 +96,7 @@ class UISettingsImpl @Inject constructor(@ApplicationContext context: Context) : override fun onSettingChanged(key: String, listener: UISettings.Listener) { if (key == getString(R.string.set_key_round_mode)) { - T.d("Dispatching round mode setting change") + L.d("Dispatching round mode setting change") listener.onRoundModeChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt index 9532aa6e5..7e2e07ba9 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt @@ -32,7 +32,7 @@ import com.google.android.material.bottomsheet.BackportBottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A lifecycle-aware [DialogFragment] that automatically manages the [ViewBinding] lifecycle as a @@ -98,7 +98,7 @@ abstract class ViewBindingBottomSheetDialogFragment : final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) onBindingCreated(requireBinding(), savedInstanceState) - T.d("Fragment created") + L.d("Fragment created") } final override fun onDestroyView() { @@ -106,7 +106,7 @@ abstract class ViewBindingBottomSheetDialogFragment : onDestroyBinding(unlikelyToBeNull(_binding)) // Clear binding _binding = null - T.d("Fragment destroyed") + L.d("Fragment destroyed") } private inner class TweakedBottomSheetDialog diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt index aff6b3337..38967d653 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt @@ -25,7 +25,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. @@ -87,7 +87,7 @@ abstract class ViewBindingFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // Configure binding onBindingCreated(requireBinding(), savedInstanceState) - T.d("Fragment created") + L.d("Fragment created") } final override fun onDestroyView() { @@ -95,6 +95,6 @@ abstract class ViewBindingFragment : Fragment() { onDestroyBinding(unlikelyToBeNull(_binding)) // Clear binding _binding = null - T.d("Fragment destroyed") + L.d("Fragment destroyed") } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingMaterialDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingMaterialDialogFragment.kt index 177ba3ff1..20bb9679a 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingMaterialDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingMaterialDialogFragment.kt @@ -29,7 +29,7 @@ import androidx.viewbinding.ViewBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.oxycblt.auxio.util.fixDoubleRipple import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A lifecycle-aware [DialogFragment] that automatically manages the [ViewBinding] lifecycle as a @@ -109,7 +109,7 @@ abstract class ViewBindingMaterialDialogFragment : DialogFragm onBindingCreated(requireBinding(), savedInstanceState) // Apply the newly-configured view to the dialog. (requireDialog() as AlertDialog).setView(view) - T.d("Fragment created") + L.d("Fragment created") } override fun onStart() { @@ -127,6 +127,6 @@ abstract class ViewBindingMaterialDialogFragment : DialogFragm onDestroyBinding(unlikelyToBeNull(_binding)) // Clear binding _binding = null - T.d("Fragment destroyed") + L.d("Fragment destroyed") } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt index d48b8e531..b55801fbd 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt @@ -20,7 +20,7 @@ package org.oxycblt.auxio.ui.accent import android.os.Build import org.oxycblt.auxio.R -import timber.log.Timber as T +import timber.log.Timber as L private val accentNames = intArrayOf( @@ -142,7 +142,7 @@ class Accent private constructor(val index: Int) { */ fun from(index: Int): Accent { if (index !in 0 until MAX) { - T.w("Accent is out of bounds [idx: $index]") + L.w("Accent is out of bounds [idx: $index]") return Accent(DEFAULT) } return Accent(index) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index f68777139..d0093a88f 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -31,7 +31,7 @@ import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as T +import timber.log.Timber as L /** * A [ViewBindingMaterialDialogFragment] that allows the user to configure the current [Accent]. @@ -55,7 +55,7 @@ class AccentCustomizeDialog : return@setPositiveButton } - T.d("Applying new accent") + L.d("Applying new accent") uiSettings.accent = unlikelyToBeNull(accentAdapter.selectedAccent) requireActivity().recreate() dismiss() diff --git a/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt b/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt index e0b4cb588..09cfd8415 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/CopyleftNoticeTree.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 Auxio Project + * CopyleftNoticeTree.kt is part of Auxio. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + package org.oxycblt.auxio.util import timber.log.Timber @@ -14,8 +32,11 @@ class CopyleftNoticeTree : Timber.DebugTree() { // // Read more: John 3:16, Romans 6:23, Romans 9:10 override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - super.log(priority, tag, + super.log( + priority, + tag, "Hey! Auxio is an open-source project licensed under the GPLv3 license!" + - "You can fork this project and even add ads, but it still needs to be kept open-source with the same license!", t) + "You can fork this project and even add ads, but it still needs to be kept open-source with the same license!", + t) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 3dfb4f7d4..d9e81f8e3 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -48,7 +48,7 @@ import java.lang.IllegalArgumentException import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song -import timber.log.Timber as T +import timber.log.Timber as L /** * Get if this [View] contains the given [PointF], with optional leeway. @@ -172,8 +172,8 @@ fun NavController.navigateSafe(directions: NavDirections) = navigate(directions) } catch (e: IllegalArgumentException) { // Nothing to do. - T.e("Could not navigate from this destination.") - T.e(e.stackTraceToString()) + L.e("Could not navigate from this destination.") + L.e(e.stackTraceToString()) } /** @@ -315,7 +315,7 @@ fun Context.share(parent: MusicParent) = share(parent.songs) */ fun Context.share(songs: Collection) { if (songs.isEmpty()) return - T.d("Showing sharesheet for ${songs.size} songs") + L.d("Showing sharesheet for ${songs.size} songs") val builder = ShareCompat.IntentBuilder(this) val mimeTypes = mutableSetOf() for (song in songs) { @@ -333,7 +333,7 @@ fun Context.share(songs: Collection) { */ fun Context.openInBrowser(uri: String) { fun openAppChooser(intent: Intent) { - T.d("Opening app chooser for ${intent.action}") + L.d("Opening app chooser for ${intent.action}") val chooserIntent = Intent(Intent.ACTION_CHOOSER) .putExtra(Intent.EXTRA_INTENT, intent) @@ -341,7 +341,7 @@ fun Context.openInBrowser(uri: String) { startActivity(chooserIntent) } - T.d("Opening $uri") + L.d("Opening $uri") val browserIntent = Intent(Intent.ACTION_VIEW, uri.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -349,7 +349,7 @@ fun Context.openInBrowser(uri: String) { // Android 11 seems to now handle the app chooser situations on its own now // [along with adding a new permission that breaks the old manual code], so // we just do a typical activity launch. - T.d("Using API 30+ chooser") + L.d("Using API 30+ chooser") try { startActivity(browserIntent) } catch (e: ActivityNotFoundException) { @@ -361,7 +361,7 @@ fun Context.openInBrowser(uri: String) { // not work in all cases, especially when no default app was set. If that is the // case, we will try to manually handle these cases before we try to launch the // browser. - T.d("Resolving browser activity for chooser") + L.d("Resolving browser activity for chooser") val pkgName = packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)?.run { activityInfo.packageName @@ -370,9 +370,9 @@ fun Context.openInBrowser(uri: String) { if (pkgName != null) { if (pkgName == "android") { // No default browser [Must open app chooser, may not be supported] - T.d("No default browser found") + L.d("No default browser found") openAppChooser(browserIntent) - } else T.d("Opening browser intent") + } else L.d("Opening browser intent") try { browserIntent.setPackage(pkgName) startActivity(browserIntent) diff --git a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt index c7ea02c05..7f9f4e9a5 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import org.oxycblt.auxio.BuildConfig -import timber.log.Timber as T +import timber.log.Timber as L /** * A wrapper around [StateFlow] exposing a one-time consumable event. @@ -168,11 +168,11 @@ suspend fun SendChannel.sendWithTimeout(element: E, timeout: Long = DEFAU try { withTimeout(timeout) { send(element) } } catch (e: TimeoutCancellationException) { - T.e("Failed to send element to channel $e in ${timeout}ms.") + L.e("Failed to send element to channel $e in ${timeout}ms.") if (BuildConfig.DEBUG) { throw TimeoutException("Timed out sending element to channel: $e") } else { - T.e(e.stackTraceToString()) + L.e(e.stackTraceToString()) send(element) } } @@ -211,11 +211,11 @@ suspend fun ReceiveChannel.forEachWithTimeout( subsequent = true } } catch (e: TimeoutCancellationException) { - T.e("Failed to send element to channel $e in ${timeout}ms.") + L.e("Failed to send element to channel $e in ${timeout}ms.") if (BuildConfig.DEBUG) { throw TimeoutException("Timed out sending element to channel: $e") } else { - T.e(e.stackTraceToString()) + L.e(e.stackTraceToString()) handler() } } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 855dcbd5a..160c8e28f 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.playback.state.QueueChange import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getDimenPixels -import timber.log.Timber as T +import timber.log.Timber as L /** * A component that manages the "Now Playing" state. This is kept separate from the [WidgetProvider] @@ -77,7 +77,7 @@ private constructor( fun update() { val song = playbackManager.currentSong if (song == null) { - T.d("No song, resetting widget") + L.d("No song, resetting widget") widgetProvider.update(context, uiSettings, null) return } @@ -87,7 +87,7 @@ private constructor( val repeatMode = playbackManager.repeatMode val isShuffled = playbackManager.isShuffled - T.d("Updating widget with new playback state") + L.d("Updating widget with new playback state") bitmapProvider.load( song, object : BitmapProvider.Target { @@ -95,15 +95,15 @@ private constructor( val cornerRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12, always round the cover with the widget's inner radius - T.d("Using android 12 corner radius") + L.d("Using android 12 corner radius") context.getDimenPixels(android.R.dimen.system_app_widget_inner_radius) } else if (uiSettings.roundMode) { // < Android 12, but the user still enabled round mode. - T.d("Using default corner radius") + L.d("Using default corner radius") context.getDimenPixels(R.dimen.m3_shape_corners_large) } else { // User did not enable round mode. - T.d("Using no corner radius") + L.d("Using no corner radius") 0 } @@ -124,7 +124,7 @@ private constructor( override fun onCompleted(bitmap: Bitmap?) { val state = PlaybackState(song, bitmap, isPlaying, repeatMode, isShuffled) - T.d("Bitmap loaded, uploading state $state") + L.d("Bitmap loaded, uploading state $state") widgetProvider.update(context, uiSettings, state) } }) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index e163d3aeb..4bfdf52a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -36,7 +36,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.UISettingsImpl import org.oxycblt.auxio.util.newBroadcastPendingIntent -import timber.log.Timber as T +import timber.log.Timber as L /** * The [AppWidgetProvider] for the "Now Playing" widget. This widget shows the current playback @@ -81,7 +81,7 @@ class WidgetProvider : AppWidgetProvider() { fun update(context: Context, uiSettings: UISettings, state: WidgetComponent.PlaybackState?) { if (state == null) { // No state, use the default widget. - T.d("No state provided, returning to default") + L.d("No state provided, returning to default") reset(context, uiSettings) return } @@ -118,7 +118,7 @@ class WidgetProvider : AppWidgetProvider() { while (victims.size > 0) { try { awm.updateAppWidgetCompat(context, component, views) - T.d("Successfully updated RemoteViews layout") + L.d("Successfully updated RemoteViews layout") return } catch (e: IllegalArgumentException) { val msg = e.message ?: return @@ -138,12 +138,12 @@ class WidgetProvider : AppWidgetProvider() { victims.remove(victim) } catch (e: Exception) { // Layout update failed, gracefully degrade to the default widget. - T.w("Unable to update widget: $e") + L.w("Unable to update widget: $e") reset(context, uiSettings) } } // We flat-out cannot fit the bitmap into the widget. Weird. - T.w("Unable to update widget: Bitmap too large") + L.w("Unable to update widget: Bitmap too large") reset(context, uiSettings) } @@ -153,7 +153,7 @@ class WidgetProvider : AppWidgetProvider() { * @param context [Context] required to update the widget layout. */ fun reset(context: Context, uiSettings: UISettings) { - T.d("Using default layout") + L.d("Using default layout") AppWidgetManager.getInstance(context) .updateAppWidget( ComponentName(context, this::class.java), newDefaultLayout(context, uiSettings)) @@ -167,7 +167,7 @@ class WidgetProvider : AppWidgetProvider() { * @param context [Context] required to send update request broadcast. */ private fun requestUpdate(context: Context) { - T.d("Sending update intent to PlaybackService") + L.d("Sending update intent to PlaybackService") val intent = Intent(ACTION_WIDGET_UPDATE).addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) context.sendBroadcast(intent) } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt index 5135b3526..a5e32881b 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -29,7 +29,7 @@ import androidx.annotation.IdRes import androidx.annotation.LayoutRes import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.newMainPendingIntent -import timber.log.Timber as T +import timber.log.Timber as L /** * Create a [RemoteViews] instance with the specified layout and an automatic click handler to open @@ -103,7 +103,7 @@ fun AppWidgetManager.updateAppWidgetCompat( width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) } - T.d("Assuming dimens are ${width}x$height") + L.d("Assuming dimens are ${width}x$height") // Find the layout with the greatest area that fits entirely within // the app widget. This is what we will use. Fall back to the smallest layout @@ -113,7 +113,7 @@ fun AppWidgetManager.updateAppWidgetCompat( .filter { it.width <= width && it.height <= height } .maxByOrNull { it.height * it.width } ?: views.minBy { it.key.width * it.key.height }.key - T.d("Using layout $layout ${views.contains(layout)}") + L.d("Using layout $layout ${views.contains(layout)}") updateAppWidget(id, views[layout]) } diff --git a/build.gradle b/build.gradle index 13d37e8f4..5f26f4094 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false id "com.google.devtools.ksp" version '2.0.21-1.0.25' apply false - id "com.diffplug.spotless" version "6.20.0" apply false + id "com.diffplug.spotless" version "6.25.0" apply false } tasks.register('clean', Delete) { From 9a01fe471e881e8e9617a0d8ad71e1338d0676a6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 16:15:10 -0600 Subject: [PATCH 097/550] detail: fix squished disc headers --- app/src/main/res/layout/item_disc_header.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/item_disc_header.xml b/app/src/main/res/layout/item_disc_header.xml index cae2a6e0e..9559b913b 100644 --- a/app/src/main/res/layout/item_disc_header.xml +++ b/app/src/main/res/layout/item_disc_header.xml @@ -14,7 +14,7 @@ Date: Fri, 18 Oct 2024 16:19:22 -0600 Subject: [PATCH 098/550] ui: fix broken toolbar anims --- .../org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt | 2 +- app/src/main/java/org/oxycblt/auxio/ui/Animations.kt | 6 ++++-- app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index 042fcede9..b20d6c47b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -63,7 +63,7 @@ class AnimatedMaterialButton : RippleFixMaterialButton { animator?.cancel() animator = anim - .genericFloat(currentCornerRadiusRatio, targetRadius, ::updateCornerRadiusRatio) + .genericFloat(currentCornerRadiusRatio, targetRadius, 0, ::updateCornerRadiusRatio) .also { it.start() } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt index 9a5b1a898..20f6befe5 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt @@ -29,11 +29,13 @@ data class Anim(val interpolator: TimeInterpolator, val duration: Long) { inline fun genericFloat( from: Float, to: Float, + delayMs: Long = 0, crossinline update: (Float) -> Unit ): ValueAnimator = ValueAnimator.ofFloat(from, to).apply { - duration = duration - interpolator = interpolator + startDelay = delayMs + duration = this@Anim.duration + interpolator = this@Anim.interpolator addUpdateListener { update(animatedValue as Float) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt index 931bf358d..ee0609bf1 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt @@ -96,7 +96,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } val inAnimator = - inAnim.genericFloat(toView.alpha, 1f) { + inAnim.genericFloat(toView.alpha, 1f, outAnim.duration) { toView.apply { scaleX = 1 - 0.05f * (1 - it) scaleY = 1 - 0.05f * (1 - it) From 64354f7f6e6821d4f6f82b06180191f215ed6ea2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 16:35:35 -0600 Subject: [PATCH 099/550] widget: add live preview for android 15 --- .../org/oxycblt/auxio/widgets/WidgetProvider.kt | 15 ++++++++++----- .../java/org/oxycblt/auxio/widgets/WidgetUtil.kt | 6 ++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 4bfdf52a6..2c88e9f46 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -86,16 +86,20 @@ class WidgetProvider : AppWidgetProvider() { return } + val awm = AppWidgetManager.getInstance(context) + // Create and configure each possible layout for the widget. These dimensions seem // arbitrary, but they are actually the minimum dimensions required to fit all of // the widget elements, plus some leeway for text sizing. + val defaultLayout = newThinDockedLayout(context, uiSettings, state) + awm.setWidgetPreviewCompat(ComponentName(context, this::class.java), defaultLayout) val views = mapOf( SizeF(180f, 48f) to newThinStickLayout(context, state), SizeF(304f, 48f) to newWideStickLayout(context, state), SizeF(180f, 100f) to newThinWaferLayout(context, uiSettings, state), SizeF(304f, 100f) to newWideWaferLayout(context, uiSettings, state), - SizeF(180f, 152f) to newThinDockedLayout(context, uiSettings, state), + SizeF(180f, 152f) to defaultLayout, SizeF(304f, 152f) to newWideDockedLayout(context, uiSettings, state), SizeF(180f, 272f) to newThinPaneLayout(context, uiSettings, state), SizeF(304f, 272f) to newWidePaneLayout(context, uiSettings, state)) @@ -113,7 +117,6 @@ class WidgetProvider : AppWidgetProvider() { ) // Manually update AppWidgetManager with the new views. - val awm = AppWidgetManager.getInstance(context) val component = ComponentName(context, this::class.java) while (victims.size > 0) { try { @@ -154,9 +157,11 @@ class WidgetProvider : AppWidgetProvider() { */ fun reset(context: Context, uiSettings: UISettings) { L.d("Using default layout") - AppWidgetManager.getInstance(context) - .updateAppWidget( - ComponentName(context, this::class.java), newDefaultLayout(context, uiSettings)) + val layout = newDefaultLayout(context, uiSettings) + AppWidgetManager.getInstance(context).apply { + setWidgetPreviewCompat(ComponentName(context, this::class.java), layout) + updateAppWidget(ComponentName(context, this::class.java), layout) + } } // --- INTERNAL METHODS --- diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt index a5e32881b..049531c88 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.widgets import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.Context import android.os.Build @@ -65,6 +66,11 @@ fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) { setInt(viewId, "setLayoutDirection", layoutDirection) } +fun AppWidgetManager.setWidgetPreviewCompat(component: ComponentName, remoteViews: RemoteViews) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + setWidgetPreview(component, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, remoteViews) + } +} /** * Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with an * adaptive layout, in a version-compatible manner. From 4befe1910f097901539a22d325b88a16799b48ff Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 16:36:43 -0600 Subject: [PATCH 100/550] info: update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f3b55e92..f3545a2b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,19 +3,23 @@ ## 4.0.0 #### What's New +- Android 15 support - New app branding and icon - Refreshed playback design +- Live widget preview on Android 15+ #### What's Improved - M3U playlist file name is now proposed if one cannot be found within the file - Sorting songs by date now uses songs date first, before the earliest album date #### What's Fixed -- Playback no longer briefly pauses when adding songs to playlists - Music loader no longer spawns thousands of threads when scanning - Excessive CPU no longer spent showing music loading process - Fixed playback sheet flickering on warm start +#### Dev/Meta +- No longer using custom logging setup + ## 3.6.2 #### What's Fixed From bba4ae81e723509084f1737b721b9034252727ca Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 16:37:49 -0600 Subject: [PATCH 101/550] ui: phase out custom track color --- app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt | 6 ------ .../java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt | 5 ----- app/src/main/res/color/sel_track.xml | 4 ---- app/src/main/res/values/styles_ui.xml | 2 +- 4 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 app/src/main/res/color/sel_track.xml diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 6dd428f98..2a628996b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -74,7 +74,6 @@ import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedMethod import org.oxycblt.auxio.util.navigateSafe @@ -141,11 +140,6 @@ class HomeFragment : MenuCompat.setGroupDividerEnabled(menu, true) } - // Load the track color in manually as it's unclear whether the track actually supports - // using a ColorStateList in the resources - binding.homeIndexingProgress.trackColor = - requireContext().getColorCompat(R.color.sel_track).defaultColor - binding.homePager.apply { // Update HomeViewModel whenever the user swipes through the ViewPager. // This would be implemented in HomeFragment itself, but OnPageChangeCallback diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index c40b17a9f..a47f53123 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -71,11 +71,6 @@ class PlaybackBarFragment : ViewBindingFragment() { // Set up actions binding.playbackPlayPause.setOnClickListener { playbackModel.togglePlaying() } - // Load the track color in manually as it's unclear whether the track actually supports - // using a ColorStateList in the resources. - binding.playbackProgressBar.trackColor = - context.getColorCompat(R.color.sel_track).defaultColor - // binding.playbackProgressBar.wavelength = 48 // binding.playbackProgressBar.speed = 20 // binding.playbackProgressBar.amplitude = 5 diff --git a/app/src/main/res/color/sel_track.xml b/app/src/main/res/color/sel_track.xml deleted file mode 100644 index e3a847564..000000000 --- a/app/src/main/res/color/sel_track.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index b9e7e8a60..2d9a779a9 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -27,7 +27,7 @@ From de36f263949a211a7ae2d45a96295e5d7f0bc830 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 18 Oct 2024 16:46:26 -0600 Subject: [PATCH 102/550] build: bump media --- media | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/media b/media index 34b33175c..fa5bbe28d 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 34b33175c00183dc95cdcb8c735033b6785041e1 +Subproject commit fa5bbe28de0afb2ab704c3c10a0dd1a801aaeb53 From 50829a54d37cb90874f9b2d5ecffc40af9ba1226 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 19 Oct 2024 12:24:50 -0600 Subject: [PATCH 103/550] detail: fix extra divider on playlist edit --- app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index e122de45d..2f108917b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -599,7 +599,6 @@ constructor( val list = mutableListOf() if (edited.isNotEmpty()) { val header = EditHeader(R.string.lbl_songs) - list.add(PlainDivider(header)) list.add(header) list.addAll(edited) } From 22ce9988c891ca92ae3dd695a7db987ac3f3442a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 19 Oct 2024 12:26:02 -0600 Subject: [PATCH 104/550] ui: start moving to pre-packaged anims --- .../oxycblt/auxio/detail/DetailFragment.kt | 4 +- .../oxycblt/auxio/home/ThemedSpeedDialView.kt | 8 +- .../home/fastscroll/FastScrollRecyclerView.kt | 47 ++--- .../playback/ui/AnimatedMaterialButton.kt | 23 +-- .../java/org/oxycblt/auxio/ui/Animations.kt | 164 +++++++++++++----- .../java/org/oxycblt/auxio/ui/MultiToolbar.kt | 63 +------ 6 files changed, 155 insertions(+), 154 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 675f5c198..355c9961e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -115,8 +115,8 @@ abstract class DetailFragment

: val outRatio = min(ratio * 2, 1f) val detailHeader = binding.detailHeader - detailHeader.scaleX = 1 - 0.05f * outRatio - detailHeader.scaleY = 1 - 0.05f * outRatio + detailHeader.scaleX = 1 - 0.2f * outRatio / (5f / 3f) + detailHeader.scaleY = 1 - 0.2f * outRatio / (5f / 3f) detailHeader.alpha = 1 - outRatio val inRatio = max(ratio - 0.5f, 0f) * 2 diff --git a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt index 2637fb331..d111899e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt @@ -46,7 +46,7 @@ import com.leinardi.android.speeddial.SpeedDialView import kotlin.math.roundToInt import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.R -import org.oxycblt.auxio.ui.StationaryAnim +import org.oxycblt.auxio.ui.AnimConfig import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimenPixels @@ -78,7 +78,7 @@ class ThemedSpeedDialView : SpeedDialView { @AttrRes defStyleAttr: Int ) : super(context, attrs, defStyleAttr) - private val inAnim = StationaryAnim.forMediumComponent(context) + private val stationaryConfig = AnimConfig.of(context, AnimConfig.STANDARD, AnimConfig.MEDIUM2) init { // Work around ripple bug on Android 12 when useCompatPadding = true. @@ -142,7 +142,7 @@ class ThemedSpeedDialView : SpeedDialView { } private fun createMainFabAnimator(isOpen: Boolean): Animator { - val totalDuration = inAnim.duration + val totalDuration = stationaryConfig.duration val partialDuration = totalDuration / 2 // This is half of the total duration val delay = totalDuration / 4 // This is one fourth of the total duration @@ -174,7 +174,7 @@ class ThemedSpeedDialView : SpeedDialView { val animatorSet = AnimatorSet().apply { playTogether(backgroundTintAnimator, imageTintAnimator, levelAnimator) - interpolator = inAnim.interpolator + interpolator = stationaryConfig.interpolator } animatorSet.start() return animatorSet diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index 4631750e1..535d47fb8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.home.fastscroll +import android.animation.Animator import android.content.Context import android.graphics.Canvas import android.graphics.Rect @@ -37,8 +38,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.list.recycler.AuxioRecyclerView -import org.oxycblt.auxio.ui.InAnim -import org.oxycblt.auxio.ui.OutAnim +import org.oxycblt.auxio.ui.MaterialFader import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.isRtl @@ -84,9 +84,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr background = context.getDrawableCompat(R.drawable.ui_scroll_thumb) } - private val thumbEnter = InAnim.forSmallComponent(context) - private val thumbExit = OutAnim.forSmallComponent(context) - private val thumbWidth = thumbView.background.intrinsicWidth private val thumbHeight = thumbView.background.intrinsicHeight private val thumbPadding = Rect(0, 0, 0, 0) @@ -114,8 +111,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } - private val popupEnter = InAnim.forSmallComponent(context) - private val popupExit = OutAnim.forSmallComponent(context) + private val fader = MaterialFader.forSmallComponent(context) + private var thumbAnimator: Animator? = null + private var popupAnimator: Animator? = null private var showingPopup = false @@ -426,12 +424,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingThumb = true - thumbView - .animate() - .scaleX(1f) - .setInterpolator(thumbEnter.interpolator) - .setDuration(thumbEnter.duration) - .start() + thumbAnimator?.cancel() + thumbAnimator = fader.fadeIn(thumbView).also { it.start() } } private fun hideScrollbar() { @@ -440,12 +434,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingThumb = false - thumbView - .animate() - .scaleX(0f) - .setInterpolator(thumbExit.interpolator) - .setDuration(thumbExit.duration) - .start() + thumbAnimator?.cancel() + thumbAnimator = fader.fadeOut(thumbView).also { it.start() } } private fun showPopup() { @@ -458,13 +448,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr popupView.alpha = 1f showingPopup = true - popupView - .animate() - .scaleX(1f) - .scaleY(1f) - .setInterpolator(popupEnter.interpolator) - .setDuration(popupEnter.duration) - .start() + popupAnimator?.cancel() + popupAnimator = fader.fadeIn(popupView).also { it.start() } } private fun hidePopup() { @@ -473,14 +458,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } showingPopup = false - popupView - .animate() - .alpha(0f) - .scaleX(0.75f) - .scaleY(0.75f) - .setInterpolator(popupExit.interpolator) - .setDuration(popupExit.duration) - .start() + popupAnimator?.cancel() + popupAnimator = fader.fadeOut(popupView).also { it.start() } } // --- LAYOUT STATE --- diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index b20d6c47b..29accb6f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -18,12 +18,12 @@ package org.oxycblt.auxio.playback.ui -import android.animation.ValueAnimator +import android.animation.Animator import android.content.Context import android.util.AttributeSet import com.google.android.material.button.MaterialButton +import org.oxycblt.auxio.ui.MaterialCornerAnim import org.oxycblt.auxio.ui.RippleFixMaterialButton -import org.oxycblt.auxio.ui.StationaryAnim import timber.log.Timber as L /** @@ -43,9 +43,8 @@ class AnimatedMaterialButton : RippleFixMaterialButton { defStyleAttr: Int ) : super(context, attrs, defStyleAttr) - private var currentCornerRadiusRatio = 0f - private var animator: ValueAnimator? = null - private val anim = StationaryAnim.forMediumComponent(context) + private var animator: Animator? = null + private val anim = MaterialCornerAnim(context) override fun setActivated(activated: Boolean) { super.setActivated(activated) @@ -55,22 +54,12 @@ class AnimatedMaterialButton : RippleFixMaterialButton { if (!isLaidOut) { // Not laid out, initialize it without animation before drawing. L.d("Not laid out, immediately updating corner radius") - updateCornerRadiusRatio(targetRadius) + shapeAppearanceModel = shapeAppearanceModel.withCornerSize { it.width() * targetRadius } return } L.d("Starting corner radius animation") animator?.cancel() - animator = - anim - .genericFloat(currentCornerRadiusRatio, targetRadius, 0, ::updateCornerRadiusRatio) - .also { it.start() } - } - - private fun updateCornerRadiusRatio(ratio: Float) { - currentCornerRadiusRatio = ratio - // Can't reproduce the intrinsic ratio corner radius, just manually implement it with - // a dimension value. - shapeAppearanceModel = shapeAppearanceModel.withCornerSize { it.width() * ratio } + animator = anim.animate(this, width * targetRadius).also { it.start() } } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt index 20f6befe5..db54ba9eb 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt @@ -18,14 +18,58 @@ package org.oxycblt.auxio.ui +import android.animation.Animator +import android.animation.AnimatorSet import android.animation.TimeInterpolator import android.animation.ValueAnimator import android.content.Context +import android.graphics.Rect +import android.view.View +import androidx.annotation.AttrRes +import androidx.core.graphics.toRectF +import androidx.core.view.isInvisible import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.google.android.material.R as MR +import com.google.android.material.button.MaterialButton import com.google.android.material.motion.MotionUtils -data class Anim(val interpolator: TimeInterpolator, val duration: Long) { +class AnimConfig( + context: Context, + @AttrRes interpolatorRes: Int, + @AttrRes durationRes: Int, + defaultDuration: Int +) { + val interpolator: TimeInterpolator = + MotionUtils.resolveThemeInterpolator(context, interpolatorRes, FastOutSlowInInterpolator()) + val duration: Long = + MotionUtils.resolveThemeDuration(context, durationRes, defaultDuration).toLong() + + companion object { + val STANDARD = MR.attr.motionEasingStandardInterpolator + val EMPHASIZED = MR.attr.motionEasingEmphasizedInterpolator + val EMPHASIZED_ACCELERATE = MR.attr.motionEasingEmphasizedAccelerateInterpolator + val EMPHASIZED_DECELERATE = MR.attr.motionEasingEmphasizedDecelerateInterpolator + val SHORT1 = MR.attr.motionDurationShort1 to 50 + val SHORT2 = MR.attr.motionDurationShort2 to 100 + val SHORT3 = MR.attr.motionDurationShort3 to 150 + val SHORT4 = MR.attr.motionDurationShort4 to 200 + val MEDIUM1 = MR.attr.motionDurationMedium1 to 250 + val MEDIUM2 = MR.attr.motionDurationMedium2 to 300 + val MEDIUM3 = MR.attr.motionDurationMedium3 to 350 + val MEDIUM4 = MR.attr.motionDurationMedium4 to 400 + val LONG1 = MR.attr.motionDurationLong1 to 450 + val LONG2 = MR.attr.motionDurationLong2 to 500 + val LONG3 = MR.attr.motionDurationLong3 to 550 + val LONG4 = MR.attr.motionDurationLong4 to 600 + val EXTRA_LONG1 = MR.attr.motionDurationExtraLong1 to 700 + val EXTRA_LONG2 = MR.attr.motionDurationExtraLong2 to 800 + val EXTRA_LONG3 = MR.attr.motionDurationExtraLong3 to 900 + val EXTRA_LONG4 = MR.attr.motionDurationExtraLong4 to 1000 + + fun of(context: Context, @AttrRes interpolator: Int, duration: Pair) = + AnimConfig(context, interpolator, duration.first, duration.second) + } + inline fun genericFloat( from: Float, to: Float, @@ -34,52 +78,94 @@ data class Anim(val interpolator: TimeInterpolator, val duration: Long) { ): ValueAnimator = ValueAnimator.ofFloat(from, to).apply { startDelay = delayMs - duration = this@Anim.duration - interpolator = this@Anim.interpolator + duration = this@AnimConfig.duration + interpolator = this@AnimConfig.interpolator addUpdateListener { update(animatedValue as Float) } } } -object StationaryAnim { - fun forMediumComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, MR.attr.motionEasingStandardInterpolator, FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300).toLong()) +class MaterialCornerAnim(context: Context) { + private val config = AnimConfig.of(context, AnimConfig.STANDARD, AnimConfig.MEDIUM2) + + fun animate(button: MaterialButton, sizeDp: Float): Animator { + val shapeModel = button.shapeAppearanceModel + val bounds = Rect(0, 0, button.width, button.height) + val start = shapeModel.topRightCornerSize.getCornerSize(bounds.toRectF()) + return config.genericFloat(start, sizeDp) { size -> + button.shapeAppearanceModel = shapeModel.withCornerSize { size } + } + } } -object InAnim { - fun forSmallComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingStandardDecelerateInterpolator, - FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium1, 300).toLong()) +class MaterialFader private constructor(context: Context, private val scale: Float) { + private val alphaOutConfig = + AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.SHORT3) + private val scaleOutConfig = + AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.MEDIUM1) + private val inConfig = AnimConfig.of(context, AnimConfig.EMPHASIZED, AnimConfig.LONG2) - fun forMediumComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedDecelerateInterpolator, - FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationMedium2, 300).toLong()) + fun jumpToFadeOut(view: View) { + view.apply { + alpha = 0f + scaleX = scale + scaleY = scale + isInvisible = true + } + } + + fun jumpToFadeIn(view: View) { + view.apply { + alpha = 1f + scaleX = 1.0f + scaleY = 1.0f + isInvisible = false + } + } + + fun fadeOut(view: View): Animator { + if (!view.isLaidOut) { + jumpToFadeOut(view) + return AnimatorSet() + } + + val alphaAnimator = alphaOutConfig.genericFloat(view.alpha, 0f) { view.alpha = it } + val scaleXAnimator = scaleOutConfig.genericFloat(view.scaleX, scale) { view.scaleX = it } + val scaleYAnimator = scaleOutConfig.genericFloat(view.scaleY, scale) { view.scaleY = it } + return AnimatorSet().apply { playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator) } + } + + fun fadeIn(view: View): Animator { + if (!view.isLaidOut) { + jumpToFadeIn(view) + return AnimatorSet() + } + val alphaAnimator = + inConfig.genericFloat(view.alpha, 1f) { + view.alpha = it + view.isInvisible = view.alpha == 0f + } + val scaleXAnimator = inConfig.genericFloat(view.scaleX, 1.0f) { view.scaleX = it } + val scaleYAnimator = inConfig.genericFloat(view.scaleY, 1.0f) { view.scaleY = it } + return AnimatorSet().apply { playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator) } + } + + companion object { + fun forSmallComponent(context: Context) = MaterialFader(context, 0.4f) + + fun forLargeComponent(context: Context) = MaterialFader(context, 0.9f) + } } -object OutAnim { - fun forSmallComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingStandardAccelerateInterpolator, - FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort2, 100).toLong()) +class MaterialFlipper(context: Context) { + private val fader = MaterialFader.forLargeComponent(context) - fun forMediumComponent(context: Context) = - Anim( - MotionUtils.resolveThemeInterpolator( - context, - MR.attr.motionEasingEmphasizedAccelerateInterpolator, - FastOutSlowInInterpolator()), - MotionUtils.resolveThemeDuration(context, MR.attr.motionDurationShort4, 250).toLong()) + fun jump(from: View) { + fader.jumpToFadeOut(from) + } + + fun flip(from: View, to: View): Animator { + val outAnimator = fader.fadeOut(from) + val inAnimator = fader.fadeIn(to).apply { startDelay = outAnimator.totalDuration } + return AnimatorSet().apply { playTogether(outAnimator, inAnimator) } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt index ee0609bf1..f24872842 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt @@ -18,33 +18,27 @@ package org.oxycblt.auxio.ui -import android.animation.AnimatorSet +import android.animation.Animator import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.annotation.IdRes -import androidx.appcompat.widget.Toolbar import androidx.core.view.children -import androidx.core.view.isInvisible import timber.log.Timber as L class MultiToolbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { - private var animator: AnimatorSet? = null + private var animator: Animator? = null private var currentlyVisible = 0 - private val outAnim = OutAnim.forMediumComponent(context) - private val inAnim = InAnim.forMediumComponent(context) + private val flipper = MaterialFlipper(context) override fun onFinishInflate() { super.onFinishInflate() for (i in 1 until childCount) { - getChildAt(i).apply { - alpha = 0f - isInvisible = true - } + getChildAt(i).apply { flipper.jump(this) } } } @@ -59,56 +53,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // TODO: Animate nicer Material Fade transitions using animators (Normal transitions // don't work due to translation) // Set up the target transitions for both the inner and selection toolbars. - val targetFromAlpha = 0f - val targetToAlpha = 1f - val fromView = getChildAt(from) as Toolbar - val toView = getChildAt(to) as Toolbar - - if (fromView.alpha == targetFromAlpha && toView.alpha == targetToAlpha) { - // Nothing to do. - return false - } - - if (!isLaidOut) { - // Not laid out, just change it immediately while are not shown to the user. - // This is an initialization, so we return false despite changing. - L.d("Not laid out, immediately updating visibility") - fromView.apply { - alpha = 0f - isInvisible = true - } - toView.apply { - alpha = 1f - isInvisible = false - } - return false - } - L.d("Changing toolbar visibility $from -> 0f, $to -> 1f") animator?.cancel() - val outAnimator = - outAnim.genericFloat(fromView.alpha, 0f) { - fromView.apply { - scaleX = 1 - 0.05f * (1 - it) - scaleY = 1 - 0.05f * (1 - it) - alpha = it - isInvisible = alpha == 0f - } - } - val inAnimator = - inAnim.genericFloat(toView.alpha, 1f, outAnim.duration) { - toView.apply { - scaleX = 1 - 0.05f * (1 - it) - scaleY = 1 - 0.05f * (1 - it) - alpha = it - isInvisible = alpha == 0f - } - } - animator = - AnimatorSet().apply { - playTogether(outAnimator, inAnimator) - start() - } + animator = flipper.flip(getChildAt(from), getChildAt(to)).also { it.start() } return true } From 59fd4b5e1837ab1ac01bc482d01595237236d1d2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 19 Oct 2024 12:29:38 -0600 Subject: [PATCH 105/550] playback: make repeat/shuffle on icons thicker --- app/src/main/res/drawable/ic_repeat_on_24.xml | 12 ++++++------ app/src/main/res/drawable/ic_repeat_one_24.xml | 12 ++++++------ app/src/main/res/drawable/ic_shuffle_on_24.xml | 11 +++++------ 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/drawable/ic_repeat_on_24.xml b/app/src/main/res/drawable/ic_repeat_on_24.xml index 592f3d843..256407c6e 100644 --- a/app/src/main/res/drawable/ic_repeat_on_24.xml +++ b/app/src/main/res/drawable/ic_repeat_on_24.xml @@ -1,10 +1,10 @@ - + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorPrimary"> + diff --git a/app/src/main/res/drawable/ic_repeat_one_24.xml b/app/src/main/res/drawable/ic_repeat_one_24.xml index 307b27ecf..e0c4acac0 100644 --- a/app/src/main/res/drawable/ic_repeat_one_24.xml +++ b/app/src/main/res/drawable/ic_repeat_one_24.xml @@ -1,10 +1,10 @@ - + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + diff --git a/app/src/main/res/drawable/ic_shuffle_on_24.xml b/app/src/main/res/drawable/ic_shuffle_on_24.xml index 52d82252f..a6dcd0ec4 100644 --- a/app/src/main/res/drawable/ic_shuffle_on_24.xml +++ b/app/src/main/res/drawable/ic_shuffle_on_24.xml @@ -1,11 +1,10 @@ - - + android:viewportHeight="960" + android:tint="?attr/colorPrimary"> + From 89110c2798eb63dd40c4ef163713d0b068ff69a7 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 19 Oct 2024 12:58:58 -0600 Subject: [PATCH 106/550] image: new cover selection animation --- .../java/org/oxycblt/auxio/image/CoverView.kt | 46 +++------------- .../java/org/oxycblt/auxio/ui/Animations.kt | 55 +++++++++++++++---- 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index faa3ecb04..29248f195 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -18,7 +18,7 @@ package org.oxycblt.auxio.image -import android.animation.ValueAnimator +import android.animation.Animator import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas @@ -56,12 +56,12 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.ui.MaterialFader import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDrawableCompat -import org.oxycblt.auxio.util.getInteger /** * Auxio's extension of [ImageView] that enables cover art loading and playing indicator and @@ -93,7 +93,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private val selectionBadge: ImageView? private val iconSize: Int? - private var fadeAnimator: ValueAnimator? = null + private val fader = MaterialFader.forSmallComponent(context) + private var fadeAnimator: Animator? = null private val indicatorMatrix = Matrix() private val indicatorMatrixSrc = RectF() private val indicatorMatrixDst = RectF() @@ -294,43 +295,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } private fun invalidateSelectionIndicatorAlpha(selectionBadge: ImageView) { - // Set up a target transition for the selection indicator. - val targetAlpha: Float - val targetDuration: Long - - if (isActivated) { - // View is "activated" (i.e marked as selected), so show the selection indicator. - targetAlpha = 1f - targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong() - } else { - // View is not "activated", hide the selection indicator. - targetAlpha = 0f - targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong() - } - - if (selectionBadge.alpha == targetAlpha) { - // Nothing to do. - return - } - - if (!isLaidOut) { - // Not laid out, initialize it without animation before drawing. - selectionBadge.alpha = targetAlpha - return - } - - if (fadeAnimator != null) { - // Cancel any previous animation. - fadeAnimator?.cancel() - fadeAnimator = null - } - + fadeAnimator?.cancel() fadeAnimator = - ValueAnimator.ofFloat(selectionBadge.alpha, targetAlpha).apply { - duration = targetDuration - addUpdateListener { selectionBadge.alpha = it.animatedValue as Float } - start() - } + (if (isActivated) fader.fadeIn(selectionBadge) else fader.fadeOut(selectionBadge)) + .also { it.start() } } /** diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt index db54ba9eb..86d2f06fd 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt @@ -97,12 +97,21 @@ class MaterialCornerAnim(context: Context) { } } -class MaterialFader private constructor(context: Context, private val scale: Float) { - private val alphaOutConfig = - AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.SHORT3) - private val scaleOutConfig = - AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.MEDIUM1) - private val inConfig = AnimConfig.of(context, AnimConfig.EMPHASIZED, AnimConfig.LONG2) +class MaterialFader +private constructor( + context: Context, + private val scale: Float, + @AttrRes outInterpolator: Int, + alphaOutDuration: Pair, + scaleOutDuration: Pair, + inInterpolator: Int, + alphaInDuration: Pair, + scaleInDuration: Pair +) { + private val alphaOutConfig = AnimConfig.of(context, outInterpolator, alphaOutDuration) + private val scaleOutConfig = AnimConfig.of(context, outInterpolator, scaleOutDuration) + private val alphaInConfig = AnimConfig.of(context, inInterpolator, alphaInDuration) + private val scaleInConfig = AnimConfig.of(context, inInterpolator, scaleInDuration) fun jumpToFadeOut(view: View) { view.apply { @@ -128,7 +137,11 @@ class MaterialFader private constructor(context: Context, private val scale: Flo return AnimatorSet() } - val alphaAnimator = alphaOutConfig.genericFloat(view.alpha, 0f) { view.alpha = it } + val alphaAnimator = + alphaOutConfig.genericFloat(view.alpha, 0f) { + view.alpha = it + view.isInvisible = view.alpha == 0f + } val scaleXAnimator = scaleOutConfig.genericFloat(view.scaleX, scale) { view.scaleX = it } val scaleYAnimator = scaleOutConfig.genericFloat(view.scaleY, scale) { view.scaleY = it } return AnimatorSet().apply { playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator) } @@ -140,19 +153,37 @@ class MaterialFader private constructor(context: Context, private val scale: Flo return AnimatorSet() } val alphaAnimator = - inConfig.genericFloat(view.alpha, 1f) { + alphaInConfig.genericFloat(view.alpha, 1f) { view.alpha = it view.isInvisible = view.alpha == 0f } - val scaleXAnimator = inConfig.genericFloat(view.scaleX, 1.0f) { view.scaleX = it } - val scaleYAnimator = inConfig.genericFloat(view.scaleY, 1.0f) { view.scaleY = it } + val scaleXAnimator = scaleInConfig.genericFloat(view.scaleX, 1.0f) { view.scaleX = it } + val scaleYAnimator = scaleInConfig.genericFloat(view.scaleY, 1.0f) { view.scaleY = it } return AnimatorSet().apply { playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator) } } companion object { - fun forSmallComponent(context: Context) = MaterialFader(context, 0.4f) + fun forSmallComponent(context: Context) = + MaterialFader( + context, + 0.6f, + AnimConfig.EMPHASIZED_ACCELERATE, + AnimConfig.SHORT1, + AnimConfig.SHORT3, + AnimConfig.EMPHASIZED_DECELERATE, + AnimConfig.SHORT1, + AnimConfig.MEDIUM3) - fun forLargeComponent(context: Context) = MaterialFader(context, 0.9f) + fun forLargeComponent(context: Context) = + MaterialFader( + context, + 0.9f, + AnimConfig.EMPHASIZED_ACCELERATE, + AnimConfig.SHORT3, + AnimConfig.MEDIUM1, + AnimConfig.EMPHASIZED, + AnimConfig.LONG2, + AnimConfig.LONG2) } } From bd685f1f9cfd25b27d4b863375bcc3bf118afe37 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 22 Oct 2024 21:45:01 -0600 Subject: [PATCH 107/550] ui: change materialfader anim sepcs Probably a little more in-line w/the docs. --- .../auxio/home/fastscroll/FastScrollRecyclerView.kt | 2 +- .../main/java/org/oxycblt/auxio/image/CoverView.kt | 2 +- app/src/main/java/org/oxycblt/auxio/ui/Animations.kt | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index 535d47fb8..4a0e35873 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -111,7 +111,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } - private val fader = MaterialFader.forSmallComponent(context) + private val fader = MaterialFader.quickLopsided(context) private var thumbAnimator: Animator? = null private var popupAnimator: Animator? = null diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index 29248f195..f8c4ee328 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -93,7 +93,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private val selectionBadge: ImageView? private val iconSize: Int? - private val fader = MaterialFader.forSmallComponent(context) + private val fader = MaterialFader.quickLopsided(context) private var fadeAnimator: Animator? = null private val indicatorMatrix = Matrix() private val indicatorMatrixSrc = RectF() diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt index 86d2f06fd..1d889c5a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt @@ -163,7 +163,7 @@ private constructor( } companion object { - fun forSmallComponent(context: Context) = + fun quickLopsided(context: Context) = MaterialFader( context, 0.6f, @@ -174,21 +174,21 @@ private constructor( AnimConfig.SHORT1, AnimConfig.MEDIUM3) - fun forLargeComponent(context: Context) = + fun symmetric(context: Context) = MaterialFader( context, 0.9f, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.SHORT3, AnimConfig.MEDIUM1, - AnimConfig.EMPHASIZED, - AnimConfig.LONG2, - AnimConfig.LONG2) + AnimConfig.EMPHASIZED_DECELERATE, + AnimConfig.SHORT3, + AnimConfig.MEDIUM1) } } class MaterialFlipper(context: Context) { - private val fader = MaterialFader.forLargeComponent(context) + private val fader = MaterialFader.symmetric(context) fun jump(from: View) { fader.jumpToFadeOut(from) From 97b0a8aa68ffc7fdaa3d3ff2adc04a9fd03a3a0a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 22 Oct 2024 21:57:14 -0600 Subject: [PATCH 108/550] ui: haromize bottom sheet radii w/cover radii --- .../auxio/playback/PlaybackBottomSheetBehavior.kt | 2 +- .../auxio/playback/queue/QueueBottomSheetBehavior.kt | 2 +- app/src/main/res/values/styles_ui.xml | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt index b6731e3ce..84c5df5e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt @@ -48,7 +48,7 @@ class PlaybackBottomSheetBehavior(context: Context, attributeSet: Attr shapeAppearanceModel = ShapeAppearanceModel.builder( context, - MR.style.ShapeAppearance_Material3_Corner_ExtraLarge, + R.style.ShapeAppearance_Auxio_BottomSheet, MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) .build() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt index 35eead292..02da14b5b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt @@ -72,7 +72,7 @@ class QueueBottomSheetBehavior(context: Context, attributeSet: Attribu shapeAppearanceModel = ShapeAppearanceModel.builder( context, - MR.style.ShapeAppearance_Material3_Corner_ExtraLarge, + R.style.ShapeAppearance_Auxio_BottomSheet, MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) .build() } diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 2d9a779a9..3e8f51a6e 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -37,10 +37,15 @@ false false false - @style/ShapeAppearance.Material3.Corner.None + @style/ShapeAppearance.Auxio.BottomSheet true + + + + - \ No newline at end of file From f25c98aa7ec487874462af0ca6215013771f1928 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 7 Nov 2024 13:10:11 -0700 Subject: [PATCH 119/550] build: bump deps --- app/build.gradle | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3f8ef1651..838c925a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,7 +87,7 @@ dependencies { // --- SUPPORT --- // General - implementation "androidx.core:core-ktx:1.13.1" + implementation "androidx.core:core-ktx:1.15.0" implementation "androidx.appcompat:appcompat:1.7.0" implementation "androidx.activity:activity-ktx:1.9.3" implementation "androidx.fragment:fragment-ktx:1.8.5" @@ -134,7 +134,7 @@ dependencies { // Exoplayer (Vendored) implementation project(":media-lib-exoplayer") implementation project(":media-lib-decoder-ffmpeg") - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.2" + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.3" // Image loading implementation 'io.coil-kt:coil-base:2.4.0' diff --git a/build.gradle b/build.gradle index 1a6137cb5..cf3c05b03 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } plugins { - id "com.android.application" version '8.7.1' apply false + id "com.android.application" version '8.7.2' apply false id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false id "com.google.devtools.ksp" version '2.0.21-1.0.25' apply false From 211b815a20d0d0597682be8a5b3d440bb45ff70f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 7 Nov 2024 13:38:54 -0700 Subject: [PATCH 120/550] ui: handle round mode again --- .../BackportBottomSheetBehavior.java | 4 +++ .../java/org/oxycblt/auxio/MainFragment.kt | 6 ++++ .../java/org/oxycblt/auxio/image/CoverView.kt | 19 +++++++----- .../playback/PlaybackBottomSheetBehavior.kt | 31 ++++++++++++------- .../queue/QueueBottomSheetBehavior.kt | 17 +++++----- .../auxio/ui/BaseBottomSheetBehavior.kt | 8 +++-- .../auxio/ui/BottomSheetContentBehavior.kt | 14 ++------- .../ViewBindingBottomSheetDialogFragment.kt | 5 +++ 8 files changed, 66 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java index 4b92cfc2e..1ca251dc0 100644 --- a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java +++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java @@ -1390,6 +1390,10 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo return shouldRemoveExpandedCorners; } + public void killCorners() { + materialShapeDrawable.setCornerSize(0f); + } + /** * Gets the current state of the bottom sheet. * diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 3eade4d09..d5c3f4840 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -40,6 +40,7 @@ import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialView import dagger.hilt.android.AndroidEntryPoint import java.lang.reflect.Method +import javax.inject.Inject import kotlin.math.max import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentMainBinding @@ -58,6 +59,7 @@ import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior import org.oxycblt.auxio.ui.DialogAwareNavigationListener +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately @@ -95,6 +97,7 @@ class MainFragment : private var normalCornerSize = 0f private var maxScaleXDistance = 0f private var sheetRising: Boolean? = null + @Inject lateinit var uiSettings: UISettings override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -109,8 +112,11 @@ class MainFragment : val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior + playbackSheetBehavior.uiSettings = uiSettings + playbackSheetBehavior.makeBackgroundDrawable(requireContext()) val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior? + queueSheetBehavior?.uiSettings = uiSettings elevationNormal = binding.context.getDimen(MR.dimen.m3_sys_elevation_level1) diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index f8c4ee328..90fcf17d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -108,14 +108,19 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val shapeAppearanceRes = styledAttrs.getResourceId(R.styleable.CoverView_shapeAppearance, 0) shapeAppearance = - if (shapeAppearanceRes != 0) { - ShapeAppearanceModel.builder(context, shapeAppearanceRes, -1).build() + if (uiSettings.roundMode) { + if (shapeAppearanceRes != 0) { + ShapeAppearanceModel.builder(context, shapeAppearanceRes, -1).build() + } else { + ShapeAppearanceModel.builder( + context, + com.google.android.material.R.style + .ShapeAppearance_Material3_Corner_Medium, + -1) + .build() + } } else { - ShapeAppearanceModel.builder( - context, - com.google.android.material.R.style.ShapeAppearance_Material3_Corner_Medium, - -1) - .build() + ShapeAppearanceModel.builder().build() } iconSize = styledAttrs.getDimensionPixelSize(R.styleable.CoverView_iconSize, -1).takeIf { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt index 84c5df5e2..49cef16f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt @@ -30,6 +30,7 @@ import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.BaseBottomSheetBehavior +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat @@ -42,16 +43,24 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat */ class PlaybackBottomSheetBehavior(context: Context, attributeSet: AttributeSet?) : BaseBottomSheetBehavior(context, attributeSet) { - val sheetBackgroundDrawable = - MaterialShapeDrawable.createWithElevationOverlay(context).apply { - fillColor = context.getAttrColorCompat(MR.attr.colorSurfaceContainerLow) - shapeAppearanceModel = - ShapeAppearanceModel.builder( - context, - R.style.ShapeAppearance_Auxio_BottomSheet, - MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) - .build() - } + lateinit var sheetBackgroundDrawable: MaterialShapeDrawable + + fun makeBackgroundDrawable(context: Context) { + sheetBackgroundDrawable = + MaterialShapeDrawable.createWithElevationOverlay(context).apply { + fillColor = context.getAttrColorCompat(MR.attr.colorSurfaceContainerLow) + shapeAppearanceModel = + if (uiSettings.roundMode) { + ShapeAppearanceModel.builder( + context, + R.style.ShapeAppearance_Auxio_BottomSheet, + MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) + .build() + } else { + ShapeAppearanceModel.Builder().build() + } + } + } init { isHideable = true @@ -68,7 +77,7 @@ class PlaybackBottomSheetBehavior(context: Context, attributeSet: Attr // Note: This is an extension to Auxio's vendored BottomSheetBehavior override fun isHideableWhenDragging() = false - override fun createBackground(context: Context) = + override fun createBackground(context: Context, uiSettings: UISettings) = LayerDrawable( arrayOf( // Add another colored background so that there is always an obscuring diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt index 02da14b5b..8e56dad16 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt @@ -28,6 +28,7 @@ import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.BaseBottomSheetBehavior +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat @@ -65,16 +66,18 @@ class QueueBottomSheetBehavior(context: Context, attributeSet: Attribu return false } - override fun createBackground(context: Context) = + override fun createBackground(context: Context, uiSettings: UISettings) = MaterialShapeDrawable.createWithElevationOverlay(context).apply { // The queue sheet's background is a static elevated background. fillColor = context.getAttrColorCompat(MR.attr.colorSurfaceContainerHigh) - shapeAppearanceModel = - ShapeAppearanceModel.builder( - context, - R.style.ShapeAppearance_Auxio_BottomSheet, - MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) - .build() + if (uiSettings.roundMode) { + shapeAppearanceModel = + ShapeAppearanceModel.builder( + context, + R.style.ShapeAppearance_Auxio_BottomSheet, + MR.style.ShapeAppearanceOverlay_Material3_Corner_Top) + .build() + } } override fun applyWindowInsets(child: View, insets: WindowInsets): WindowInsets { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt index 2d64c17d4..8f4488a7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -46,6 +46,10 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: private var initalized = false private val idealBottomGestureInsets = context.getDimenPixels(R.dimen.spacing_medium) + // I can't manually inject this, MainFragment must be the one to do it. + // TODO: Just use another library. Tired of Hilt. + lateinit var uiSettings: UISettings + init { // Disable isFitToContents to make the bottom sheet expand to the top of the screen and // not just how much the content takes up. @@ -58,7 +62,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: * @param context [Context] that can be used to draw the [Drawable]. * @return A background drawable. */ - abstract fun createBackground(context: Context): Drawable + abstract fun createBackground(context: Context, uiSettings: UISettings): Drawable /** Get the ideal bar height to use before the bar is properly measured. */ abstract fun getIdealBarHeight(context: Context): Int @@ -101,7 +105,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: // Set up compat elevation attributes. These are only shown below API 28. translationZ = context.getDimen(MR.dimen.m3_sys_elevation_level1) // Background differs depending on concrete implementation. - background = createBackground(context) + background = createBackground(context, uiSettings) setOnApplyWindowInsetsListener(::applyWindowInsets) } initalized = true diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt index 50c266ee0..96d494a47 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt @@ -69,7 +69,7 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri L.d("Consumed amount changed, re-applying insets") lastConsumed = consumed lastInsets?.let(child::dispatchApplyWindowInsets) - measureContent(parent, child, consumed) + measureContent(parent, child) layoutContent(child) return true } @@ -85,15 +85,7 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri parentHeightMeasureSpec: Int, heightUsed: Int ): Boolean { - val dep = dep ?: return false - val behavior = dep.coordinatorLayoutBehavior as BackportBottomSheetBehavior - val consumed = behavior.calculateConsumedByBar() - if (consumed == Int.MIN_VALUE) { - return false - } - - measureContent(parent, child, consumed) - + measureContent(parent, child) return true } @@ -123,7 +115,7 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri return true } - private fun measureContent(parent: View, child: View, consumed: Int) { + private fun measureContent(parent: View, child: View) { val contentWidthSpec = View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY) val contentHeightSpec = diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt index 7e2e07ba9..53e3af574 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt @@ -30,6 +30,7 @@ import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import com.google.android.material.bottomsheet.BackportBottomSheetDialog import com.google.android.material.bottomsheet.BackportBottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import javax.inject.Inject import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.unlikelyToBeNull import timber.log.Timber as L @@ -43,6 +44,7 @@ import timber.log.Timber as L abstract class ViewBindingBottomSheetDialogFragment : BackportBottomSheetDialogFragment() { private var _binding: VB? = null + @Inject lateinit var uiSettings: UISettings override fun onCreateDialog(savedInstanceState: Bundle?): BackportBottomSheetDialog = TweakedBottomSheetDialog(requireContext(), theme) @@ -97,6 +99,9 @@ abstract class ViewBindingBottomSheetDialogFragment : final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (!uiSettings.roundMode) { + (dialog as BackportBottomSheetDialog).behavior.killCorners() + } onBindingCreated(requireBinding(), savedInstanceState) L.d("Fragment created") } From 1d19d00798ba859ba4c28e17b712742078621325 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 7 Nov 2024 13:42:55 -0700 Subject: [PATCH 121/550] detail: add icons to play/shuffle --- app/src/main/res/layout-h360dp/fragment_detail.xml | 2 ++ app/src/main/res/layout-h480dp/fragment_detail.xml | 2 ++ app/src/main/res/layout-sw600dp/fragment_detail.xml | 2 ++ app/src/main/res/layout-w600dp/fragment_detail.xml | 2 ++ app/src/main/res/values/styles_ui.xml | 8 ++++++-- 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout-h360dp/fragment_detail.xml b/app/src/main/res/layout-h360dp/fragment_detail.xml index a04d23b7b..1defb7e9b 100644 --- a/app/src/main/res/layout-h360dp/fragment_detail.xml +++ b/app/src/main/res/layout-h360dp/fragment_detail.xml @@ -113,6 +113,7 @@ android:layout_marginEnd="@dimen/spacing_small" android:layout_marginBottom="@dimen/spacing_mid_medium" android:text="@string/lbl_play" + app:icon="@drawable/ic_play_24" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button" app:layout_constraintHorizontal_bias="0.5" @@ -126,6 +127,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_small" android:text="@string/lbl_shuffle" + app:icon="@drawable/ic_shuffle_off_24" app:layout_constraintBottom_toBottomOf="@+id/detail_play_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/detail_play_button" diff --git a/app/src/main/res/layout-h480dp/fragment_detail.xml b/app/src/main/res/layout-h480dp/fragment_detail.xml index b219830c9..5f6aa1d25 100644 --- a/app/src/main/res/layout-h480dp/fragment_detail.xml +++ b/app/src/main/res/layout-h480dp/fragment_detail.xml @@ -97,6 +97,7 @@ android:layout_marginTop="@dimen/spacing_mid_medium" android:layout_marginEnd="@dimen/spacing_small" android:text="@string/lbl_play" + app:icon="@drawable/ic_play_24" app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/detail_info" /> @@ -108,6 +109,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_small" android:text="@string/lbl_shuffle" + app:icon="@drawable/ic_shuffle_off_24" app:layout_constraintBottom_toBottomOf="@+id/detail_play_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/detail_play_button" diff --git a/app/src/main/res/layout-sw600dp/fragment_detail.xml b/app/src/main/res/layout-sw600dp/fragment_detail.xml index df3e5dfeb..213852b74 100644 --- a/app/src/main/res/layout-sw600dp/fragment_detail.xml +++ b/app/src/main/res/layout-sw600dp/fragment_detail.xml @@ -110,6 +110,7 @@ android:layout_marginStart="@dimen/spacing_medium" android:layout_marginEnd="@dimen/spacing_small" android:text="@string/lbl_play" + app:icon="@drawable/ic_play_24" app:layout_constraintBottom_toBottomOf="@+id/detail_cover" app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button" app:layout_constraintHorizontal_bias="0.5" @@ -123,6 +124,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_small" android:text="@string/lbl_shuffle" + app:icon="@drawable/ic_shuffle_off_24" app:layout_constraintBottom_toBottomOf="@+id/detail_play_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/detail_play_button" diff --git a/app/src/main/res/layout-w600dp/fragment_detail.xml b/app/src/main/res/layout-w600dp/fragment_detail.xml index a04d23b7b..1defb7e9b 100644 --- a/app/src/main/res/layout-w600dp/fragment_detail.xml +++ b/app/src/main/res/layout-w600dp/fragment_detail.xml @@ -113,6 +113,7 @@ android:layout_marginEnd="@dimen/spacing_small" android:layout_marginBottom="@dimen/spacing_mid_medium" android:text="@string/lbl_play" + app:icon="@drawable/ic_play_24" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button" app:layout_constraintHorizontal_bias="0.5" @@ -126,6 +127,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_small" android:text="@string/lbl_shuffle" + app:icon="@drawable/ic_shuffle_off_24" app:layout_constraintBottom_toBottomOf="@+id/detail_play_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/detail_play_button" diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 3e8f51a6e..dabb72a44 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -247,9 +247,13 @@ - - - + + - + + From 20be8c17fe87262a6aa014d9a3c6b1779311bb8f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 12:07:57 -0700 Subject: [PATCH 422/550] music: complete indexing after post-update steps Not the most ideal, but results in less state bugs with the current jank "pick folder" visibility in home. --- app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 86cbf834e..0262efac7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -397,8 +397,6 @@ constructor( // later. musicSettings.revision = newRevision - emitIndexingCompletion(null) - // We want to make sure that all reads and writes are synchronized due to the sheer // amount of consumers of MusicRepository. // TODO: Would Atomics not be a better fit here? @@ -435,6 +433,8 @@ constructor( // to really lucky cancellations. Clean those up now that it's impossible for // the rest of the app to be using them. covers.cleanup(newLibrary.songs.mapNotNull { it.cover }) + + emitIndexingCompletion(null) } private suspend fun emitIndexingProgress(progress: IndexingProgress) { From b0faad6380118dbc96d1368dc7230c475dfb808d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 12:14:37 -0700 Subject: [PATCH 423/550] build: bump to version 4.0.0-beta1 --- app/build.gradle | 4 ++-- .../src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 762edc9b6..ab4a11900 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId namespace - versionName "3.6.3" - versionCode 53 + versionName "4.0.0-BETA1" + versionCode 54 minSdk target_sdk targetSdk target_sdk diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index c9cbc2777..88d897006 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -41,7 +41,7 @@ import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.util.correctWhitespace import org.oxycblt.musikr.util.splitEscaped -@Database(entities = [CachedSong::class], version = 50, exportSchema = false) +@Database(entities = [CachedSong::class], version = 54, exportSchema = false) internal abstract class CacheDatabase : RoomDatabase() { abstract fun visibleDao(): VisibleCacheDao From b4a9f9af967a84180b8f82fc66f103f35c36a21c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 12:52:27 -0700 Subject: [PATCH 424/550] image: fix broken cover provider fetching --- app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt index d12fb8cc2..aededbd43 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt @@ -39,7 +39,7 @@ class CoverProvider : ContentProvider() { val coverId = requireNotNull(SiloedCoverId.parse(id)) { "Invalid ID: $id" } return runBlocking { val siloedCovers = SiloedCovers.from(requireContext(), coverId.silo) - when (val res = siloedCovers.obtain(coverId.id)) { + when (val res = siloedCovers.obtain(id)) { is ObtainResult.Hit -> res.cover.fd() is ObtainResult.Miss -> null } From 07a0d01a060f2f13d66548ba935e48b2ca86f885 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 12:54:24 -0700 Subject: [PATCH 425/550] image: fix bad coverprovider conventions --- app/src/main/AndroidManifest.xml | 2 +- .../org/oxycblt/auxio/image/CoverProvider.kt | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a060858b..afba82e8f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -105,7 +105,7 @@ --> diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt index aededbd43..5a34bfc45 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt @@ -15,10 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.image import android.content.ContentProvider +import android.content.ContentResolver import android.content.ContentValues import android.content.UriMatcher import android.database.Cursor @@ -59,25 +60,28 @@ class CoverProvider : ContentProvider() { sortOrder: String? ): Cursor = throw UnsupportedOperationException() - override fun insert(uri: Uri, values: ContentValues?): Uri = - throw UnsupportedOperationException() + override fun insert(uri: Uri, values: ContentValues?): Uri? = null - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = - throw UnsupportedOperationException() + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array? - ): Int = throw UnsupportedOperationException() + ): Int = 0 companion object { - private const val AUTHORITY = "org.oxycblt.auxio.image" + private const val AUTHORITY = "org.oxycblt.auxio.image.CoverProvider" private const val IMAGES_PATH = "covers" private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { addURI(AUTHORITY, "$IMAGES_PATH/*", 1) } - val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/$IMAGES_PATH") + val CONTENT_URI: Uri = + Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .appendPath(IMAGES_PATH) + .build() } } From 2d5ca0b351bd418dba9716bd88a4caae79205fbb Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 12:54:40 -0700 Subject: [PATCH 426/550] music: connect mediaitems to cover provider --- .../music/service/MediaItemTranslation.kt | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt index b983d81aa..74dd082c7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.service import android.content.Context import android.graphics.BitmapFactory +import android.net.Uri import android.os.Bundle import android.support.v4.media.MediaBrowserCompat.MediaItem import android.support.v4.media.MediaDescriptionCompat @@ -27,6 +28,7 @@ import androidx.annotation.StringRes import androidx.media.utils.MediaConstants import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R +import org.oxycblt.auxio.image.CoverProvider import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.formatDurationDs @@ -38,6 +40,7 @@ import org.oxycblt.musikr.Music import org.oxycblt.musikr.MusicParent import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Song +import timber.log.Timber sealed interface MediaSessionUID { data class Tab(val node: TabNode) : MediaSessionUID { @@ -115,7 +118,7 @@ fun Song.toMediaDescription(context: Context, vararg sugar: Sugar): MediaDescrip .setTitle(name.resolve(context)) .setSubtitle(artists.resolveNames(context)) .setDescription(album.name.resolve(context)) - // .setIconUri(cover.mediaStoreCoverUri) + .setIconUri(cover?.let { Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) }) .setMediaUri(uri) .setExtras(extras) .build() @@ -135,7 +138,11 @@ fun Album.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setTitle(name.resolve(context)) .setSubtitle(artists.resolveNames(context)) .setDescription(counts) - // .setIconUri(cover.single.mediaStoreCoverUri) + .setIconUri( + covers.covers + .firstOrNull() + ?.let { Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) } + .also { Timber.d("Album cover: $it") }) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) @@ -163,7 +170,10 @@ fun Artist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setTitle(name.resolve(context)) .setSubtitle(counts) .setDescription(genres.resolveNames(context)) - // .setIconUri(cover.single.mediaStoreCoverUri) + .setIconUri( + covers.covers.firstOrNull()?.let { + Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) + }) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) @@ -183,7 +193,10 @@ fun Genre.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setMediaId(mediaSessionUID.toString()) .setTitle(name.resolve(context)) .setSubtitle(counts) - // .setIconUri(cover.single.mediaStoreCoverUri) + .setIconUri( + covers.covers.firstOrNull()?.let { + Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) + }) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) @@ -204,7 +217,10 @@ fun Playlist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setTitle(name.resolve(context)) .setSubtitle(counts) .setDescription(durationMs.formatDurationDs(true)) - // .setIconUri(cover?.single?.mediaStoreCoverUri) + .setIconUri( + covers.covers.firstOrNull()?.let { + Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) + }) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) From 3431e13cde8e8e2aea7ed3a57d111d92868ca058 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 12:55:27 -0700 Subject: [PATCH 427/550] image: fix format problem --- app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt index 5a34bfc45..871069ae5 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.image import android.content.ContentProvider From fddd527975caab54c3c58e0d59d0e1ddc8b3e9b9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 13:40:52 -0700 Subject: [PATCH 428/550] musikr.tag: handle compilation flag --- .../org/oxycblt/musikr/tag/parse/TagFields.kt | 20 +++++++++---------- .../org/oxycblt/musikr/tag/parse/TagParser.kt | 13 ++++++++++-- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt index a4c48d76a..08620d399 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt @@ -205,18 +205,16 @@ internal fun Metadata.genreNames() = xiph["GENRE"] ?: mp4["©gen"] ?: mp4["gnre" // Compilation Flag internal fun Metadata.isCompilation() = // TCMP is a non-standard itunes extension + // We also only look for tags that are actually valid + // (i.e. 1 for true, 0 for false) (xiph["COMPILATION"] - ?: xiph["ITUNESCOMPILATION"] - ?: mp4["cpil"] - ?: mp4["----:COM.APPLE.ITUNES:COMPILATION"] - ?: mp4["----:COM.APPLE.ITUNES:ITUNESCOMPILATION"] - ?: id3v2["TCMP"] - ?: id3v2["TXXX:COMPILATION"] - ?: id3v2["TXXX:ITUNESCOMPILATION"]) - ?.let { - // Ignore invalid instances of this tag - it == listOf("1") - } + ?: xiph["ITUNESCOMPILATION"] + ?: mp4["cpil"] + ?: mp4["----:COM.APPLE.ITUNES:COMPILATION"] + ?: mp4["----:COM.APPLE.ITUNES:ITUNESCOMPILATION"] + ?: id3v2["TCMP"] + ?: id3v2["TXXX:COMPILATION"] + ?: id3v2["TXXX:ITUNESCOMPILATION"]) == listOf("1") // ReplayGain information internal fun Metadata.replayGainTrackAdjustment() = diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt index fcf3e48b8..42d76af43 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt @@ -32,6 +32,7 @@ internal interface TagParser { private data object TagParserImpl : TagParser { override fun parse(file: DeviceFile, metadata: Metadata): ParsedTags { + val compilation = metadata.isCompilation() return ParsedTags( durationMs = metadata.properties.durationMs, replayGainTrackAdjustment = metadata.replayGainTrackAdjustment(), @@ -46,12 +47,20 @@ private data object TagParserImpl : TagParser { albumMusicBrainzId = metadata.albumMusicBrainzId(), albumName = metadata.albumName(), albumSortName = metadata.albumSortName(), - releaseTypes = metadata.releaseTypes() ?: listOf(), + // Compilation flag implies a compilation release type in the case that + // we don't have any other release types + releaseTypes = + metadata.releaseTypes() ?: listOf("compilation").takeIf { compilation } ?: listOf(), artistMusicBrainzIds = metadata.artistMusicBrainzIds() ?: listOf(), artistNames = metadata.artistNames() ?: listOf(), artistSortNames = metadata.artistSortNames() ?: listOf(), albumArtistMusicBrainzIds = metadata.albumArtistMusicBrainzIds() ?: listOf(), - albumArtistNames = metadata.albumArtistNames() ?: listOf(), + // Compilation pretty heavily implies various artists in the case that we don't + // have any other album artists + albumArtistNames = + metadata.albumArtistNames() + ?: listOf("Various Artists").takeIf { compilation } + ?: listOf(), albumArtistSortNames = metadata.albumArtistSortNames() ?: listOf(), genreNames = metadata.genreNames() ?: listOf()) } From a4d7b54db72a7f3be4fcf9268826f95dd6d585b6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 15:55:59 -0700 Subject: [PATCH 429/550] musikr: add back tag whitespace fixes Requires me to rejig the JNI integration, but it's overall good since it allows me to strip away a lot of the logic. --- musikr/proguard-rules.pro | 3 +- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 93 +++++++------- musikr/src/main/cpp/JVMTagMap.cpp | 120 ++++++++++-------- musikr/src/main/cpp/JVMTagMap.h | 22 +++- musikr/src/main/cpp/taglib_jni.cpp | 2 +- musikr/src/main/cpp/{log.h => util.h} | 7 +- .../oxycblt/musikr/metadata/NativeTagMap.kt | 35 +++++ 7 files changed, 173 insertions(+), 109 deletions(-) rename musikr/src/main/cpp/{log.h => util.h} (92%) create mode 100644 musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt diff --git a/musikr/proguard-rules.pro b/musikr/proguard-rules.pro index 3f066b310..8cc776ba8 100644 --- a/musikr/proguard-rules.pro +++ b/musikr/proguard-rules.pro @@ -22,4 +22,5 @@ -keep class org.oxycblt.musikr.metadata.NativeInputStream { *; } -keep class org.oxycblt.musikr.metadata.Metadata { *; } --keep class org.oxycblt.musikr.metadata.Properties { *; } \ No newline at end of file +-keep class org.oxycblt.musikr.metadata.Properties { *; } +-keep class org.oxycblt.musikr.metadata.NativeTagMap { *; } \ No newline at end of file diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index 1db08a59d..afb92a43b 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -18,7 +18,7 @@ #include "JVMMetadataBuilder.h" -#include "log.h" +#include "util.h" #include #include @@ -37,18 +37,18 @@ void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) { for (auto frame : tag.frameList()) { if (auto txxxFrame = dynamic_cast(frame)) { + TagLib::String id = frame->frameID(); TagLib::StringList frameText = txxxFrame->fieldList(); // Frame text starts with the description then the remaining values auto begin = frameText.begin(); - TagLib::String key = TagLib::String(frame->frameID()) + ":" - + begin->upper(); + TagLib::String description = *begin; frameText.erase(begin); - id3v2.add(key, frameText); + id3v2.add_combined(id, description, frameText); } else if (auto textFrame = dynamic_cast(frame)) { TagLib::String key = frame->frameID(); TagLib::StringList frameText = textFrame->fieldList(); - id3v2.add(key, frameText); + id3v2.add_id(key, frameText); } else { continue; } @@ -59,7 +59,23 @@ void JVMMetadataBuilder::setXiph(const TagLib::Ogg::XiphComment &tag) { for (auto field : tag.fieldListMap()) { auto key = field.first.upper(); auto values = field.second; - xiph.add(key, values); + xiph.add_custom(key, values); + } +} + +template +void mp4AddImpl(JVMTagMap &map, TagLib::String &itemName, T itemValue) { + if (itemName.startsWith("----")) { + // Split this into it's atom name and description + auto split = itemName.split(":"); + if (split.size() != 2) { + throw std::runtime_error("Invalid atom name"); + } + auto atomName = split[0]; + auto atomDescription = split[1]; + map.add_combined(atomName, atomDescription, itemValue); + } else { + map.add_id(itemName, itemValue); } } @@ -67,47 +83,36 @@ void JVMMetadataBuilder::setMp4(const TagLib::MP4::Tag &tag) { auto map = tag.itemMap(); for (auto item : map) { auto itemName = item.first; - if (itemName.startsWith("----")) { - // Capitalize description atoms only - // Other standard atoms are cased so we want to avoid collissions there. - itemName = itemName.upper(); - } auto itemValue = item.second; auto type = itemValue.type(); - // Only read out the atoms for the reasonable tags we are expecting. - // None of the crazy binary atoms. - if (type == TagLib::MP4::Item::Type::StringList) { - auto value = itemValue.toStringList(); - mp4.add(itemName, value); - continue; - } - - // Assume that taggers will be unhinged and store track numbers - // as ints, uints, or longs. - if (type == TagLib::MP4::Item::Type::Int) { - auto value = std::to_string(itemValue.toInt()); - id3v2.add(itemName, value); - continue; - } - if (type == TagLib::MP4::Item::Type::UInt) { - auto value = std::to_string(itemValue.toUInt()); - id3v2.add(itemName, value); - continue; - } - if (type == TagLib::MP4::Item::Type::LongLong) { - auto value = std::to_string(itemValue.toLongLong()); - id3v2.add(itemName, value); - continue; - } - if (type == TagLib::MP4::Item::Type::IntPair) { - // It's inefficient going from the integer representation back into - // a string, but I fully expect taggers to just write "NN/TT" strings - // anyway, and musikr doesn't have to do as much fiddly variant handling. - auto value = std::to_string(itemValue.toIntPair().first) + "/" - + std::to_string(itemValue.toIntPair().second); - id3v2.add(itemName, value); - continue; + std::string serializedValue; + switch (type) { + // Normal expected MP4 items + case TagLib::MP4::Item::Type::StringList: + mp4AddImpl(mp4, itemName, itemValue.toStringList()); + break; + // Weird MP4 items I'm 90% sure I'll encounter. + case TagLib::MP4::Item::Type::Int: + serializedValue = std::to_string(itemValue.toInt()); + break; + case TagLib::MP4::Item::Type::UInt: + serializedValue = std::to_string(itemValue.toUInt()); + break; + case TagLib::MP4::Item::Type::LongLong: + serializedValue = std::to_string(itemValue.toLongLong()); + break; + case TagLib::MP4::Item::Type::IntPair: + // It's inefficient going from the integer representation back into + // a string, but I fully expect taggers to just write "NN/TT" strings + // anyway, and musikr doesn't have to do as much fiddly variant handling. + serializedValue = std::to_string(itemValue.toIntPair().first) + "/" + + std::to_string(itemValue.toIntPair().second); + break; + default: + // Don't care about the other types + continue; } + mp4AddImpl(mp4, itemName, TagLib::String(serializedValue)); } } diff --git a/musikr/src/main/cpp/JVMTagMap.cpp b/musikr/src/main/cpp/JVMTagMap.cpp index 70bfe342d..272b33aec 100644 --- a/musikr/src/main/cpp/JVMTagMap.cpp +++ b/musikr/src/main/cpp/JVMTagMap.cpp @@ -18,76 +18,86 @@ #include "JVMTagMap.h" -JVMTagMap::JVMTagMap(JNIEnv *env) : env(env) { - jclass hashMapClass = env->FindClass("java/util/HashMap"); - jmethodID init = env->GetMethodID(hashMapClass, "", "()V"); - hashMap = env->NewObject(hashMapClass, init); - hashMapGetMethod = env->GetMethodID(hashMapClass, "get", - "(Ljava/lang/Object;)Ljava/lang/Object;"); - hashMapPutMethod = env->GetMethodID(hashMapClass, "put", - "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); - env->DeleteLocalRef(hashMapClass); +#include "util.h" - jclass arrayListClass = env->FindClass("java/util/ArrayList"); +JVMTagMap::JVMTagMap(JNIEnv *env) : env(env) { + jclass tagMapClass = env->FindClass("org/oxycblt/musikr/metadata/NativeTagMap"); + jmethodID init = env->GetMethodID(tagMapClass, "", "()V"); + tagMap = env->NewObject(tagMapClass, init); + tagMapAddIdSingleMethod = env->GetMethodID(tagMapClass, "addID", + "(Ljava/lang/String;Ljava/lang/String;)V"); + tagMapAddIdListMethod = env->GetMethodID(tagMapClass, "addID", + "(Ljava/lang/String;Ljava/util/List;)V"); + tagMapAddCustomSingleMethod = env->GetMethodID(tagMapClass, "addCustom", + "(Ljava/lang/String;Ljava/lang/String;)V"); + tagMapAddCustomListMethod = env->GetMethodID(tagMapClass, "addCustom", + "(Ljava/lang/String;Ljava/util/List;)V"); + tagMapAddCombinedSingleMethod = env->GetMethodID(tagMapClass, "addCombined", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + tagMapAddCombinedListMethod = env->GetMethodID(tagMapClass, "addCombined", + "(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V"); + tagMapGetObjectMethod = env->GetMethodID(tagMapClass, "getObject", + "()Ljava/util/Map;"); + env->DeleteLocalRef(tagMapClass); + + arrayListClass = env->FindClass("java/util/ArrayList"); arrayListInitMethod = env->GetMethodID(arrayListClass, "", "()V"); arrayListAddMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); - env->DeleteLocalRef(arrayListClass); } JVMTagMap::~JVMTagMap() { - env->DeleteLocalRef(hashMap); + env->DeleteLocalRef(tagMap); + env->DeleteLocalRef(arrayListClass); } -void JVMTagMap::add(TagLib::String &key, std::string_view value) { - jstring jKey = env->NewStringUTF(key.toCString(true)); - jstring jValue = env->NewStringUTF(value.data()); - - // check if theres already a value arraylist in the map - jobject existingValue = env->CallObjectMethod(hashMap, hashMapGetMethod, - jKey); - // if there is, add to the value to the existing arraylist - if (existingValue != nullptr) { - env->CallBooleanMethod(existingValue, arrayListAddMethod, jValue); - } else { - // if there isn't, create a new arraylist and add the value to it - jclass arrayListClass = env->FindClass("java/util/ArrayList"); - jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); - env->CallBooleanMethod(arrayList, arrayListAddMethod, jValue); - env->CallObjectMethod(hashMap, hashMapPutMethod, jKey, arrayList); - env->DeleteLocalRef(arrayListClass); - } +void JVMTagMap::add_id(TagLib::String &id, TagLib::String &value) { + env->CallVoidMethod(tagMap, tagMapAddIdSingleMethod, + env->NewStringUTF(id.toCString(true)), env->NewStringUTF(value.toCString(true))); } -void JVMTagMap::add(TagLib::String &key, TagLib::StringList &value) { - if (value.isEmpty()) { - // Nothing to add - return; +void JVMTagMap::add_id(TagLib::String &id, TagLib::StringList &value) { + jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); + for (auto &item : value) { + env->CallBooleanMethod(arrayList, arrayListAddMethod, + env->NewStringUTF(item.toCString(true))); } - jstring jKey = env->NewStringUTF(key.toCString(true)); + env->CallVoidMethod(tagMap, tagMapAddIdListMethod, + env->NewStringUTF(id.toCString(true)), arrayList); +} - // check if theres already a value arraylist in the map - jobject existingValue = env->CallObjectMethod(hashMap, hashMapGetMethod, - jKey); - // if there is, add to the value to the existing arraylist - if (existingValue != nullptr) { - for (auto &val : value) { - jstring jValue = env->NewStringUTF(val.toCString(true)); - env->CallBooleanMethod(existingValue, arrayListAddMethod, jValue); - } - } else { - // if there isn't, create a new arraylist and add the value to it - jclass arrayListClass = env->FindClass("java/util/ArrayList"); - jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); - for (auto &val : value) { - jstring jValue = env->NewStringUTF(val.toCString(true)); - env->CallBooleanMethod(arrayList, arrayListAddMethod, jValue); - } - env->CallObjectMethod(hashMap, hashMapPutMethod, jKey, arrayList); - env->DeleteLocalRef(arrayListClass); +void JVMTagMap::add_custom(TagLib::String &description, TagLib::String &value) { + env->CallVoidMethod(tagMap, tagMapAddCustomSingleMethod, + env->NewStringUTF(description.toCString(true)), env->NewStringUTF(value.toCString(true))); +} + +void JVMTagMap::add_custom(TagLib::String &description, TagLib::StringList &value) { + jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); + for (auto &item : value) { + env->CallBooleanMethod(arrayList, arrayListAddMethod, + env->NewStringUTF(item.toCString(true))); } + env->CallVoidMethod(tagMap, tagMapAddCustomListMethod, + env->NewStringUTF(description.toCString(true)), arrayList); +} + +void JVMTagMap::add_combined(TagLib::String &id, TagLib::String &description, TagLib::String &value) { + env->CallVoidMethod(tagMap, tagMapAddCombinedSingleMethod, + env->NewStringUTF(id.toCString(true)), env->NewStringUTF(description.toCString(true)), + env->NewStringUTF(value.toCString(true))); +} + +void JVMTagMap::add_combined(TagLib::String &id, TagLib::String &description, TagLib::StringList &value) { + jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); + for (auto &item : value) { + env->CallBooleanMethod(arrayList, arrayListAddMethod, + env->NewStringUTF(item.toCString(true))); + } + env->CallVoidMethod(tagMap, tagMapAddCombinedListMethod, + env->NewStringUTF(id.toCString(true)), env->NewStringUTF(description.toCString(true)), + arrayList); } jobject JVMTagMap::getObject() { - return hashMap; + return env->CallObjectMethod(tagMap, tagMapGetObjectMethod); } diff --git a/musikr/src/main/cpp/JVMTagMap.h b/musikr/src/main/cpp/JVMTagMap.h index 3090730bb..c75067ef1 100644 --- a/musikr/src/main/cpp/JVMTagMap.h +++ b/musikr/src/main/cpp/JVMTagMap.h @@ -32,16 +32,28 @@ public: JVMTagMap(const JVMTagMap&) = delete; JVMTagMap& operator=(const JVMTagMap&) = delete; - void add(TagLib::String &key, std::string_view value); - void add(TagLib::String &key, TagLib::StringList &value); + void add_id(TagLib::String &id, TagLib::String &value); + void add_id(TagLib::String &id, TagLib::StringList &value); + + void add_custom(TagLib::String &description, TagLib::String &value); + void add_custom(TagLib::String &description, TagLib::StringList &value); + + void add_combined(TagLib::String &id, TagLib::String &description, TagLib::String &value); + void add_combined(TagLib::String &id, TagLib::String &description, TagLib::StringList &value); jobject getObject(); private: JNIEnv *env; - jobject hashMap; - jmethodID hashMapGetMethod; - jmethodID hashMapPutMethod; + jobject tagMap; + jmethodID tagMapAddIdSingleMethod; + jmethodID tagMapAddIdListMethod; + jmethodID tagMapAddCustomSingleMethod; + jmethodID tagMapAddCustomListMethod; + jmethodID tagMapAddCombinedSingleMethod; + jmethodID tagMapAddCombinedListMethod; + jmethodID tagMapGetObjectMethod; + jclass arrayListClass; jmethodID arrayListInitMethod; jmethodID arrayListAddMethod; }; diff --git a/musikr/src/main/cpp/taglib_jni.cpp b/musikr/src/main/cpp/taglib_jni.cpp index e8fca822c..c5d72bf6e 100644 --- a/musikr/src/main/cpp/taglib_jni.cpp +++ b/musikr/src/main/cpp/taglib_jni.cpp @@ -20,7 +20,7 @@ #include #include "JVMInputStream.h" #include "JVMMetadataBuilder.h" -#include "log.h" +#include "util.h" #include "taglib/fileref.h" #include "taglib/flacfile.h" diff --git a/musikr/src/main/cpp/log.h b/musikr/src/main/cpp/util.h similarity index 92% rename from musikr/src/main/cpp/log.h rename to musikr/src/main/cpp/util.h index 2be505c6a..1c223a142 100644 --- a/musikr/src/main/cpp/log.h +++ b/musikr/src/main/cpp/util.h @@ -16,9 +16,10 @@ * along with this program. If not, see . */ -#ifndef AUXIO_LOG_H -#define AUXIO_LOG_H +#ifndef AUXIO_UTIL_H +#define AUXIO_UTIL_H +#include #include #define LOG_TAG "taglib_jni" @@ -27,4 +28,4 @@ #define LOGD(...) \ ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)) -#endif //AUXIO_LOG_H +#endif //AUXIO_UTIL_H diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt new file mode 100644 index 000000000..7740fea51 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt @@ -0,0 +1,35 @@ +package org.oxycblt.musikr.metadata + +import org.oxycblt.musikr.util.correctWhitespace + +class NativeTagMap { + private val map = mutableMapOf>() + + fun addID(id: String, value: String) { + addID(id, listOf(value)) + } + + fun addID(id: String, values: List) { + map[id] = values.mapNotNull { it.correctWhitespace() } + } + + fun addCustom(description: String, value: String) { + addCustom(description, listOf(value)) + } + + fun addCustom(description: String, values: List) { + map[description.uppercase()] = values.mapNotNull { it.correctWhitespace() } + } + + fun addCombined(id: String, description: String, value: String) { + addCombined(id, description, listOf(value)) + } + + fun addCombined(id: String, description: String, values: List) { + map["$id:${description.uppercase()}"] = values.mapNotNull { it.correctWhitespace() } + } + + fun getObject(): Map> { + return map + } +} \ No newline at end of file From 32b152e1553fc48e7c5b33bb6c8b135bf844c8ab Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 15:57:03 -0700 Subject: [PATCH 430/550] musikr: reformat --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 48 +++++++++---------- musikr/src/main/cpp/JVMTagMap.cpp | 25 ++++++---- musikr/src/main/cpp/JVMTagMap.h | 6 ++- musikr/src/main/cpp/util.h | 2 +- .../oxycblt/musikr/metadata/NativeTagMap.kt | 20 +++++++- 5 files changed, 64 insertions(+), 37 deletions(-) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index afb92a43b..d70279f45 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -87,30 +87,30 @@ void JVMMetadataBuilder::setMp4(const TagLib::MP4::Tag &tag) { auto type = itemValue.type(); std::string serializedValue; switch (type) { - // Normal expected MP4 items - case TagLib::MP4::Item::Type::StringList: - mp4AddImpl(mp4, itemName, itemValue.toStringList()); - break; - // Weird MP4 items I'm 90% sure I'll encounter. - case TagLib::MP4::Item::Type::Int: - serializedValue = std::to_string(itemValue.toInt()); - break; - case TagLib::MP4::Item::Type::UInt: - serializedValue = std::to_string(itemValue.toUInt()); - break; - case TagLib::MP4::Item::Type::LongLong: - serializedValue = std::to_string(itemValue.toLongLong()); - break; - case TagLib::MP4::Item::Type::IntPair: - // It's inefficient going from the integer representation back into - // a string, but I fully expect taggers to just write "NN/TT" strings - // anyway, and musikr doesn't have to do as much fiddly variant handling. - serializedValue = std::to_string(itemValue.toIntPair().first) + "/" - + std::to_string(itemValue.toIntPair().second); - break; - default: - // Don't care about the other types - continue; + // Normal expected MP4 items + case TagLib::MP4::Item::Type::StringList: + mp4AddImpl(mp4, itemName, itemValue.toStringList()); + break; + // Weird MP4 items I'm 90% sure I'll encounter. + case TagLib::MP4::Item::Type::Int: + serializedValue = std::to_string(itemValue.toInt()); + break; + case TagLib::MP4::Item::Type::UInt: + serializedValue = std::to_string(itemValue.toUInt()); + break; + case TagLib::MP4::Item::Type::LongLong: + serializedValue = std::to_string(itemValue.toLongLong()); + break; + case TagLib::MP4::Item::Type::IntPair: + // It's inefficient going from the integer representation back into + // a string, but I fully expect taggers to just write "NN/TT" strings + // anyway, and musikr doesn't have to do as much fiddly variant handling. + serializedValue = std::to_string(itemValue.toIntPair().first) + "/" + + std::to_string(itemValue.toIntPair().second); + break; + default: + // Don't care about the other types + continue; } mp4AddImpl(mp4, itemName, TagLib::String(serializedValue)); } diff --git a/musikr/src/main/cpp/JVMTagMap.cpp b/musikr/src/main/cpp/JVMTagMap.cpp index 272b33aec..d8030dea0 100644 --- a/musikr/src/main/cpp/JVMTagMap.cpp +++ b/musikr/src/main/cpp/JVMTagMap.cpp @@ -21,7 +21,8 @@ #include "util.h" JVMTagMap::JVMTagMap(JNIEnv *env) : env(env) { - jclass tagMapClass = env->FindClass("org/oxycblt/musikr/metadata/NativeTagMap"); + jclass tagMapClass = env->FindClass( + "org/oxycblt/musikr/metadata/NativeTagMap"); jmethodID init = env->GetMethodID(tagMapClass, "", "()V"); tagMap = env->NewObject(tagMapClass, init); tagMapAddIdSingleMethod = env->GetMethodID(tagMapClass, "addID", @@ -53,7 +54,8 @@ JVMTagMap::~JVMTagMap() { void JVMTagMap::add_id(TagLib::String &id, TagLib::String &value) { env->CallVoidMethod(tagMap, tagMapAddIdSingleMethod, - env->NewStringUTF(id.toCString(true)), env->NewStringUTF(value.toCString(true))); + env->NewStringUTF(id.toCString(true)), + env->NewStringUTF(value.toCString(true))); } void JVMTagMap::add_id(TagLib::String &id, TagLib::StringList &value) { @@ -68,10 +70,12 @@ void JVMTagMap::add_id(TagLib::String &id, TagLib::StringList &value) { void JVMTagMap::add_custom(TagLib::String &description, TagLib::String &value) { env->CallVoidMethod(tagMap, tagMapAddCustomSingleMethod, - env->NewStringUTF(description.toCString(true)), env->NewStringUTF(value.toCString(true))); + env->NewStringUTF(description.toCString(true)), + env->NewStringUTF(value.toCString(true))); } -void JVMTagMap::add_custom(TagLib::String &description, TagLib::StringList &value) { +void JVMTagMap::add_custom(TagLib::String &description, + TagLib::StringList &value) { jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); for (auto &item : value) { env->CallBooleanMethod(arrayList, arrayListAddMethod, @@ -81,21 +85,24 @@ void JVMTagMap::add_custom(TagLib::String &description, TagLib::StringList &valu env->NewStringUTF(description.toCString(true)), arrayList); } -void JVMTagMap::add_combined(TagLib::String &id, TagLib::String &description, TagLib::String &value) { +void JVMTagMap::add_combined(TagLib::String &id, TagLib::String &description, + TagLib::String &value) { env->CallVoidMethod(tagMap, tagMapAddCombinedSingleMethod, - env->NewStringUTF(id.toCString(true)), env->NewStringUTF(description.toCString(true)), + env->NewStringUTF(id.toCString(true)), + env->NewStringUTF(description.toCString(true)), env->NewStringUTF(value.toCString(true))); } -void JVMTagMap::add_combined(TagLib::String &id, TagLib::String &description, TagLib::StringList &value) { +void JVMTagMap::add_combined(TagLib::String &id, TagLib::String &description, + TagLib::StringList &value) { jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); for (auto &item : value) { env->CallBooleanMethod(arrayList, arrayListAddMethod, env->NewStringUTF(item.toCString(true))); } env->CallVoidMethod(tagMap, tagMapAddCombinedListMethod, - env->NewStringUTF(id.toCString(true)), env->NewStringUTF(description.toCString(true)), - arrayList); + env->NewStringUTF(id.toCString(true)), + env->NewStringUTF(description.toCString(true)), arrayList); } jobject JVMTagMap::getObject() { diff --git a/musikr/src/main/cpp/JVMTagMap.h b/musikr/src/main/cpp/JVMTagMap.h index c75067ef1..842f6872d 100644 --- a/musikr/src/main/cpp/JVMTagMap.h +++ b/musikr/src/main/cpp/JVMTagMap.h @@ -38,8 +38,10 @@ public: void add_custom(TagLib::String &description, TagLib::String &value); void add_custom(TagLib::String &description, TagLib::StringList &value); - void add_combined(TagLib::String &id, TagLib::String &description, TagLib::String &value); - void add_combined(TagLib::String &id, TagLib::String &description, TagLib::StringList &value); + void add_combined(TagLib::String &id, TagLib::String &description, + TagLib::String &value); + void add_combined(TagLib::String &id, TagLib::String &description, + TagLib::StringList &value); jobject getObject(); diff --git a/musikr/src/main/cpp/util.h b/musikr/src/main/cpp/util.h index 1c223a142..ce9ad7255 100644 --- a/musikr/src/main/cpp/util.h +++ b/musikr/src/main/cpp/util.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2024 Auxio Project - * log.h is part of Auxio. + * util.h is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt index 7740fea51..022c9bdbd 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2025 Auxio Project + * NativeTagMap.kt is part of Auxio. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + package org.oxycblt.musikr.metadata import org.oxycblt.musikr.util.correctWhitespace @@ -32,4 +50,4 @@ class NativeTagMap { fun getObject(): Map> { return map } -} \ No newline at end of file +} From 91665807033a5bbbd7f19978ff43f452f5f7a434 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 16:03:31 -0700 Subject: [PATCH 431/550] all: remove debug logs --- .../org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt | 2 -- .../org/oxycblt/auxio/music/service/MediaItemTranslation.kt | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index b317ce9a5..e543322b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -282,8 +282,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr popupLayoutParams.height) popupView.measure(widthMeasureSpec, heightMeasureSpec) - Timber.d( - "Updating popup text to ${popupView.measuredHeight} ${popupView.measuredWidth}") } val popupWidth = popupView.measuredWidth diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt index 74dd082c7..3f5b63e92 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt @@ -141,8 +141,7 @@ fun Album.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setIconUri( covers.covers .firstOrNull() - ?.let { Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) } - .also { Timber.d("Album cover: $it") }) + ?.let { Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) }) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) From 710e279d8f5596e0991661ff7e49d88d21469fe6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 17:43:09 -0700 Subject: [PATCH 432/550] musikr: tweak api --- .../main/java/org/oxycblt/musikr/cover/Covers.kt | 14 +++++++------- .../org/oxycblt/musikr/metadata/NativeTagMap.kt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt index 8fe021e2d..d88eeecae 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt @@ -30,6 +30,12 @@ interface MutableCovers : Covers { suspend fun cleanup(excluding: Collection) } +sealed interface ObtainResult { + data class Hit(val cover: T) : ObtainResult + + class Miss : ObtainResult +} + interface Cover { val id: String @@ -47,10 +53,4 @@ class CoverCollection private constructor(val covers: List) { .sortedByDescending { it.value.size } .map { it.value.first() }) } -} - -sealed interface ObtainResult { - data class Hit(val cover: T) : ObtainResult - - class Miss : ObtainResult -} +} \ No newline at end of file diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt index 022c9bdbd..dd78da17a 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt @@ -20,7 +20,7 @@ package org.oxycblt.musikr.metadata import org.oxycblt.musikr.util.correctWhitespace -class NativeTagMap { +internal class NativeTagMap { private val map = mutableMapOf>() fun addID(id: String, value: String) { From d486dc39cc4ced00f124c117157835b9e7609592 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 17:47:19 -0700 Subject: [PATCH 433/550] list: add haptic feedback to popup scroll --- .../auxio/list/recycler/FastScrollRecyclerView.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index e543322b3..a14a39e6b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -22,14 +22,17 @@ import android.animation.Animator import android.content.Context import android.graphics.Canvas import android.graphics.Rect +import android.os.Build import android.text.TextUtils import android.util.AttributeSet import android.view.Gravity +import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration import android.view.ViewGroup import android.view.WindowInsets +import android.view.accessibility.AccessibilityEvent import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.core.view.isInvisible @@ -260,6 +263,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams if (popupView.text != popupText) { + performHapticFeedback( + if (Build.VERSION.SDK_INT >= 27) { + HapticFeedbackConstants.TEXT_HANDLE_MOVE + } else { + HapticFeedbackConstants.KEYBOARD_TAP + } + ) popupView.text = popupText val widthMeasureSpec = From 4809bf50cce8ae1b3e2e11580d357f0cee6fbb1b Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 17:47:37 -0700 Subject: [PATCH 434/550] build: fix min sdk --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index ab4a11900..a405c41a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ android { versionName "4.0.0-BETA1" versionCode 54 - minSdk target_sdk + minSdk min_sdk targetSdk target_sdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 156b2fe1f0ad2db5b33b66702abf25ceaff803dd Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 17:51:15 -0700 Subject: [PATCH 435/550] list: fix fast scroller haptics --- .../list/recycler/FastScrollRecyclerView.kt | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index a14a39e6b..ce1bbcee9 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -32,7 +32,6 @@ import android.view.View import android.view.ViewConfiguration import android.view.ViewGroup import android.view.WindowInsets -import android.view.accessibility.AccessibilityEvent import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.core.view.isInvisible @@ -53,7 +52,6 @@ import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.isRtl import org.oxycblt.auxio.util.isUnder import org.oxycblt.auxio.util.systemBarInsetsCompat -import timber.log.Timber /** * A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of @@ -263,13 +261,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams if (popupView.text != popupText) { - performHapticFeedback( - if (Build.VERSION.SDK_INT >= 27) { - HapticFeedbackConstants.TEXT_HANDLE_MOVE - } else { - HapticFeedbackConstants.KEYBOARD_TAP - } - ) popupView.text = popupText val widthMeasureSpec = @@ -292,6 +283,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr popupLayoutParams.height) popupView.measure(widthMeasureSpec, heightMeasureSpec) + if (showingPopup) { + doPopupVibration() + } } val popupWidth = popupView.measuredWidth @@ -484,6 +478,16 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr popupAnimator = popupSlider.slideOut(popupView).also { it.start() } } + private fun doPopupVibration() { + performHapticFeedback( + if (Build.VERSION.SDK_INT >= 27) { + HapticFeedbackConstants.TEXT_HANDLE_MOVE + } else { + HapticFeedbackConstants.KEYBOARD_TAP + } + ) + } + // --- LAYOUT STATE --- private val thumbOffsetRange: Int From 9fe508a906439b88f3a04725a329439ac300b78d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 17:51:40 -0700 Subject: [PATCH 436/550] all: fix formatting --- .../oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt | 3 +-- .../oxycblt/auxio/music/service/MediaItemTranslation.kt | 7 +++---- musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index ce1bbcee9..9e7badd49 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -484,8 +484,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr HapticFeedbackConstants.TEXT_HANDLE_MOVE } else { HapticFeedbackConstants.KEYBOARD_TAP - } - ) + }) } // --- LAYOUT STATE --- diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt index 3f5b63e92..d3e117ec6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt @@ -40,7 +40,6 @@ import org.oxycblt.musikr.Music import org.oxycblt.musikr.MusicParent import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Song -import timber.log.Timber sealed interface MediaSessionUID { data class Tab(val node: TabNode) : MediaSessionUID { @@ -139,9 +138,9 @@ fun Album.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setSubtitle(artists.resolveNames(context)) .setDescription(counts) .setIconUri( - covers.covers - .firstOrNull() - ?.let { Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) }) + covers.covers.firstOrNull()?.let { + Uri.withAppendedPath(CoverProvider.CONTENT_URI, it.id) + }) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt index d88eeecae..dfdf96ede 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt @@ -53,4 +53,4 @@ class CoverCollection private constructor(val covers: List) { .sortedByDescending { it.value.size } .map { it.value.first() }) } -} \ No newline at end of file +} From 4679785b78ce5d53880f277940b1a69103b2969f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 4 Jan 2025 17:53:18 -0700 Subject: [PATCH 437/550] list: update fastscrollrecyclerview credits --- .../org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index 9e7badd49..cc3c65464 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -76,6 +76,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * - Added drag listener * - Added documentation * - Completely new design + * - New scroll position backend * * @author Hai Zhang, Alexander Capehart (OxygenCobalt) * @@ -338,6 +339,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private fun updateThumbState() { // Then calculate the thumb position, which is just: // [proportion of scroll position to scroll range] * [total thumb range] + // This is somewhat adapted from the androidx RecyclerView FastScroller implementation. val offsetY = computeVerticalScrollOffset() if (computeVerticalScrollRange() < height || childCount == 0) { fastScrollingPossible = false From 6be97943bce85a08758ac98f23a5d6d0cc3a6813 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 08:15:28 -0700 Subject: [PATCH 438/550] musikr: fix broken minification --- musikr/consumer-rules.pro | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/musikr/consumer-rules.pro b/musikr/consumer-rules.pro index 9c2a098af..f15090dbc 100644 --- a/musikr/consumer-rules.pro +++ b/musikr/consumer-rules.pro @@ -1,3 +1,4 @@ -keep class org.oxycblt.musikr.metadata.NativeInputStream { *; } -keep class org.oxycblt.musikr.metadata.Metadata { *; } --keep class org.oxycblt.musikr.metadata.Properties { *; } \ No newline at end of file +-keep class org.oxycblt.musikr.metadata.Properties { *; } +-keep class org.oxycblt.musikr.metadata.NativeTagMap { *; } \ No newline at end of file From 3e54c032fe0279b976125c2d5a0d199dc55880e5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 08:25:17 -0700 Subject: [PATCH 439/550] app: fix cover provider authority conflict Between release and debug builds --- app/src/debug/res/values/donottranslate.xml | 1 + app/src/main/AndroidManifest.xml | 2 +- app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt | 3 ++- app/src/main/res/values/donottranslate.xml | 3 +++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/debug/res/values/donottranslate.xml b/app/src/debug/res/values/donottranslate.xml index dd28633f5..caaf4f429 100644 --- a/app/src/debug/res/values/donottranslate.xml +++ b/app/src/debug/res/values/donottranslate.xml @@ -1,4 +1,5 @@ Auxio Debug + org.oxycblt.auxio.debug.image.CoverProvider \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index afba82e8f..2d0499edd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -105,7 +105,7 @@ --> diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt index 871069ae5..653c4f393 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt @@ -26,6 +26,7 @@ import android.database.Cursor import android.net.Uri import android.os.ParcelFileDescriptor import kotlinx.coroutines.runBlocking +import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.image.covers.SiloedCoverId import org.oxycblt.auxio.image.covers.SiloedCovers import org.oxycblt.musikr.cover.ObtainResult @@ -72,7 +73,7 @@ class CoverProvider : ContentProvider() { ): Int = 0 companion object { - private const val AUTHORITY = "org.oxycblt.auxio.image.CoverProvider" + private const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.image.CoverProvider" private const val IMAGES_PATH = "covers" private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { addURI(AUTHORITY, "$IMAGES_PATH/*", 1) } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 5031e41d2..a1cd46ef0 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -16,5 +16,8 @@ Microsoft WAVE Ogg %s + + org.oxycblt.auxio.image.CoverProvider + \ No newline at end of file From 6d09e06424e2b38d867348bfc92fa29d23e5eac6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 08:26:17 -0700 Subject: [PATCH 440/550] list: fix fastscroll layout issues --- .../list/recycler/FastScrollRecyclerView.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index cc3c65464..0663a7f3f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -79,8 +79,6 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * - New scroll position backend * * @author Hai Zhang, Alexander Capehart (OxygenCobalt) - * - * TODO: Add vibration when popup changes */ class FastScrollRecyclerView @JvmOverloads @@ -123,11 +121,16 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr .getDimenPixels(com.google.android.material.R.dimen.m3_sys_elevation_level1) .toFloat() background = context.getDrawableCompat(R.drawable.ui_popup) - updatePaddingRelative(end = context.getDimenPixels(R.dimen.spacing_tiny) / 2) + val paddingStart = context.getDimenPixels(R.dimen.spacing_medium) + val paddingEnd = paddingStart + context.getDimenPixels(R.dimen.spacing_tiny) / 2 + updatePaddingRelative(start = paddingStart, end = paddingEnd) layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - .apply { gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP } + .apply { + marginEnd = context.getDimenPixels(R.dimen.size_touchable_small) + gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP + } } private val popupSlider = MaterialFadingSlider(MaterialSlider.large(context, popupView.minimumWidth / 2)).apply { @@ -293,14 +296,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val popupHeight = popupView.measuredHeight val popupLeft = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) { - thumbPadding.left + thumbWidth + popupLayoutParams.leftMargin + popupWidth / 2 + thumbPadding.left + thumbWidth + popupLayoutParams.leftMargin } else { width - thumbPadding.right - thumbWidth - popupLayoutParams.rightMargin - - popupWidth - - popupWidth / 2 + popupWidth } val popupAnchorY = popupHeight / 2 From 9952579cc4d4ca699d12bc92fbb959cfda099906 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 08:28:56 -0700 Subject: [PATCH 441/550] musikr.tag: fix correction creating empty tag lists --- .../oxycblt/musikr/metadata/NativeTagMap.kt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt index dd78da17a..0e04519b2 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt @@ -28,7 +28,14 @@ internal class NativeTagMap { } fun addID(id: String, values: List) { - map[id] = values.mapNotNull { it.correctWhitespace() } + if (values.isEmpty()) { + return + } + val correctedValues = values.mapNotNull { it.correctWhitespace() } + if (correctedValues.isEmpty()) { + return + } + map[id] = correctedValues } fun addCustom(description: String, value: String) { @@ -36,7 +43,14 @@ internal class NativeTagMap { } fun addCustom(description: String, values: List) { - map[description.uppercase()] = values.mapNotNull { it.correctWhitespace() } + if (values.isEmpty()) { + return + } + val correctedValues = values.mapNotNull { it.correctWhitespace() } + if (correctedValues.isEmpty()) { + return + } + map[description.uppercase()] = correctedValues } fun addCombined(id: String, description: String, value: String) { @@ -44,7 +58,14 @@ internal class NativeTagMap { } fun addCombined(id: String, description: String, values: List) { - map["$id:${description.uppercase()}"] = values.mapNotNull { it.correctWhitespace() } + if (values.isEmpty()) { + return + } + val correctedValues = values.mapNotNull { it.correctWhitespace() } + if (correctedValues.isEmpty()) { + return + } + map["$id:${description.uppercase()}"] = correctedValues } fun getObject(): Map> { From 1fb6097b9d85da4765d949e7b92a831586746fec Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 08:29:15 -0700 Subject: [PATCH 442/550] all: reformat --- .../oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index 0663a7f3f..4fe661b6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -298,11 +298,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (layoutDirection == View.LAYOUT_DIRECTION_RTL) { thumbPadding.left + thumbWidth + popupLayoutParams.leftMargin } else { - width - - thumbPadding.right - - thumbWidth - - popupLayoutParams.rightMargin - - popupWidth + width - thumbPadding.right - thumbWidth - popupLayoutParams.rightMargin - popupWidth } val popupAnchorY = popupHeight / 2 From bbc4db156e2d5579a3f5ca66468cad720973be01 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 11:23:04 -0700 Subject: [PATCH 443/550] musikr: fix equality issues --- .../auxio/image/covers/SiloedCovers.kt | 3 +- .../oxycblt/auxio/music/MusicRepository.kt | 44 ++++++++++--------- .../java/org/oxycblt/musikr/cover/Covers.kt | 4 ++ .../org/oxycblt/musikr/cover/FileCovers.kt | 3 +- .../org/oxycblt/musikr/fs/app/AppFiles.kt | 2 +- .../org/oxycblt/musikr/model/AlbumImpl.kt | 6 ++- .../oxycblt/musikr/model/LibraryFactory.kt | 2 +- 7 files changed, 38 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt index 3ff8a90d3..253d0dacf 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt @@ -82,7 +82,8 @@ private constructor( } } -class SiloedCover(silo: CoverSilo, val innerCover: FileCover) : FileCover by innerCover { +data class SiloedCover(private val silo: CoverSilo, val innerCover: FileCover) : + FileCover by innerCover { private val innerId = SiloedCoverId(silo, innerCover.id) override val id = innerId.toString() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 0262efac7..e0b3873a7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -396,12 +396,33 @@ constructor( // Music loading completed, update the revision right now so we re-use this work // later. musicSettings.revision = newRevision + // Deliver the library to the rest of the app + // This will more or less block until all required item translation and + // cleanup finishes. + emitLibrary(newLibrary) + // Old cover revisions may be lying around, even during a normal refresh due + // to really lucky cancellations. Now that it is impossible for the rest of + // the app to possible be using them, clean them up. + covers.cleanup(newLibrary.songs.mapNotNull { it.cover }) + // Finish up loading. + emitIndexingCompletion(null) + } - // We want to make sure that all reads and writes are synchronized due to the sheer - // amount of consumers of MusicRepository. - // TODO: Would Atomics not be a better fit here? + private suspend fun emitIndexingProgress(progress: IndexingProgress) { + yield() + synchronized(this) { + currentIndexingState = IndexingState.Indexing(progress) + for (listener in indexingListeners) { + listener.onIndexingStateChanged() + } + } + } + + private suspend fun emitLibrary(newLibrary: MutableLibrary) { val deviceLibraryChanged: Boolean val userLibraryChanged: Boolean + // We want to make sure that all reads and writes are synchronized due to the sheer + // amount of consumers of MusicRepository. synchronized(this) { // It's possible that this reload might have changed nothing, so make sure that // hasn't happened before dispatching a change to all consumers. @@ -428,23 +449,6 @@ constructor( withContext(Dispatchers.Main) { dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged) } - - // Old cover revisions may be lying around, even during a normal refresh due - // to really lucky cancellations. Clean those up now that it's impossible for - // the rest of the app to be using them. - covers.cleanup(newLibrary.songs.mapNotNull { it.cover }) - - emitIndexingCompletion(null) - } - - private suspend fun emitIndexingProgress(progress: IndexingProgress) { - yield() - synchronized(this) { - currentIndexingState = IndexingState.Indexing(progress) - for (listener in indexingListeners) { - listener.onIndexingStateChanged() - } - } } private suspend fun emitIndexingCompletion(error: Exception?) { diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt index dfdf96ede..e3f41b386 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt @@ -43,6 +43,10 @@ interface Cover { } class CoverCollection private constructor(val covers: List) { + override fun hashCode() = covers.hashCode() + + override fun equals(other: Any?) = other is CoverCollection && covers == other.covers + companion object { fun from(covers: Collection) = CoverCollection( diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt index 5a8a54195..4c8390869 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt @@ -57,7 +57,8 @@ interface FileCover : Cover { suspend fun fd(): ParcelFileDescriptor? } -private class FileCoverImpl(override val id: String, private val appFile: AppFile) : FileCover { +private data class FileCoverImpl(override val id: String, private val appFile: AppFile) : + FileCover { override suspend fun fd() = appFile.fd() override suspend fun open() = appFile.open() diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt index 151a9d158..9c8f2a407 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt @@ -96,7 +96,7 @@ private class AppFilesImpl(private val dir: File) : AppFiles { } } -private class AppFileImpl(private val file: File) : AppFile { +private data class AppFileImpl(private val file: File) : AppFile { override suspend fun fd() = withContext(Dispatchers.IO) { try { diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt index 10b7aabc3..53a17152d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt @@ -29,7 +29,7 @@ import org.oxycblt.musikr.util.update internal interface AlbumCore { val preAlbum: PreAlbum - val songs: List + val songs: Set fun resolveArtists(): List } @@ -39,7 +39,7 @@ internal interface AlbumCore { * * @author Alexander Capehart (OxygenCobalt) */ -internal class AlbumImpl(private val core: AlbumCore) : Album { +class AlbumImpl internal constructor(private val core: AlbumCore) : Album { private val preAlbum = core.preAlbum override val uid = @@ -73,4 +73,6 @@ internal class AlbumImpl(private val core: AlbumCore) : Album { other is AlbumImpl && uid == other.uid && preAlbum == other.preAlbum && songs == other.songs override fun toString() = "Album(uid=$uid, name=$name)" + + fun a(other: AlbumImpl) = uid == other.uid } diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt index bffe06b0c..1bcd6d752 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt @@ -87,7 +87,7 @@ private class LibraryFactoryImpl() : LibraryFactory { private class AlbumVertexCore(private val vertex: AlbumVertex) : AlbumCore { override val preAlbum = vertex.preAlbum - override val songs = vertex.songVertices.map { SongImpl(SongVertexCore(it)) } + override val songs = vertex.songVertices.mapTo(mutableSetOf()) { it.tag as Song } override fun resolveArtists() = vertex.artistVertices.map { it.tag as Artist } } From 298a30da6df1254d216f4f7762c9d7e401fded98 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 11:32:03 -0700 Subject: [PATCH 444/550] image: fix provider caching issues - Covers would hypothetically not be updated in android auto if the setting changed to off - Cover fetching might fail in weird ways due to the current error throwing --- .../java/org/oxycblt/auxio/image/CoverProvider.kt | 11 ++++++----- .../java/org/oxycblt/auxio/image/covers/NullCovers.kt | 10 +++++----- .../org/oxycblt/auxio/image/covers/SettingCovers.kt | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt index 653c4f393..976fc64e4 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt @@ -35,12 +35,13 @@ class CoverProvider : ContentProvider() { override fun onCreate(): Boolean = true override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { - check(mode == "r") { "Unsupported mode: $mode" } - check(uriMatcher.match(uri) == 1) { "Unknown URI: $uri" } - val id = requireNotNull(uri.lastPathSegment) { "No ID in URI: $uri" } - val coverId = requireNotNull(SiloedCoverId.parse(id)) { "Invalid ID: $id" } + if (mode != "r" || uriMatcher.match(uri) != 1) { + return null + } + val id = uri.lastPathSegment ?: return null + val coverId = SiloedCoverId.parse(id) ?: return null return runBlocking { - val siloedCovers = SiloedCovers.from(requireContext(), coverId.silo) + val siloedCovers = SiloedCovers.from(requireNotNull(context), coverId.silo) when (val res = siloedCovers.obtain(id)) { is ObtainResult.Hit -> res.cover.fd() is ObtainResult.Miss -> null diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt index dec9e8152..71be523ab 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt @@ -20,21 +20,21 @@ package org.oxycblt.auxio.image.covers import android.content.Context import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverIdentifier import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.cover.ObtainResult -class NullCovers(private val context: Context, private val identifier: CoverIdentifier) : +class NullCovers(private val context: Context) : MutableCovers { - override suspend fun obtain(id: String) = ObtainResult.Hit(NullCover(id)) + override suspend fun obtain(id: String) = ObtainResult.Hit(NullCover) - override suspend fun write(data: ByteArray): Cover = NullCover(identifier.identify(data)) + override suspend fun write(data: ByteArray): Cover = NullCover override suspend fun cleanup(excluding: Collection) { context.coversDir().listFiles()?.forEach { it.deleteRecursively() } } } -class NullCover(override val id: String) : Cover { +data object NullCover : Cover { + override val id = "null" override suspend fun open() = null } diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt index 7f5d642bc..63e1dfd8d 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt @@ -37,7 +37,7 @@ constructor(private val imageSettings: ImageSettings, private val identifier: Co SettingCovers { override suspend fun create(context: Context, revision: UUID): MutableCovers = when (imageSettings.coverMode) { - CoverMode.OFF -> NullCovers(context, identifier) + CoverMode.OFF -> NullCovers(context) CoverMode.SAVE_SPACE -> siloedCovers(context, revision, CoverParams.of(500, 70)) CoverMode.BALANCED -> siloedCovers(context, revision, CoverParams.of(750, 85)) CoverMode.HIGH_QUALITY -> siloedCovers(context, revision, CoverParams.of(1000, 100)) From b328a6ea030a5cf8c7f7f1772236f2a003d9fa7e Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 11:41:01 -0700 Subject: [PATCH 445/550] musikr: add temp logging To debug metadata issues. --- musikr/src/main/cpp/taglib_jni.cpp | 2 ++ .../java/org/oxycblt/musikr/pipeline/ExtractStep.kt | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/musikr/src/main/cpp/taglib_jni.cpp b/musikr/src/main/cpp/taglib_jni.cpp index c5d72bf6e..a1ec002a4 100644 --- a/musikr/src/main/cpp/taglib_jni.cpp +++ b/musikr/src/main/cpp/taglib_jni.cpp @@ -38,6 +38,7 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, JVMInputStream stream {env, inputStream}; TagLib::FileRef fileRef {&stream}; if (fileRef.isNull()) { + LOGE("Error opening file"); return nullptr; } TagLib::File *file = fileRef.file(); @@ -65,6 +66,7 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, builder.setId3v2(*wavFile->ID3v2Tag()); } else { // While taglib supports other formats, ExoPlayer does not. Ignore them. + LOGE("Unsupported file format"); return nullptr; } diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index ad34d8f82..b77c962b9 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -19,6 +19,7 @@ package org.oxycblt.musikr.pipeline import android.content.Context +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel @@ -119,7 +120,14 @@ private class ExtractStepImpl( metadataExtractor .extract(fileWith.with) ?.let { FileWith(fileWith.file, it) } - .also { withContext(Dispatchers.IO) { fileWith.with.close() } } + .also { + if (it == null) { + Log.d( + "ExtractStep", + "Failed to extract metadata for ${fileWith.file.path}") + } + withContext(Dispatchers.IO) { fileWith.with.close() } + } } } .flowOn(Dispatchers.IO) From 6587d2259b4e718f4e272e0f160c0c154d4b5f19 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 11:44:13 -0700 Subject: [PATCH 446/550] all: reformat --- .../main/java/org/oxycblt/auxio/image/covers/NullCovers.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt index 71be523ab..7aeb4da5d 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt @@ -23,8 +23,7 @@ import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.cover.ObtainResult -class NullCovers(private val context: Context) : - MutableCovers { +class NullCovers(private val context: Context) : MutableCovers { override suspend fun obtain(id: String) = ObtainResult.Hit(NullCover) override suspend fun write(data: ByteArray): Cover = NullCover @@ -36,5 +35,6 @@ class NullCovers(private val context: Context) : data object NullCover : Cover { override val id = "null" + override suspend fun open() = null } From 5e168860e74ee6ab8e9450536270c63b6ea4ffbd Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 13:16:31 -0700 Subject: [PATCH 447/550] musikr: bundle cleanup into api Prevents as much footguns. --- .../oxycblt/auxio/music/MusicRepository.kt | 10 ++++----- .../main/java/org/oxycblt/musikr/Config.kt | 2 +- .../main/java/org/oxycblt/musikr/Musikr.kt | 22 +++++++++++++++++-- .../musikr/tag/interpret/Interpretation.kt | 1 + 4 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpretation.kt diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index e0b3873a7..2d2e04230 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -391,7 +391,7 @@ constructor( val storage = Storage(cache, covers, storedPlaylists) val interpretation = Interpretation(nameFactory, separators) - val newLibrary = + val result = Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress) // Music loading completed, update the revision right now so we re-use this work // later. @@ -399,11 +399,9 @@ constructor( // Deliver the library to the rest of the app // This will more or less block until all required item translation and // cleanup finishes. - emitLibrary(newLibrary) - // Old cover revisions may be lying around, even during a normal refresh due - // to really lucky cancellations. Now that it is impossible for the rest of - // the app to possible be using them, clean them up. - covers.cleanup(newLibrary.songs.mapNotNull { it.cover }) + emitLibrary(result.library) + // Clean up old data that is now impossible for the app to be using. + result.cleanup() // Finish up loading. emitIndexingCompletion(null) } diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index cdd5a3f56..6670131f2 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -30,4 +30,4 @@ data class Storage( val storedPlaylists: StoredPlaylists ) -data class Interpretation(val naming: Naming, val separators: Separators) +data class Interpretation(val naming: Naming, val separators: Separators) \ No newline at end of file diff --git a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt index 55081a442..af40c1102 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt @@ -34,17 +34,24 @@ interface Musikr { suspend fun run( locations: List, onProgress: suspend (IndexingProgress) -> Unit = {} - ): MutableLibrary + ): LibraryResult companion object { fun new(context: Context, storage: Storage, interpretation: Interpretation): Musikr = MusikrImpl( + storage, ExploreStep.from(context, storage), ExtractStep.from(context, storage), EvaluateStep.new(storage, interpretation)) } } +interface LibraryResult { + val library: MutableLibrary + + suspend fun cleanup() +} + /** * Represents the current progress of music loading. * @@ -57,6 +64,7 @@ sealed interface IndexingProgress { } private class MusikrImpl( + private val storage: Storage, private val exploreStep: ExploreStep, private val extractStep: ExtractStep, private val evaluateStep: EvaluateStep @@ -79,6 +87,16 @@ private class MusikrImpl( .buffer(Channel.UNLIMITED) .onEach { onProgress(IndexingProgress.Songs(++extractedCount, exploredCount)) } .onCompletion { onProgress(IndexingProgress.Indeterminate) } - evaluateStep.evaluate(extracted) + val library = evaluateStep.evaluate(extracted) + LibraryResultImpl(storage, library) } } + +private class LibraryResultImpl( + private val storage: Storage, + override val library: MutableLibrary) : LibraryResult { + + override suspend fun cleanup() { + storage.storedCovers.cleanup(library.songs.mapNotNull { it.cover }) + } +} \ No newline at end of file diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpretation.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpretation.kt new file mode 100644 index 000000000..414d2363a --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpretation.kt @@ -0,0 +1 @@ +package org.oxycblt.musikr.tag.interpret From fdf71cedd28bd791f2966ab8af6efdb8771d3d94 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 13:17:27 -0700 Subject: [PATCH 448/550] musikr: fix formatting --- musikr/src/main/java/org/oxycblt/musikr/Config.kt | 2 +- musikr/src/main/java/org/oxycblt/musikr/Musikr.kt | 5 +++-- .../java/org/oxycblt/musikr/tag/interpret/Interpretation.kt | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpretation.kt diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index 6670131f2..cdd5a3f56 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -30,4 +30,4 @@ data class Storage( val storedPlaylists: StoredPlaylists ) -data class Interpretation(val naming: Naming, val separators: Separators) \ No newline at end of file +data class Interpretation(val naming: Naming, val separators: Separators) diff --git a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt index af40c1102..3f061c5f5 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt @@ -94,9 +94,10 @@ private class MusikrImpl( private class LibraryResultImpl( private val storage: Storage, - override val library: MutableLibrary) : LibraryResult { + override val library: MutableLibrary +) : LibraryResult { override suspend fun cleanup() { storage.storedCovers.cleanup(library.songs.mapNotNull { it.cover }) } -} \ No newline at end of file +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpretation.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpretation.kt deleted file mode 100644 index 414d2363a..000000000 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpretation.kt +++ /dev/null @@ -1 +0,0 @@ -package org.oxycblt.musikr.tag.interpret From 1d84ba23b41372262a97578427bd6579e164c131 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 13:54:22 -0700 Subject: [PATCH 449/550] build: update submodules --- media | 2 +- musikr/src/main/cpp/taglib | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/media b/media index 02f99bba5..4b3084e1b 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 02f99bba5efd6f258ab359d0443d22d9c76122c8 +Subproject commit 4b3084e1b63185eaeffa7cac9d7015040e0e2aa5 diff --git a/musikr/src/main/cpp/taglib b/musikr/src/main/cpp/taglib index 225c73e18..648f5e588 160000 --- a/musikr/src/main/cpp/taglib +++ b/musikr/src/main/cpp/taglib @@ -1 +1 @@ -Subproject commit 225c73e1816c3200c67703557972586e287fa665 +Subproject commit 648f5e588209464702e4955de614e391d3768ec8 From 2b4677421531d1367d12ad9819540d9de19ef9c7 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 14:12:24 -0700 Subject: [PATCH 450/550] musikr: fix internal frame parsing --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index d70279f45..ca6a9b006 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -67,12 +67,9 @@ template void mp4AddImpl(JVMTagMap &map, TagLib::String &itemName, T itemValue) { if (itemName.startsWith("----")) { // Split this into it's atom name and description - auto split = itemName.split(":"); - if (split.size() != 2) { - throw std::runtime_error("Invalid atom name"); - } - auto atomName = split[0]; - auto atomDescription = split[1]; + auto split = itemName.find(':'); + auto atomName = itemName.substr(0, split); + auto atomDescription = itemName.substr(split + 1); map.add_combined(atomName, atomDescription, itemValue); } else { map.add_id(itemName, itemValue); From 3b97c61b7dae1d1d08b3cd653ffeeaf060389ba1 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 14:27:13 -0700 Subject: [PATCH 451/550] build: change version code to -dev As is convention --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a405c41a8..b66d918b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ android { defaultConfig { applicationId namespace - versionName "4.0.0-BETA1" + versionName "4.0.0-dev" versionCode 54 minSdk min_sdk From 447f2da294b4f71025602d4a1adb818eb5fec71b Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 6 Jan 2025 21:44:23 -0700 Subject: [PATCH 452/550] info: update icon Use a new stacked design that is a lot more in line with M3 icon design. --- app/src/main/ic_launcher-playstore.png | Bin 17736 -> 12338 bytes .../res/drawable/ic_launcher_background.xml | 49 +++--- .../res/drawable/ic_launcher_foreground.xml | 27 +++- app/src/main/res/drawable/ic_splash_anim.xml | 149 +++++++++++------- app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 3056 -> 2266 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 2020 -> 1534 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 4374 -> 3206 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 6594 -> 5106 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 9168 -> 7298 bytes 9 files changed, 143 insertions(+), 82 deletions(-) diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index 438492724d93db7adaaddb8b9d0a97a4acb427a9..cc059cc53a65114841a088d90873d9e95fbb5113 100644 GIT binary patch literal 12338 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 literal 17736 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 z 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 diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 0298020d5..2011ea038 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -11,29 +11,30 @@ android:strokeColor="#00000000" /> - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index c9b0b6b07..3a7b2746b 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,9 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/ic_splash_anim.xml b/app/src/main/res/drawable/ic_splash_anim.xml index 9b05d15c3..d5b74057e 100644 --- a/app/src/main/res/drawable/ic_splash_anim.xml +++ b/app/src/main/res/drawable/ic_splash_anim.xml @@ -17,43 +17,66 @@ android:strokeColor="#00000000" /> - - - - - - - + android:translateY="-64"> + + + + + + + + + + + + + + + @@ -64,46 +87,62 @@ - + - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 9fed66e5f76f12b4e138b5704e22f3650f500f5b..c9676da47bcc836acbd6ee48725ef92c3816497a 100644 GIT binary patch literal 2266 zcmV<02qpJYNk&G}2mki{z3rzE5fjieN)_5M z%*sa-J=40i-LU1X==NO2wr$(CZQJIhZQHhOTZe5MyQMRlUrfQnoV zup6KjSdIh$ESvuSM`1bDwoh|AGl>I9l1y9Dvu%FcwvFA{*MA0X8!1XV%e%+$2Cz!? zHZ??P+sH+2YNUb(9X-dhbA`YO5RuB%mby_N-u~%kv3JI5JO==^p;>efy-9!5I@+s^ zX;Tk8sP(#k|3-IzKy3gxo|QN@Kpp5x`hs?85Gr&P;uHG3?)!l4Saw7J&?I_=_6xyP zrchs{3TvtIur1v@->=WPc(hs_An>fI0yL9;q!2vMZ^;d9>F04SYJLY;Pz(Z4FZxgl z?$_rgt?zR#zHNyhV1$YQ-AX4!{pc0GP@mJucYx}QNiDQjf-8hGo8UrwU7IPYgND*Z zkx&`8z?KQFHC72@2nV5pdLpK3MNyqBUkJzaTA3xeLb+x7TtVr%sBSVt`Q~#?&dNAnK0k& z?>pB8r4iDWj~D+u!WEjXvBEbt$}5(u^>&>UQix8c&{j@X1cO+t#9N{_!a>z7OgGfh zwqr06xt>Nzb2?q+G_jk9Nr0YV>y&x+z+ zJOx_-B061$-mbSUD8?cs`}T815IvV&p+RBX z5@_+y*TqXvELz`YJ`%}yC0w~Ndh&?A@-yLE1kL?K&{?p0Oukjt;Pf}d-~q64fwj`X zTUrHO6FToyhwHn6F|p z6lxJ@sq~|(j-(vGOP0OKa~WU3P4~kM+a|_JKP@85Ja8Z~{u0TyZA_?Cso}0AB&{!a zTv@Pg2s79L_OVyGcuQ6qnwB>PfkgAj@$r*IyH|Hb5a2{lWr&^frsaf*7Y1((&KRQ2 z9SQ59bg645DVgC^N0xhdxwU3 z%iN`HZdvt%k=yI!k)>qGcHO1NviN)P&Zcv&5f_&mjA@uw&4-A$ZgY{xS1B} zT@qxXJm&u_KMF2?^o0jdfk-zU5+)fu1s2*}67jM&OPt{*@9vY&eBb~c5l$)*+ze}i zIyl)j#ubF6cH47yiI+G<8SFTxbNKtpPR{L7som6xhi2PSzk@JVo;`<%xN&7x=hXg5 z8$fqrQTU~q#_-vH4`bit-r~oPU-p4mhG)Pb+J#%VYuG({v8?nP2y^w`zDtPs(O7vL zwD0Z9L;$dfI7d*$Zo}Ygb2NDhRy(qFO(W&V`(gMf|Vu7-{i5<-^{Yjjb zlZh^dGX-L{?d-}hnD$SOiTVUKQeK1SfM|iS8Cg#i{v~CA3;;5R_;G@_8vjVxyf`*m z+;nFVu`Z|(=j)%x6{5?UME}%Nr-Ff`--W*xCkQ`*NDRcC54Bj!KdKDCw=+? z_&cl7kBEMt;t(VHMO zqb2d&a&r+TRT9^~(O*}5?R8q(pZf9G841~Fv+bWvV`U}*Vi?ydf5TPTL%d#`TVlMP zhC7DWTAM$XbrLWalh_cidCPCCuMCa3R^Z-M1y|S4R`tdM;WQglSQC%5xN|?0f_z2c zti1Y)eEsxKY4LwQW&ylFVI(H=;y8(?Kf5bed^=AFLvle!ha|3PmW_|;R*nf zgUX42zHUnbYxEM;OI)Y1^CvF)v2CYbVxOr|m-u}BPvZL|tT#D~)MYH|;q7~K{P^of oh{xZLeEso5!`naC<-;U$%~WziVH9yHc|zr-Pu_YX$@-E40FA$LjsO4v literal 3056 zcmVCNk&HK3jhFDMM6+kP&iE63jhEwN5Byf6^DYhZ5W3?>>UgdF#+Dy(nLFo z(T<1VOWFQ^QP>KFLs1$;-brzf_3 zY8H=dJE@gcO~VK1*mhFUS%wAYozJ_|1>l_uaa0;VpPlk%LhlHzZ^n=iB55e`6z?61$)D?(&l&^as9s);_C1&{rKMWPTJPp^t<<0tuFL zR@o@~MB)00XFEl4Q9e?J0d`n)I3To-NFnl}d8Er^LR2^Xz6(X1_jnY=MR`bx#CP9G zWF4&p2rae>k?oE35w5SFx)xVyY7$`=?olWml&t_ok{DrZ4SnRR#;U$$w`z71PrLvm zh%nHtxJ{aW(6uh54J&Hm^%MeKHKjF;8l^>HlQf&&L&8Xv>Bvcj zh)oAJAO3WqML}a$)O`0cuyymRP>aU6nAbhFAQ-7Ug=w(x9UpzGugvGh!tM;VcK-Mk zHo?V$?s5TOELNPi1dfLn+RQDS?8!BEU>t=XXSRSlfHE2Bt_tb3*W) zb>F1QipJVZQn8aGCQM>;Vsed;VqdQ1IEVNGQ9h{%QnJyywW#Y8@r*YQMV~h|F*!A@ zhWvWvM&{=zZveANvD0_1bWY_-(y>A6>6Ya`w~+2V_lCef3xLAG|6FZdL^*FBvIoN_ zW~U~$*sH>ZaW&xqKzIO)8G(~|c7zUE#E(h|G7zaZmCGONfsla~Sf|SH#(tb^qKO1S zO7AWiwoUWH8ie?{gSb))28{$6ZuBZxbG(`e|2{>_&PLJg=FTq;7n>@Q_MZ_TUIvR@ z6`M{EN9xA(B?2v~Z&LN9$~)KC^P&&yKRZ(ff#O!Mcv+#Hp6^$00a+#pK3RYAcxrwI zePo^o1Bjo25WV?JkY!m$uZ~o0E7>aNy!MrnFA#mi^Z*dQ6>^5@U2Z>}2HAmm_eVc% zW+m^G7pF_~agt}imd=RPR?T2H1oVM-VA<(0LYvvhBikf>Sl`((-awJ7Z(8Ji9B*N{ z)17xa<5evZ-(h@aJUMy|eXk^MN$!qV$yJdaZ&;timS*JTE_(0-I| zVX^r*m*WC%UAc)J4*ApAF`ju$|y%8q$yc?8AFO3qNdsTlD+h^sCGi1{QE z3}L!?87vB{r|W2SqQ^V|B)~=0dU1p1YpQqwVQ~dzD0(&=A3hLFa|b9Q!86wX#Fm1e zMP$A0WhvuT85RX(f}NF{ibrAU3^1hR zNbbzN^Vj47K({fyg;X5QotGA#9j7YRm~J0B{JW8De$idDGLxzR6S#*-#uvz-#<!74EQCS`$jxU?^8gqm1@h7yXW^!h zP0a5evWdoNwr#xu>H7tbetm=S;U^(*JZB1Qh>1whR%3Q0z-i$sb>beKeL*!5dk_n| zqX{mEaS}5*g-%9%jM#h3weKyiGq+ZPt+S#Mg3|NccQ+3nl;CpH-w;39jRa-Ak>+TK_zH%eZqN%=|Ua)Elz2XLK34S z1)oT>?%groz*W}gagXp_9bU)(4-qW=qrM%IV-`S1Dj1xd0l=cdfq$tF9|&7 z#*^Sb3lj^`galzlQHWP^ zRh16Z(+rnPsc7KI2y*D&(rm}>T0-b}!-`&41a&9HBt)#l=E9KsEMNA8){m`;k&>F< zJ?5ZP`TAuk(HZ+~k$Lz1?T?H&FIr*(5cmYUdgyh9o9*^%29p$FbcK2~le$IWK zBk~=)h}|o~S4V44ci*SIwpZ5Qr(Xl-H&`igu{Zp8Af(O1q6j?5PDN?Z-0e2Oj4;oA zRiXRLUUHvjyHnln=B11x#&%e-!hHQ;(tUmLA3Jny&fmOh7YV{J=)DlQL;V35z8Z&P z@jfdh#X&jnjN-P@1fD4mN`P#X#OB>f#t--LogQ9)hhO@%svly%Kb{U50c}A1uO!4D z)*FMDVAaN4jKbsO{LM&KPRjCATbRa1f90Yq6IrdI@^}+2{_ray?Sp-KGv~iML4aMY z-Y3e4SZ!bUhLBLReKOmp_pietv&$q2x!A$ y4~$<|1l;<ghKQJeY!*+YByF1_ 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 C@!Qq_ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 328236a61bd93da08a9a9670fa327536eac49ac1..ba9db7c78694b82793361ba0157201b8181c2069 100644 GIT binary patch literal 3206 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 literal 4374 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 Q$ufdAg)=glWLXo=1F9Kmp#T5? diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 45f286fd31c1d5b9a002645942d40f7427492bce..9e519c6b3b1be80e3cde9d69342662c3a6c9899e 100644 GIT binary patch literal 5106 zcmVpLphS)ESyB!MVnrmTPtk`vBdzO;>gV_ILypgW@ct)W@ct)W@d&9Gcz+C zGcz-v?Wc}CGk!DU!rz=FD@@)gL-;^e<+A02Z8$SqhTSV|BeWD+r3VFT)zMMOzMn1b zmceQ*g{;$y(A|K;FfN81=EPFST5aR2#5R&7C!K#x_W?*uiU=^3r@?U~*-k5|@o@B9 z3Bfx_G4Mr!wyoolv|>9cSH*VHDp%XKZQHhO8)Ktw+g8~Qeq0G|8%dHRXH7;rjybog z_6u@j+gA3bJeir9nVFf{7PAx?GG=CGX117_;rs6U=iXPr--8R#e9`Yn5UPRyMy59- zO3jXx87fd_EWjO55u9=ZjubkxXB^tLT}J)?KgvsLO^vZ_*GwP?0GK2eC*AFJ*xkCs zZh>vke@1TGMv@cMUFX(ip1{BDEeHd`j6hfX)AOj#vAcr9rAWtA)Amxw{gn&>G z;MUvk)#9I~`2U>z&Cj2rpYqv^$cdZ^83fr4xeY18I%xr&Frn{2Zb5eKduqfujEIw{ zTM*J3vI+8FFTGwZlm;SE18LDfDH>>!6vbjmOjPt*_gX|ea6z8uoe{3rJ0Z;=J0Kr) zIW1(OMe%5Wf$}JpM2n0uK4=R_Qzw_pb>9v|3z-Lbjxp9kYauKi30WSC24V*pV@EfL zmgj9gu11h_$T3JQ#k0DB$|Az1L5#IWEsZWD-s8!xG~;<=a*so&dWpekgJ3% zaYTf9g2J>xMfmm=_ZCrk5dp-iSM<5X2+?i51JFT+aQ03GBthD0S!N<$Pk;!SCcyptds z;e_O~aK$qzmMRIHgcXg*FN25iwG7_~2c!gRi837pl@C&c-m?%ec0;!D0WCkW z@rmq34q&Wa&2HjE6HtlY08HM-kQdOf5^patj{$1Ma)D#r4MBM}1bhrlSJM?^4+~7@ zD-c_uTsmFnqHM^x4k%1tMuz+N8t5Qp?7Z_mF#-rElMUY#ICsP9l+S>L_c2kVq3{zMHy2K%t20I8rX=QOUoVXme_(eG?q?QnJ7(Vc?jU}HZ-p> zQwWbe=#wiJi<=ltwLw1SAz2V+pgbvB%CQEDMGQ!;Ud|A#B3Gnh0?dI3Lf)n30Cr2P zV%m=`xo`{;sD#i|9~9)I%ZJcYi$)L%dsxJYU6l;=$Wr7|RK5aJV02g}90>qsU8ZAE z;tZVUt=|QajD{LO$Qoc4mbm7GBR(+$0Cp1|U<-F3!csx!mP0>*lhBR_733L!AF$Eq zE0cj`50{WK1f8?ILquyJ7FPrl(FVYzEp88B)n@}1B~HP9neT1f;qcFW2}$4o@Eq*1 zx2HY75V3}rSm7A#n+0xKhKC{);qC2F(j9%S3N*#C#5MRr61i(H09PdoDjf+;c$4wy zM0e~Jt;;HG;U4Up2A}mTo2}xQjC$*f&CHHyFPbxe+n5`iB$3tpQr}h0w3G%F4x{q7 zyxa!}sE-lXJeajq$sTS&ZkQcWTfR(NbxuUSA>4LKQu9K5u6?PyE7Av>B~HRIom%Ux zP+}QTavjhsRYFF9P?f%1yHI%Vqt_h}KbqWQGT5GWrAIKqQohL}_H2!7=j#(*q3~X;b ze&ZCI&JCuS*a*zdd4O;iGM9n9W&`fZ>>qk!p{F+y8!inRGK8Ew0Z5Vo41FF0GkON3 zjd{%h+GC-o2j;8J|3t`n`7i{U=x6lVH~JXjn($p#Sf?rQE?8*kMMO(1R-XT9P;epg zZ;3v-3km;=FsLcz08M~51KydGeTXodzxE`;l1D~y%I}5CPbMV?HVrkhhkf$$ngf5( z0gF~x~?s`%xk?Z$&L8N5^+?D?3EZQ%S3+iuY%Q@Oa`E8$SYQ!Q}LWM|M+J+oec58gwb;P zrgJoB4stF&0OGATgr$#b1o!}DJ#MpsUv2jHP&tZS?c7 z(vkIB3IwqZn3$8H9ReioivS=i#5|nAbll&=Wuefc#t)Z&q(BRG!f33IqXHnEfxg(o za6B?x+aLyMZ~!BFMG7EvdGG?pCQ)!tc?!sKjl=?ca<|2kw2{-AT_B4hx9vRZ`4t zr{W2~*e@!pi<*GH%?VR8_aHOcCv0ULVc*qCUrIDY^s3upNQnTU7J3nDPPh;%KXIl$ zo*i-gW$@Lc0PR+MGIPtI0;H!+&zCmS5ZY?=Pv@{3B7ol zMk)5K%x2mduJmxIZ*5T$)@e@Dky5H2a6PSseB0|2e{?RQcc9xxMnb0RraJ-zBN7WR z6yP_c%`#)#frn)EvdI@I2y0cl>Eu-!l@V(UT*u@9#!?yoN-+-S-*co_?@U*Gc9ejm z$J3=Y1k&I)f3slw$l=7YBqDpoc|Llj*_LYpZ-SVHuIlZ`FT^fAI1l7K>A&$=Q27B2 z1F)|-Jn;b2Jp9}UAf;~)@qvPMpZrCoql6xzt!6)+D4nVxb}1vTA@FYc`B)O*_g4d> z^Y721YuXKfcLBJ=plo&z+E%3w?Ot3w&_jZA>i?Yx>O3iIC({a$E4|7hRvxj-?Ty3k zZ_N1R+|=KK?#+fql|ob-vH?VABJpQ4aHu}8NGQ6}!%3Ckmg85MslCp#kyU|eq+Is@ zLdGvEg4c5PXJ;U32~suv;LVy{kcOqn6VM(G?}vpz5FG~lW2yHqTm^<+=sjy zln(VtGQgILYyV7?kmOTZbWd{y(nLKU{D%aoh4|X|+ymG@6(Z*i8ZrWL@3|u#%1|#L zzQBL$qXyH)IPCw2OwcCHPUxm|e~fqzO4dGukU-I6a0@{8ChM^b zEe(ZQ!zJCEtuUHt=UtzK=B<|#>b{KIV|es&`aLL?7oZvjlj)7DFAucP9@I6+hXR^v z=YvT|@+T#`Ab1|zP3`CP>U(Hbe1Dh#u8BEU30Nf5Q|6k3fazcV^F0NyY3Nn*O5hq}3}p#jkD0r1Vl!y3RXp#G12 zZ~%2Q*G`9%fF#{jz_>gE1ovngvbywlDj5-NDv>OWvQYH2 z>E3Tdlhwh*d>o04A4L)qJDl4wv-mCu5065OZvk#3o zL^&y6FG!13hQSj%#<87vqNuQF36@loHIH-1FSuhq)M|hqS zhM zqPrMgvHmcvqQd(UHs*n>3q+1x^}!sF&tzLBz}_{ce=)tno%=vP5Ke&SK8%=wZ|`c& zohm_TefZ?$-i5K5-TJ`LIZSo`Wwc?5V6!1`0^A#n&Bel?5pZ+matj4O4z7$qwqsaa z&4NpiIv5ZAYt9Qu9s&ozrNZ|-*3C(66_Bk6ocL^8%}DLj*pv!z?Y!>1YZ`VTZOC4L z;v@ZH84!2ob?H1FHwuA&ie`cBNbK;%(YbYXx@GRR%SU>6qv%q2#OoOYgVdX%E@8K&irA&*fyRwoI-ukEa43S;s{fP@2YfpBfrm0_;5*71jY@4 zn1j2{Mfi36c@?_q+$}?1@(fl@YkF|w-mwgZCrgF@=JXqdzvraxza6rP(G1USeNZ_t z2X$}YpPMqkk(Jhc<6yd#?6PhGWU*!|42HL-@pPJ%N>jlHthOZP&xM?6CyJOiq|l056;Xi2uV1X<4Z%g4d_-3)njePjeFb zV{IVmQbeghJ5CUUb{V>y>|8N7p}+p`di3j=nw7S~A37z3tx-LfW5Je(cOM*g-*{Dc z9UI+@fzM^qQ8G9b`Zv`MNw2TGKiC2onjHi))))Uil7MSHZC{TxjBPXG<0M( zU9fj70PU6u;bTY<$AV+RJ>oY882Z7H)*GHwhl;>CXmvj(y_kJta(Nm)%uQ9x|8|Yd zdys@lk`HPKVkfLv4CJJ$WVXvO4p2`7Av->x4? zYjov=0Z;*AKhi9@cg+1@Unp&8e4lHYNhp<2CZVREdso^*yY`Mbx!Ffz^Ae7(UO(c# zv1WQGq4So2#B~o(1{n6)b;W+L@7lfluDh<-JLY}flYIEPCv4d{A(Xx*wPOs1J?aPW z0D>jLNuh*!gPr4JTM~-JcMi^*5K0O!;kEjsb}GksQbcLgpJF}6>s?b%Uhgs16n~Vc UrOp{!DKSnbJ0X`&fdJdJJPYf4DgXcg literal 6594 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 literal 9168 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 a)#@R7gY$+|B@zu}4uohxB+sK|jR62;a+!Gm From 605800e9a5a0abed19fb0d87837c5e0a04b4b926 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 7 Jan 2025 10:02:28 -0700 Subject: [PATCH 453/550] musikr: handle possible null pointers in id3v2 --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index ca6a9b006..b8a857c9e 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -35,11 +35,14 @@ void JVMMetadataBuilder::setMimeType(const std::string_view type) { void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) { for (auto frame : tag.frameList()) { + if (frame == nullptr) + continue; if (auto txxxFrame = dynamic_cast(frame)) { TagLib::String id = frame->frameID(); TagLib::StringList frameText = txxxFrame->fieldList(); - // Frame text starts with the description then the remaining values + if (frameText.isEmpty()) + continue; auto begin = frameText.begin(); TagLib::String description = *begin; frameText.erase(begin); From 6a6d15f3e839e9767d1b717dd8e51f8db036dd99 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 7 Jan 2025 12:30:42 -0700 Subject: [PATCH 454/550] info: tweak splash --- app/src/main/res/drawable/ic_splash_anim.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/drawable/ic_splash_anim.xml b/app/src/main/res/drawable/ic_splash_anim.xml index d5b74057e..242c0f3de 100644 --- a/app/src/main/res/drawable/ic_splash_anim.xml +++ b/app/src/main/res/drawable/ic_splash_anim.xml @@ -23,7 +23,7 @@ android:scaleX="2.835" android:scaleY="2.835" android:translateX="19.98" - android:translateY="-64"> + android:translateY="19.98"> + android:translateY="19.98"> + android:translateY="19.98"> + android:translateY="19.98"> @@ -110,7 +110,7 @@ android:interpolator="@interpolator/m3_sys_motion_easing_emphasized_decelerate" android:propertyName="translateY" android:startOffset="0" - android:valueFrom="-256" + android:valueFrom="-30" android:valueTo="19.98" android:valueType="floatType" tools:ignore="PrivateResource" /> @@ -126,7 +126,7 @@ android:interpolator="@interpolator/m3_sys_motion_easing_emphasized_decelerate" android:propertyName="translateY" android:startOffset="0" - android:valueFrom="-128" + android:valueFrom="78" android:valueTo="19.98" android:valueType="floatType" tools:ignore="PrivateResource" /> @@ -139,9 +139,9 @@ From 0ead77d6e6e53fb5b8c31aaf1fec5a6d18ee0e46 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 7 Jan 2025 12:40:17 -0700 Subject: [PATCH 455/550] info: switch splash motion --- app/src/main/res/drawable/ic_splash_anim.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/drawable/ic_splash_anim.xml b/app/src/main/res/drawable/ic_splash_anim.xml index 242c0f3de..3de8da311 100644 --- a/app/src/main/res/drawable/ic_splash_anim.xml +++ b/app/src/main/res/drawable/ic_splash_anim.xml @@ -93,9 +93,9 @@ @@ -108,9 +108,9 @@ @@ -124,9 +124,9 @@ @@ -139,9 +139,9 @@ From 7b9c14a118fd3a4ced915184a1292702910beecc Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 7 Jan 2025 13:05:32 -0700 Subject: [PATCH 456/550] musikr: add temp logging --- musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index b77c962b9..707969edc 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -117,6 +117,7 @@ private class ExtractStepImpl( val metadata = fds.mapNotNull { fileWith -> wrap(fileWith.file) { _ -> + Log.d("ExtractStep", "Extracting ${fileWith.file.path}") metadataExtractor .extract(fileWith.with) ?.let { FileWith(fileWith.file, it) } From 4318e70052d8c5545fa8a0ee89f8cb89bf240f78 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 7 Jan 2025 13:09:42 -0700 Subject: [PATCH 457/550] info: make splash branding better --- app/src/main/res/drawable/ic_splash_anim.xml | 58 ++++++++++++-------- app/src/main/res/values-v31/styles_core.xml | 1 + 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/app/src/main/res/drawable/ic_splash_anim.xml b/app/src/main/res/drawable/ic_splash_anim.xml index 3de8da311..9ba552d91 100644 --- a/app/src/main/res/drawable/ic_splash_anim.xml +++ b/app/src/main/res/drawable/ic_splash_anim.xml @@ -12,19 +12,33 @@ android:pivotX="56" android:pivotY="56"> - + + + + @@ -106,12 +120,12 @@ @@ -122,12 +136,12 @@ @@ -137,12 +151,12 @@ diff --git a/app/src/main/res/values-v31/styles_core.xml b/app/src/main/res/values-v31/styles_core.xml index 714a9a9fb..d538418f2 100644 --- a/app/src/main/res/values-v31/styles_core.xml +++ b/app/src/main/res/values-v31/styles_core.xml @@ -3,6 +3,7 @@ From 5375c862b36b436114dd5480d572593434c516b2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 7 Jan 2025 17:22:35 -0700 Subject: [PATCH 458/550] info: further standardize splash --- app/src/main/res/drawable/ic_splash_anim.xml | 81 ++++++++++---------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/app/src/main/res/drawable/ic_splash_anim.xml b/app/src/main/res/drawable/ic_splash_anim.xml index 9ba552d91..bf2bb92d2 100644 --- a/app/src/main/res/drawable/ic_splash_anim.xml +++ b/app/src/main/res/drawable/ic_splash_anim.xml @@ -3,93 +3,94 @@ xmlns:aapt="http://schemas.android.com/aapt"> + android:width="432dp" + android:height="432dp" + android:viewportWidth="432" + android:viewportHeight="432"> + android:strokeColor="#00000000" + android:pathData="M 408,216 +A 192,192 0 0 1 216,408 192,192 0 0 1 24,216 192,192 0 0 1 216,24 192,192 0 0 1 408,216 +Z" /> + android:scaleX="8.5" + android:scaleY="8.5" + android:translateX="114" + android:translateY="114"> + android:scaleX="8.5" + android:scaleY="8.5" + android:translateX="114" + android:translateY="114"> + android:scaleX="8.5" + android:scaleY="8.5" + android:translateX="114" + android:translateY="114"> + android:scaleX="8.5" + android:scaleY="8.5" + android:translateX="114" + android:translateY="114"> + android:scaleX="8.5" + android:scaleY="8.5" + android:translateX="114" + android:translateY="114"> @@ -105,10 +106,10 @@ Date: Tue, 7 Jan 2025 18:31:59 -0700 Subject: [PATCH 459/550] musikr: more debug logging Trying to track down this thorny segfault. --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index b8a857c9e..647583ec0 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -35,22 +35,34 @@ void JVMMetadataBuilder::setMimeType(const std::string_view type) { void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) { for (auto frame : tag.frameList()) { + LOGD("Frame Check"); if (frame == nullptr) continue; + LOGD("Text Frame Check"); if (auto txxxFrame = dynamic_cast(frame)) { + LOGD("TXXX ID"); TagLib::String id = frame->frameID(); + LOGD("TXXX Fields"); TagLib::StringList frameText = txxxFrame->fieldList(); + LOGD("TXXX Check"); if (frameText.isEmpty()) continue; + LOGD("TXXX Begin"); auto begin = frameText.begin(); + LOGD("TXXX Desc"); TagLib::String description = *begin; + LOGD("TXXX Erase"); frameText.erase(begin); + LOGD("TXXX Add"); id3v2.add_combined(id, description, frameText); } else if (auto textFrame = dynamic_cast(frame)) { + LOGD("T*** ID"); TagLib::String key = frame->frameID(); + LOGD("T*** Fields"); TagLib::StringList frameText = textFrame->fieldList(); + LOGD("T*** Add"); id3v2.add_id(key, frameText); } else { continue; From e519e8f8be8d0a584d0596bd265221c6845207b1 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 7 Jan 2025 19:34:30 -0700 Subject: [PATCH 460/550] musikr: handle null tags --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 14 --------- musikr/src/main/cpp/taglib_jni.cpp | 35 +++++++++++++++++----- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index 647583ec0..6f2630177 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -35,34 +35,20 @@ void JVMMetadataBuilder::setMimeType(const std::string_view type) { void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) { for (auto frame : tag.frameList()) { - LOGD("Frame Check"); - if (frame == nullptr) - continue; - LOGD("Text Frame Check"); if (auto txxxFrame = dynamic_cast(frame)) { - LOGD("TXXX ID"); TagLib::String id = frame->frameID(); - LOGD("TXXX Fields"); TagLib::StringList frameText = txxxFrame->fieldList(); - LOGD("TXXX Check"); if (frameText.isEmpty()) continue; - LOGD("TXXX Begin"); auto begin = frameText.begin(); - LOGD("TXXX Desc"); TagLib::String description = *begin; - LOGD("TXXX Erase"); frameText.erase(begin); - LOGD("TXXX Add"); id3v2.add_combined(id, description, frameText); } else if (auto textFrame = dynamic_cast(frame)) { - LOGD("T*** ID"); TagLib::String key = frame->frameID(); - LOGD("T*** Fields"); TagLib::StringList frameText = textFrame->fieldList(); - LOGD("T*** Add"); id3v2.add_id(key, frameText); } else { continue; diff --git a/musikr/src/main/cpp/taglib_jni.cpp b/musikr/src/main/cpp/taglib_jni.cpp index a1ec002a4..7e476a84e 100644 --- a/musikr/src/main/cpp/taglib_jni.cpp +++ b/musikr/src/main/cpp/taglib_jni.cpp @@ -46,24 +46,45 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, if (auto *mpegFile = dynamic_cast(file)) { builder.setMimeType("audio/mpeg"); - builder.setId3v2(*mpegFile->ID3v2Tag()); + auto tag = mpegFile->ID3v2Tag(); + if (tag != nullptr) { + builder.setId3v2(*tag); + } } else if (auto *mp4File = dynamic_cast(file)) { builder.setMimeType("audio/mp4"); - builder.setMp4(*mp4File->tag()); + auto tag = mp4File->tag(); + if (tag != nullptr) { + builder.setMp4(*tag); + } } else if (auto *flacFile = dynamic_cast(file)) { builder.setMimeType("audio/flac"); - builder.setId3v2(*flacFile->ID3v2Tag()); - builder.setXiph(*flacFile->xiphComment()); + auto id3v2Tag = flacFile->ID3v2Tag(); + if (id3v2Tag != nullptr) { + builder.setId3v2(*id3v2Tag); + } + auto xiphComment = flacFile->xiphComment(); + if (xiphComment != nullptr) { + builder.setXiph(*xiphComment); + } } else if (auto *opusFile = dynamic_cast(file)) { builder.setMimeType("audio/opus"); - builder.setXiph(*opusFile->tag()); + auto tag = opusFile->tag(); + if (tag != nullptr) { + builder.setXiph(*tag); + } } else if (auto *vorbisFile = dynamic_cast(file)) { builder.setMimeType("audio/vorbis"); - builder.setXiph(*vorbisFile->tag()); + auto tag = vorbisFile->tag(); + if (tag != nullptr) { + builder.setXiph(*tag); + } } else if (auto *wavFile = dynamic_cast(file)) { builder.setMimeType("audio/wav"); - builder.setId3v2(*wavFile->ID3v2Tag()); + auto tag = wavFile->ID3v2Tag(); + if (tag != nullptr) { + builder.setId3v2(*tag); + } } else { // While taglib supports other formats, ExoPlayer does not. Ignore them. LOGE("Unsupported file format"); From b3f4fdfb4a2a08125414a9bbd205146399cd2435 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 10:36:48 -0700 Subject: [PATCH 461/550] build: bump version Bump to version 4.0.0-dev2. --- app/build.gradle | 4 ++-- .../src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b66d918b9..44f809ccc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId namespace - versionName "4.0.0-dev" - versionCode 54 + versionName "4.0.0-dev2" + versionCode 55 minSdk min_sdk targetSdk target_sdk diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index 88d897006..7dca9bcf3 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -41,7 +41,7 @@ import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.util.correctWhitespace import org.oxycblt.musikr.util.splitEscaped -@Database(entities = [CachedSong::class], version = 54, exportSchema = false) +@Database(entities = [CachedSong::class], version = 55, exportSchema = false) internal abstract class CacheDatabase : RoomDatabase() { abstract fun visibleDao(): VisibleCacheDao From e94b74edd43cb3ac8625a0c55e1f41231724b465 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 11:11:48 -0700 Subject: [PATCH 462/550] musikr: do custom picture handling TagLib's picture handling is inadequate for our use case. --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 68 ++++++++++++++++------ musikr/src/main/cpp/JVMMetadataBuilder.h | 8 +-- musikr/src/main/cpp/taglib_jni.cpp | 3 +- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index 6f2630177..e6ecf0bc3 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -22,6 +22,7 @@ #include #include +#include #include @@ -33,7 +34,10 @@ void JVMMetadataBuilder::setMimeType(const std::string_view type) { this->mimeType = type; } -void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) { +void JVMMetadataBuilder::setId3v2(TagLib::ID3v2::Tag &tag) { + // We want to ideally find the front cover, fall back to the first picture otherwise. + std::optional firstPic; + std::optional frontCoverPic; for (auto frame : tag.frameList()) { if (auto txxxFrame = dynamic_cast(frame)) { @@ -50,18 +54,37 @@ void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) { TagLib::String key = frame->frameID(); TagLib::StringList frameText = textFrame->fieldList(); id3v2.add_id(key, frameText); + } else if (auto pictureFrame = + dynamic_cast(frame)) { + if (!firstPic) { + firstPic = pictureFrame; + } + if (!frontCoverPic + && pictureFrame->type() + == TagLib::ID3v2::AttachedPictureFrame::FrontCover) { + frontCoverPic = pictureFrame; + } } else { continue; } } + if (frontCoverPic) { + auto pic = *frontCoverPic; + cover = pic->picture(); + } else if (firstPic) { + auto pic = *firstPic; + cover = pic->picture(); + } } -void JVMMetadataBuilder::setXiph(const TagLib::Ogg::XiphComment &tag) { +void JVMMetadataBuilder::setXiph(TagLib::Ogg::XiphComment &tag) { for (auto field : tag.fieldListMap()) { auto key = field.first.upper(); auto values = field.second; xiph.add_custom(key, values); } + auto pics = tag.pictureList(); + setFlacPictures(pics); } template @@ -77,11 +100,27 @@ void mp4AddImpl(JVMTagMap &map, TagLib::String &itemName, T itemValue) { } } -void JVMMetadataBuilder::setMp4(const TagLib::MP4::Tag &tag) { +void JVMMetadataBuilder::setMp4(TagLib::MP4::Tag &tag) { auto map = tag.itemMap(); + std::optional < TagLib::MP4::CoverArt > firstCover; for (auto item : map) { auto itemName = item.first; auto itemValue = item.second; + if (itemName == "covr") { + // Special cover case. + // MP4 has no types, so just prioritize easier to decode covers (PNG, JPEG) + auto pics = itemValue.toCoverArtList(); + for (auto &pic : pics) { + auto format = pic.format(); + if (format == TagLib::MP4::CoverArt::PNG + || format == TagLib::MP4::CoverArt::JPEG) { + cover = pic.data(); + return; + } + } + cover = pics.front().data(); + return; + } auto type = itemValue.type(); std::string serializedValue; switch (type) { @@ -114,23 +153,18 @@ void JVMMetadataBuilder::setMp4(const TagLib::MP4::Tag &tag) { } } -void JVMMetadataBuilder::setCover( - const TagLib::List covers) { - if (covers.isEmpty()) { - return; - } - // Find the cover with a "front cover" type - for (auto cover : covers) { - auto type = cover["pictureType"].toString(); - if (type == "Front Cover") { - this->cover = cover["data"].toByteVector(); +void JVMMetadataBuilder::setFlacPictures( + TagLib::List &pics) { + // Find the front cover image. If it doesn't exist, fall back to the first image. + for (auto pic : pics) { + if (pic->type() == TagLib::FLAC::Picture::FrontCover) { + cover = pic->data(); return; } } - // No front cover, just pick first. - // TODO: Consider having cascading fallbacks to increasingly less - // relevant covers perhaps - this->cover = covers.front()["data"].toByteVector(); + if (!pics.isEmpty()) { + cover = pics.front()->data(); + } } void JVMMetadataBuilder::setProperties(TagLib::AudioProperties *properties) { diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.h b/musikr/src/main/cpp/JVMMetadataBuilder.h index df2a7f8bf..b093dd01d 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.h +++ b/musikr/src/main/cpp/JVMMetadataBuilder.h @@ -35,10 +35,10 @@ public: JVMMetadataBuilder(JNIEnv *env); void setMimeType(const std::string_view type); - void setId3v2(const TagLib::ID3v2::Tag &tag); - void setXiph(const TagLib::Ogg::XiphComment &tag); - void setMp4(const TagLib::MP4::Tag &tag); - void setCover(const TagLib::List covers); + void setId3v2(TagLib::ID3v2::Tag &tag); + void setXiph(TagLib::Ogg::XiphComment &tag); + void setMp4(TagLib::MP4::Tag &tag); + void setFlacPictures(TagLib::List &pics); void setProperties(TagLib::AudioProperties *properties); jobject build(); diff --git a/musikr/src/main/cpp/taglib_jni.cpp b/musikr/src/main/cpp/taglib_jni.cpp index 7e476a84e..8590cbb25 100644 --- a/musikr/src/main/cpp/taglib_jni.cpp +++ b/musikr/src/main/cpp/taglib_jni.cpp @@ -66,6 +66,8 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, if (xiphComment != nullptr) { builder.setXiph(*xiphComment); } + auto pics = flacFile->pictureList(); + builder.setFlacPictures(pics); } else if (auto *opusFile = dynamic_cast(file)) { builder.setMimeType("audio/opus"); auto tag = opusFile->tag(); @@ -92,7 +94,6 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, } builder.setProperties(file->audioProperties()); - builder.setCover(file->tag()->complexProperties("PICTURE")); return builder.build(); } catch (std::runtime_error e) { LOGE("Error opening file: %s", e.what()); From 58e0956cada5970681b94c1ba0747d06b85c0028 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 11:31:52 -0700 Subject: [PATCH 463/550] musikr: dont stop parsing mp4 atoms I cannot believe I have made this mistake twice. --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index e6ecf0bc3..9488b9ecd 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -115,11 +115,11 @@ void JVMMetadataBuilder::setMp4(TagLib::MP4::Tag &tag) { if (format == TagLib::MP4::CoverArt::PNG || format == TagLib::MP4::CoverArt::JPEG) { cover = pic.data(); - return; + continue; } } cover = pics.front().data(); - return; + continue; } auto type = itemValue.type(); std::string serializedValue; From 6f2b7abbef65149f20c93f0bcb8a7531fca59f5d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 12:10:42 -0700 Subject: [PATCH 464/550] music: commit playlist rewrites --- app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 2d2e04230..3c7d614c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -347,7 +347,8 @@ constructor( override suspend fun rewritePlaylist(playlist: Playlist, songs: List) { val library = synchronized(this) { library ?: return } L.d("Rewriting $playlist with ${songs.size} songs") - library.rewritePlaylist(playlist, songs) + val newLibrary = library.rewritePlaylist(playlist, songs) + synchronized(this) { this.library = newLibrary } withContext(Dispatchers.Main) { dispatchLibraryChange(device = false, user = true) } } From 3bd40278023c78ace0152c3a81e633e04eafa664 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 12:34:24 -0700 Subject: [PATCH 465/550] home: add retry to error dialog --- .../main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt index e88ed1175..0cbd697ce 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt @@ -24,9 +24,11 @@ import android.os.Build import android.os.Bundle import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogErrorDetailsBinding +import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.openInBrowser @@ -42,10 +44,14 @@ import org.oxycblt.auxio.util.showToast class ErrorDetailsDialog : ViewBindingMaterialDialogFragment() { private val args: ErrorDetailsDialogArgs by navArgs() private var clipboardManager: ClipboardManager? = null + private val musicModel: MusicViewModel by viewModels<>() override fun onConfigDialog(builder: AlertDialog.Builder) { builder .setTitle(R.string.lbl_error_info) + .setNeutralButton( + R.string.lbl_retry + ) { _, _ -> musicModel.refresh() } .setPositiveButton(R.string.lbl_report) { _, _ -> requireContext().openInBrowser(LINK_ISSUES) } From ff074d0e3a3ef74f1891dbf877cd3c94a1cefaa9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 12:42:44 -0700 Subject: [PATCH 466/550] all: fix formatting --- .../main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt index 0cbd697ce..933ee6848 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt @@ -44,14 +44,12 @@ import org.oxycblt.auxio.util.showToast class ErrorDetailsDialog : ViewBindingMaterialDialogFragment() { private val args: ErrorDetailsDialogArgs by navArgs() private var clipboardManager: ClipboardManager? = null - private val musicModel: MusicViewModel by viewModels<>() + private val musicModel: MusicViewModel by viewModels() override fun onConfigDialog(builder: AlertDialog.Builder) { builder .setTitle(R.string.lbl_error_info) - .setNeutralButton( - R.string.lbl_retry - ) { _, _ -> musicModel.refresh() } + .setNeutralButton(R.string.lbl_retry) { _, _ -> musicModel.refresh() } .setPositiveButton(R.string.lbl_report) { _, _ -> requireContext().openInBrowser(LINK_ISSUES) } From 8c4b8dfb56242983562d22002957684f719b603d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 12:53:04 -0700 Subject: [PATCH 467/550] musikr: improve dead vertex error reporting --- .../org/oxycblt/musikr/graph/MusicGraph.kt | 29 ++++++++++----- .../oxycblt/musikr/model/LibraryFactory.kt | 36 ++++++++++++------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt index 121a53cf4..4b33dc966 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt @@ -322,33 +322,46 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder { } } +internal interface Vertex { + val tag: Any? +} + internal class SongVertex( val preSong: PreSong, var albumVertex: AlbumVertex, var artistVertices: MutableList, var genreVertices: MutableList -) { - var tag: Any? = null +) : Vertex { + override var tag: Any? = null + + override fun toString() = "SongVertex(preSong=$preSong)" } -internal class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList) { +internal class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList) : + Vertex { val songVertices = mutableSetOf() - var tag: Any? = null + override var tag: Any? = null + + override fun toString() = "AlbumVertex(preAlbum=$preAlbum)" } internal class ArtistVertex( val preArtist: PreArtist, -) { +) : Vertex { val songVertices = mutableSetOf() val albumVertices = mutableSetOf() val genreVertices = mutableSetOf() - var tag: Any? = null + override var tag: Any? = null + + override fun toString() = "ArtistVertex(preArtist=$preArtist)" } -internal class GenreVertex(val preGenre: PreGenre) { +internal class GenreVertex(val preGenre: PreGenre) : Vertex { val songVertices = mutableSetOf() val artistVertices = mutableSetOf() - var tag: Any? = null + override var tag: Any? = null + + override fun toString() = "GenreVertex(preGenre=$preGenre)" } internal class PlaylistVertex(val prePlaylist: PrePlaylist) { diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt index 1bcd6d752..92296c80c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt @@ -21,6 +21,7 @@ package org.oxycblt.musikr.model import org.oxycblt.musikr.Album import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Genre +import org.oxycblt.musikr.Music import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.Song import org.oxycblt.musikr.graph.AlbumVertex @@ -29,6 +30,7 @@ import org.oxycblt.musikr.graph.GenreVertex import org.oxycblt.musikr.graph.MusicGraph import org.oxycblt.musikr.graph.PlaylistVertex import org.oxycblt.musikr.graph.SongVertex +import org.oxycblt.musikr.graph.Vertex import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter @@ -77,44 +79,52 @@ private class LibraryFactoryImpl() : LibraryFactory { private class SongVertexCore(private val vertex: SongVertex) : SongCore { override val preSong = vertex.preSong - override fun resolveAlbum() = vertex.albumVertex.tag as Album + override fun resolveAlbum(): Album = tag(vertex.albumVertex) - override fun resolveArtists() = vertex.artistVertices.map { it.tag as Artist } + override fun resolveArtists(): List = vertex.artistVertices.map { tag(it) } - override fun resolveGenres() = vertex.genreVertices.map { it.tag as Genre } + override fun resolveGenres(): List = vertex.genreVertices.map { tag(it) } } private class AlbumVertexCore(private val vertex: AlbumVertex) : AlbumCore { override val preAlbum = vertex.preAlbum - override val songs = vertex.songVertices.mapTo(mutableSetOf()) { it.tag as Song } + override val songs: Set = vertex.songVertices.mapTo(mutableSetOf()) { tag(it) } - override fun resolveArtists() = vertex.artistVertices.map { it.tag as Artist } + override fun resolveArtists(): List = vertex.artistVertices.map { tag(it) } } private class ArtistVertexCore(private val vertex: ArtistVertex) : ArtistCore { override val preArtist = vertex.preArtist - override val songs = vertex.songVertices.mapTo(mutableSetOf()) { it.tag as Song } + override val songs: Set = vertex.songVertices.mapTo(mutableSetOf()) { tag(it) } - override val albums = vertex.albumVertices.mapTo(mutableSetOf()) { it.tag as Album } + override val albums: Set = vertex.albumVertices.mapTo(mutableSetOf()) { tag(it) } - override fun resolveGenres() = - vertex.genreVertices.mapTo(mutableSetOf()) { it.tag as Genre } + override fun resolveGenres(): Set = + vertex.genreVertices.mapTo(mutableSetOf()) { tag(it) } } private class GenreVertexCore(vertex: GenreVertex) : GenreCore { override val preGenre = vertex.preGenre - override val songs = vertex.songVertices.mapTo(mutableSetOf()) { it.tag as Song } + override val songs: Set = vertex.songVertices.mapTo(mutableSetOf()) { tag(it) } - override val artists = vertex.artistVertices.mapTo(mutableSetOf()) { it.tag as Artist } + override val artists: Set = vertex.artistVertices.mapTo(mutableSetOf()) { tag(it) } } private class PlaylistVertexCore(vertex: PlaylistVertex) : PlaylistCore { override val prePlaylist = vertex.prePlaylist - override val songs = - vertex.songVertices.mapNotNull { vertex -> vertex?.let { it.tag as Song } } + override val songs: List = + vertex.songVertices.mapNotNull { vertex -> vertex?.let { tag(it) } } + } + + private companion object { + private inline fun tag(vertex: Vertex): T { + val tag = vertex.tag + check(tag is T) { "Dead Vertex Detected: $vertex" } + return tag + } } } From 4a08809e50bc5a3322ac08227a6d51ea5233f1d9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 12:58:58 -0700 Subject: [PATCH 468/550] home: hide loading indicator by default Prevents flickering during navigation. --- app/src/main/res/layout/fragment_home.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index b5469a95f..314aaae15 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -72,6 +72,7 @@ android:layout_height="wrap_content" android:layout_gravity="top|start" android:transitionGroup="true" + android:visibility="invisible" android:layout_margin="@dimen/spacing_medium"> Date: Wed, 8 Jan 2025 13:14:53 -0700 Subject: [PATCH 469/550] main: fix unusable fast scroll below fab --- .../oxycblt/auxio/home/ThemedSpeedDialView.kt | 17 +++++++++++++++-- .../main/res/layout-w720dp/fragment_main.xml | 1 + app/src/main/res/layout/fragment_main.xml | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt index d111899e1..7f686dec3 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt @@ -190,6 +190,8 @@ class ThemedSpeedDialView : SpeedDialView { val overlayColor = surfaceColor.defaultColor.withModulatedAlpha(0.87f) overlayLayout.setBackgroundColor(overlayColor) } + // Fix default margins added by library + (mainFab.layoutParams as LayoutParams).setMargins(0, 0, 0, 0) } private fun Int.withModulatedAlpha( @@ -230,13 +232,24 @@ class ThemedSpeedDialView : SpeedDialView { return super.addActionItem(actionItem, position, animate)?.apply { fab.apply { updateLayoutParams { - val horizontalMargin = context.getDimenPixels(R.dimen.spacing_mid_large) - setMargins(horizontalMargin, 0, horizontalMargin, 0) + val rightMargin = context.getDimenPixels(R.dimen.spacing_tiny) + if (position == actionItems.lastIndex) { + val bottomMargin = context.getDimenPixels(R.dimen.spacing_small) + setMargins(0, 0, rightMargin, bottomMargin) + } else { + setMargins(0, 0, rightMargin, 0) + } } useCompatPadding = false } labelBackground.apply { + updateLayoutParams { + if (position == actionItems.lastIndex) { + val bottomMargin = context.getDimenPixels(R.dimen.spacing_small) + setMargins(0, 0, rightMargin, bottomMargin) + } + } useCompatPadding = false setContentPadding(spacingSmall, spacingSmall, spacingSmall, spacingSmall) background = diff --git a/app/src/main/res/layout-w720dp/fragment_main.xml b/app/src/main/res/layout-w720dp/fragment_main.xml index a364dd452..90feb3b88 100644 --- a/app/src/main/res/layout-w720dp/fragment_main.xml +++ b/app/src/main/res/layout-w720dp/fragment_main.xml @@ -28,6 +28,7 @@ android:layout_gravity="bottom|end" android:clipChildren="false" android:clipToPadding="false" + android:layout_margin="@dimen/spacing_medium" app:layout_anchor="@id/home_content"> @@ -54,7 +55,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom|end" - android:layout_margin="@dimen/spacing_medium" android:contentDescription="@string/lbl_shuffle" android:src="@drawable/ic_shuffle_off_24" /> From 6ee43b106fcb006ecdf83c5a1b375820c10be74a Mon Sep 17 00:00:00 2001 From: "._______166" <62702353+dot166@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:03:39 +0000 Subject: [PATCH 470/550] add android 15 to android version list in issue template --- .github/ISSUE_TEMPLATE/bug-crash-report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug-crash-report.yml b/.github/ISSUE_TEMPLATE/bug-crash-report.yml index abc516401..094d55e76 100644 --- a/.github/ISSUE_TEMPLATE/bug-crash-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-crash-report.yml @@ -34,6 +34,7 @@ body: attributes: label: What android version do you use? options: + - Android 15 - Android 14 - Android 13 - Android 12L From 802e215482be69eb8b887788492bccf2e8133343 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 15:05:50 -0700 Subject: [PATCH 471/550] musikr: remove extractstep debug logging Not needed right now --- .../main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 707969edc..3022a555d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -117,16 +117,10 @@ private class ExtractStepImpl( val metadata = fds.mapNotNull { fileWith -> wrap(fileWith.file) { _ -> - Log.d("ExtractStep", "Extracting ${fileWith.file.path}") metadataExtractor .extract(fileWith.with) ?.let { FileWith(fileWith.file, it) } .also { - if (it == null) { - Log.d( - "ExtractStep", - "Failed to extract metadata for ${fileWith.file.path}") - } withContext(Dispatchers.IO) { fileWith.with.close() } } } From 8c3750778ff1d76c73fd7163ea2fb158eea455a9 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 15:06:25 -0700 Subject: [PATCH 472/550] musikr: add id3v1 support Forgot to go ahead and implement this. --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 9 +++++++++ musikr/src/main/cpp/JVMMetadataBuilder.h | 2 ++ musikr/src/main/cpp/JVMTagMap.cpp | 18 +++++++++--------- musikr/src/main/cpp/JVMTagMap.h | 16 ++++++++-------- musikr/src/main/cpp/taglib_jni.cpp | 18 +++++++++++++----- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index 9488b9ecd..d7254e845 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -34,6 +34,15 @@ void JVMMetadataBuilder::setMimeType(const std::string_view type) { this->mimeType = type; } +void JVMMetadataBuilder::setId3v1(TagLib::ID3v1::Tag &tag) { + id3v2.add_id("TIT2", tag.title()); + id3v2.add_id("TPE1", tag.artist()); + id3v2.add_id("TALB", tag.album()); + id3v2.add_id("TRCK", std::to_string(tag.track())); + id3v2.add_id("TYER", std::to_string(tag.year())); + id3v2.add_id("TCON", std::to_string(tag.genreNumber())); +} + void JVMMetadataBuilder::setId3v2(TagLib::ID3v2::Tag &tag) { // We want to ideally find the front cover, fall back to the first picture otherwise. std::optional firstPic; diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.h b/musikr/src/main/cpp/JVMMetadataBuilder.h index b093dd01d..1ee52db3a 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.h +++ b/musikr/src/main/cpp/JVMMetadataBuilder.h @@ -23,6 +23,7 @@ #include #include +#include "taglib/id3v1tag.h" #include "taglib/id3v2tag.h" #include "taglib/xiphcomment.h" #include "taglib/mp4tag.h" @@ -35,6 +36,7 @@ public: JVMMetadataBuilder(JNIEnv *env); void setMimeType(const std::string_view type); + void setId3v1(TagLib::ID3v1::Tag &tag); void setId3v2(TagLib::ID3v2::Tag &tag); void setXiph(TagLib::Ogg::XiphComment &tag); void setMp4(TagLib::MP4::Tag &tag); diff --git a/musikr/src/main/cpp/JVMTagMap.cpp b/musikr/src/main/cpp/JVMTagMap.cpp index d8030dea0..19ec9b733 100644 --- a/musikr/src/main/cpp/JVMTagMap.cpp +++ b/musikr/src/main/cpp/JVMTagMap.cpp @@ -52,13 +52,13 @@ JVMTagMap::~JVMTagMap() { env->DeleteLocalRef(arrayListClass); } -void JVMTagMap::add_id(TagLib::String &id, TagLib::String &value) { +void JVMTagMap::add_id(TagLib::String id, TagLib::String value) { env->CallVoidMethod(tagMap, tagMapAddIdSingleMethod, env->NewStringUTF(id.toCString(true)), env->NewStringUTF(value.toCString(true))); } -void JVMTagMap::add_id(TagLib::String &id, TagLib::StringList &value) { +void JVMTagMap::add_id(TagLib::String id, TagLib::StringList value) { jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); for (auto &item : value) { env->CallBooleanMethod(arrayList, arrayListAddMethod, @@ -68,14 +68,14 @@ void JVMTagMap::add_id(TagLib::String &id, TagLib::StringList &value) { env->NewStringUTF(id.toCString(true)), arrayList); } -void JVMTagMap::add_custom(TagLib::String &description, TagLib::String &value) { +void JVMTagMap::add_custom(TagLib::String description, TagLib::String value) { env->CallVoidMethod(tagMap, tagMapAddCustomSingleMethod, env->NewStringUTF(description.toCString(true)), env->NewStringUTF(value.toCString(true))); } -void JVMTagMap::add_custom(TagLib::String &description, - TagLib::StringList &value) { +void JVMTagMap::add_custom(TagLib::String description, + TagLib::StringList value) { jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); for (auto &item : value) { env->CallBooleanMethod(arrayList, arrayListAddMethod, @@ -85,16 +85,16 @@ void JVMTagMap::add_custom(TagLib::String &description, env->NewStringUTF(description.toCString(true)), arrayList); } -void JVMTagMap::add_combined(TagLib::String &id, TagLib::String &description, - TagLib::String &value) { +void JVMTagMap::add_combined(TagLib::String id, TagLib::String description, + TagLib::String value) { env->CallVoidMethod(tagMap, tagMapAddCombinedSingleMethod, env->NewStringUTF(id.toCString(true)), env->NewStringUTF(description.toCString(true)), env->NewStringUTF(value.toCString(true))); } -void JVMTagMap::add_combined(TagLib::String &id, TagLib::String &description, - TagLib::StringList &value) { +void JVMTagMap::add_combined(TagLib::String id, TagLib::String description, + TagLib::StringList value) { jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); for (auto &item : value) { env->CallBooleanMethod(arrayList, arrayListAddMethod, diff --git a/musikr/src/main/cpp/JVMTagMap.h b/musikr/src/main/cpp/JVMTagMap.h index 842f6872d..07db6443d 100644 --- a/musikr/src/main/cpp/JVMTagMap.h +++ b/musikr/src/main/cpp/JVMTagMap.h @@ -32,16 +32,16 @@ public: JVMTagMap(const JVMTagMap&) = delete; JVMTagMap& operator=(const JVMTagMap&) = delete; - void add_id(TagLib::String &id, TagLib::String &value); - void add_id(TagLib::String &id, TagLib::StringList &value); + void add_id(TagLib::String id, TagLib::String value); + void add_id(TagLib::String id, TagLib::StringList value); - void add_custom(TagLib::String &description, TagLib::String &value); - void add_custom(TagLib::String &description, TagLib::StringList &value); + void add_custom(TagLib::String description, TagLib::String value); + void add_custom(TagLib::String description, TagLib::StringList value); - void add_combined(TagLib::String &id, TagLib::String &description, - TagLib::String &value); - void add_combined(TagLib::String &id, TagLib::String &description, - TagLib::StringList &value); + void add_combined(TagLib::String id, TagLib::String description, + TagLib::String value); + void add_combined(TagLib::String id, TagLib::String description, + TagLib::StringList value); jobject getObject(); diff --git a/musikr/src/main/cpp/taglib_jni.cpp b/musikr/src/main/cpp/taglib_jni.cpp index 8590cbb25..a0c393c2c 100644 --- a/musikr/src/main/cpp/taglib_jni.cpp +++ b/musikr/src/main/cpp/taglib_jni.cpp @@ -46,9 +46,13 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, if (auto *mpegFile = dynamic_cast(file)) { builder.setMimeType("audio/mpeg"); - auto tag = mpegFile->ID3v2Tag(); - if (tag != nullptr) { - builder.setId3v2(*tag); + auto id3v1Tag = mpegFile->ID3v1Tag(); + if (id3v1Tag != nullptr) { + builder.setId3v1(*id3v1Tag); + } + auto id3v2Tag = mpegFile->ID3v2Tag(); + if (id3v2Tag != nullptr) { + builder.setId3v2(*id3v2Tag); } } else if (auto *mp4File = dynamic_cast(file)) { builder.setMimeType("audio/mp4"); @@ -58,6 +62,10 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, } } else if (auto *flacFile = dynamic_cast(file)) { builder.setMimeType("audio/flac"); + auto id3v1Tag = flacFile->ID3v1Tag(); + if (id3v1Tag != nullptr) { + builder.setId3v1(*id3v1Tag); + } auto id3v2Tag = flacFile->ID3v2Tag(); if (id3v2Tag != nullptr) { builder.setId3v2(*id3v2Tag); @@ -89,14 +97,14 @@ Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, } } else { // While taglib supports other formats, ExoPlayer does not. Ignore them. - LOGE("Unsupported file format"); + LOGD("Unsupported file format"); return nullptr; } builder.setProperties(file->audioProperties()); return builder.build(); } catch (std::runtime_error e) { - LOGE("Error opening file: %s", e.what()); + LOGD("Error opening file: %s", e.what()); return nullptr; } From 0e34a28dfb180114c4aa8b353051705eb6ddc8a4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 15:49:49 -0700 Subject: [PATCH 473/550] musikr: fix stream seeking Foolishly changed offset sign in seek from end. --- .../main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt index 5adcd015e..fd6c9481b 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt @@ -43,7 +43,7 @@ internal class NativeInputStream(fis: FileInputStream) { } fun seekFromEnd(offset: Long) { - channel.position(channel.size() - offset) + channel.position(channel.size() + offset) } fun tell() = channel.position() From 3d690eb637cd9e503583f7de74832ca299c64d82 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 17:17:05 -0700 Subject: [PATCH 474/550] musikr: fix graphing error w/certain link steps I wasn't correctly linking genres, which would cascade to a dead vertex down the line. Will need better diagnostics here. --- musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt index 4b33dc966..ebf3101a9 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt @@ -18,6 +18,7 @@ package org.oxycblt.musikr.graph +import android.util.Log import org.oxycblt.musikr.Music import org.oxycblt.musikr.playlist.SongPointer import org.oxycblt.musikr.playlist.interpret.PrePlaylist @@ -148,12 +149,14 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder { } } - return MusicGraph( + val graph = MusicGraph( songVertices.values.toList(), albumVertices.values.toList(), artistVertices.values.toList(), genreVertices.values.toList(), playlistVertices) + + return graph } private fun simplifyGenreCluster(cluster: Collection) { @@ -240,6 +243,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder { // Link all songs and albums from the irrelevant artist to the relevant artist. dst.songVertices.addAll(src.songVertices) dst.albumVertices.addAll(src.albumVertices) + dst.genreVertices.addAll(src.genreVertices) // Update all songs, albums, and genres to point to the relevant artist. src.songVertices.forEach { val index = it.artistVertices.indexOf(src) From e0059e9dc0e636eea66878847544dd901e29d582 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 17:19:02 -0700 Subject: [PATCH 475/550] musikr: reformat --- .../java/org/oxycblt/musikr/graph/MusicGraph.kt | 14 +++++++------- .../org/oxycblt/musikr/pipeline/ExtractStep.kt | 5 +---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt index ebf3101a9..1b7b2f910 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt @@ -18,7 +18,6 @@ package org.oxycblt.musikr.graph -import android.util.Log import org.oxycblt.musikr.Music import org.oxycblt.musikr.playlist.SongPointer import org.oxycblt.musikr.playlist.interpret.PrePlaylist @@ -149,12 +148,13 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder { } } - val graph = MusicGraph( - songVertices.values.toList(), - albumVertices.values.toList(), - artistVertices.values.toList(), - genreVertices.values.toList(), - playlistVertices) + val graph = + MusicGraph( + songVertices.values.toList(), + albumVertices.values.toList(), + artistVertices.values.toList(), + genreVertices.values.toList(), + playlistVertices) return graph } diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 3022a555d..ad34d8f82 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -19,7 +19,6 @@ package org.oxycblt.musikr.pipeline import android.content.Context -import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel @@ -120,9 +119,7 @@ private class ExtractStepImpl( metadataExtractor .extract(fileWith.with) ?.let { FileWith(fileWith.file, it) } - .also { - withContext(Dispatchers.IO) { fileWith.with.close() } - } + .also { withContext(Dispatchers.IO) { fileWith.with.close() } } } } .flowOn(Dispatchers.IO) From 998375f28a89c1d714e902bfe0b3d41b4d0235e1 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 18:02:56 -0700 Subject: [PATCH 476/550] home: stop fabs from eating touch events --- app/src/main/java/org/oxycblt/auxio/MainFragment.kt | 4 ++++ app/src/main/res/layout-w720dp/fragment_main.xml | 1 + app/src/main/res/layout/fragment_main.xml | 1 + 3 files changed, 6 insertions(+) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 6ed7ee62a..5da7c3b93 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -27,6 +27,7 @@ import androidx.activity.BackEventCompat import androidx.activity.OnBackPressedCallback import androidx.core.view.ViewCompat import androidx.core.view.isInvisible +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController @@ -367,6 +368,9 @@ class MainFragment : requireNotNull(sheetBackCallback) { "SheetBackPressedCallback was not available" } .invalidateEnabled() + // Stop the FrameLayout containing the fabs from eating touch events elsewhere + binding.mainFabContainer.isVisible = binding.homeNewPlaylistFab.mainFab.isVisible && binding.homeShuffleFab.isVisible + return true } diff --git a/app/src/main/res/layout-w720dp/fragment_main.xml b/app/src/main/res/layout-w720dp/fragment_main.xml index 90feb3b88..2b8a21ad3 100644 --- a/app/src/main/res/layout-w720dp/fragment_main.xml +++ b/app/src/main/res/layout-w720dp/fragment_main.xml @@ -23,6 +23,7 @@ tools:layout="@layout/fragment_home" /> Date: Wed, 8 Jan 2025 18:06:18 -0700 Subject: [PATCH 477/550] app: reformat --- app/src/main/java/org/oxycblt/auxio/MainFragment.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 5da7c3b93..911cd4db2 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -258,9 +258,9 @@ class MainFragment : } override fun onPreDraw(): Boolean { - // TODO: Due to draw caching even *this* isn't effective enough to avoid the bottom - // sheets continually getting stuck. I need something with even more frequent updates, - // or otherwise bottom sheets get stuck. + // This is where I shove literally all the UI logic that won't behave any callback + // or "normal" method I've tried. Surely running this on every frame will actually cause + // it to work properly! // We overload CoordinatorLayout far too much to rely on any of it's typical // listener functionality. Just update all transitions before every draw. Should @@ -369,7 +369,8 @@ class MainFragment : .invalidateEnabled() // Stop the FrameLayout containing the fabs from eating touch events elsewhere - binding.mainFabContainer.isVisible = binding.homeNewPlaylistFab.mainFab.isVisible && binding.homeShuffleFab.isVisible + binding.mainFabContainer.isVisible = + binding.homeNewPlaylistFab.mainFab.isVisible || binding.homeShuffleFab.isVisible return true } From 9b82b5aee0d36e2286ab8ecab437108c70899ceb Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 18:19:37 -0700 Subject: [PATCH 478/550] build: bump to 4.0.0-dev3 --- app/build.gradle | 4 ++-- .../src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 44f809ccc..3f394d053 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId namespace - versionName "4.0.0-dev2" - versionCode 55 + versionName "4.0.0-dev3" + versionCode 56 minSdk min_sdk targetSdk target_sdk diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index 7dca9bcf3..f294c9d6f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -41,7 +41,7 @@ import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.util.correctWhitespace import org.oxycblt.musikr.util.splitEscaped -@Database(entities = [CachedSong::class], version = 55, exportSchema = false) +@Database(entities = [CachedSong::class], version = 56, exportSchema = false) internal abstract class CacheDatabase : RoomDatabase() { abstract fun visibleDao(): VisibleCacheDao From 8bd89c5967cbffd898dceecddad808ccbad71051 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 8 Jan 2025 18:27:08 -0700 Subject: [PATCH 479/550] musikr: ignore genre numbers of 255 --- musikr/src/main/cpp/JVMMetadataBuilder.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/musikr/src/main/cpp/JVMMetadataBuilder.cpp b/musikr/src/main/cpp/JVMMetadataBuilder.cpp index d7254e845..17003c5a3 100644 --- a/musikr/src/main/cpp/JVMMetadataBuilder.cpp +++ b/musikr/src/main/cpp/JVMMetadataBuilder.cpp @@ -40,7 +40,10 @@ void JVMMetadataBuilder::setId3v1(TagLib::ID3v1::Tag &tag) { id3v2.add_id("TALB", tag.album()); id3v2.add_id("TRCK", std::to_string(tag.track())); id3v2.add_id("TYER", std::to_string(tag.year())); - id3v2.add_id("TCON", std::to_string(tag.genreNumber())); + const int genreNumber = tag.genreNumber(); + if (genreNumber != 255) { + id3v2.add_id("TCON", std::to_string(genreNumber)); + } } void JVMMetadataBuilder::setId3v2(TagLib::ID3v2::Tag &tag) { From 29320f426e8c62f3c517c4662285a35e2e640abb Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 9 Jan 2025 12:47:07 -0700 Subject: [PATCH 480/550] playback: dont use off-standard colors for btns Use colorSecondary instead of colorPrimaryFixedDim --- app/src/main/res/values-night/styles_ui.xml | 6 +++++- app/src/main/res/values/styles_ui.xml | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-night/styles_ui.xml b/app/src/main/res/values-night/styles_ui.xml index 3610f09b6..b2f3e631f 100644 --- a/app/src/main/res/values-night/styles_ui.xml +++ b/app/src/main/res/values-night/styles_ui.xml @@ -1,8 +1,12 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 559daf6a3..65b6ca8c4 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -377,7 +377,7 @@ \ No newline at end of file From c3590487218b144b4daa8fc945da5eae4a3c1d12 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 9 Jan 2025 12:54:48 -0700 Subject: [PATCH 481/550] playback: remove unused button theme --- app/src/main/res/values-night/styles_ui.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/res/values-night/styles_ui.xml b/app/src/main/res/values-night/styles_ui.xml index b2f3e631f..83b8900f6 100644 --- a/app/src/main/res/values-night/styles_ui.xml +++ b/app/src/main/res/values-night/styles_ui.xml @@ -4,9 +4,4 @@ ?attr/colorPrimaryContainer ?attr/colorOnPrimaryContainer - - \ No newline at end of file From ae6a0438be5eaa5a336f12ff3c35785c0271354a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 9 Jan 2025 19:29:15 -0700 Subject: [PATCH 482/550] musikr: clarify added/modified timestamp apis Clearly indicate their new millisecond nature. --- .../org/oxycblt/auxio/home/list/SongListFragment.kt | 2 +- app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt | 4 ++-- musikr/src/main/java/org/oxycblt/musikr/Music.kt | 11 +++++++---- .../java/org/oxycblt/musikr/cache/CacheDatabase.kt | 2 +- .../main/java/org/oxycblt/musikr/cache/StoredCache.kt | 2 +- .../src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt | 2 +- .../main/java/org/oxycblt/musikr/model/AlbumImpl.kt | 2 +- .../main/java/org/oxycblt/musikr/model/SongImpl.kt | 4 ++-- .../java/org/oxycblt/musikr/tag/interpret/PreMusic.kt | 4 ++-- .../oxycblt/musikr/tag/interpret/TagInterpreter.kt | 4 ++-- 10 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 4c1d1323b..20d247772 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -128,7 +128,7 @@ class SongListFragment : // Last added -> Format as date is Sort.Mode.ByDateAdded -> { - val dateAddedMillis = song.dateAdded.secsToMs() + val dateAddedMillis = song.addedMs.secsToMs() formatterSb.setLength(0) DateUtils.formatDateRange( context, diff --git a/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt index d6536ca2b..db29012e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt @@ -360,8 +360,8 @@ data class Sort(val mode: Mode, val direction: Direction) { override fun sortSongs(songs: MutableList, direction: Direction) { songs.sortBy { it.name } when (direction) { - Direction.ASCENDING -> songs.sortBy { it.dateAdded } - Direction.DESCENDING -> songs.sortByDescending { it.dateAdded } + Direction.ASCENDING -> songs.sortBy { it.addedMs } + Direction.DESCENDING -> songs.sortByDescending { it.addedMs } } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/Music.kt b/musikr/src/main/java/org/oxycblt/musikr/Music.kt index 011cfc3a6..da0335efe 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Music.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Music.kt @@ -272,10 +272,13 @@ interface Song : Music { val sampleRateHz: Int /** The ReplayGain adjustment to apply during playback. */ val replayGainAdjustment: ReplayGainAdjustment - /** The date last modified the audio file was last modified, as a unix epoch timestamp. */ - val lastModified: Long - /** The date the audio file was added to the device, as a unix epoch timestamp. */ - val dateAdded: Long + /** + * The date last modified the audio file was last modified, in milliseconds since the unix + * epoch. + */ + val modifiedMs: Long + /** The time the audio file was added to the device, in milliseconds since the unix epoch. */ + val addedMs: Long /** Useful information to quickly obtain the album cover. */ val cover: Cover? /** diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index f294c9d6f..98bfc85d2 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -174,7 +174,7 @@ internal data class CachedSong( fun fromRawSong(rawSong: RawSong) = CachedSong( uri = rawSong.file.uri.toString(), - modifiedMs = rawSong.file.lastModified, + modifiedMs = rawSong.file.modifiedMs, addedMs = rawSong.addedMs, // Should be strictly monotonic so we don't prune this // by accident later. diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt index 8e5144f67..4707ffe3f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt @@ -55,7 +55,7 @@ private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeD BaseStoredCache(writeDao) { override suspend fun read(file: DeviceFile, covers: Covers): CacheResult { val song = visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null) - if (song.modifiedMs != file.lastModified) { + if (song.modifiedMs != file.modifiedMs) { // We *found* this file earlier, but it's out of date. // Send back it with the timestamp so it will be re-used. // The touch timestamp will be updated on write. diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt index 98025a75a..6baac772f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt @@ -25,5 +25,5 @@ internal data class DeviceFile( val mimeType: String, val path: Path, val size: Long, - val lastModified: Long + val modifiedMs: Long ) diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt index 53a17152d..79d177a47 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt @@ -55,7 +55,7 @@ class AlbumImpl internal constructor(private val core: AlbumCore) : Album { override val name = preAlbum.name override val releaseType = preAlbum.releaseType override val durationMs = core.songs.sumOf { it.durationMs } - override val dateAdded = core.songs.minOf { it.dateAdded } + override val dateAdded = core.songs.minOf { it.addedMs } override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }) override val dates: Date.Range? = core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) } diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt index 2747edd1d..6a34168c6 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt @@ -55,8 +55,8 @@ internal class SongImpl(private val handle: SongCore) : Song { override val bitrateKbps = preSong.bitrateKbps override val sampleRateHz = preSong.sampleRateHz override val replayGainAdjustment = preSong.replayGainAdjustment - override val lastModified = preSong.lastModified - override val dateAdded = preSong.dateAdded + override val modifiedMs = preSong.modifiedMs + override val addedMs = preSong.addedMs override val cover = preSong.cover override val album: Album get() = handle.resolveAlbum() diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt index 413f351a9..4d8831acf 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt @@ -46,8 +46,8 @@ internal data class PreSong( val bitrateKbps: Int, val sampleRateHz: Int, val replayGainAdjustment: ReplayGainAdjustment, - val lastModified: Long, - val dateAdded: Long, + val modifiedMs: Long, + val addedMs: Long, val cover: Cover?, val preAlbum: PreAlbum, val preArtists: List, diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt index e4eb3ea29..1deb269aa 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt @@ -64,8 +64,8 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T path = song.file.path, size = song.file.size, format = Format.infer(song.file.mimeType, song.properties.mimeType), - lastModified = song.file.lastModified, - dateAdded = song.addedMs, + modifiedMs = song.file.modifiedMs, + addedMs = song.addedMs, musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(), name = interpretation.naming.name(song.tags.name, song.tags.sortName), rawName = song.tags.name, From e6b326a571b9e55c972c69406db22242566f8c8f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 9 Jan 2025 19:31:32 -0700 Subject: [PATCH 483/550] musikr: clarify album added timestamp api Same reasons, should be milliseconds --- .../java/org/oxycblt/auxio/home/list/AlbumListFragment.kt | 3 +-- app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt | 4 ++-- musikr/src/main/java/org/oxycblt/musikr/Music.kt | 4 ++-- musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 848a685c0..23a031c21 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -43,7 +43,6 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs -import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.musikr.Album import org.oxycblt.musikr.Music @@ -128,7 +127,7 @@ class AlbumListFragment : // Last added -> Format as date is Sort.Mode.ByDateAdded -> { - val dateAddedMillis = album.dateAdded.secsToMs() + val dateAddedMillis = album.addedMs formatterSb.setLength(0) DateUtils.formatDateRange( context, diff --git a/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt index db29012e0..64b033218 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt @@ -368,8 +368,8 @@ data class Sort(val mode: Mode, val direction: Direction) { override fun sortAlbums(albums: MutableList, direction: Direction) { albums.sortBy { it.name } when (direction) { - Direction.ASCENDING -> albums.sortBy { it.dateAdded } - Direction.DESCENDING -> albums.sortByDescending { it.dateAdded } + Direction.ASCENDING -> albums.sortBy { it.addedMs } + Direction.DESCENDING -> albums.sortByDescending { it.addedMs } } } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/Music.kt b/musikr/src/main/java/org/oxycblt/musikr/Music.kt index da0335efe..5ae85f2d2 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Music.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Music.kt @@ -317,8 +317,8 @@ interface Album : MusicParent { val covers: CoverCollection /** The duration of all songs in the album, in milliseconds. */ val durationMs: Long - /** The earliest date a song in this album was added, as a unix epoch timestamp. */ - val dateAdded: Long + /** The earliest date a song in this album was added, in milliseconds since the unix epoch. */ + val addedMs: Long /** * The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more than * one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song], album artists diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt index 79d177a47..99817b6eb 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt @@ -55,7 +55,7 @@ class AlbumImpl internal constructor(private val core: AlbumCore) : Album { override val name = preAlbum.name override val releaseType = preAlbum.releaseType override val durationMs = core.songs.sumOf { it.durationMs } - override val dateAdded = core.songs.minOf { it.addedMs } + override val addedMs = core.songs.minOf { it.addedMs } override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }) override val dates: Date.Range? = core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) } From 1132e486cadfc3619de031c088f438b331d790a0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 9 Jan 2025 19:32:52 -0700 Subject: [PATCH 484/550] home: do not convert addedms to to secs --- .../main/java/org/oxycblt/auxio/home/list/SongListFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 20d247772..899617e6a 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -128,7 +128,7 @@ class SongListFragment : // Last added -> Format as date is Sort.Mode.ByDateAdded -> { - val dateAddedMillis = song.addedMs.secsToMs() + val dateAddedMillis = song.addedMs formatterSb.setLength(0) DateUtils.formatDateRange( context, From 85a2952ae1973c8a735131bfcd38a7a72c0c9ff4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 10:00:03 -0700 Subject: [PATCH 485/550] main: fix fab shadow By reverting the previous changes to stop touch events from being eaten? Not sure why this works. --- app/src/main/res/layout-w720dp/fragment_main.xml | 7 ++++--- app/src/main/res/layout/fragment_main.xml | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout-w720dp/fragment_main.xml b/app/src/main/res/layout-w720dp/fragment_main.xml index 2b8a21ad3..90a55472a 100644 --- a/app/src/main/res/layout-w720dp/fragment_main.xml +++ b/app/src/main/res/layout-w720dp/fragment_main.xml @@ -22,6 +22,7 @@ app:navGraph="@navigation/inner" tools:layout="@layout/fragment_home" /> + @@ -49,9 +50,9 @@ android:id="@+id/home_shuffle_fab" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="bottom|end" android:layout_margin="@dimen/spacing_medium" - android:contentDescription="@string/desc_shuffle_all" + android:layout_gravity="bottom|end" + android:contentDescription="@string/lbl_shuffle" android:src="@drawable/ic_shuffle_off_24" /> diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index e6458b2bd..ad115ed4c 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -34,7 +34,6 @@ android:layout_height="match_parent" android:layout_gravity="bottom|end" android:clipChildren="false" - android:layout_margin="@dimen/spacing_medium" android:clipToPadding="false" app:layout_anchor="@id/home_content"> @@ -47,6 +46,7 @@ android:contentDescription="@string/lbl_new_playlist" android:focusable="true" android:gravity="bottom|end" + android:layout_margin="@dimen/spacing_medium" app:sdMainFabAnimationRotateAngle="135" app:sdMainFabClosedIconColor="@android:color/white" app:sdMainFabClosedSrc="@drawable/ic_add_24" /> @@ -55,6 +55,7 @@ android:id="@+id/home_shuffle_fab" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_medium" android:layout_gravity="bottom|end" android:contentDescription="@string/lbl_shuffle" android:src="@drawable/ic_shuffle_off_24" /> From 698f0bc13c807d1e18051fc4ac4f14ec382ff4e5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 10:10:20 -0700 Subject: [PATCH 486/550] detail: fix bouncing when navigating to song --- .../java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index c3ffb50a0..48785f276 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -22,6 +22,7 @@ import android.os.Bundle import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding @@ -298,6 +299,11 @@ class AlbumDetailFragment : DetailFragment() { // RecyclerView will scroll assuming it has the total height of the screen (i.e a // collapsed appbar), so we need to collapse the appbar if that's the case. binding.detailAppbar.setExpanded(false) + if (!binding.detailRecycler.canScroll()) { + // Don't scroll if the RecyclerView goes off screen. If we go anyway, overscroll + // kicks in and creates a weird bounce effect. + return + } binding.detailRecycler.post { // Use a custom smooth scroller that will settle the item in the middle of // the screen rather than the end. @@ -323,4 +329,6 @@ class AlbumDetailFragment : DetailFragment() { } } } + + private fun RecyclerView.canScroll() = computeVerticalScrollRange() > height } From 04e871f42151803c804f639d52f5ad7c7a72445c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 10:18:36 -0700 Subject: [PATCH 487/550] all: reformat --- .../main/java/org/oxycblt/auxio/home/list/SongListFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 899617e6a..08700b6a8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -42,7 +42,6 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs -import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.musikr.Music import org.oxycblt.musikr.MusicParent From 2f43113ce2111efa93b6755b16302b1a54974889 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 16:24:39 -0700 Subject: [PATCH 488/550] ui: make brown/grey themes distinct These would otherwise be red and blue unless I enable color match. --- app/src/main/res/values-night/colors_ui.xml | 434 ++++++++++---------- app/src/main/res/values/colors_ui.xml | 354 ++++++++-------- 2 files changed, 394 insertions(+), 394 deletions(-) diff --git a/app/src/main/res/values-night/colors_ui.xml b/app/src/main/res/values-night/colors_ui.xml index 5b6d8d22a..87bd21bc7 100644 --- a/app/src/main/res/values-night/colors_ui.xml +++ b/app/src/main/res/values-night/colors_ui.xml @@ -1988,287 +1988,287 @@ #312822 #3C332C - #FFB59B - #55200B - #72351F - #FFDBCF - #E7BDB0 - #442A21 - #5D4036 - #FFDBCF - #D5C68E - #393005 - #51461A - #F2E2A7 + #E7BDB0 + #442A21 + #6D4E43 + #FFF7F5 + #D8C2BB + #3B2D29 + #483A35 + #E2CCC5 + #CFC7A1 + #353116 + #5B5537 + #FFF8E3 #FFB4AB #690005 #93000A #FFDAD6 - #1A110F - #F1DFD9 - #1A110F - #F1DFD9 - #53433E - #D8C2BB - #A08D86 - #53433E + #161312 + #E9E1DF + #161312 + #E9E1DF + #504440 + #D4C3BE + #9D8E89 + #504440 #000000 - #F1DFD9 - #392E2B - #8F4C34 + #E9E1DF + #33302E + #77574C #FFDBCF - #380D00 - #FFB59B - #72351F - #FFDBCF - #2C160D - #E7BDB0 - #5D4036 - #F2E2A7 - #221B00 - #D5C68E - #51461A - #1A110F - #423733 - #140C0A - #231A16 - #271D1A - #322824 - #3D322F - #FFBBA3 - #2F0900 - #CB7D61 + #2C160D + #E7BDB0 + #5D4036 + #F5DED6 + #251915 + #D8C2BB + #53433E + #ECE3BB + #201C04 + #CFC7A1 + #4C472A + #161312 + #3C3837 + #100E0D + #1E1B1A + #221F1E + #2D2928 + #383433 + #EBC2B4 + #261009 + #AD887C #000000 - #EBC2B4 - #261009 - #AD887C + #DCC6BF + #1F1410 + #A08D86 #000000 - #DACB91 - #1C1600 - #9E905D + #D4CBA5 + #1A1602 + #98916E #000000 #FFBAB1 #370001 #FF5449 #000000 - #1A110F - #F1DFD9 - #1A110F + #161312 + #E9E1DF + #161312 #FFF9F8 - #53433E - #DCC6BF - #B39F98 - #927F79 + #504440 + #D8C7C2 + #AFA09B + #8E807C #000000 - #F1DFD9 - #322824 - #733720 + #E9E1DF + #2D2928 + #5E4137 #FFDBCF - #270600 - #FFB59B - #5D2510 - #FFDBCF - #200B05 - #E7BDB0 - #4A3026 - #F2E2A7 - #161100 - #D5C68E - #3F360A - #1A110F - #423733 - #140C0A - #231A16 - #271D1A - #322824 - #3D322F + #200B05 + #E7BDB0 + #4A3026 + #F5DED6 + #190F0B + #D8C2BB + #41332E + #ECE3BB + #151100 + #CFC7A1 + #3B361B + #161312 + #3C3837 + #100E0D + #1E1B1A + #221F1E + #2D2928 + #383433 #FFF9F8 #000000 - #FFBBA3 + #EBC2B4 #000000 #FFF9F8 #000000 - #EBC2B4 + #DCC6BF #000000 - #FFFAF5 + #FFFAF4 #000000 - #DACB91 + #D4CBA5 #000000 #FFF9F9 #000000 #FFBAB1 #000000 - #1A110F - #F1DFD9 - #1A110F + #161312 + #E9E1DF + #161312 #FFFFFF - #53433E + #504440 #FFF9F8 - #DCC6BF - #DCC6BF + #D8C7C2 + #D8C7C2 #000000 - #F1DFD9 + #E9E1DF #000000 - #4D1A05 + #3D241B #FFE0D7 #000000 - #FFBBA3 - #2F0900 - #FFE0D7 + #EBC2B4 + #261009 + #F9E2DB #000000 - #EBC2B4 - #261009 - #F7E7AB + #DCC6BF + #1F1410 + #F1E7C0 #000000 - #DACB91 - #1C1600 - #1A110F - #423733 - #140C0A - #231A16 - #271D1A - #322824 - #3D322F + #D4CBA5 + #1A1602 + #161312 + #3C3837 + #100E0D + #1E1B1A + #221F1E + #2D2928 + #383433 - #82D3E0 - #00363D - #004F58 - #9EEFFD - #B1CBD0 - #1C3438 - #334B4F - #CDE7EC - #BAC6EA - #24304D - #3B4664 - #DAE2FF + #C7C6C6 + #303031 + #6C6C6C + #FFFFFF + #C8C6C5 + #303030 + #3E3D3D + #D3D0D0 + #CBC5C7 + #323031 + #6F6B6D + #FFFFFF #FFB4AB #690005 #93000A #FFDAD6 - #0E1415 - #DEE3E5 - #0E1415 - #DEE3E5 - #3F484A - #BFC8CA - #899294 - #3F484A + #141313 + #E5E2E1 + #141313 + #E5E2E1 + #444748 + #C4C7C7 + #8E9192 + #444748 #000000 - #DEE3E5 - #2B3133 - #006874 - #9EEFFD - #001F24 - #82D3E0 - #004F58 - #CDE7EC - #051F23 - #B1CBD0 - #334B4F - #DAE2FF - #0E1B37 - #BAC6EA - #3B4664 - #0E1415 - #343A3B - #090F10 - #171D1E - #1B2122 - #252B2C - #303637 - #86D7E5 - #001A1D - #499CA9 + #E5E2E1 + #313030 + #5E5E5E + #E3E2E2 + #1B1C1C + #C7C6C6 + #464747 + #E5E2E1 + #1B1C1C + #C8C6C5 + #474746 + #E7E1E3 + #1D1B1D + #CBC5C7 + #494648 + #141313 + #3A3939 + #0E0E0E + #1C1B1B + #201F1F + #2A2A2A + #353434 + #CBCACA + #151617 + #919090 #000000 - #B5CFD4 - #011A1D - #7C959A + #CCCACA + #161616 + #929090 #000000 - #BECAEF - #081531 - #8490B2 + #CFC9CB + #181617 + #948F91 #000000 #FFBAB1 #370001 #FF5449 #000000 - #0E1415 - #DEE3E5 - #0E1415 - #F6FCFD - #3F484A - #C3CCCE - #9BA5A6 - #7B8587 + #141313 + #E5E2E1 + #141313 + #FEFAF9 + #444748 + #C8CBCC + #A0A3A4 + #808484 #000000 - #DEE3E5 - #252B2C - #005059 - #9EEFFD - #001417 - #82D3E0 - #003C44 - #CDE7EC - #001417 - #B1CBD0 - #223A3E - #DAE2FF - #03102C - #BAC6EA - #2A3553 - #0E1415 - #343A3B - #090F10 - #171D1E - #1B2122 - #252B2C - #303637 - #F1FDFF + #E5E2E1 + #2B2A2A + #474848 + #E3E2E2 + #101111 + #C7C6C6 + #353636 + #E5E2E1 + #111111 + #C8C6C5 + #363636 + #E7E1E3 + #121112 + #CBC5C7 + #383537 + #141313 + #3A3939 + #0E0E0E + #1C1B1B + #201F1F + #2A2A2A + #353434 + #FCFAFA #000000 - #86D7E5 + #CBCACA #000000 - #F1FDFF + #FDFAF9 #000000 - #B5CFD4 + #CCCACA #000000 - #FCFAFF + #FFF9FA #000000 - #BECAEF + #CFC9CB #000000 #FFF9F9 #000000 #FFBAB1 #000000 - #0E1415 - #DEE3E5 - #0E1415 + #141313 + #E5E2E1 + #141313 #FFFFFF - #3F484A - #F3FCFE - #C3CCCE - #C3CCCE + #444748 + #F9FBFC + #C8CBCC + #C8CBCC #000000 - #DEE3E5 + #E5E2E1 #000000 - #002F35 - #AAF3FF + #292A2A + #E8E6E6 #000000 - #86D7E5 - #001A1D - #D1ECF0 + #CBCACA + #151617 + #E9E6E6 #000000 - #B5CFD4 - #011A1D - #E0E6FF + #CCCACA + #161616 + #EBE5E7 #000000 - #BECAEF - #081531 - #0E1415 - #343A3B - #090F10 - #171D1E - #1B2122 - #252B2C - #303637 + #CFC9CB + #181617 + #141313 + #3A3939 + #0E0E0E + #1C1B1B + #201F1F + #2A2A2A + #353434 \ No newline at end of file diff --git a/app/src/main/res/values/colors_ui.xml b/app/src/main/res/values/colors_ui.xml index 605149368..984e35604 100644 --- a/app/src/main/res/values/colors_ui.xml +++ b/app/src/main/res/values/colors_ui.xml @@ -1988,287 +1988,287 @@ #F5E5DB #EFE0D6 - #8F4C34 + #604238 #FFFFFF - #FFDBCF - #380D00 - #77574C + #87665A + #FFFFFF + #6B5B55 #FFFFFF - #FFDBCF - #2C160D - #695E2F + #F9E2DB + #574742 + #4F4A2D #FFFFFF - #F2E2A7 - #221B00 + #746E4E + #FFFFFF #BA1A1A #FFFFFF #FFDAD6 #410002 #FFF8F6 - #231A16 + #1E1B1A #FFF8F6 - #231A16 - #F5DED7 - #53433E - #85736D - #D8C2BB + #1E1B1A + #F1DFD9 + #504440 + #827470 + #D4C3BE #000000 - #392E2B - #FFEDE8 - #FFB59B + #33302E + #F7EFED + #E7BDB0 #FFDBCF - #380D00 - #FFB59B - #72351F - #FFDBCF - #2C160D - #E7BDB0 - #5D4036 - #F2E2A7 - #221B00 - #D5C68E - #51461A - #E8D6D1 + #2C160D + #E7BDB0 + #5D4036 + #F5DED6 + #251915 + #D8C2BB + #53433E + #ECE3BB + #201C04 + #CFC7A1 + #4C472A + #E0D8D6 #FFF8F6 #FFFFFF - #FFF1EC - #FCEAE5 - #F7E4DF - #F1DFD9 - #6D321B + #FAF2F0 + #F4ECEA + #EFE6E4 + #E9E1DF + #593C32 #FFFFFF - #A96247 + #87665A #FFFFFF - #593C32 + #4F3F3B #FFFFFF - #8F6D61 + #83716B #FFFFFF - #4C4216 + #484327 #FFFFFF - #807443 + #746E4E #FFFFFF #8C0009 #FFFFFF #DA342E #FFFFFF #FFF8F6 - #231A16 + #1E1B1A #FFF8F6 - #231A16 - #F5DED7 - #4F403B - #6C5B56 - #897771 + #1E1B1A + #F1DFD9 + #4C403D + #695C58 + #867873 #000000 - #392E2B - #FFEDE8 - #FFB59B - #A96247 + #33302E + #F7EFED + #E7BDB0 + #8F6D61 #FFFFFF - #8C4A31 + #74554A #FFFFFF - #8F6D61 + #83716B #FFFFFF - #74554A + #695853 #FFFFFF - #807443 + #7B7554 #FFFFFF - #675C2D + #625C3E #FFFFFF - #E8D6D1 + #E0D8D6 #FFF8F6 #FFFFFF - #FFF1EC - #FCEAE5 - #F7E4DF - #F1DFD9 - #421201 + #FAF2F0 + #F4ECEA + #EFE6E4 + #E9E1DF + #341C14 #FFFFFF - #6D321B + #593C32 #FFFFFF - #341C14 + #2C1F1B #FFFFFF - #593C32 + #4F3F3B #FFFFFF - #292200 + #272209 #FFFFFF - #4C4216 + #484327 #FFFFFF #4E0002 #FFFFFF #8C0009 #FFFFFF #FFF8F6 - #231A16 + #1E1B1A #FFF8F6 #000000 - #F5DED7 - #2E211D - #4F403B - #4F403B + #F1DFD9 + #2B221F + #4C403D + #4C403D #000000 - #392E2B + #33302E #FFFFFF #FFE7E0 - #6D321B + #593C32 #FFFFFF - #501C07 + #40261D #FFFFFF - #593C32 + #4F3F3B #FFFFFF - #40261D + #372A25 #FFFFFF - #4C4216 + #484327 #FFFFFF - #352C03 + #312D12 #FFFFFF - #E8D6D1 + #E0D8D6 #FFF8F6 #FFFFFF - #FFF1EC - #FCEAE5 - #F7E4DF - #F1DFD9 + #FAF2F0 + #F4ECEA + #EFE6E4 + #E9E1DF - #006874 + #505151 #FFFFFF - #9EEFFD - #001F24 - #4A6267 + #757575 + #FFFFFF + #5F5E5E #FFFFFF - #CDE7EC - #051F23 - #525E7D + #E6E3E2 + #494848 + #535052 #FFFFFF - #DAE2FF - #0E1B37 + #787476 + #FFFFFF #BA1A1A #FFFFFF #FFDAD6 #410002 - #F5FAFB - #171D1E - #F5FAFB - #171D1E - #DBE4E6 - #3F484A - #6F797A - #BFC8CA + #FCF8F8 + #1C1B1B + #FCF8F8 + #1C1B1B + #E0E3E3 + #444748 + #747878 + #C4C7C7 #000000 - #2B3133 - #ECF2F3 - #82D3E0 - #9EEFFD - #001F24 - #82D3E0 - #004F58 - #CDE7EC - #051F23 - #B1CBD0 - #334B4F - #DAE2FF - #0E1B37 - #BAC6EA - #3B4664 - #D5DBDC - #F5FAFB + #313030 + #F4F0EF + #C7C6C6 + #E3E2E2 + #1B1C1C + #C7C6C6 + #464747 + #E5E2E1 + #1B1C1C + #C8C6C5 + #474746 + #E7E1E3 + #1D1B1D + #CBC5C7 + #494648 + #DDD9D8 + #FCF8F8 #FFFFFF - #EFF5F6 - #E9EFF0 - #E3E9EA - #DEE3E5 - #004A53 + #F7F3F2 + #F1EDEC + #EBE7E7 + #E5E2E1 + #424343 #FFFFFF - #25808C + #757575 #FFFFFF - #2F474B + #434343 #FFFFFF - #60797D + #757474 #FFFFFF - #374260 + #454244 #FFFFFF - #687495 + #787476 #FFFFFF #8C0009 #FFFFFF #DA342E #FFFFFF - #F5FAFB - #171D1E - #F5FAFB - #171D1E - #DBE4E6 - #3B4446 - #576162 - #737C7E + #FCF8F8 + #1C1B1B + #FCF8F8 + #1C1B1B + #E0E3E3 + #404344 + #5C6060 + #787B7C #000000 - #2B3133 - #ECF2F3 - #82D3E0 - #25808C + #313030 + #F4F0EF + #C7C6C6 + #747474 #FFFFFF - #006671 + #5C5C5C #FFFFFF - #60797D + #757474 #FFFFFF - #486064 + #5C5C5C #FFFFFF - #687495 + #777375 #FFFFFF - #505B7B + #5E5B5D #FFFFFF - #D5DBDC - #F5FAFB + #DDD9D8 + #FCF8F8 #FFFFFF - #EFF5F6 - #E9EFF0 - #E3E9EA - #DEE3E5 - #00272C + #F7F3F2 + #F1EDEC + #EBE7E7 + #E5E2E1 + #212222 #FFFFFF - #004A53 + #424343 #FFFFFF - #0C2629 + #222222 #FFFFFF - #2F474B + #434343 #FFFFFF - #15213E + #242223 #FFFFFF - #374260 + #454244 #FFFFFF #4E0002 #FFFFFF #8C0009 #FFFFFF - #F5FAFB - #171D1E - #F5FAFB + #FCF8F8 + #1C1B1B + #FCF8F8 #000000 - #DBE4E6 - #1C2527 - #3B4446 - #3B4446 + #E0E3E3 + #212525 + #404344 + #404344 #000000 - #2B3133 + #313030 #FFFFFF - #BFF5FF - #004A53 + #EDECEB + #424343 #FFFFFF - #003238 + #2C2D2D #FFFFFF - #2F474B + #434343 #FFFFFF - #183034 + #2D2D2D #FFFFFF - #374260 + #454244 #FFFFFF - #202C49 + #2E2C2E #FFFFFF - #D5DBDC - #F5FAFB + #DDD9D8 + #FCF8F8 #FFFFFF - #EFF5F6 - #E9EFF0 - #E3E9EA - #DEE3E5 + #F7F3F2 + #F1EDEC + #EBE7E7 + #E5E2E1 \ No newline at end of file From cc6c5084ff1b8bed52201623d2cf332e6f7c3612 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 19:15:18 -0700 Subject: [PATCH 489/550] playback: reduce more skipping on tight reloads --- .../auxio/playback/service/ExoPlaybackStateHolder.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 3b5944bd2..e3665dfce 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -59,6 +59,7 @@ import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.playback.state.StateAck import org.oxycblt.musikr.MusicParent import org.oxycblt.musikr.Song +import kotlin.math.abs import timber.log.Timber as L class ExoPlaybackStateHolder( @@ -372,7 +373,6 @@ class ExoPlaybackStateHolder( ) { var sendNewPlaybackEvent = false var shouldSeek = false - L.d("invalidating parent ${this.parent?.songs} ${parent?.songs}") if (this.parent != parent) { this.parent = parent sendNewPlaybackEvent = true @@ -393,9 +393,12 @@ class ExoPlaybackStateHolder( } repeatMode(repeatMode) - // Positions in milliseconds will drift during tight restores (i.e what the music loader - // does to sanitize the state), compare by seconds instead. - if (positionMs.msToSecs() != player.currentPosition.msToSecs() || shouldSeek) { + // See if we differ by more than a second. This allows us to avoid a meaningless seek + // in the case of a "tight restore" (i.e music was reloaded). + // In the case that this is a false positive, it's not very percievable (at least compared + // to skipping when updating the library). + // TODO: Introduce a better state management system rather than do something finicky like this. + if (shouldSeek || abs(player.currentPosition - positionMs) > 1000L) { player.seekTo(positionMs) } From 08e09af5b3bd317bfd6358b9d5c34abc82ef42f0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 19:16:59 -0700 Subject: [PATCH 490/550] all: reformat --- .../auxio/playback/service/ExoPlaybackStateHolder.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index e3665dfce..c685a5065 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -35,6 +35,7 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecSelector import androidx.media3.exoplayer.source.MediaSource import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject +import kotlin.math.abs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -45,7 +46,6 @@ import kotlinx.coroutines.yield import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.playback.PlaybackSettings -import org.oxycblt.auxio.playback.msToSecs import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.state.DeferredPlayback @@ -59,7 +59,6 @@ import org.oxycblt.auxio.playback.state.ShuffleMode import org.oxycblt.auxio.playback.state.StateAck import org.oxycblt.musikr.MusicParent import org.oxycblt.musikr.Song -import kotlin.math.abs import timber.log.Timber as L class ExoPlaybackStateHolder( @@ -397,7 +396,8 @@ class ExoPlaybackStateHolder( // in the case of a "tight restore" (i.e music was reloaded). // In the case that this is a false positive, it's not very percievable (at least compared // to skipping when updating the library). - // TODO: Introduce a better state management system rather than do something finicky like this. + // TODO: Introduce a better state management system rather than do something finicky like + // this. if (shouldSeek || abs(player.currentPosition - positionMs) > 1000L) { player.seekTo(positionMs) } From ad4b9a3859450a7128fe57551c39998ba44d88f5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 19:51:19 -0700 Subject: [PATCH 491/550] playback: re-add file playback --- .../service/ExoPlaybackStateHolder.kt | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index c685a5065..c76239d7f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -21,6 +21,7 @@ package org.oxycblt.auxio.playback.service import android.content.Context import android.content.Intent import android.media.audiofx.AudioEffect +import android.provider.OpenableColumns import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem @@ -181,13 +182,34 @@ class ExoPlaybackStateHolder( // Open -> Try to find the Song for the given file and then play it from all songs is DeferredPlayback.Open -> { L.d("Opening specified file") - // library.findSongForUri(context, action.uri)?.let { song -> - // playbackManager.play( - // requireNotNull(commandFactory.song(song, - // ShuffleMode.IMPLICIT)) { - // "Invalid playback parameters" - // }) - // } + context.applicationContext.contentResolver + .query( + action.uri, + arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), + null, + null, + null) + ?.use { cursor -> + val displayNameIndex = + cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) + val sizeIndex = cursor.getColumnIndexOrThrow(OpenableColumns.SIZE) + if (cursor.moveToFirst()) { + val displayName = cursor.getString(displayNameIndex) + val size = cursor.getLong(sizeIndex) + val song = + library.songs.find { + it.path.name == displayName && it.size == size + } + if (song != null) { + val command = + requireNotNull( + commandFactory.songFromAll(song, ShuffleMode.IMPLICIT)) { + "Invalid playback command" + } + playbackManager.play(command) + } + } + } } } From a1289ffacafa493f09a2f5c96349a127a5f9c904 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 20:01:01 -0700 Subject: [PATCH 492/550] service: attempt to band-aid foreground limit --- .../java/org/oxycblt/auxio/AuxioService.kt | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index 256e62e79..9a435c822 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -34,8 +34,15 @@ import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.oxycblt.auxio.music.service.MusicServiceFragment import org.oxycblt.auxio.playback.service.PlaybackServiceFragment +import timber.log.Timber as L @AndroidEntryPoint class AuxioService : @@ -46,6 +53,10 @@ class AuxioService : @Inject lateinit var musicFragmentFactory: MusicServiceFragment.Factory private lateinit var musicFragment: MusicServiceFragment + private val delayScopeJob = Job() + Dispatchers.Main + private val delayScope = CoroutineScope(delayScopeJob) + private var currentDelayJob: Job? = null + @SuppressLint("WrongConstant") override fun onCreate() { super.onCreate() @@ -68,9 +79,17 @@ class AuxioService : } private fun onHandleForeground(intent: Intent?) { - val startId = intent?.getIntExtra(INTENT_KEY_START_ID, -1) ?: -1 - musicFragment.start() - playbackFragment.start(startId) + currentDelayJob?.cancel() + currentDelayJob = + delayScope.launch { + // The foreground limiter is fussy and doesn't like us starting a foreground + // service too early despite having the right to do so at this point. Comply + // and artificially delay (to user detriment...) + delay(250) + val startId = intent?.getIntExtra(INTENT_KEY_START_ID, -1) ?: -1 + musicFragment.start() + playbackFragment.start(startId) + } } override fun onTaskRemoved(rootIntent: Intent?) { @@ -80,6 +99,7 @@ class AuxioService : override fun onDestroy() { super.onDestroy() + delayScopeJob.cancel() musicFragment.release() playbackFragment.release() } From 10aaf0afd29946748375e20b10fab0a2d24a9035 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 11 Jan 2025 20:02:00 -0700 Subject: [PATCH 493/550] all: reformat --- app/src/main/java/org/oxycblt/auxio/AuxioService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index 9a435c822..b01ab6338 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -42,7 +42,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.oxycblt.auxio.music.service.MusicServiceFragment import org.oxycblt.auxio.playback.service.PlaybackServiceFragment -import timber.log.Timber as L @AndroidEntryPoint class AuxioService : From f134d3e11b0370c1884d6631c4b2e70531a9f7a0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 13 Jan 2025 11:49:14 -0700 Subject: [PATCH 494/550] app: remove custom edge to edge setup I think this conflicts with the weird default behavior of Android 15. --- .../main/java/org/oxycblt/auxio/MainActivity.kt | 14 ++------------ app/src/main/res/values-night-v27/styles_core.xml | 10 ---------- app/src/main/res/values-night/colors_android.xml | 6 ------ app/src/main/res/values-v27/styles_core.xml | 10 ---------- app/src/main/res/values-v29/styles_core.xml | 15 --------------- app/src/main/res/values-v31/styles_core.xml | 2 +- app/src/main/res/values/colors_android.xml | 9 --------- app/src/main/res/values/styles_core.xml | 13 +------------ 8 files changed, 4 insertions(+), 75 deletions(-) delete mode 100644 app/src/main/res/values-night-v27/styles_core.xml delete mode 100644 app/src/main/res/values-v27/styles_core.xml delete mode 100644 app/src/main/res/values-v29/styles_core.xml diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 9430e6fd6..6a7ccce9d 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -21,6 +21,7 @@ package org.oxycblt.auxio import android.content.Intent import android.os.Bundle import android.view.View +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate @@ -56,12 +57,12 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var uiSettings: UISettings override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setupTheme() // Inflate the views after setting up the theme so that the theme attributes are applied. val binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - setupEdgeToEdge(binding.root) L.d("Activity created") } @@ -98,17 +99,6 @@ class MainActivity : AppCompatActivity() { } } - private fun setupEdgeToEdge(contentView: View) { - WindowCompat.setDecorFitsSystemWindows(window, false) - contentView.setOnApplyWindowInsetsListener { view, insets -> - // Automatically inset the view to the left/right, as component support for - // these insets are highly lacking. - val bars = insets.systemBarInsetsCompat - view.updatePadding(left = bars.left, right = bars.right) - insets - } - } - /** * Transform an [Intent] given to [MainActivity] into a [DeferredPlayback] that can be used in * the playback system. diff --git a/app/src/main/res/values-night-v27/styles_core.xml b/app/src/main/res/values-night-v27/styles_core.xml deleted file mode 100644 index 41e8408da..000000000 --- a/app/src/main/res/values-night-v27/styles_core.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/colors_android.xml b/app/src/main/res/values-night/colors_android.xml index 36d82280f..fb3023ee7 100644 --- a/app/src/main/res/values-night/colors_android.xml +++ b/app/src/main/res/values-night/colors_android.xml @@ -1,11 +1,5 @@ - - #01000000 - @color/material_dynamic_secondary20 @color/material_dynamic_neutral90 diff --git a/app/src/main/res/values-v27/styles_core.xml b/app/src/main/res/values-v27/styles_core.xml deleted file mode 100644 index 562529e99..000000000 --- a/app/src/main/res/values-v27/styles_core.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles_core.xml b/app/src/main/res/values-v29/styles_core.xml deleted file mode 100644 index 4fd811f41..000000000 --- a/app/src/main/res/values-v29/styles_core.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v31/styles_core.xml b/app/src/main/res/values-v31/styles_core.xml index d538418f2..b27d7450c 100644 --- a/app/src/main/res/values-v31/styles_core.xml +++ b/app/src/main/res/values-v31/styles_core.xml @@ -1,7 +1,7 @@ - diff --git a/app/src/main/res/values/colors_android.xml b/app/src/main/res/values/colors_android.xml index f107ae056..f94ea37f9 100644 --- a/app/src/main/res/values/colors_android.xml +++ b/app/src/main/res/values/colors_android.xml @@ -1,14 +1,5 @@ - - #80000000 - - - #01ffffff - @color/material_dynamic_primary95 @color/material_dynamic_neutral20 diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index 6cc3b58da..9164374c3 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -2,23 +2,12 @@ - - + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors_android.xml b/app/src/main/res/values-night/colors_android.xml index fb3023ee7..36d82280f 100644 --- a/app/src/main/res/values-night/colors_android.xml +++ b/app/src/main/res/values-night/colors_android.xml @@ -1,5 +1,11 @@ + + #01000000 + @color/material_dynamic_secondary20 @color/material_dynamic_neutral90 diff --git a/app/src/main/res/values-v27/styles_core.xml b/app/src/main/res/values-v27/styles_core.xml new file mode 100644 index 000000000..562529e99 --- /dev/null +++ b/app/src/main/res/values-v27/styles_core.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles_core.xml b/app/src/main/res/values-v29/styles_core.xml new file mode 100644 index 000000000..4fd811f41 --- /dev/null +++ b/app/src/main/res/values-v29/styles_core.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v31/styles_core.xml b/app/src/main/res/values-v31/styles_core.xml index b27d7450c..d538418f2 100644 --- a/app/src/main/res/values-v31/styles_core.xml +++ b/app/src/main/res/values-v31/styles_core.xml @@ -1,7 +1,7 @@ - diff --git a/app/src/main/res/values/colors_android.xml b/app/src/main/res/values/colors_android.xml index f94ea37f9..f107ae056 100644 --- a/app/src/main/res/values/colors_android.xml +++ b/app/src/main/res/values/colors_android.xml @@ -1,5 +1,14 @@ + + #80000000 + + + #01ffffff + @color/material_dynamic_primary95 @color/material_dynamic_neutral20 diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index 9164374c3..6cc3b58da 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -2,12 +2,23 @@ + +