diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index f106a3aac..f0aff366e 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -23,8 +23,8 @@ jobs:
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- - name: Test app with Gradle
- run: ./gradlew app:testDebug
+ # - name: Test app with Gradle
+ # run: ./gradlew app:testDebug
- name: Build debug APK with Gradle
run: ./gradlew app:packageDebug
- name: Upload debug APK artifact
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63626d664..0c2556181 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,32 @@
# Changelog
+## 3.1.1
+
+#### What's New
+- Added ability to share a track
+
+#### What's Improved
+- Tracks with no disc number now default to "No Disc" instead of "Disc 1"
+- Albums implicitly linked only via "artist" tags are now placed in a special
+"appears on" section in the artist view
+- Album covers that are not 1:1 aspect ratio are no longer cropped
+- Optimized library creation phase of the music loading process
+
+#### What's Fixed
+- Prevented options such as "Add to queue" from being selected on empty artists and playlists
+- Fixed issue where an item would be indicated as "playing" after playback ended
+- Items should no longer be indicated as playing if the currently playing song is not contained
+within it
+- Fixed blurry playing indicator in album/artist/genre/playlist items
+- Fixed incorrect songs being displayed when adding albums to the end of the queue
+- Fixed freezing occuring when scrolling through large music libraries
+- Fixed app not responding once music loading completes for large libraries
+- Fixed crash when the last song of the queue gets removed while playing
+- Fixed playback UI and notification not re-appearing after playback ends
+
+#### What's Changed
+- Android Lollipop and Marshmallow support have been dropped
+
## 3.1.0
#### What's New
diff --git a/README.md b/README.md
index fb8ede52d..f5e5ebabe 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
Auxio
A simple, rational music player for android.
-
-
+
+
@@ -11,7 +11,7 @@
-
+
diff --git a/app/build.gradle b/app/build.gradle
index 4f968216b..cc1075aad 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -20,10 +20,10 @@ android {
defaultConfig {
applicationId namespace
- versionName "3.1.0"
- versionCode 30
+ versionName "3.1.1"
+ versionCode 31
- minSdk 21
+ minSdk 24
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -86,13 +86,13 @@ dependencies {
// General
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.core:core-ktx:1.10.1"
- implementation "androidx.activity:activity-ktx:1.7.1"
+ implementation "androidx.activity:activity-ktx:1.7.2"
implementation "androidx.fragment:fragment-ktx:1.5.7"
// UI
implementation "androidx.recyclerview:recyclerview:1.3.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
- implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
+ implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
implementation 'androidx.core:core-ktx:1.10.1'
// Lifecycle
@@ -125,7 +125,7 @@ dependencies {
implementation project(":media-lib-decoder-ffmpeg")
// Image loading
- implementation 'io.coil-kt:coil-base:2.3.0'
+ implementation 'io.coil-kt:coil-base:2.4.0'
// Material
// TODO: Stuck on 1.8.0-alpha01 until ripple bug with tab layout is actually available
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 a472ce7ab..c6560c151 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
@@ -16,8 +16,6 @@
package com.google.android.material.bottomsheet;
-import com.google.android.material.R;
-
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static java.lang.Math.max;
import static java.lang.Math.min;
@@ -44,6 +42,7 @@ import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
+
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
@@ -63,11 +62,14 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit
import androidx.core.view.accessibility.AccessibilityViewCommand;
import androidx.customview.view.AbsSavedState;
import androidx.customview.widget.ViewDragHelper;
+
+import com.google.android.material.R;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.internal.ViewUtils.RelativePadding;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
@@ -1334,6 +1336,19 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo
return state;
}
+ /**
+ * Gets the target state of the bottom sheet if currently attempting to settle, or the current
+ * state otherwise.
+ * @return One of {@link #STATE_EXPANDED}, {@link #STATE_HALF_EXPANDED}, {@link #STATE_COLLAPSED},
+ * or {@link #STATE_DRAGGING}
+ */
+ public int getTargetState() {
+ if (state != STATE_SETTLING) {
+ return state;
+ }
+ return stateSettlingTracker.targetState;
+ }
+
void setStateInternal(@State int state) {
if (this.state == state) {
return;
diff --git a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java
index 066429513..26de5108b 100644
--- a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java
+++ b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java
@@ -16,20 +16,16 @@
package com.google.android.material.divider;
-import com.google.android.material.R;
-
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
+
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DimenRes;
@@ -39,6 +35,11 @@ import androidx.annotation.Px;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.ViewCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
+
+import com.google.android.material.R;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.resources.MaterialResources;
diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
index d29b513a6..725f60444 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
@@ -36,6 +36,7 @@ import org.oxycblt.auxio.playback.system.PlaybackService
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
/**
@@ -50,8 +51,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* TODO: Unit testing
* TODO: Fix UID naming
* TODO: Leverage FlexibleListAdapter more in dialogs (Disable item anims)
- * TODO: Add more logging
- * TODO: Try to move on from synchronized and volatile in shared objs
+ * TODO: Improve multi-threading support in shared objects
*/
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -121,6 +121,7 @@ class MainActivity : AppCompatActivity() {
private fun startIntentAction(intent: Intent?): Boolean {
if (intent == null) {
// Nothing to do.
+ logD("No intent to handle")
return false
}
@@ -129,6 +130,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")
return true
}
intent.putExtra(KEY_INTENT_USED, true)
@@ -137,8 +139,12 @@ class MainActivity : AppCompatActivity() {
when (intent.action) {
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
- else -> return false
+ else -> {
+ logW("Unexpected intent ${intent.action}")
+ return false
+ }
}
+ logD("Translated intent to $action")
playbackModel.startAction(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 665fc7bdc..ba09eddf0 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
@@ -26,11 +26,13 @@ import androidx.activity.OnBackPressedCallback
import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.core.view.updatePadding
+import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.NavDestination
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.shape.MaterialShapeDrawable
import com.google.android.material.transition.MaterialFadeThrough
@@ -50,13 +52,24 @@ import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
import org.oxycblt.auxio.ui.ViewBindingFragment
-import org.oxycblt.auxio.util.*
+import org.oxycblt.auxio.util.collect
+import org.oxycblt.auxio.util.collectImmediately
+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.logD
+import org.oxycblt.auxio.util.navigateSafe
+import org.oxycblt.auxio.util.systemBarInsetsCompat
+import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A wrapper around the home fragment that shows the playback fragment and controls the more
* high-level navigation features.
*
* @author Alexander Capehart (OxygenCobalt)
+ *
+ * TODO: Break up the god navigation setup going on here
*/
@AndroidEntryPoint
class MainFragment :
@@ -68,7 +81,10 @@ class MainFragment :
private val playbackModel: PlaybackViewModel by activityViewModels()
private val selectionModel: SelectionViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
- private val callback = DynamicBackPressedCallback()
+ private var sheetBackCallback: SheetBackPressedCallback? = null
+ private var detailBackCallback: DetailBackPressedCallback? = null
+ private var selectionBackCallback: SelectionBackPressedCallback? = null
+ private var exploreBackCallback: ExploreBackPressedCallback? = null
private var lastInsets: WindowInsets? = null
private var elevationNormal = 0f
private var initialNavDestinationChange = true
@@ -84,13 +100,38 @@ class MainFragment :
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
+ val playbackSheetBehavior =
+ binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
+ val queueSheetBehavior =
+ binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
+
elevationNormal = binding.context.getDimen(R.dimen.elevation_normal)
+ // 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
+ // correct order.
+ val sheetBackCallback =
+ SheetBackPressedCallback(
+ playbackSheetBehavior = playbackSheetBehavior,
+ queueSheetBehavior = queueSheetBehavior)
+ .also { sheetBackCallback = it }
+ val detailBackCallback =
+ DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
+ val selectionBackCallback =
+ SelectionBackPressedCallback(selectionModel).also { selectionBackCallback = it }
+ val exploreBackCallback =
+ ExploreBackPressedCallback(binding.exploreNavHost).also { exploreBackCallback = it }
+
// --- UI SETUP ---
val context = requireActivity()
// Override the back pressed listener so we can map back navigation to collapsing
// navigation, navigation out of detail views, etc.
- context.onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
+ context.onBackPressedDispatcher.apply {
+ addCallback(viewLifecycleOwner, exploreBackCallback)
+ addCallback(viewLifecycleOwner, selectionBackCallback)
+ addCallback(viewLifecycleOwner, detailBackCallback)
+ addCallback(viewLifecycleOwner, sheetBackCallback)
+ }
binding.root.setOnApplyWindowInsetsListener { _, insets ->
lastInsets = insets
@@ -103,13 +144,10 @@ class MainFragment :
ViewCompat.setAccessibilityPaneTitle(
binding.queueSheet, context.getString(R.string.lbl_queue))
- val queueSheetBehavior =
- binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null) {
- // Bottom sheet mode, set up click listeners.
- val playbackSheetBehavior =
- binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
+ // In portrait mode, set up click listeners on the stacked sheets.
+ logD("Configuring stacked bottom sheets")
+ unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener {
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is expanded and queue sheet is collapsed, we can expand it.
@@ -118,14 +156,15 @@ class MainFragment :
}
} else {
// Dual-pane mode, manually style the static queue sheet.
+ logD("Configuring dual-pane bottom sheet")
binding.queueSheet.apply {
// Emulate the elevated bottom sheet style.
background =
MaterialShapeDrawable.createWithElevationOverlay(context).apply {
- fillColor = context.getAttrColorCompat(R.attr.colorSurface)
+ fillColor = context.getAttrColorCompat(MR.attr.colorSurface)
elevation = context.getDimen(R.dimen.elevation_normal)
}
- // Apply bar insets for the queue's RecyclerView to usee.
+ // Apply bar insets for the queue's RecyclerView to use.
setOnApplyWindowInsetsListener { v, insets ->
v.updatePadding(top = insets.systemBarInsetsCompat.top)
insets
@@ -134,13 +173,15 @@ class MainFragment :
}
// --- VIEWMODEL SETUP ---
- collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
- collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
- collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
+ collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
+ collectImmediately(selectionModel.selected, selectionBackCallback::invalidateEnabled)
collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
collect(musicModel.playlistToRename.flow, ::handleRenamePlaylist)
collect(musicModel.playlistToDelete.flow, ::handleDeletePlaylist)
collect(musicModel.songsToAdd.flow, ::handleAddToPlaylist)
+ collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
+ collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
+ collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
collectImmediately(playbackModel.song, ::updateSong)
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
@@ -165,6 +206,14 @@ class MainFragment :
binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
}
+ override fun onDestroyBinding(binding: FragmentMainBinding) {
+ super.onDestroyBinding(binding)
+ sheetBackCallback = null
+ detailBackCallback = null
+ selectionBackCallback = null
+ exploreBackCallback = null
+ }
+
override fun onPreDraw(): Boolean {
// We overload CoordinatorLayout far too much to rely on any of it's typical
// listener functionality. Just update all transitions before every draw. Should
@@ -250,7 +299,8 @@ class MainFragment :
// Since the navigation listener is also reliant on the bottom sheets, we must also update
// it every frame.
- callback.invalidateEnabled()
+ requireNotNull(sheetBackCallback) { "SheetBackPressedCallback was not available" }
+ .invalidateEnabled()
return true
}
@@ -263,6 +313,8 @@ class MainFragment :
// Drop the initial call by NavController that simply provides us with the current
// destination. This would cause the selection state to be lost every time the device
// rotates.
+ requireNotNull(exploreBackCallback) { "ExploreBackPressedCallback was not available" }
+ .invalidateEnabled()
if (!initialNavDestinationChange) {
initialNavDestinationChange = true
return
@@ -271,19 +323,15 @@ class MainFragment :
}
private fun handleMainNavigation(action: MainNavigationAction?) {
- if (action == null) {
- // Nothing to do.
- return
+ if (action != null) {
+ when (action) {
+ is MainNavigationAction.OpenPlaybackPanel -> tryOpenPlaybackPanel()
+ is MainNavigationAction.ClosePlaybackPanel -> tryClosePlaybackPanel()
+ is MainNavigationAction.Directions ->
+ findNavController().navigateSafe(action.directions)
+ }
+ navModel.mainNavigationAction.consume()
}
-
- when (action) {
- is MainNavigationAction.OpenPlaybackPanel -> tryOpenPlaybackPanel()
- is MainNavigationAction.ClosePlaybackPanel -> tryClosePlaybackPanel()
- is MainNavigationAction.Directions ->
- findNavController().navigateSafe(action.directions)
- }
-
- navModel.mainNavigationAction.consume()
}
private fun handleExploreNavigation(item: Music?) {
@@ -368,6 +416,7 @@ class MainFragment :
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is not expanded and not hidden, we can expand it.
+ logD("Expanding playback sheet")
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
return
}
@@ -378,6 +427,7 @@ class MainFragment :
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
// Queue sheet and playback sheet is expanded, close the queue sheet so the
// playback panel can eb shown.
+ logD("Collapsing queue sheet")
queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
}
}
@@ -388,6 +438,7 @@ class MainFragment :
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
// Playback sheet (and possibly queue) needs to be collapsed.
+ logD("Collapsing playback and queue sheets")
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
@@ -399,7 +450,8 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_HIDDEN) {
+ if (playbackSheetBehavior.targetState == BackportBottomSheetBehavior.STATE_HIDDEN) {
+ logD("Unhiding and enabling playback sheet")
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
// Queue sheet behavior is either collapsed or expanded, no hiding needed
@@ -416,10 +468,12 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) {
+ if (playbackSheetBehavior.targetState != BackportBottomSheetBehavior.STATE_HIDDEN) {
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
+ logD("Hiding and disabling playback and queue sheets")
+
// Make both bottom sheets non-draggable so the user can't halt the hiding event.
queueSheetBehavior?.apply {
isDraggable = false
@@ -433,71 +487,86 @@ class MainFragment :
}
}
- /**
- * A [OnBackPressedCallback] that overrides the back button to first navigate out of internal
- * app components, such as the Bottom Sheets or Explore Navigation.
- */
- private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
- override fun handleOnBackPressed() {
- val binding = requireBinding()
- val playbackSheetBehavior =
- binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- val queueSheetBehavior =
- binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
+ // TODO: Use targetState more
+ private class SheetBackPressedCallback(
+ private val playbackSheetBehavior: PlaybackBottomSheetBehavior<*>,
+ private val queueSheetBehavior: QueueBottomSheetBehavior<*>?
+ ) : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
// If expanded, collapse the queue sheet first.
- if (queueSheetBehavior != null &&
- queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
- playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
- queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
+ if (queueSheetShown()) {
+ unlikelyToBeNull(queueSheetBehavior).state =
+ BackportBottomSheetBehavior.STATE_COLLAPSED
+ logD("Collapsed queue sheet")
return
}
// If expanded, collapse the playback sheet next.
- if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
- playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) {
+ if (playbackSheetShown()) {
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
+ logD("Collapsed playback sheet")
return
}
-
- // Clear out pending playlist edits.
- if (detailModel.dropPlaylistEdit()) {
- return
- }
-
- // Clear out any prior selections.
- if (selectionModel.drop()) {
- return
- }
-
- // Then try to navigate out of the explore navigation fragments (i.e Detail Views)
- binding.exploreNavHost.findNavController().navigateUp()
}
- /**
- * Force this instance to update whether it's enabled or not. If there are no app components
- * that the back button should close first, the instance is disabled and back navigation is
- * delegated to the system.
- *
- * Normally, this listener would have just called the [MainActivity.onBackPressed] if there
- * were no components to close, but that prevents adaptive back navigation from working on
- * Android 14+, so we must do it this way.
- */
fun invalidateEnabled() {
- val binding = requireBinding()
- val playbackSheetBehavior =
- binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- val queueSheetBehavior =
- binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
- val exploreNavController = binding.exploreNavHost.findNavController()
+ isEnabled = queueSheetShown() || playbackSheetShown()
+ }
+ private fun playbackSheetShown() =
+ playbackSheetBehavior.targetState != BackportBottomSheetBehavior.STATE_COLLAPSED &&
+ playbackSheetBehavior.targetState != BackportBottomSheetBehavior.STATE_HIDDEN
+
+ private fun queueSheetShown() =
+ queueSheetBehavior != null &&
+ playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
+ queueSheetBehavior.targetState != BackportBottomSheetBehavior.STATE_COLLAPSED
+ }
+
+ private class DetailBackPressedCallback(private val detailModel: DetailViewModel) :
+ OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ if (detailModel.dropPlaylistEdit()) {
+ logD("Dropped playlist edits")
+ }
+ }
+
+ fun invalidateEnabled(playlistEdit: List?) {
+ isEnabled = playlistEdit != null
+ }
+ }
+
+ private inner class SelectionBackPressedCallback(
+ private val selectionModel: SelectionViewModel
+ ) : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ if (selectionModel.drop()) {
+ logD("Dropped selection")
+ }
+ }
+
+ fun invalidateEnabled(selection: List) {
+ isEnabled = selection.isNotEmpty()
+ }
+ }
+
+ private inner class ExploreBackPressedCallback(
+ private val exploreNavHost: FragmentContainerView
+ ) : OnBackPressedCallback(false) {
+ // Note: We cannot cache the NavController in a variable since it's current destination
+ // value goes stale for some reason.
+
+ override fun handleOnBackPressed() {
+ exploreNavHost.findNavController().navigateUp()
+ logD("Forwarded back navigation to explore nav host")
+ }
+
+ fun invalidateEnabled() {
+ val exploreNavController = exploreNavHost.findNavController()
isEnabled =
- queueSheetBehavior?.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
- playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
- detailModel.editedPlaylist.value != null ||
- selectionModel.selected.value.isNotEmpty() ||
- exploreNavController.currentDestination?.id !=
- exploreNavController.graph.startDestinationId
+ exploreNavController.currentDestination?.id !=
+ exploreNavController.graph.startDestinationId
}
}
}
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 d1f13160c..d32f5254b 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
@@ -51,7 +51,16 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
-import org.oxycblt.auxio.util.*
+import org.oxycblt.auxio.util.canScroll
+import org.oxycblt.auxio.util.collect
+import org.oxycblt.auxio.util.collectImmediately
+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.share
+import org.oxycblt.auxio.util.showToast
+import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A [ListFragment] that shows information about an [Album].
@@ -156,7 +165,14 @@ class AlbumDetailFragment :
musicModel.addToPlaylist(currentAlbum)
true
}
- else -> false
+ R.id.action_share -> {
+ requireContext().share(currentAlbum)
+ true
+ }
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
@@ -210,7 +226,7 @@ class AlbumDetailFragment :
private fun updateAlbum(album: Album?) {
if (album == null) {
- // Album we were showing no longer exists.
+ logD("No album to show, navigating away")
findNavController().navigateUp()
return
}
@@ -219,12 +235,8 @@ class AlbumDetailFragment :
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
- if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
- albumListAdapter.setPlaying(song, isPlaying)
- } else {
- // Clear the ViewHolders if the mode isn't ALL_SONGS
- albumListAdapter.setPlaying(null, isPlaying)
- }
+ albumListAdapter.setPlaying(
+ song.takeIf { parent == detailModel.currentAlbum.value }, isPlaying)
}
private fun handleNavigation(item: Music?) {
@@ -291,7 +303,7 @@ class AlbumDetailFragment :
boxStart: Int,
boxEnd: Int,
snapPreference: Int
- ): Int =
+ ) =
(boxStart + (boxEnd - boxStart) / 2) -
(viewStart + (viewEnd - viewStart) / 2)
}
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 4677aee62..601c2ed50 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
@@ -49,7 +49,15 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
-import org.oxycblt.auxio.util.*
+import org.oxycblt.auxio.util.collect
+import org.oxycblt.auxio.util.collectImmediately
+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.share
+import org.oxycblt.auxio.util.showToast
+import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A [ListFragment] that shows information about an [Artist].
@@ -153,7 +161,14 @@ class ArtistDetailFragment :
musicModel.addToPlaylist(currentArtist)
true
}
- else -> false
+ R.id.action_share -> {
+ requireContext().share(currentArtist)
+ true
+ }
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
@@ -222,11 +237,23 @@ class ArtistDetailFragment :
private fun updateArtist(artist: Artist?) {
if (artist == null) {
- // Artist we were showing no longer exists.
+ logD("No artist to show, navigating away")
findNavController().navigateUp()
return
}
- requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext())
+ requireBinding().detailNormalToolbar.apply {
+ title = artist.name.resolve(requireContext())
+
+ // Disable options that make no sense with an empty artist
+ val playable = artist.songs.isNotEmpty()
+ if (!playable) {
+ logD("Artist is empty, disabling playback/playlist/share options")
+ }
+ menu.findItem(R.id.action_play_next).isEnabled = playable
+ menu.findItem(R.id.action_queue_add).isEnabled = playable
+ menu.findItem(R.id.action_playlist_add).isEnabled = playable
+ menu.findItem(R.id.action_share).isEnabled = playable
+ }
artistHeaderAdapter.setParent(artist)
}
@@ -234,14 +261,14 @@ class ArtistDetailFragment :
val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value)
val playingItem =
when (parent) {
- // Always highlight a playing album if it's from this artist.
- is Album -> parent
+ // Always highlight a playing album if it's from this artist, and if the currently
+ // playing song is contained within.
+ is Album -> parent.takeIf { song?.album == it }
// If the parent is the artist itself, use the currently playing song.
currentArtist -> song
// Nothing is playing from this artist.
else -> null
}
-
artistListAdapter.setPlaying(playingItem, isPlaying)
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
index 15b803ae6..28c1f65f7 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
@@ -35,6 +35,7 @@ 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
@@ -77,7 +78,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
(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..
+ // animation.
alpha = 0f
}
@@ -101,12 +102,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (titleShown == visible) return
titleShown = visible
- val titleAnimator = titleAnimator
- if (titleAnimator != null) {
- titleAnimator.cancel()
- this.titleAnimator = null
- }
-
// 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()
@@ -126,7 +121,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
return
}
- this.titleAnimator =
+ logD("Changing title visibility [from: $from to: $to]")
+ titleAnimator?.cancel()
+ titleAnimator =
ValueAnimator.ofFloat(from, to).apply {
addUpdateListener { titleView.alpha = it.animatedValue as Float }
duration =
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 92052a955..f18321430 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt
@@ -30,6 +30,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.DiscHeader
import org.oxycblt.auxio.detail.list.EditHeader
import org.oxycblt.auxio.detail.list.SortHeader
import org.oxycblt.auxio.list.BasicHeader
@@ -37,12 +38,22 @@ import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.UpdateInstructions
-import org.oxycblt.auxio.music.*
-import org.oxycblt.auxio.music.info.Disc
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicRepository
+import org.oxycblt.auxio.music.MusicSettings
+import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.metadata.AudioProperties
import org.oxycblt.auxio.playback.PlaybackSettings
-import org.oxycblt.auxio.util.*
+import org.oxycblt.auxio.util.Event
+import org.oxycblt.auxio.util.MutableEvent
+import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.logW
/**
* [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the
@@ -60,7 +71,7 @@ constructor(
private val playbackSettings: PlaybackSettings
) : ViewModel(), MusicRepository.UpdateListener {
// --- SONG ---
-
+
private var currentSongJob: Job? = null
private val _currentSong = MutableStateFlow(null)
@@ -219,9 +230,9 @@ constructor(
if (changes.userLibrary && userLibrary != null) {
val playlist = currentPlaylist.value
if (playlist != null) {
- logD("Updated playlist to ${currentPlaylist.value}")
_currentPlaylist.value =
userLibrary.findPlaylist(playlist.uid)?.also(::refreshPlaylistList)
+ logD("Updated playlist to ${currentPlaylist.value}")
}
}
}
@@ -233,8 +244,11 @@ constructor(
* @param uid The UID of the [Song] to load. Must be valid.
*/
fun setSong(uid: Music.UID) {
- logD("Opening Song [uid: $uid]")
+ logD("Opening song $uid")
_currentSong.value = musicRepository.deviceLibrary?.findSong(uid)?.also(::refreshAudioInfo)
+ if (_currentSong.value == null) {
+ logW("Given song UID was invalid")
+ }
}
/**
@@ -244,9 +258,12 @@ 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: $uid]")
+ logD("Opening album $uid")
_currentAlbum.value =
musicRepository.deviceLibrary?.findAlbum(uid)?.also(::refreshAlbumList)
+ if (_currentAlbum.value == null) {
+ logW("Given album UID was invalid")
+ }
}
/**
@@ -256,9 +273,12 @@ 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: $uid]")
+ logD("Opening artist $uid")
_currentArtist.value =
musicRepository.deviceLibrary?.findArtist(uid)?.also(::refreshArtistList)
+ if (_currentArtist.value == null) {
+ logW("Given artist UID was invalid")
+ }
}
/**
@@ -268,9 +288,12 @@ 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: $uid]")
+ logD("Opening genre $uid")
_currentGenre.value =
musicRepository.deviceLibrary?.findGenre(uid)?.also(::refreshGenreList)
+ if (_currentGenre.value == null) {
+ logW("Given genre UID was invalid")
+ }
}
/**
@@ -280,9 +303,12 @@ 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: $uid]")
+ logD("Opening playlist $uid")
_currentPlaylist.value =
musicRepository.userLibrary?.findPlaylist(uid)?.also(::refreshPlaylistList)
+ if (_currentPlaylist.value == null) {
+ logW("Given playlist UID was invalid")
+ }
}
/** Start a playlist editing session. Does nothing if a playlist is not being shown. */
@@ -300,6 +326,7 @@ constructor(
fun savePlaylistEdit() {
val playlist = _currentPlaylist.value ?: return
val editedPlaylist = _editedPlaylist.value ?: return
+ logD("Committing playlist edits")
viewModelScope.launch {
musicRepository.rewritePlaylist(playlist, editedPlaylist)
// TODO: The user could probably press some kind of button if they were fast enough.
@@ -320,6 +347,7 @@ constructor(
// Nothing to do.
return false
}
+ logD("Discarding playlist edits")
_editedPlaylist.value = null
refreshPlaylistList(playlist)
return true
@@ -341,6 +369,7 @@ constructor(
if (realFrom !in editedPlaylist.indices || realTo !in editedPlaylist.indices) {
return false
}
+ logD("Moving playlist song from $realFrom [$from] to $realTo [$to]")
editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo))
_editedPlaylist.value = editedPlaylist
refreshPlaylistList(playlist, UpdateInstructions.Move(from, to))
@@ -359,6 +388,7 @@ constructor(
if (realAt !in editedPlaylist.indices) {
return
}
+ logD("Removing playlist song at $realAt [$at]")
editedPlaylist.removeAt(realAt)
_editedPlaylist.value = editedPlaylist
refreshPlaylistList(
@@ -366,11 +396,13 @@ constructor(
if (editedPlaylist.isNotEmpty()) {
UpdateInstructions.Remove(at, 1)
} else {
+ logD("Playlist will be empty after removal, removing header")
UpdateInstructions.Remove(at - 2, 3)
})
}
private fun refreshAudioInfo(song: Song) {
+ logD("Refreshing audio info")
// Clear any previous job in order to avoid stale data from appearing in the UI.
currentSongJob?.cancel()
_songAudioProperties.value = null
@@ -378,6 +410,7 @@ constructor(
viewModelScope.launch(Dispatchers.IO) {
val info = audioPropertiesFactory.extract(song)
yield()
+ logD("Updating audio info to $info")
_songAudioProperties.value = info
}
}
@@ -399,12 +432,11 @@ constructor(
// To create a good user experience regarding disc numbers, we group the album's
// songs up by disc and then delimit the groups by a disc header.
val songs = albumSongSort.songs(album.songs)
- // Songs without disc tags become part of Disc 1.
- val byDisc = songs.groupBy { it.disc ?: Disc(1, null) }
+ val byDisc = songs.groupBy { it.disc }
if (byDisc.size > 1) {
logD("Album has more than one disc, interspersing headers")
for (entry in byDisc.entries) {
- list.add(entry.key)
+ list.add(DiscHeader(entry.key))
list.addAll(entry.value)
}
} else {
@@ -412,6 +444,7 @@ constructor(
list.addAll(songs)
}
+ logD("Update album list to ${list.size} items with $instructions")
_albumInstructions.put(instructions)
_albumList.value = list
}
@@ -419,10 +452,9 @@ constructor(
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
logD("Refreshing artist list")
val list = mutableListOf- ()
- val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums)
- val byReleaseGroup =
- albums.groupBy {
+ val grouping =
+ artist.explicitAlbums.groupByTo(sortedMapOf()) {
// Remap the complicated ReleaseType data structure into an easier
// "AlbumGrouping" enum that will automatically group and sort
// the artist's albums.
@@ -436,15 +468,25 @@ constructor(
is ReleaseType.Single -> AlbumGrouping.SINGLES
is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS
is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS
- is ReleaseType.Mix -> AlbumGrouping.MIXES
+ is ReleaseType.Mix -> AlbumGrouping.DJMIXES
is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES
}
}
}
- logD("Release groups for this artist: ${byReleaseGroup.keys}")
+ if (artist.implicitAlbums.isNotEmpty()) {
+ // 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")
+ @Suppress("UNCHECKED_CAST")
+ (grouping as MutableMap>)[AlbumGrouping.APPEARANCES] =
+ artist.implicitAlbums
+ }
- for (entry in byReleaseGroup.entries.sortedBy { it.key }) {
+ logD("Release groups for this artist: ${grouping.keys}")
+
+ for (entry in grouping.entries) {
val header = BasicHeader(entry.key.headerTitleRes)
list.add(Divider(header))
list.add(header)
@@ -465,6 +507,7 @@ constructor(
list.addAll(artistSongSort.songs(artist.songs))
}
+ logD("Updating artist list to ${list.size} items with $instructions")
_artistInstructions.put(instructions)
_artistList.value = list.toList()
}
@@ -483,12 +526,14 @@ constructor(
list.add(songHeader)
val instructions =
if (replace) {
- // Intentional so that the header item isn't replaced with the songs
+ // Intentional so that the header item isn't replaced alongside the songs
UpdateInstructions.Replace(list.size)
} else {
UpdateInstructions.Diff
}
list.addAll(genreSongSort.songs(genre.songs))
+
+ logD("Updating genre list to ${list.size} items with $instructions")
_genreInstructions.put(instructions)
_genreList.value = list
}
@@ -508,6 +553,7 @@ constructor(
list.addAll(songs)
}
+ logD("Updating playlist list to ${list.size} items with $instructions")
_playlistInstructions.put(instructions)
_playlistList.value = list
}
@@ -524,8 +570,9 @@ constructor(
SINGLES(R.string.lbl_singles),
COMPILATIONS(R.string.lbl_compilations),
SOUNDTRACKS(R.string.lbl_soundtracks),
- MIXES(R.string.lbl_mixes),
+ DJMIXES(R.string.lbl_mixes),
MIXTAPES(R.string.lbl_mixtapes),
+ APPEARANCES(R.string.lbl_appears_on),
LIVE(R.string.lbl_live_group),
REMIXES(R.string.lbl_remix_group),
}
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 4ef67d581..3968c1379 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
@@ -41,10 +41,24 @@ import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.selection.SelectionViewModel
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+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.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
-import org.oxycblt.auxio.util.*
+import org.oxycblt.auxio.util.collect
+import org.oxycblt.auxio.util.collectImmediately
+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.share
+import org.oxycblt.auxio.util.showToast
+import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A [ListFragment] that shows information for a particular [Genre].
@@ -146,7 +160,14 @@ class GenreDetailFragment :
musicModel.addToPlaylist(currentGenre)
true
}
- else -> false
+ R.id.action_share -> {
+ requireContext().share(currentGenre)
+ true
+ }
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
@@ -213,7 +234,7 @@ class GenreDetailFragment :
private fun updatePlaylist(genre: Genre?) {
if (genre == null) {
- // Genre we were showing no longer exists.
+ logD("No genre to show, navigating away")
findNavController().navigateUp()
return
}
@@ -222,15 +243,18 @@ class GenreDetailFragment :
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
- var playingMusic: Music? = null
- if (parent is Artist) {
- playingMusic = parent
- }
- // Prefer songs that might be playing from this genre.
- if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
- playingMusic = song
- }
- genreListAdapter.setPlaying(playingMusic, isPlaying)
+ val currentGenre = unlikelyToBeNull(detailModel.currentGenre.value)
+ val playingItem =
+ when (parent) {
+ // Always highlight a playing artist if it's from this genre, and if the currently
+ // playing song is contained within.
+ is Artist -> parent.takeIf { song?.run { artists.contains(it) } ?: false }
+ // If the parent is the artist itself, use the currently playing song.
+ currentGenre -> song
+ // Nothing is playing from this artist.
+ else -> null
+ }
+ genreListAdapter.setPlaying(playingItem, isPlaying)
}
private fun handleNavigation(item: Music?) {
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 c7cf92cde..7cdf9443c 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt
@@ -44,10 +44,24 @@ import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.selection.SelectionViewModel
-import org.oxycblt.auxio.music.*
+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.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
-import org.oxycblt.auxio.util.*
+import org.oxycblt.auxio.util.collect
+import org.oxycblt.auxio.util.collectImmediately
+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.share
+import org.oxycblt.auxio.util.showToast
+import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A [ListFragment] that shows information for a particular [Playlist].
@@ -197,11 +211,18 @@ class PlaylistDetailFragment :
musicModel.deletePlaylist(currentPlaylist)
true
}
+ R.id.action_share -> {
+ requireContext().share(currentPlaylist)
+ true
+ }
R.id.action_save -> {
detailModel.savePlaylistEdit()
true
}
- else -> false
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
@@ -238,19 +259,26 @@ class PlaylistDetailFragment :
return
}
val binding = requireBinding()
- binding.detailNormalToolbar.title = playlist.name.resolve(requireContext())
- binding.detailEditToolbar.title = "Editing ${playlist.name.resolve(requireContext())}"
+ binding.detailNormalToolbar.apply {
+ title = playlist.name.resolve(requireContext())
+ // Disable options that make no sense with an empty playlist
+ val playable = playlist.songs.isNotEmpty()
+ if (!playable) {
+ logD("Playlist is empty, disabling playback/share options")
+ }
+ menu.findItem(R.id.action_play_next).isEnabled = playable
+ menu.findItem(R.id.action_queue_add).isEnabled = playable
+ menu.findItem(R.id.action_share).isEnabled = playable
+ }
+ binding.detailEditToolbar.title =
+ getString(R.string.fmt_editing, playlist.name.resolve(requireContext()))
playlistHeaderAdapter.setParent(playlist)
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
- // Prefer songs that might be playing from this playlist.
- if (parent is Playlist &&
- parent.uid == unlikelyToBeNull(detailModel.currentPlaylist.value).uid) {
- playlistListAdapter.setPlaying(song, isPlaying)
- } else {
- playlistListAdapter.setPlaying(null, isPlaying)
- }
+ // Prefer songs that are playing from this playlist.
+ playlistListAdapter.setPlaying(
+ song.takeIf { parent == detailModel.currentPlaylist.value }, isPlaying)
}
private fun handleNavigation(item: Music?) {
@@ -287,6 +315,7 @@ class PlaylistDetailFragment :
selectionModel.drop()
if (editedPlaylist != null) {
+ logD("Updating save button state")
requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).apply {
isEnabled = editedPlaylist != detailModel.currentPlaylist.value?.songs
}
@@ -308,9 +337,18 @@ class PlaylistDetailFragment :
private fun updateMultiToolbar() {
val id =
when {
- detailModel.editedPlaylist.value != null -> R.id.detail_edit_toolbar
- selectionModel.selected.value.isNotEmpty() -> R.id.detail_selection_toolbar
- else -> R.id.detail_normal_toolbar
+ detailModel.editedPlaylist.value != null -> {
+ logD("Currently editing playlist, showing edit toolbar")
+ R.id.detail_edit_toolbar
+ }
+ selectionModel.selected.value.isNotEmpty() -> {
+ logD("Currently selecting, showing selection toolbar")
+ R.id.detail_selection_toolbar
+ }
+ else -> {
+ logD("Using normal toolbar")
+ R.id.detail_normal_toolbar
+ }
}
requireBinding().detailToolbar.setVisible(id)
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt
index f03ad5c31..716d70c60 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt
@@ -22,8 +22,8 @@ import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.View
+import androidx.appcompat.R
import com.google.android.material.textfield.TextInputEditText
-import org.oxycblt.auxio.R
/**
* A [TextInputEditText] that deliberately restricts all input except for selection. This will work
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 5ba78ea8f..ca38c061e 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt
@@ -41,6 +41,7 @@ import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.concatLocalized
+import org.oxycblt.auxio.util.logD
/**
* A [ViewBindingDialogFragment] that shows information about a Song.
@@ -73,7 +74,7 @@ class SongDetailDialog : ViewBindingDialogFragment() {
private fun updateSong(song: Song?, info: AudioProperties?) {
if (song == null) {
- // Song we were showing no longer exists.
+ logD("No song to show, navigating away")
findNavController().navigateUp()
return
}
@@ -86,7 +87,7 @@ class SongDetailDialog : ViewBindingDialogFragment() {
add(SongProperty(R.string.lbl_album, song.album.zipName(context)))
add(SongProperty(R.string.lbl_artists, song.artists.zipNames(context)))
add(SongProperty(R.string.lbl_genres, song.genres.resolveNames(context)))
- song.date?.let { add(SongProperty(R.string.lbl_date, it.resolveDate(context))) }
+ song.date?.let { add(SongProperty(R.string.lbl_date, it.resolve(context))) }
song.track?.let {
add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it)))
}
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
index 3b346975e..02303a566 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt
@@ -29,6 +29,7 @@ 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.
@@ -91,6 +92,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
// 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
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
index 06317f5e2..247875432 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt
@@ -20,6 +20,7 @@ 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.
@@ -47,6 +48,7 @@ abstract class DetailHeaderAdapter?,
listener: DetailHeaderAdapter.Listener
) {
- // TODO: Debug perpetually re-binding images
- binding.detailCover.bind(playlist, editedPlaylist)
+ 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.
@@ -103,12 +113,17 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
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 = playlist.songs.isNotEmpty() && editedPlaylist == null
+ isEnabled = playable
setOnClickListener { listener.onPlay() }
}
binding.detailShuffleButton.apply {
- isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null
+ isEnabled = playable
setOnClickListener { listener.onShuffle() }
}
}
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 b7217a681..66fc29d7c 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
@@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
@@ -37,6 +38,7 @@ import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
+import org.oxycblt.auxio.util.logD
/**
* An [DetailListAdapter] implementing the header and sub-items for the [Album] detail view.
@@ -49,14 +51,14 @@ class AlbumDetailListAdapter(private val listener: Listener) :
override fun getItemViewType(position: Int) =
when (getItem(position)) {
// Support sub-headers for each disc, and special album songs.
- is Disc -> DiscViewHolder.VIEW_TYPE
+ is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
is Song -> AlbumSongViewHolder.VIEW_TYPE
else -> super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
when (viewType) {
- DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent)
+ DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.from(parent)
AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent)
else -> super.onCreateViewHolder(parent, viewType)
}
@@ -64,7 +66,7 @@ class AlbumDetailListAdapter(private val listener: Listener) :
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
when (val item = getItem(position)) {
- is Disc -> (holder as DiscViewHolder).bind(item)
+ is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
}
}
@@ -76,7 +78,7 @@ class AlbumDetailListAdapter(private val listener: Listener) :
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
when {
oldItem is Disc && newItem is Disc ->
- DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
+ DiscHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
oldItem is Song && newItem is Song ->
AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
@@ -88,23 +90,37 @@ class AlbumDetailListAdapter(private val listener: Listener) :
}
/**
- * A [RecyclerView.ViewHolder] that displays a [Disc] to delimit different disc groups. Use [from]
- * to create an instance.
+ * A wrapper around [Disc] signifying that a header should be shown for a disc group.
*
* @author Alexander Capehart (OxygenCobalt)
*/
-private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) :
+data class DiscHeader(val inner: Disc?) : Item
+
+/**
+ * A [RecyclerView.ViewHolder] that displays a [DiscHeader] to delimit different disc groups. Use
+ * [from] to create an instance.
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
- * @param disc The new [disc] to bind.
+ * @param discHeader The new [DiscHeader] to bind.
*/
- fun bind(disc: Disc) {
- binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
- binding.discName.apply {
- text = disc.name
- isGone = disc.name == null
+ fun bind(discHeader: DiscHeader) {
+ val disc = discHeader.inner
+ if (disc != null) {
+ binding.discNumber.text = binding.context.getString(R.string.fmt_disc_no, disc.number)
+ binding.discName.apply {
+ text = disc.name
+ isGone = text == null
+ }
+ } else {
+ logD("Disc is null, defaulting to no disc")
+ binding.discNumber.text = binding.context.getString(R.string.def_disc)
+ binding.discName.isGone = true
}
}
@@ -119,7 +135,7 @@ private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) :
* @return A new instance.
*/
fun from(parent: View) =
- DiscViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater))
+ DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater))
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
@@ -147,31 +163,33 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
fun bind(song: Song, listener: SelectableListListener) {
listener.bind(song, this, menuButton = binding.songMenu)
- binding.songTrack.apply {
- if (song.track != null) {
- // Instead of an album cover, we show the track number, as the song list
- // within the album detail view would have homogeneous album covers otherwise.
+ val track = song.track
+ if (track != null) {
+ binding.songTrackCover.contentDescription =
+ binding.context.getString(R.string.desc_track_number, track)
+ binding.songTrackText.apply {
+ isVisible = true
text = context.getString(R.string.fmt_number, song.track)
- isInvisible = false
- contentDescription = context.getString(R.string.desc_track_number, song.track)
- } else {
- // No track, do not show a number, instead showing a generic icon.
- text = ""
- isInvisible = true
- contentDescription = context.getString(R.string.def_track)
}
+ binding.songTrackPlaceholder.isInvisible = true
+ } else {
+ binding.songTrackCover.contentDescription =
+ binding.context.getString(R.string.def_track)
+ binding.songTrackText.apply {
+ isInvisible = true
+ text = null
+ }
+ binding.songTrackPlaceholder.isVisible = true
}
binding.songName.text = song.name.resolve(binding.context)
-
- // Use duration instead of album or artist for each song, as this text would
- // be homogenous otherwise.
+ // Use duration instead of album or artist for each song to be more contextually relevant.
binding.songDuration.text = song.durationMs.formatDurationMs(false)
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
- binding.songTrackBg.isPlaying = isPlaying
+ binding.songTrackCover.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt
index e281d9982..524c27792 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt
@@ -29,7 +29,10 @@ import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
@@ -107,7 +110,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
- binding.parentImage.isPlaying = isPlaying
+ binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@@ -159,7 +162,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
- binding.songAlbumCover.isPlaying = isPlaying
+ binding.songAlbumCover.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
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 9c43dc875..ba350e7b3 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
@@ -31,8 +31,10 @@ 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.*
-import org.oxycblt.auxio.list.recycler.*
+import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
+import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
+import org.oxycblt.auxio.list.recycler.BasicHeaderViewHolder
+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
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 7b4147621..06c5be29b 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
@@ -27,6 +27,7 @@ import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
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
@@ -47,6 +48,7 @@ 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
/**
* A [DetailListAdapter] implementing the header, sub-items, and editing state for the [Playlist]
@@ -97,6 +99,7 @@ class PlaylistDetailListAdapter(private val listener: Listener) :
// Nothing to do.
return
}
+ logD("Updating editing state [old: $isEditing new: $editing]")
this.isEditing = editing
notifyItemRangeChanged(1, currentList.size - 1, PAYLOAD_EDITING_CHANGED)
}
@@ -213,7 +216,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(R.attr.colorSurface)
+ fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface)
elevation = binding.context.getDimen(R.dimen.elevation_normal)
alpha = 0
}
@@ -223,7 +226,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
LayerDrawable(
arrayOf(
MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply {
- fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
+ fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface)
},
background))
}
@@ -253,7 +256,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.interactBody.isSelected = isActive
- binding.songAlbumCover.isPlaying = isPlaying
+ binding.songAlbumCover.setPlaying(isPlaying)
}
override fun updateEditing(editing: Boolean) {
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt
index 690f3a792..29f4bf2d2 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt
@@ -24,7 +24,8 @@ import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemSongPropertyBinding
import org.oxycblt.auxio.list.Item
-import org.oxycblt.auxio.list.adapter.*
+import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
+import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
diff --git a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
index 2b0cd3d5e..a03adccfd 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
@@ -22,8 +22,8 @@ import android.content.Context
import android.util.AttributeSet
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
+import com.google.android.material.R
import com.google.android.material.floatingactionbutton.FloatingActionButton
-import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.logD
/**
@@ -44,23 +44,32 @@ constructor(
override fun show() {
// Will already show eventually, need to do nothing.
- if (flipping) return
+ if (flipping) {
+ logD("Already flipping, aborting show")
+ return
+ }
// Apply the new configuration possibly set in flipTo. This should occur even if
// a flip was canceled by a hide.
pendingConfig?.run {
+ this@FlipFloatingActionButton.logD("Applying pending configuration")
setImageResource(iconRes)
contentDescription = context.getString(contentDescriptionRes)
setOnClickListener(clickListener)
}
pendingConfig = null
+ logD("Beginning show")
super.show()
}
override fun hide() {
+ if (flipping) {
+ logD("Hide was called, aborting flip")
+ }
// Not flipping anymore, disable the flag so that the FAB is not re-shown.
flipping = false
// Don't pass any kind of listener so that future flip operations will not be able
// to show the FAB again.
+ logD("Beginning hide")
super.hide()
}
@@ -82,9 +91,12 @@ constructor(
// Already hiding for whatever reason, apply the configuration when the FAB is shown again.
if (!isOrWillBeHidden) {
+ logD("Starting hide for flip")
flipping = true
// We will re-show the FAB later, assuming that there was not a prior flip operation.
super.hide(FlipVisibilityListener())
+ } else {
+ logD("Already hiding, will apply config later")
}
}
@@ -97,7 +109,7 @@ constructor(
private inner class FlipVisibilityListener : OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton) {
if (!flipping) return
- logD("Showing for a flip operation")
+ logD("Starting show for flip")
flipping = false
show()
}
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 5f26f32d5..f1f856684 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
@@ -46,16 +46,39 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeBinding
-import org.oxycblt.auxio.home.list.*
+import org.oxycblt.auxio.home.list.AlbumListFragment
+import org.oxycblt.auxio.home.list.ArtistListFragment
+import org.oxycblt.auxio.home.list.GenreListFragment
+import org.oxycblt.auxio.home.list.PlaylistListFragment
+import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.list.selection.SelectionViewModel
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.IndexingProgress
+import org.oxycblt.auxio.music.IndexingState
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicViewModel
+import org.oxycblt.auxio.music.NoAudioPermissionException
+import org.oxycblt.auxio.music.NoMusicException
+import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO
+import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.MainNavigationAction
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
-import org.oxycblt.auxio.util.*
+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.logD
+import org.oxycblt.auxio.util.logW
+import org.oxycblt.auxio.util.navigateSafe
+import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation
@@ -188,54 +211,65 @@ class HomeFragment :
return true
}
- when (item.itemId) {
+ return when (item.itemId) {
// Handle main actions (Search, Settings, About)
R.id.action_search -> {
logD("Navigating to search")
setupAxisTransitions(MaterialSharedAxis.Z)
findNavController().navigateSafe(HomeFragmentDirections.actionShowSearch())
+ true
}
R.id.action_settings -> {
logD("Navigating to settings")
navModel.mainNavigateTo(
MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings()))
+ true
}
R.id.action_about -> {
logD("Navigating to about")
navModel.mainNavigateTo(
MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout()))
+ true
}
// Handle sort menu
R.id.submenu_sorting -> {
// Junk click event when opening the menu
+ true
}
R.id.option_sort_asc -> {
+ logD("Switching to ascending sorting")
item.isChecked = true
homeModel.setSortForCurrentTab(
homeModel
.getSortForTab(homeModel.currentTabMode.value)
.withDirection(Sort.Direction.ASCENDING))
+ true
}
R.id.option_sort_dec -> {
+ logD("Switching to descending sorting")
item.isChecked = true
homeModel.setSortForCurrentTab(
homeModel
.getSortForTab(homeModel.currentTabMode.value)
.withDirection(Sort.Direction.DESCENDING))
+ true
}
else -> {
- // Sorting option was selected, mark it as selected and update the mode
- item.isChecked = true
- homeModel.setSortForCurrentTab(
- homeModel
- .getSortForTab(homeModel.currentTabMode.value)
- .withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
+ val newMode = Sort.Mode.fromItemId(item.itemId)
+ if (newMode != null) {
+ // Sorting option was selected, mark it as selected and update the mode
+ logD("Updating sort mode")
+ item.isChecked = true
+ homeModel.setSortForCurrentTab(
+ homeModel.getSortForTab(homeModel.currentTabMode.value).withMode(newMode))
+ true
+ } else {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
-
- // Always handling it one way or another, so always return true
- return true
}
private fun setupPager(binding: FragmentHomeBinding) {
@@ -246,6 +280,7 @@ class HomeFragment :
if (homeModel.currentTabModes.size == 1) {
// A single tab makes the tab layout redundant, hide it and disable the collapsing
// behavior.
+ logD("Single tab shown, disabling TabLayout")
binding.homeTabs.isVisible = false
binding.homeAppbar.setExpanded(true, false)
toolbarParams.scrollFlags = 0
@@ -270,17 +305,26 @@ class HomeFragment :
val isVisible: (Int) -> Boolean =
when (tabMode) {
// Disallow sorting by count for songs
- MusicMode.SONGS -> { id -> id != R.id.option_sort_count }
+ MusicMode.SONGS -> {
+ logD("Using song-specific menu options")
+ ({ id -> id != R.id.option_sort_count })
+ }
// Disallow sorting by album for albums
- MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album }
+ MusicMode.ALBUMS -> {
+ logD("Using album-specific menu options")
+ ({ id -> id != R.id.option_sort_album })
+ }
// Only allow sorting by name, count, and duration for parents
- else -> { id ->
+ else -> {
+ logD("Using parent-specific menu options")
+ ({ id ->
id == R.id.option_sort_asc ||
id == R.id.option_sort_dec ||
id == R.id.option_sort_name ||
id == R.id.option_sort_count ||
id == R.id.option_sort_duration
- }
+ })
+ }
}
val sortMenu =
@@ -288,18 +332,29 @@ class HomeFragment :
val toHighlight = homeModel.getSortForTab(tabMode)
for (option in sortMenu) {
- // Check the ascending option and corresponding sort option to align with
+ val isCurrentMode = option.itemId == toHighlight.mode.itemId
+ val isCurrentlyAscending =
+ option.itemId == R.id.option_sort_asc &&
+ toHighlight.direction == Sort.Direction.ASCENDING
+ val isCurrentlyDescending =
+ option.itemId == R.id.option_sort_dec &&
+ toHighlight.direction == Sort.Direction.DESCENDING
+ // Check the corresponding direction and mode sort options to align with
// the current sort of the tab.
- if (option.itemId == toHighlight.mode.itemId ||
- (option.itemId == R.id.option_sort_asc &&
- toHighlight.direction == Sort.Direction.ASCENDING) ||
- (option.itemId == R.id.option_sort_dec &&
- toHighlight.direction == Sort.Direction.DESCENDING)) {
+ if (isCurrentMode || isCurrentlyAscending || isCurrentlyDescending) {
+ logD(
+ "Checking $option option [mode: $isCurrentMode asc: $isCurrentlyAscending dec: $isCurrentlyDescending]")
+ // Note: We cannot inline this boolean assignment since it unchecks all other radio
+ // buttons (even when setting it to false), which would result in nothing being
+ // selected.
option.isChecked = true
}
// Disable options that are not allowed by the isVisible lambda
option.isVisible = isVisible(option.itemId)
+ if (!option.isVisible) {
+ logD("Hiding $option option")
+ }
}
// Update the scrolling view in AppBarLayout to align with the current tab's
@@ -315,10 +370,12 @@ class HomeFragment :
}
if (tabMode != MusicMode.PLAYLISTS) {
+ logD("Flipping to shuffle button")
binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) {
playbackModel.shuffleAll()
}
} else {
+ logD("Flipping to playlist button")
binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) {
musicModel.createPlaylist()
}
@@ -328,6 +385,7 @@ class HomeFragment :
private fun handleRecreate(recreate: Unit?) {
if (recreate == null) return
val binding = requireBinding()
+ logD("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.
@@ -364,7 +422,7 @@ class HomeFragment :
binding.homeIndexingProgress.visibility = View.INVISIBLE
when (error) {
is NoAudioPermissionException -> {
- logD("Updating UI to permission request state")
+ logD("Showing permission prompt")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms)
// Configure the action to act as a permission launcher.
binding.homeIndexingAction.apply {
@@ -379,7 +437,7 @@ class HomeFragment :
}
}
is NoMusicException -> {
- logD("Updating UI to no music state")
+ logD("Showing no music error")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_music)
// Configure the action to act as a reload trigger.
binding.homeIndexingAction.apply {
@@ -389,7 +447,7 @@ class HomeFragment :
}
}
else -> {
- logD("Updating UI to error state")
+ logD("Showing generic error")
binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed)
// Configure the action to act as a reload trigger.
binding.homeIndexingAction.apply {
@@ -432,8 +490,10 @@ class HomeFragment :
// 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) {
+ logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]")
binding.homeFab.hide()
} else {
+ logD("Showing fab")
binding.homeFab.show()
}
}
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 60d3144e7..4e468ec95 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
@@ -26,6 +26,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@@ -67,15 +68,18 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context)
override fun migrate() {
if (sharedPreferences.contains(OLD_KEY_LIB_TABS)) {
+ logD("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")
// The playlist tab is now parsed, but it needs to be made visible.
val playlistIndex = oldTabs.indexOfFirst { it.mode == MusicMode.PLAYLISTS }
- if (playlistIndex > -1) { // Sanity check
- oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS)
- }
+ check(playlistIndex > -1) // This should exist, otherwise we are in big trouble
+ oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS)
+ logD("New tabs: $oldTabs")
+
sharedPreferences.edit {
putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(oldTabs))
remove(OLD_KEY_LIB_TABS)
@@ -85,8 +89,14 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context)
override fun onSettingChanged(key: String, listener: HomeSettings.Listener) {
when (key) {
- getString(R.string.set_key_home_tabs) -> listener.onTabsChanged()
- getString(R.string.set_key_hide_collaborators) -> listener.onHideCollaboratorsChanged()
+ getString(R.string.set_key_home_tabs) -> {
+ logD("Dispatching tab setting change")
+ listener.onTabsChanged()
+ }
+ getString(R.string.set_key_hide_collaborators) -> {
+ logD("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 8b4e6d581..4e471758a 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
@@ -26,7 +26,14 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.UpdateInstructions
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicRepository
+import org.oxycblt.auxio.music.MusicSettings
+import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
@@ -68,8 +75,7 @@ constructor(
private val _artistsList = MutableStateFlow(listOf())
/**
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that
- * if "Hide collaborators" is on, this list will not include [Artist]s where
- * [Artist.isCollaborator] is true.
+ * if "Hide collaborators" is on, this list will not include collaborator [Artist]s.
*/
val artistsList: MutableStateFlow
>
get() = _artistsList
@@ -137,7 +143,6 @@ constructor(
override fun onMusicChanges(changes: MusicRepository.Changes) {
val deviceLibrary = musicRepository.deviceLibrary
- logD(changes.deviceLibrary)
if (changes.deviceLibrary && deviceLibrary != null) {
logD("Refreshing library")
// Get the each list of items in the library to use as our list data.
@@ -150,9 +155,11 @@ constructor(
_artistsList.value =
musicSettings.artistSort.artists(
if (homeSettings.shouldHideCollaborators) {
+ logD("Filtering collaborator artists")
// Hide Collaborators is enabled, filter out collaborators.
- deviceLibrary.artists.filter { !it.isCollaborator }
+ deviceLibrary.artists.filter { it.explicitAlbums.isNotEmpty() }
} else {
+ logD("Using all artists")
deviceLibrary.artists
})
_genresInstructions.put(UpdateInstructions.Diff)
@@ -170,12 +177,14 @@ constructor(
override fun onTabsChanged() {
// Tabs changed, update the current tabs and set up a re-create event.
currentTabModes = makeTabModes()
+ logD("Updating tabs: ${currentTabMode.value}")
_shouldRecreate.put(Unit)
}
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")
onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = false))
}
@@ -200,30 +209,34 @@ constructor(
* @param sort The new [Sort] to apply. Assumed to be an allowed sort for the current [Tab].
*/
fun setSortForCurrentTab(sort: Sort) {
- logD("Updating ${_currentTabMode.value} sort to $sort")
// Can simply re-sort the current list of items without having to access the library.
- when (_currentTabMode.value) {
+ when (val mode = _currentTabMode.value) {
MusicMode.SONGS -> {
+ logD("Updating song [$mode] sort mode to $sort")
musicSettings.songSort = sort
_songsInstructions.put(UpdateInstructions.Replace(0))
_songsList.value = sort.songs(_songsList.value)
}
MusicMode.ALBUMS -> {
+ logD("Updating album [$mode] sort mode to $sort")
musicSettings.albumSort = sort
_albumsInstructions.put(UpdateInstructions.Replace(0))
_albumsLists.value = sort.albums(_albumsLists.value)
}
MusicMode.ARTISTS -> {
+ logD("Updating artist [$mode] sort mode to $sort")
musicSettings.artistSort = sort
_artistsInstructions.put(UpdateInstructions.Replace(0))
_artistsList.value = sort.artists(_artistsList.value)
}
MusicMode.GENRES -> {
+ logD("Updating genre [$mode] sort mode to $sort")
musicSettings.genreSort = sort
_genresInstructions.put(UpdateInstructions.Replace(0))
_genresList.value = sort.genres(_genresList.value)
}
MusicMode.PLAYLISTS -> {
+ logD("Updating playlist [$mode] sort mode to $sort")
musicSettings.playlistSort = sort
_playlistsInstructions.put(UpdateInstructions.Replace(0))
_playlistsList.value = sort.playlists(_playlistsList.value)
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 3a848edf9..620ac018f 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
@@ -33,6 +33,7 @@ import android.text.TextUtils
import android.util.AttributeSet
import android.view.Gravity
import androidx.core.widget.TextViewCompat
+import com.google.android.material.R as MR
import com.google.android.material.textview.MaterialTextView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getAttrColorCompat
@@ -53,7 +54,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
minimumHeight = context.getDimenPixels(R.dimen.fast_scroll_popup_min_height)
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
- setTextColor(context.getAttrColorCompat(R.attr.colorOnSecondary))
+ setTextColor(context.getAttrColorCompat(MR.attr.colorOnSecondary))
ellipsize = TextUtils.TruncateAt.MIDDLE
gravity = Gravity.CENTER
includeFontPadding = false
@@ -67,7 +68,10 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
private val paint: Paint =
Paint().apply {
isAntiAlias = true
- color = context.getAttrColorCompat(R.attr.colorSecondary).defaultColor
+ color =
+ context
+ .getAttrColorCompat(com.google.android.material.R.attr.colorSecondary)
+ .defaultColor
style = Paint.Style.FILL
}
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 1d0cc6737..991fc6f4e 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
@@ -37,7 +37,12 @@ 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.util.*
+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
/**
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
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 a17172d08..3495bc85a 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
@@ -30,13 +30,18 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
-import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
import org.oxycblt.auxio.list.selection.SelectionViewModel
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicParent
+import org.oxycblt.auxio.music.MusicViewModel
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
@@ -78,7 +83,8 @@ class AlbumListFragment :
collectImmediately(homeModel.albumsList, ::updateAlbums)
collectImmediately(selectionModel.selected, ::updateSelection)
- collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
+ collectImmediately(
+ playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
@@ -101,7 +107,7 @@ class AlbumListFragment :
is Sort.Mode.ByArtist -> album.artists[0].name.thumb
// Date -> Use minimum date (Maximum dates are not sorted by, so showing them is odd)
- is Sort.Mode.ByDate -> album.dates?.run { min.resolveDate(requireContext()) }
+ is Sort.Mode.ByDate -> album.dates?.run { min.resolve(requireContext()) }
// Duration -> Use formatted duration
is Sort.Mode.ByDuration -> album.durationMs.formatDurationMs(false)
@@ -147,9 +153,11 @@ class AlbumListFragment :
albumAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
}
- private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
- // If an album is playing, highlight it within this adapter.
- albumAdapter.setPlaying(parent as? Album, isPlaying)
+ private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
+ // Only highlight the album if it is currently playing, and if the currently
+ // playing song is also contained within.
+ val album = (parent as? Album)?.takeIf { song?.album == it }
+ albumAdapter.setPlaying(album, isPlaying)
}
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
index 7eb5c88a0..e270fa7d2 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
@@ -28,8 +28,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
-import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
@@ -39,11 +39,11 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately
-import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.nonZeroOrNull
/**
@@ -78,7 +78,8 @@ class ArtistListFragment :
collectImmediately(homeModel.artistsList, ::updateArtists)
collectImmediately(selectionModel.selected, ::updateSelection)
- collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
+ collectImmediately(
+ playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
@@ -121,16 +122,18 @@ class ArtistListFragment :
}
private fun updateArtists(artists: List) {
- artistAdapter.update(artists, homeModel.artistsInstructions.consume().also { logD(it) })
+ artistAdapter.update(artists, homeModel.artistsInstructions.consume())
}
private fun updateSelection(selection: List) {
artistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
}
- private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
- // If an artist is playing, highlight it within this adapter.
- artistAdapter.setPlaying(parent as? Artist, isPlaying)
+ private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
+ // Only highlight the artist if it is currently playing, and if the currently
+ // playing song is also contained within.
+ val artist = (parent as? Artist)?.takeIf { song?.run { artists.contains(it) } ?: false }
+ artistAdapter.setPlaying(artist, isPlaying)
}
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
index 8b2cab6f3..ee9544d55 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
@@ -28,8 +28,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
-import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.GenreViewHolder
@@ -39,11 +39,11 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately
-import org.oxycblt.auxio.util.logD
/**
* A [ListFragment] that shows a list of [Genre]s.
@@ -77,7 +77,8 @@ class GenreListFragment :
collectImmediately(homeModel.genresList, ::updateGenres)
collectImmediately(selectionModel.selected, ::updateSelection)
- collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
+ collectImmediately(
+ playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
@@ -120,16 +121,18 @@ class GenreListFragment :
}
private fun updateGenres(genres: List) {
- genreAdapter.update(genres, homeModel.genresInstructions.consume().also { logD(it) })
+ genreAdapter.update(genres, homeModel.genresInstructions.consume())
}
private fun updateSelection(selection: List) {
genreAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
}
- private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
- // If a genre is playing, highlight it within this adapter.
- genreAdapter.setPlaying(parent as? Genre, isPlaying)
+ private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
+ // Only highlight the genre if it is currently playing, and if the currently
+ // playing song is also contained within.
+ val genre = (parent as? Genre)?.takeIf { song?.run { genres.contains(it) } ?: false }
+ genreAdapter.setPlaying(genre, isPlaying)
}
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt
index 4c5d8d19a..61fa54b7c 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt
@@ -27,8 +27,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
-import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.PlaylistViewHolder
@@ -38,18 +38,16 @@ import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately
-import org.oxycblt.auxio.util.logD
/**
* A [ListFragment] that shows a list of [Playlist]s.
*
* @author Alexander Capehart (OxygenCobalt)
- *
- * TODO: Show a placeholder when there are no playlists.
*/
class PlaylistListFragment :
ListFragment(),
@@ -77,7 +75,8 @@ class PlaylistListFragment :
collectImmediately(homeModel.playlistsList, ::updatePlaylists)
collectImmediately(selectionModel.selected, ::updateSelection)
- collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
+ collectImmediately(
+ playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
@@ -120,17 +119,18 @@ class PlaylistListFragment :
}
private fun updatePlaylists(playlists: List) {
- playlistAdapter.update(
- playlists, homeModel.playlistsInstructions.consume().also { logD(it) })
+ playlistAdapter.update(playlists, homeModel.playlistsInstructions.consume())
}
private fun updateSelection(selection: List) {
playlistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
}
- private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
- // If a playlist is playing, highlight it within this adapter.
- playlistAdapter.setPlaying(parent as? Playlist, isPlaying)
+ private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
+ // Only highlight the playlist if it is currently playing, and if the currently
+ // playing song is also contained within.
+ val playlist = (parent as? Playlist)?.takeIf { it.songs.contains(song) }
+ playlistAdapter.setPlaying(playlist, isPlaying)
}
/**
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 a21a470df..62643f4cf 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
@@ -30,8 +30,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
-import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder
@@ -155,12 +155,8 @@ class SongListFragment :
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
- if (parent == null) {
- songAdapter.setPlaying(song, isPlaying)
- } else {
- // Ignore playback that is not from all songs
- songAdapter.setPlaying(null, isPlaying)
- }
+ // Only indicate playback that is from all songs
+ songAdapter.setPlaying(song.takeIf { parent == null }, isPlaying)
}
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt
index 718c99855..36aed93bf 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt
@@ -23,7 +23,6 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicMode
-import org.oxycblt.auxio.util.logD
/**
* A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations
@@ -67,20 +66,11 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List) :
// Use expected sw* size thresholds when choosing a configuration.
when {
// On small screens, only display an icon.
- width < 370 -> {
- logD("Using icon-only configuration")
- tab.setIcon(icon).setContentDescription(string)
- }
+ width < 370 -> tab.setIcon(icon).setContentDescription(string)
// On large screens, display an icon and text.
- width < 600 -> {
- logD("Using text-only configuration")
- tab.setText(string)
- }
+ width < 600 -> tab.setText(string)
// On medium-size screens, display text.
- else -> {
- logD("Using icon-and-text configuration")
- tab.setIcon(icon).setText(string)
- }
+ else -> tab.setIcon(icon).setText(string)
}
}
}
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 30425e6d8..2fddd1b4a 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
@@ -20,6 +20,7 @@ package org.oxycblt.auxio.home.tabs
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logE
+import org.oxycblt.auxio.util.logW
/**
* A representation of a library tab suitable for configuration.
@@ -84,6 +85,10 @@ sealed class Tab(open val mode: MusicMode) {
fun toIntCode(tabs: Array): Int {
// Like when deserializing, make sure there are no duplicate tabs for whatever reason.
val distinct = tabs.distinctBy { it.mode }
+ if (tabs.size != distinct.size) {
+ logW(
+ "Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]")
+ }
var sequence = 0
var shift = MAX_SEQUENCE_IDX * 4
@@ -127,6 +132,10 @@ sealed class Tab(open val mode: MusicMode) {
// Make sure there are no duplicate tabs
val distinct = tabs.distinctBy { it.mode }
+ if (tabs.size != distinct.size) {
+ logW(
+ "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) {
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 9e778cca1..277c0c39b 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,6 +28,7 @@ import org.oxycblt.auxio.list.EditClickListListener
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.inflater
+import org.oxycblt.auxio.util.logD
/**
* A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration.
@@ -52,6 +53,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")
tabs = newTabs
@Suppress("NotifyDatasetChanged") notifyDataSetChanged()
}
@@ -63,6 +65,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]")
tabs[at] = tab
// Use a payload to avoid an item change animation.
notifyItemChanged(at, PAYLOAD_TAB_CHANGED)
@@ -75,6 +78,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]")
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 dae73e93e..c7dadd8d2 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
@@ -91,14 +91,15 @@ class TabCustomizeDialog :
// We will need the exact index of the tab to update on in order to
// notify the adapter of the change.
val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode }
- val tab = tabAdapter.tabs[index]
- tabAdapter.setTab(
- index,
- when (tab) {
+ val old = tabAdapter.tabs[index]
+ val new =
+ when (old) {
// Invert the visibility of the tab
- is Tab.Visible -> Tab.Invisible(tab.mode)
- is Tab.Invisible -> Tab.Visible(tab.mode)
- })
+ is Tab.Visible -> Tab.Invisible(old.mode)
+ is Tab.Invisible -> Tab.Visible(old.mode)
+ }
+ logD("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.
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt
index 064d5f8dd..49af3b57b 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt
@@ -63,7 +63,9 @@ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callbac
return true
}
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+ throw IllegalStateException()
+ }
// We use a custom drag handle, so disable the long press action.
override fun isLongPressDragEnabled() = false
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 bd19c3a87..ad81c25a9 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt
@@ -27,7 +27,6 @@ import coil.request.ImageRequest
import coil.size.Size
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
-import org.oxycblt.auxio.image.extractor.SquareFrameTransform
import org.oxycblt.auxio.music.Song
/**
@@ -97,16 +96,11 @@ constructor(
ImageRequest.Builder(context)
.data(listOf(song))
// Use ORIGINAL sizing, as we are not loading into any View-like component.
- .size(Size.ORIGINAL)
- .transformations(SquareFrameTransform.INSTANCE))
- // Override the target in order to deliver the bitmap to the given
- // listener.
+ .size(Size.ORIGINAL))
.target(
onSuccess = {
synchronized(this) {
if (currentHandle == handle) {
- // Has not been superseded by a new request, can deliver
- // this result.
target.onCompleted(it.toBitmap())
}
}
@@ -114,8 +108,6 @@ constructor(
onError = {
synchronized(this) {
if (currentHandle == handle) {
- // Has not been superseded by a new request, can deliver
- // this result.
target.onCompleted(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
new file mode 100644
index 000000000..a4d0e6917
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
@@ -0,0 +1,441 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * CoverView.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.image
+
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Matrix
+import android.graphics.PixelFormat
+import android.graphics.RectF
+import android.graphics.drawable.AnimationDrawable
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.annotation.AttrRes
+import androidx.annotation.DimenRes
+import androidx.annotation.DrawableRes
+import androidx.core.content.res.getIntOrThrow
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.children
+import androidx.core.view.updateMarginsRelative
+import androidx.core.widget.ImageViewCompat
+import coil.ImageLoader
+import coil.request.ImageRequest
+import coil.util.CoilUtils
+import com.google.android.material.R as MR
+import com.google.android.material.shape.MaterialShapeDrawable
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.music.Album
+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.UISettings
+import org.oxycblt.auxio.util.getAttrColorCompat
+import org.oxycblt.auxio.util.getColorCompat
+import org.oxycblt.auxio.util.getDimen
+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
+ * selection badge. In practice, it's three [ImageView]'s in a [FrameLayout] trenchcoat. By default,
+ * all of this functionality is enabled. The playback indicator and selection badge selectively
+ * disabled with the "playbackIndicatorEnabled" and "selectionBadgeEnabled" attributes, and image
+ * itself can be overridden if populated like a normal [FrameLayout].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class CoverView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
+ FrameLayout(context, attrs, defStyleAttr) {
+ @Inject lateinit var imageLoader: ImageLoader
+ @Inject lateinit var uiSettings: UISettings
+
+ private val image: ImageView
+
+ data class PlaybackIndicator(
+ val view: ImageView,
+ val playingDrawable: AnimationDrawable,
+ val pausedDrawable: Drawable
+ )
+ private val playbackIndicator: PlaybackIndicator?
+ private val selectionBadge: ImageView?
+
+ @DimenRes private val iconSizeRes: Int?
+ @DimenRes private val cornerRadiusRes: Int?
+
+ private var fadeAnimator: ValueAnimator? = null
+ private val indicatorMatrix = Matrix()
+ private val indicatorMatrixSrc = RectF()
+ private val indicatorMatrixDst = RectF()
+
+ init {
+ // Obtain some StyledImageView attributes to use later when theming the custom view.
+ @SuppressLint("CustomViewStyleable")
+ val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.CoverView)
+
+ val sizing = styledAttrs.getIntOrThrow(R.styleable.CoverView_sizing)
+ iconSizeRes = SIZING_ICON_SIZE[sizing]
+ cornerRadiusRes =
+ if (uiSettings.roundMode) {
+ SIZING_CORNER_RADII[sizing]
+ } else {
+ null
+ }
+
+ val playbackIndicatorEnabled =
+ styledAttrs.getBoolean(R.styleable.CoverView_enablePlaybackIndicator, true)
+ val selectionBadgeEnabled =
+ styledAttrs.getBoolean(R.styleable.CoverView_enableSelectionBadge, true)
+
+ styledAttrs.recycle()
+
+ image = ImageView(context, attrs)
+
+ // Initialize the playback indicator if enabled.
+ playbackIndicator =
+ if (playbackIndicatorEnabled) {
+ PlaybackIndicator(
+ ImageView(context).apply {
+ scaleType = ImageView.ScaleType.MATRIX
+ ImageViewCompat.setImageTintList(
+ this, context.getColorCompat(R.color.sel_on_cover_bg))
+ },
+ context.getDrawableCompat(R.drawable.ic_playing_indicator_24)
+ as AnimationDrawable,
+ context.getDrawableCompat(R.drawable.ic_paused_indicator_24))
+ } else {
+ null
+ }
+
+ // Initialize the selection badge if enabled.
+ selectionBadge =
+ if (selectionBadgeEnabled) {
+ ImageView(context).apply {
+ imageTintList = context.getAttrColorCompat(MR.attr.colorOnPrimary)
+ setImageResource(R.drawable.ic_check_20)
+ setBackgroundResource(R.drawable.ui_selection_badge_bg)
+ }
+ } else {
+ null
+ }
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ // The image isn't added if other children have populated the body. This is by design.
+ if (childCount == 0) {
+ addView(image)
+ }
+
+ playbackIndicator?.run { addView(view) }
+
+ // Add backgrounds to each child for visual consistency
+ for (child in children) {
+ child.apply {
+ // If there are rounded corners, we want to make sure view content will be cropped
+ // with it.
+ clipToOutline = this != image
+ background =
+ MaterialShapeDrawable().apply {
+ fillColor = context.getColorCompat(R.color.sel_cover_bg)
+ setCornerSize(cornerRadiusRes?.let(context::getDimen) ?: 0f)
+ }
+ }
+ }
+
+ // The selection badge has it's own background we don't want overridden, add it after
+ // all other elements.
+ selectionBadge?.let {
+ addView(
+ it,
+ // Position the selection badge to the bottom right.
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
+ // Override the layout params of the indicator so that it's in the
+ // bottom left corner.
+ gravity = Gravity.BOTTOM or Gravity.END
+ val spacing = context.getDimenPixels(R.dimen.spacing_tiny)
+ updateMarginsRelative(bottom = spacing, end = spacing)
+ })
+ }
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+
+ // AnimatedVectorDrawable cannot be placed in a StyledDrawable, we must replicate the
+ // behavior with a matrix.
+ val playbackIndicator = (playbackIndicator ?: return).view
+ val iconSize = iconSizeRes?.let(context::getDimenPixels) ?: (measuredWidth / 2)
+ playbackIndicator.apply {
+ imageMatrix =
+ indicatorMatrix.apply {
+ reset()
+ drawable?.let { drawable ->
+ // First scale the icon up to the desired size.
+ indicatorMatrixSrc.set(
+ 0f,
+ 0f,
+ drawable.intrinsicWidth.toFloat(),
+ drawable.intrinsicHeight.toFloat())
+ indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
+ indicatorMatrix.setRectToRect(
+ indicatorMatrixSrc, indicatorMatrixDst, Matrix.ScaleToFit.CENTER)
+
+ // Then actually center it into the icon.
+ indicatorMatrix.postTranslate(
+ (measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f)
+ }
+ }
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ invalidateRootAlpha()
+ invalidatePlaybackIndicatorAlpha(playbackIndicator ?: return)
+ invalidateSelectionIndicatorAlpha(selectionBadge ?: return)
+ }
+
+ override fun setEnabled(enabled: Boolean) {
+ super.setEnabled(enabled)
+ invalidateRootAlpha()
+ }
+
+ override fun setSelected(selected: Boolean) {
+ super.setSelected(selected)
+ invalidateRootAlpha()
+ invalidatePlaybackIndicatorAlpha(playbackIndicator ?: return)
+ }
+
+ override fun setActivated(activated: Boolean) {
+ super.setActivated(activated)
+ invalidateSelectionIndicatorAlpha(selectionBadge ?: return)
+ }
+
+ /**
+ * Set if the playback indicator should be indicated ongoing or paused playback.
+ *
+ * @param isPlaying Whether playback is ongoing or paused.
+ */
+ fun setPlaying(isPlaying: Boolean) {
+ playbackIndicator?.run {
+ if (isPlaying) {
+ playingDrawable.start()
+ view.setImageDrawable(playingDrawable)
+ } else {
+ playingDrawable.stop()
+ view.setImageDrawable(pausedDrawable)
+ }
+ }
+ }
+
+ private fun invalidateRootAlpha() {
+ alpha = if (isEnabled || isSelected) 1f else 0.5f
+ }
+
+ private fun invalidatePlaybackIndicatorAlpha(playbackIndicator: PlaybackIndicator) {
+ // Occasionally content can bleed through the rounded corners and result in a seam
+ // on the playing indicator, prevent that from occurring by disabling the visibility of
+ // all views below the playback indicator.
+ for (child in children) {
+ child.alpha =
+ when (child) {
+ // Selection badge is above the playback indicator, do nothing
+ selectionBadge -> child.alpha
+ playbackIndicator.view -> if (isSelected) 1f else 0f
+ else -> if (isSelected) 0f else 1f
+ }
+ }
+ }
+
+ 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 =
+ ValueAnimator.ofFloat(selectionBadge.alpha, targetAlpha).apply {
+ duration = targetDuration
+ addUpdateListener { selectionBadge.alpha = it.animatedValue as Float }
+ start()
+ }
+ }
+
+ /**
+ * Bind a [Song]'s image to this view.
+ *
+ * @param song The [Song] to bind to the view.
+ */
+ fun bind(song: Song) =
+ bind(
+ listOf(song),
+ context.getString(R.string.desc_album_cover, song.album.name),
+ R.drawable.ic_album_24)
+
+ /**
+ * Bind an [Album]'s image to this view.
+ *
+ * @param album The [Album] to bind to the view.
+ */
+ fun bind(album: Album) =
+ bind(
+ album.songs,
+ context.getString(R.string.desc_album_cover, album.name),
+ R.drawable.ic_album_24)
+
+ /**
+ * Bind an [Artist]'s image to this view.
+ *
+ * @param artist The [Artist] to bind to the view.
+ */
+ fun bind(artist: Artist) =
+ bind(
+ artist.songs,
+ context.getString(R.string.desc_artist_image, artist.name),
+ R.drawable.ic_artist_24)
+
+ /**
+ * Bind a [Genre]'s image to this view.
+ *
+ * @param genre The [Genre] to bind to the view.
+ */
+ fun bind(genre: Genre) =
+ bind(
+ genre.songs,
+ context.getString(R.string.desc_genre_image, genre.name),
+ R.drawable.ic_genre_24)
+
+ /**
+ * Bind a [Playlist]'s image to this view.
+ *
+ * @param playlist the [Playlist] to bind.
+ */
+ fun bind(playlist: Playlist) =
+ bind(
+ playlist.songs,
+ context.getString(R.string.desc_playlist_image, playlist.name),
+ R.drawable.ic_playlist_24)
+
+ /**
+ * Bind the covers of a generic list of [Song]s.
+ *
+ * @param songs The [Song]s to bind.
+ * @param desc The content description to describe the bound data.
+ * @param errorRes The resource of the error drawable to use if the cover cannot be loaded.
+ */
+ fun bind(songs: List, desc: String, @DrawableRes errorRes: Int) {
+ val request =
+ ImageRequest.Builder(context)
+ .data(songs)
+ .error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes))
+ .transformations(
+ RoundedCornersTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f))
+ .target(image)
+ .build()
+ // Dispose of any previous image request and load a new image.
+ CoilUtils.dispose(image)
+ imageLoader.enqueue(request)
+ contentDescription = desc
+ }
+
+ /**
+ * Since the error drawable must also share a view with an image, any kind of transform or tint
+ * must occur within a custom dialog, which is implemented here.
+ */
+ private class StyledDrawable(
+ context: Context,
+ private val inner: Drawable,
+ @DimenRes iconSizeRes: Int?
+ ) : Drawable() {
+ init {
+ // Re-tint the drawable to use the analogous "on surface" color for
+ // StyledImageView.
+ DrawableCompat.setTintList(inner, context.getColorCompat(R.color.sel_on_cover_bg))
+ }
+
+ private val dimen = iconSizeRes?.let(context::getDimenPixels)
+
+ override fun draw(canvas: Canvas) {
+ // Resize the drawable such that it's always 1/4 the size of the image and
+ // centered in the middle of the canvas.
+ val adj = dimen?.let { (bounds.width() - it) / 2 } ?: (bounds.width() / 4)
+ inner.bounds.set(adj, adj, bounds.width() - adj, bounds.height() - adj)
+ inner.draw(canvas)
+ }
+
+ // Required drawable overrides. Just forward to the wrapped drawable.
+
+ override fun setAlpha(alpha: Int) {
+ inner.alpha = alpha
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {
+ inner.colorFilter = colorFilter
+ }
+
+ override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
+ }
+
+ companion object {
+ val SIZING_CORNER_RADII =
+ arrayOf(
+ R.dimen.size_corners_small, R.dimen.size_corners_small, R.dimen.size_corners_medium)
+ 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/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt
deleted file mode 100644
index 449f489fc..000000000
--- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * Copyright (c) 2022 Auxio Project
- * ImageGroup.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.image
-
-import android.animation.ValueAnimator
-import android.annotation.SuppressLint
-import android.content.Context
-import android.util.AttributeSet
-import android.view.Gravity
-import android.view.View
-import android.widget.FrameLayout
-import android.widget.ImageView
-import androidx.annotation.AttrRes
-import androidx.core.view.updateMarginsRelative
-import com.google.android.material.shape.MaterialShapeDrawable
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.music.*
-import org.oxycblt.auxio.util.getAttrColorCompat
-import org.oxycblt.auxio.util.getColorCompat
-import org.oxycblt.auxio.util.getDimenPixels
-import org.oxycblt.auxio.util.getInteger
-
-/**
- * A super-charged [StyledImageView]. This class enables the following features in addition to
- * [StyledImageView]:
- * - A selection indicator
- * - An activation (playback) indicator
- * - Support for ONE custom view
- *
- * This class is primarily intended for list items. For other uses, [StyledImageView] is more
- * suitable.
- *
- * @author Alexander Capehart (OxygenCobalt)
- *
- * TODO: Rework content descriptions here
- * TODO: Attempt unification with StyledImageView with some kind of dynamic configuration to avoid
- * superfluous elements
- * TODO: Handle non-square covers by gracefully placing them in the layout
- */
-class ImageGroup
-@JvmOverloads
-constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
- FrameLayout(context, attrs, defStyleAttr) {
- private val innerImageView: StyledImageView
- private var customView: View? = null
- private val playbackIndicatorView: PlaybackIndicatorView
- private val selectionIndicatorView: ImageView
-
- private var fadeAnimator: ValueAnimator? = null
- private val cornerRadius: Float
-
- init {
- // Obtain some StyledImageView attributes to use later when theming the custom view.
- @SuppressLint("CustomViewStyleable")
- val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
- // Keep track of our corner radius so that we can apply the same attributes to the custom
- // view.
- cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
- styledAttrs.recycle()
-
- // Initialize what views we can here.
- innerImageView = StyledImageView(context, attrs)
- playbackIndicatorView =
- PlaybackIndicatorView(context).apply { cornerRadius = this@ImageGroup.cornerRadius }
- selectionIndicatorView =
- ImageView(context).apply {
- imageTintList = context.getAttrColorCompat(R.attr.colorOnPrimary)
- setImageResource(R.drawable.ic_check_20)
- setBackgroundResource(R.drawable.ui_selection_badge_bg)
- }
-
- // The inner StyledImageView should be at the bottom and hidden by any other elements
- // if they become visible.
- addView(innerImageView)
- }
-
- override fun onFinishInflate() {
- super.onFinishInflate()
- // Due to innerImageView, the max child count is actually 2 and not 1.
- check(childCount < 3) { "Only one custom view is allowed" }
-
- // Get the second inflated child, making sure we customize it to align with
- // the rest of this view.
- customView =
- getChildAt(1)?.apply {
- background =
- MaterialShapeDrawable().apply {
- fillColor = context.getColorCompat(R.color.sel_cover_bg)
- setCornerSize(cornerRadius)
- }
- }
-
- // Playback indicator should sit above the inner StyledImageView and custom view/
- addView(playbackIndicatorView)
- // Selection indicator should never be obscured, so place it at the top.
- addView(
- selectionIndicatorView,
- LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
- // Override the layout params of the indicator so that it's in the
- // bottom left corner.
- gravity = Gravity.BOTTOM or Gravity.END
- val spacing = context.getDimenPixels(R.dimen.spacing_tiny)
- updateMarginsRelative(bottom = spacing, end = spacing)
- })
- }
-
- override fun onAttachedToWindow() {
- super.onAttachedToWindow()
- // Initialize each component before this view is drawn.
- invalidateImageAlpha()
- invalidatePlayingIndicator()
- invalidateSelectionIndicator()
- }
-
- override fun setActivated(activated: Boolean) {
- super.setActivated(activated)
- invalidateSelectionIndicator()
- }
-
- override fun setEnabled(enabled: Boolean) {
- super.setEnabled(enabled)
- invalidateImageAlpha()
- invalidatePlayingIndicator()
- }
-
- override fun setSelected(selected: Boolean) {
- super.setSelected(selected)
- invalidateImageAlpha()
- invalidatePlayingIndicator()
- }
-
- /**
- * Bind a [Song] to the internal [StyledImageView].
- *
- * @param song The [Song] to bind to the view.
- * @see StyledImageView.bind
- */
- fun bind(song: Song) = innerImageView.bind(song)
-
- /**
- * Bind a [Album] to the internal [StyledImageView].
- *
- * @param album The [Album] to bind to the view.
- * @see StyledImageView.bind
- */
- fun bind(album: Album) = innerImageView.bind(album)
-
- /**
- * Bind a [Genre] to the internal [StyledImageView].
- *
- * @param artist The [Artist] to bind to the view.
- * @see StyledImageView.bind
- */
- fun bind(artist: Artist) = innerImageView.bind(artist)
-
- /**
- * Bind a [Genre] to the internal [StyledImageView].
- *
- * @param genre The [Genre] to bind to the view.
- * @see StyledImageView.bind
- */
- fun bind(genre: Genre) = innerImageView.bind(genre)
-
- /**
- * Bind a [Playlist]'s image to the internal [StyledImageView].
- *
- * @param playlist the [Playlist] to bind.
- * @see StyledImageView.bind
- */
- fun bind(playlist: Playlist) = innerImageView.bind(playlist)
-
- /**
- * Whether this view should be indicated to have ongoing playback or not. See
- * PlaybackIndicatorView for more information on what occurs here. Note: It's expected for this
- * view to already be marked as playing with setSelected (not the same thing) before this is set
- * to true.
- */
- var isPlaying: Boolean
- get() = playbackIndicatorView.isPlaying
- set(value) {
- playbackIndicatorView.isPlaying = value
- }
-
- private fun invalidateImageAlpha() {
- // If this view is disabled, show it at half-opacity, *unless* it is also marked
- // as playing, in which we still want to show it at full-opacity.
- alpha = if (isSelected || isEnabled) 1f else 0.5f
- }
-
- private fun invalidatePlayingIndicator() {
- if (isSelected) {
- // View is "selected" (actually marked as playing), so show the playing indicator
- // and hide all other elements except for the selection indicator.
- // TODO: Animate the other indicators?
- customView?.alpha = 0f
- innerImageView.alpha = 0f
- playbackIndicatorView.alpha = 1f
- } else {
- // View is not "selected", hide the playing indicator.
- customView?.alpha = 1f
- innerImageView.alpha = 1f
- playbackIndicatorView.alpha = 0f
- }
- }
-
- private fun invalidateSelectionIndicator() {
- // 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 (selectionIndicatorView.alpha == targetAlpha) {
- // Nothing to do.
- return
- }
-
- if (!isLaidOut) {
- // Not laid out, initialize it without animation before drawing.
- selectionIndicatorView.alpha = targetAlpha
- return
- }
-
- if (fadeAnimator != null) {
- // Cancel any previous animation.
- fadeAnimator?.cancel()
- fadeAnimator = null
- }
-
- fadeAnimator =
- ValueAnimator.ofFloat(selectionIndicatorView.alpha, targetAlpha).apply {
- duration = targetDuration
- addUpdateListener { selectionIndicatorView.alpha = it.animatedValue as Float }
- start()
- }
- }
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt
index b3bb1cf2f..a45fc4cbb 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt
@@ -22,7 +22,6 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
-import org.oxycblt.auxio.image.extractor.*
@Module
@InstallIn(SingletonComponent::class)
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 7f1aca57f..a4c13c4c9 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
@@ -73,6 +73,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)) {
+ logD("Dispatching cover mode setting change")
listener.onCoverModeChanged()
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt
deleted file mode 100644
index 68c9bcd44..000000000
--- a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (c) 2022 Auxio Project
- * PlaybackIndicatorView.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.image
-
-import android.content.Context
-import android.graphics.Matrix
-import android.graphics.RectF
-import android.graphics.drawable.AnimationDrawable
-import android.util.AttributeSet
-import androidx.annotation.AttrRes
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.core.widget.ImageViewCompat
-import com.google.android.material.shape.MaterialShapeDrawable
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-import kotlin.math.max
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.ui.UISettings
-import org.oxycblt.auxio.util.getColorCompat
-import org.oxycblt.auxio.util.getDrawableCompat
-
-/**
- * A view that displays an activation (i.e playback) indicator, with an accented styling and an
- * animated equalizer icon.
- *
- * This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable]
- * instances within custom views, this cannot be merged with [ImageGroup].
- *
- * @author Alexander Capehart (OxygenCobalt)
- */
-@AndroidEntryPoint
-class PlaybackIndicatorView
-@JvmOverloads
-constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
- AppCompatImageView(context, attrs, defStyleAttr) {
- private val playingIndicatorDrawable =
- context.getDrawableCompat(R.drawable.ic_playing_indicator_24) as AnimationDrawable
- private val pausedIndicatorDrawable =
- context.getDrawableCompat(R.drawable.ic_paused_indicator_24)
- private val indicatorMatrix = Matrix()
- private val indicatorMatrixSrc = RectF()
- private val indicatorMatrixDst = RectF()
- @Inject lateinit var uiSettings: UISettings
-
- /**
- * The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
- * to this view without any attribute hacks.
- */
- var cornerRadius = 0f
- set(value) {
- field = value
- (background as? MaterialShapeDrawable)?.let { bg ->
- if (uiSettings.roundMode) {
- bg.setCornerSize(value)
- } else {
- bg.setCornerSize(0f)
- }
- }
- }
-
- /**
- * Whether this view should be indicated to have ongoing playback or not. If true, the animated
- * playing icon will be shown. If false, the static paused icon will be shown.
- */
- var isPlaying: Boolean
- get() = drawable == playingIndicatorDrawable
- set(value) {
- if (value) {
- playingIndicatorDrawable.start()
- setImageDrawable(playingIndicatorDrawable)
- } else {
- playingIndicatorDrawable.stop()
- setImageDrawable(pausedIndicatorDrawable)
- }
- }
-
- init {
- // We will need to manually re-scale the playing/paused drawables to align with
- // StyledDrawable, so use the matrix scale type.
- scaleType = ScaleType.MATRIX
- // Tint the playing/paused drawables so they are harmonious with the background.
- ImageViewCompat.setImageTintList(this, context.getColorCompat(R.color.sel_on_cover_bg))
-
- // Use clipToOutline and a background drawable to crop images. While Coil's transformation
- // could theoretically be used to round corners, the corner radius is dependent on the
- // dimensions of the image, which will result in inconsistent corners across different
- // album covers unless we resize all covers to be the same size. clipToOutline is both
- // cheaper and more elegant. As a side-note, this also allows us to re-use the same
- // background for both the tonal background color and the corner rounding.
- clipToOutline = true
- background =
- MaterialShapeDrawable().apply {
- fillColor = context.getColorCompat(R.color.sel_cover_bg)
- setCornerSize(cornerRadius)
- }
- }
-
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-
- // Emulate StyledDrawable scaling with matrix scaling.
- val iconSize = max(measuredWidth, measuredHeight) / 2
- imageMatrix =
- indicatorMatrix.apply {
- reset()
- drawable?.let { drawable ->
- // First scale the icon up to the desired size.
- indicatorMatrixSrc.set(
- 0f,
- 0f,
- drawable.intrinsicWidth.toFloat(),
- drawable.intrinsicHeight.toFloat())
- indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
- indicatorMatrix.setRectToRect(
- indicatorMatrixSrc, indicatorMatrixDst, Matrix.ScaleToFit.CENTER)
-
- // Then actually center it into the icon.
- indicatorMatrix.postTranslate(
- (measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f)
- }
- }
- }
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/RoundedCornersTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/RoundedCornersTransformation.kt
new file mode 100644
index 000000000..c25770ec4
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/image/RoundedCornersTransformation.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * RoundedCornersTransformation.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.image
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.createBitmap
+import android.graphics.BitmapShader
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PorterDuff
+import android.graphics.RectF
+import android.graphics.Shader
+import androidx.annotation.Px
+import androidx.core.graphics.applyCanvas
+import coil.decode.DecodeUtils
+import coil.size.Scale
+import coil.size.Size
+import coil.size.pxOrElse
+import coil.transform.Transformation
+import kotlin.math.roundToInt
+
+/**
+ * A vendoring of [coil.transform.RoundedCornersTransformation] that can handle non-1:1 aspect ratio
+ * images without cropping them.
+ *
+ * @author Coil Team, Alexander Capehart (OxygenCobalt)
+ */
+class RoundedCornersTransformation(
+ @Px private val topLeft: Float = 0f,
+ @Px private val topRight: Float = 0f,
+ @Px private val bottomLeft: Float = 0f,
+ @Px private val bottomRight: Float = 0f
+) : Transformation {
+
+ constructor(@Px radius: Float) : this(radius, radius, radius, radius)
+
+ init {
+ require(topLeft >= 0 && topRight >= 0 && bottomLeft >= 0 && bottomRight >= 0) {
+ "All radii must be >= 0."
+ }
+ }
+
+ override val cacheKey = "${javaClass.name}-$topLeft,$topRight,$bottomLeft,$bottomRight"
+
+ override suspend fun transform(input: Bitmap, size: Size): Bitmap {
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
+
+ val (outputWidth, outputHeight) = calculateOutputSize(input, size)
+
+ val output = createBitmap(outputWidth, outputHeight, input.config)
+ output.applyCanvas {
+ drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
+
+ val matrix = Matrix()
+ val multiplier =
+ DecodeUtils.computeSizeMultiplier(
+ srcWidth = input.width,
+ srcHeight = input.height,
+ dstWidth = outputWidth,
+ dstHeight = outputHeight,
+ scale = Scale.FILL)
+ .toFloat()
+ val dx = (outputWidth - multiplier * input.width) / 2
+ val dy = (outputHeight - multiplier * input.height) / 2
+ matrix.setTranslate(dx, dy)
+ matrix.preScale(multiplier, multiplier)
+
+ val shader = BitmapShader(input, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+ shader.setLocalMatrix(matrix)
+ paint.shader = shader
+
+ val radii =
+ floatArrayOf(
+ topLeft,
+ topLeft,
+ topRight,
+ topRight,
+ bottomRight,
+ bottomRight,
+ bottomLeft,
+ bottomLeft,
+ )
+ val rect = RectF(0f, 0f, width.toFloat(), height.toFloat())
+ val path = Path().apply { addRoundRect(rect, radii, Path.Direction.CW) }
+ drawPath(path, paint)
+ }
+
+ return output
+ }
+
+ private fun calculateOutputSize(input: Bitmap, size: Size): Pair {
+ // MODIFICATION: Remove short-circuiting for original size and input size
+ val multiplier =
+ DecodeUtils.computeSizeMultiplier(
+ srcWidth = input.width,
+ srcHeight = input.height,
+ dstWidth = size.width.pxOrElse { Int.MIN_VALUE },
+ dstHeight = size.height.pxOrElse { Int.MIN_VALUE },
+ scale = Scale.FIT)
+ val outputWidth = (multiplier * input.width).roundToInt()
+ val outputHeight = (multiplier * input.height).roundToInt()
+ return outputWidth to outputHeight
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ return other is RoundedCornersTransformation &&
+ topLeft == other.topLeft &&
+ topRight == other.topRight &&
+ bottomLeft == other.bottomLeft &&
+ bottomRight == other.bottomRight
+ }
+
+ override fun hashCode(): Int {
+ var result = topLeft.hashCode()
+ result = 31 * result + topRight.hashCode()
+ result = 31 * result + bottomLeft.hashCode()
+ result = 31 * result + bottomRight.hashCode()
+ return result
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt
deleted file mode 100644
index 2e04617e5..000000000
--- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (c) 2022 Auxio Project
- * StyledImageView.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.image
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.ColorFilter
-import android.graphics.PixelFormat
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import androidx.annotation.AttrRes
-import androidx.annotation.DrawableRes
-import androidx.annotation.StringRes
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.core.content.res.ResourcesCompat
-import androidx.core.graphics.drawable.DrawableCompat
-import coil.ImageLoader
-import coil.request.ImageRequest
-import coil.util.CoilUtils
-import com.google.android.material.shape.MaterialShapeDrawable
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.image.extractor.SquareFrameTransform
-import org.oxycblt.auxio.music.*
-import org.oxycblt.auxio.ui.UISettings
-import org.oxycblt.auxio.util.getColorCompat
-import org.oxycblt.auxio.util.getDrawableCompat
-
-/**
- * An [AppCompatImageView] with some additional styling, including:
- * - Tonal background
- * - Rounded corners based on user preferences
- * - Built-in support for binding image data or using a static icon with the same styling as
- * placeholder drawables.
- *
- * @author Alexander Capehart (OxygenCobalt)
- */
-@AndroidEntryPoint
-class StyledImageView
-@JvmOverloads
-constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
- AppCompatImageView(context, attrs, defStyleAttr) {
- @Inject lateinit var imageLoader: ImageLoader
- @Inject lateinit var uiSettings: UISettings
-
- init {
- // Load view attributes
- val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
- val staticIcon =
- styledAttrs.getResourceId(
- R.styleable.StyledImageView_staticIcon, ResourcesCompat.ID_NULL)
- val cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
- styledAttrs.recycle()
-
- if (staticIcon != ResourcesCompat.ID_NULL) {
- // Use the static icon if specified for this image.
- setImageDrawable(StyledDrawable(context, context.getDrawableCompat(staticIcon)))
- }
-
- // Use clipToOutline and a background drawable to crop images. While Coil's transformation
- // could theoretically be used to round corners, the corner radius is dependent on the
- // dimensions of the image, which will result in inconsistent corners across different
- // album covers unless we resize all covers to be the same size. clipToOutline is both
- // cheaper and more elegant. As a side-note, this also allows us to re-use the same
- // background for both the tonal background color and the corner rounding.
- clipToOutline = true
- background =
- MaterialShapeDrawable().apply {
- fillColor = context.getColorCompat(R.color.sel_cover_bg)
- if (uiSettings.roundMode) {
- // Only use the specified corner radius when round mode is enabled.
- setCornerSize(cornerRadius)
- }
- }
- }
-
- /**
- * Bind a [Song]'s album cover to this view, also updating the content description.
- *
- * @param song The [Song] to bind.
- */
- fun bind(song: Song) = bind(song.album)
-
- /**
- * Bind an [Album]'s cover to this view, also updating the content description.
- *
- * @param album the [Album] to bind.
- */
- fun bind(album: Album) = bind(album, R.drawable.ic_album_24, R.string.desc_album_cover)
-
- /**
- * Bind an [Artist]'s image to this view, also updating the content description.
- *
- * @param artist the [Artist] to bind.
- */
- fun bind(artist: Artist) = bind(artist, R.drawable.ic_artist_24, R.string.desc_artist_image)
-
- /**
- * Bind an [Genre]'s image to this view, also updating the content description.
- *
- * @param genre the [Genre] to bind.
- */
- fun bind(genre: Genre) = bind(genre, R.drawable.ic_genre_24, R.string.desc_genre_image)
-
- /**
- * Bind a [Playlist]'s image to this view, also updating the content description.
- *
- * @param playlist The [Playlist] to bind.
- * @param songs [Song]s that can override the playlist image if it needs to differ for any
- * reason.
- */
- fun bind(playlist: Playlist, songs: List? = null) =
- if (songs != null) {
- bind(
- songs,
- context.getString(R.string.desc_playlist_image, playlist.name.resolve(context)),
- R.drawable.ic_playlist_24)
- } else {
- bind(playlist, R.drawable.ic_playlist_24, R.string.desc_playlist_image)
- }
-
- private fun bind(parent: MusicParent, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
- bind(parent.songs, context.getString(descRes, parent.name.resolve(context)), errorRes)
- }
-
- private fun bind(songs: List, desc: String, @DrawableRes errorRes: Int) {
- val request =
- ImageRequest.Builder(context)
- .data(songs)
- .error(StyledDrawable(context, context.getDrawableCompat(errorRes)))
- .transformations(SquareFrameTransform.INSTANCE)
- .target(this)
- .build()
- // Dispose of any previous image request and load a new image.
- CoilUtils.dispose(this)
- imageLoader.enqueue(request)
- contentDescription = desc
- }
-
- /**
- * A [Drawable] wrapper that re-styles the drawable to better align with the style of
- * [StyledImageView].
- *
- * @param context [Context] required for initialization.
- * @param inner The [Drawable] to wrap.
- */
- private class StyledDrawable(context: Context, private val inner: Drawable) : Drawable() {
- init {
- // Re-tint the drawable to use the analogous "on surface" color for
- // StyledImageView.
- DrawableCompat.setTintList(inner, context.getColorCompat(R.color.sel_on_cover_bg))
- }
-
- override fun draw(canvas: Canvas) {
- // Resize the drawable such that it's always 1/4 the size of the image and
- // centered in the middle of the canvas.
- val adjustWidth = bounds.width() / 4
- val adjustHeight = bounds.height() / 4
- inner.bounds.set(
- adjustWidth,
- adjustHeight,
- bounds.width() - adjustWidth,
- bounds.height() - adjustHeight)
- inner.draw(canvas)
- }
-
- // Required drawable overrides. Just forward to the wrapped drawable.
-
- override fun setAlpha(alpha: Int) {
- inner.alpha = alpha
- }
-
- override fun setColorFilter(colorFilter: ColorFilter?) {
- inner.colorFilter = colorFilter
- }
-
- override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
- }
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt
index 4e8e6d6d6..017a76747 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt
@@ -24,12 +24,12 @@ import coil.key.Keyer
import coil.request.Options
import coil.size.Size
import javax.inject.Inject
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Song
class SongKeyer @Inject constructor(private val coverExtractor: CoverExtractor) :
Keyer> {
override fun key(data: List, options: Options) =
- "${coverExtractor.computeAlbumOrdering(data).hashCode()}"
+ "${coverExtractor.computeCoverOrdering(data).hashCode()}"
}
class SongCoverFetcher
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 395429104..067b8361c 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
@@ -43,6 +43,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayInputStream
import java.io.InputStream
import javax.inject.Inject
+import kotlin.math.min
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.guava.asDeferred
import kotlinx.coroutines.withContext
@@ -50,11 +51,16 @@ import okio.buffer
import okio.source
import org.oxycblt.auxio.image.CoverMode
import org.oxycblt.auxio.image.ImageSettings
+import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.logW
+import org.oxycblt.auxio.util.logE
+/**
+ * Provides functionality for extracting album cover information. Meant for internal use only.
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
class CoverExtractor
@Inject
constructor(
@@ -62,28 +68,69 @@ constructor(
private val imageSettings: ImageSettings,
private val mediaSourceFactory: MediaSource.Factory
) {
+ /**
+ * Extract an image (in the form of [FetchResult]) to represent the given [Song]s.
+ *
+ * @param songs The [Song]s to load.
+ * @param size The [Size] of the image to load.
+ * @return If four distinct album covers could be extracted from the [Song]s, a [DrawableResult]
+ * will be returned of a mosaic composed of four album covers ordered by
+ * [computeCoverOrdering]. Otherwise, a [SourceResult] of one album cover will be returned.
+ */
suspend fun extract(songs: List, size: Size): FetchResult? {
- val albums = computeAlbumOrdering(songs)
+ val albums = computeCoverOrdering(songs)
val streams = mutableListOf()
for (album in albums) {
- openInputStream(album)?.let(streams::add)
+ openCoverInputStream(album)?.let(streams::add)
+ // We don't immediately check for mosaic feasibility from album count alone, as that
+ // does not factor in InputStreams failing to load. Instead, only check once we
+ // definitely have image data to use.
if (streams.size == 4) {
- return createMosaic(streams, size)
+ // Make sure we free the InputStreams once we've transformed them into a mosaic.
+ return createMosaic(streams, size).also {
+ withContext(Dispatchers.IO) { streams.forEach(InputStream::close) }
+ }
}
}
- return streams.firstOrNull()?.let { stream ->
- SourceResult(
- source = ImageSource(stream.source().buffer(), context),
- mimeType = null,
- dataSource = DataSource.DISK)
+ // Not enough covers for a mosaic, take the first one (if that even exists)
+ val first = streams.firstOrNull() ?: return null
+
+ // All but the first stream will be unused, free their resources
+ withContext(Dispatchers.IO) {
+ for (i in 1 until streams.size) {
+ streams[i].close()
+ }
}
+
+ return SourceResult(
+ source = ImageSource(first.source().buffer(), context),
+ mimeType = null,
+ dataSource = DataSource.DISK)
}
- fun computeAlbumOrdering(songs: List) =
- songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
+ /**
+ * Creates an [Album] list representing the order that album covers would be used in [extract].
+ *
+ * @param songs A hypothetical list of [Song]s that would be used in [extract].
+ * @return A list of [Album]s first ordered by the "representation" within the [Song]s, and then
+ * by their names. "Representation" is defined by how many [Song]s were found to be linked to
+ * the given [Album] in the given [Song] list.
+ */
+ fun computeCoverOrdering(songs: List): List {
+ // TODO: Start short-circuiting in more places
+ if (songs.isEmpty()) return listOf()
+ if (songs.size == 1) return listOf(songs.first().album)
- private suspend fun openInputStream(album: Album): InputStream? =
+ val sortedMap =
+ sortedMapOf(Sort.Mode.ByName.getAlbumComparator(Sort.Direction.ASCENDING))
+ for (song in songs) {
+ sortedMap[song.album] = (sortedMap[song.album] ?: 0) + 1
+ }
+ return sortedMap.keys.sortedByDescending { sortedMap[it] }
+ }
+
+ private suspend fun openCoverInputStream(album: Album) =
try {
when (imageSettings.coverMode) {
CoverMode.OFF -> null
@@ -91,7 +138,7 @@ constructor(
CoverMode.QUALITY -> extractQualityCover(album)
}
} catch (e: Exception) {
- logW("Unable to extract album cover due to an error: $e")
+ logE("Unable to extract album cover due to an error: $e")
null
}
@@ -148,7 +195,6 @@ constructor(
}
if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) {
- logD("Front cover found")
stream = ByteArrayInputStream(pic)
break
} else if (stream == null) {
@@ -164,7 +210,7 @@ constructor(
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) }
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
- private suspend fun createMosaic(streams: List, size: Size): FetchResult {
+ private fun createMosaic(streams: List, size: Size): FetchResult {
// Use whatever size coil gives us to create the mosaic.
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
val mosaicFrameSize =
@@ -184,10 +230,9 @@ constructor(
break
}
- // Run the bitmap through a transform to reflect the configuration of other images.
- val bitmap =
- SquareFrameTransform.INSTANCE.transform(
- BitmapFactory.decodeStream(stream), mosaicFrameSize)
+ // Crop the bitmap down to a square so it leaves no empty space
+ // TODO: Work around this
+ val bitmap = cropBitmap(BitmapFactory.decodeStream(stream), mosaicFrameSize)
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
x += bitmap.width
@@ -206,14 +251,27 @@ constructor(
dataSource = DataSource.DISK)
}
- /**
- * Get an image dimension suitable to create a mosaic with.
- *
- * @return A pixel dimension derived from the given [Dimension] that will always be even,
- * allowing it to be sub-divided.
- */
private fun Dimension.mosaicSize(): Int {
+ // Since we want the mosaic to be perfectly divisible into two, we need to round any
+ // odd image sizes upwards to prevent the mosaic creation from failing.
val size = pxOrElse { 512 }
return if (size.mod(2) > 0) size + 1 else size
}
+
+ private fun cropBitmap(input: Bitmap, size: Size): Bitmap {
+ // Find the smaller dimension and then take a center portion of the image that
+ // has that size.
+ val dstSize = min(input.width, input.height)
+ val x = (input.width - dstSize) / 2
+ val y = (input.height - dstSize) / 2
+ val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
+
+ val desiredWidth = size.width.pxOrElse { dstSize }
+ val desiredHeight = size.height.pxOrElse { dstSize }
+ if (dstSize != desiredWidth || dstSize != desiredHeight) {
+ // Image is not the desired size, upscale it.
+ return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
+ }
+ return dst
+ }
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt
deleted file mode 100644
index bdc48b49a..000000000
--- a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (c) 2022 Auxio Project
- * SquareFrameTransform.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.image.extractor
-
-import android.graphics.Bitmap
-import coil.size.Size
-import coil.size.pxOrElse
-import coil.transform.Transformation
-import kotlin.math.min
-
-/**
- * A transformation that performs a center crop-style transformation on an image. Allowing this
- * behavior to be intrinsic without any view configuration.
- *
- * @author Alexander Capehart (OxygenCobalt)
- */
-class SquareFrameTransform : Transformation {
- override val cacheKey: String
- get() = "SquareFrameTransform"
-
- override suspend fun transform(input: Bitmap, size: Size): Bitmap {
- // Find the smaller dimension and then take a center portion of the image that
- // has that size.
- val dstSize = min(input.width, input.height)
- val x = (input.width - dstSize) / 2
- val y = (input.height - dstSize) / 2
- val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
-
- val desiredWidth = size.width.pxOrElse { dstSize }
- val desiredHeight = size.height.pxOrElse { dstSize }
- if (dstSize != desiredWidth || dstSize != desiredHeight) {
- // Image is not the desired size, upscale it.
- return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
- }
- return dst
- }
-
- companion object {
- /** A re-usable instance. */
- val INSTANCE = SquareFrameTransform()
- }
-}
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 e41dd4149..8636e1579 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt
@@ -20,6 +20,7 @@ package org.oxycblt.auxio.list
import androidx.annotation.StringRes
+// 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. */
interface Item
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 49655d01b..bca4fb774 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt
@@ -18,7 +18,6 @@
package org.oxycblt.auxio.list
-import android.view.MenuItem
import android.view.View
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
@@ -28,10 +27,17 @@ import androidx.viewbinding.ViewBinding
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.selection.SelectionFragment
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+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.navigation.MainNavigationAction
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.logW
+import org.oxycblt.auxio.util.share
import org.oxycblt.auxio.util.showToast
/**
@@ -83,32 +89,45 @@ abstract class ListFragment :
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) {
logD("Launching new song menu: ${song.name}")
- openMusicMenuImpl(anchor, menuRes) {
- when (it.itemId) {
- R.id.action_play_next -> {
- playbackModel.playNext(song)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(song)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_go_artist -> {
- navModel.exploreNavigateToParentArtist(song)
- }
- R.id.action_go_album -> {
- navModel.exploreNavigateTo(song.album)
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(song)
- }
- R.id.action_song_detail -> {
- navModel.mainNavigateTo(
- MainNavigationAction.Directions(
- MainFragmentDirections.actionShowDetails(song.uid)))
- }
- else -> {
- error("Unexpected menu item selected")
+ openMenu(anchor, menuRes) {
+ setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.action_play_next -> {
+ playbackModel.playNext(song)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_queue_add -> {
+ playbackModel.addToQueue(song)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_go_artist -> {
+ navModel.exploreNavigateToParentArtist(song)
+ true
+ }
+ R.id.action_go_album -> {
+ navModel.exploreNavigateTo(song.album)
+ true
+ }
+ R.id.action_share -> {
+ requireContext().share(song)
+ true
+ }
+ R.id.action_playlist_add -> {
+ musicModel.addToPlaylist(song)
+ true
+ }
+ R.id.action_song_detail -> {
+ navModel.mainNavigateTo(
+ MainNavigationAction.Directions(
+ MainFragmentDirections.actionShowDetails(song.uid)))
+ true
+ }
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
}
@@ -125,30 +144,43 @@ abstract class ListFragment :
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
logD("Launching new album menu: ${album.name}")
- openMusicMenuImpl(anchor, menuRes) {
- when (it.itemId) {
- R.id.action_play -> {
- playbackModel.play(album)
- }
- R.id.action_shuffle -> {
- playbackModel.shuffle(album)
- }
- R.id.action_play_next -> {
- playbackModel.playNext(album)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(album)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_go_artist -> {
- navModel.exploreNavigateToParentArtist(album)
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(album)
- }
- else -> {
- error("Unexpected menu item selected")
+ openMenu(anchor, menuRes) {
+ setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.action_play -> {
+ playbackModel.play(album)
+ true
+ }
+ R.id.action_shuffle -> {
+ playbackModel.shuffle(album)
+ true
+ }
+ R.id.action_play_next -> {
+ playbackModel.playNext(album)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_queue_add -> {
+ playbackModel.addToQueue(album)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_go_artist -> {
+ navModel.exploreNavigateToParentArtist(album)
+ true
+ }
+ R.id.action_playlist_add -> {
+ musicModel.addToPlaylist(album)
+ true
+ }
+ R.id.action_share -> {
+ requireContext().share(album)
+ true
+ }
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
}
@@ -165,27 +197,50 @@ abstract class ListFragment :
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
logD("Launching new artist menu: ${artist.name}")
- openMusicMenuImpl(anchor, menuRes) {
- when (it.itemId) {
- R.id.action_play -> {
- playbackModel.play(artist)
- }
- R.id.action_shuffle -> {
- playbackModel.shuffle(artist)
- }
- R.id.action_play_next -> {
- playbackModel.playNext(artist)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(artist)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(artist)
- }
- else -> {
- error("Unexpected menu item selected")
+ openMenu(anchor, menuRes) {
+ val playable = artist.songs.isNotEmpty()
+ if (!playable) {
+ logD("Artist is empty, disabling playback/playlist/share options")
+ }
+ menu.findItem(R.id.action_play).isEnabled = playable
+ menu.findItem(R.id.action_shuffle).isEnabled = playable
+ menu.findItem(R.id.action_play_next).isEnabled = playable
+ menu.findItem(R.id.action_queue_add).isEnabled = playable
+ menu.findItem(R.id.action_playlist_add).isEnabled = playable
+ menu.findItem(R.id.action_share).isEnabled = playable
+
+ setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.action_play -> {
+ playbackModel.play(artist)
+ true
+ }
+ R.id.action_shuffle -> {
+ playbackModel.shuffle(artist)
+ true
+ }
+ R.id.action_play_next -> {
+ playbackModel.playNext(artist)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_queue_add -> {
+ playbackModel.addToQueue(artist)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_playlist_add -> {
+ musicModel.addToPlaylist(artist)
+ true
+ }
+ R.id.action_share -> {
+ requireContext().share(artist)
+ true
+ }
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
}
@@ -202,27 +257,39 @@ abstract class ListFragment :
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
logD("Launching new genre menu: ${genre.name}")
- openMusicMenuImpl(anchor, menuRes) {
- when (it.itemId) {
- R.id.action_play -> {
- playbackModel.play(genre)
- }
- R.id.action_shuffle -> {
- playbackModel.shuffle(genre)
- }
- R.id.action_play_next -> {
- playbackModel.playNext(genre)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(genre)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(genre)
- }
- else -> {
- error("Unexpected menu item selected")
+ openMenu(anchor, menuRes) {
+ setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.action_play -> {
+ playbackModel.play(genre)
+ true
+ }
+ R.id.action_shuffle -> {
+ playbackModel.shuffle(genre)
+ true
+ }
+ R.id.action_play_next -> {
+ playbackModel.playNext(genre)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_queue_add -> {
+ playbackModel.addToQueue(genre)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_playlist_add -> {
+ musicModel.addToPlaylist(genre)
+ true
+ }
+ R.id.action_share -> {
+ requireContext().share(genre)
+ true
+ }
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
}
}
}
@@ -239,44 +306,51 @@ abstract class ListFragment :
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, playlist: Playlist) {
logD("Launching new playlist menu: ${playlist.name}")
- openMusicMenuImpl(anchor, menuRes) {
- when (it.itemId) {
- R.id.action_play -> {
- playbackModel.play(playlist)
- }
- R.id.action_shuffle -> {
- playbackModel.shuffle(playlist)
- }
- R.id.action_play_next -> {
- playbackModel.playNext(playlist)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(playlist)
- requireContext().showToast(R.string.lng_queue_added)
- }
- R.id.action_rename -> {
- musicModel.renamePlaylist(playlist)
- }
- R.id.action_delete -> {
- musicModel.deletePlaylist(playlist)
- }
- else -> {
- error("Unexpected menu item selected")
- }
- }
- }
- }
-
- private fun openMusicMenuImpl(
- anchor: View,
- @MenuRes menuRes: Int,
- onMenuItemClick: (MenuItem) -> Unit
- ) {
openMenu(anchor, menuRes) {
- setOnMenuItemClickListener { item ->
- onMenuItemClick(item)
- true
+ val playable = playlist.songs.isNotEmpty()
+ menu.findItem(R.id.action_play).isEnabled = playable
+ menu.findItem(R.id.action_shuffle).isEnabled = playable
+ menu.findItem(R.id.action_play_next).isEnabled = playable
+ menu.findItem(R.id.action_queue_add).isEnabled = playable
+ menu.findItem(R.id.action_share).isEnabled = playable
+
+ setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.action_play -> {
+ playbackModel.play(playlist)
+ true
+ }
+ R.id.action_shuffle -> {
+ playbackModel.shuffle(playlist)
+ true
+ }
+ R.id.action_play_next -> {
+ playbackModel.playNext(playlist)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_queue_add -> {
+ playbackModel.addToQueue(playlist)
+ requireContext().showToast(R.string.lng_queue_added)
+ true
+ }
+ R.id.action_rename -> {
+ musicModel.renamePlaylist(playlist)
+ true
+ }
+ R.id.action_delete -> {
+ musicModel.deletePlaylist(playlist)
+ true
+ }
+ R.id.action_share -> {
+ requireContext().share(playlist)
+ true
+ }
+ else -> {
+ logW("Unexpected menu item selected")
+ false
+ }
+ }
}
}
}
@@ -295,6 +369,8 @@ abstract class ListFragment :
return
}
+ logD("Opening popup menu menu")
+
currentMenu =
PopupMenu(requireContext(), anchor).apply {
inflate(menuRes)
diff --git a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt
index 5002e60cf..8a7203182 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt
@@ -22,8 +22,13 @@ import androidx.annotation.IdRes
import kotlin.math.max
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
-import org.oxycblt.auxio.list.Sort.Mode
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+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.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc
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 b9d77b0f8..977c367c4 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
@@ -20,10 +20,12 @@ package org.oxycblt.auxio.list.adapter
import android.os.Handler
import android.os.Looper
-import androidx.recyclerview.widget.*
+import androidx.recyclerview.widget.AdapterListUpdateCallback
+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
/**
* A variant of ListDiffer with more flexible updates.
@@ -45,15 +47,18 @@ abstract class FlexibleListAdapter(
/**
* Update the adapter with new data.
*
- * @param newData The new list of data to update with.
+ * @param newList The new list of data to update with.
* @param instructions The [UpdateInstructions] to visually update the list with.
* @param callback Called when the update is completed. May be done asynchronously.
*/
fun update(
- newData: List,
+ newList: List,
instructions: UpdateInstructions?,
callback: (() -> Unit)? = null
- ) = differ.update(newData, instructions, callback)
+ ) {
+ logD("Updating list to ${newList.size} items with $instructions")
+ differ.update(newList, instructions, callback)
+ }
}
/**
@@ -164,6 +169,7 @@ private class FlexibleListDiffer(
) {
// fast simple remove all
if (newList.isEmpty()) {
+ logD("Short-circuiting diff to remove all")
val countRemoved = oldList.size
currentList = emptyList()
// notify last, after list is updated
@@ -174,6 +180,7 @@ private class FlexibleListDiffer(
// fast simple first insert
if (oldList.isEmpty()) {
+ logD("Short-circuiting diff to insert all")
currentList = newList
// notify last, after list is updated
updateCallback.onInserted(0, newList.size)
@@ -232,8 +239,10 @@ private class FlexibleListDiffer(
throw AssertionError()
}
})
+
mainThreadExecutor.execute {
if (maxScheduledGeneration == runGeneration) {
+ logD("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 67fccceac..1beddc8ef 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
@@ -22,6 +22,7 @@ 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
/**
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
@@ -58,6 +59,8 @@ 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]")
+
var updatedItem = false
if (currentItem != item) {
val oldItem = currentItem
@@ -69,7 +72,7 @@ abstract class PlayingIndicatorAdapter(
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
} else {
- logD("oldItem was not in adapter data")
+ logW("oldItem was not in adapter data")
}
}
@@ -79,7 +82,7 @@ abstract class PlayingIndicatorAdapter(
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
} else {
- logD("newItem was not in adapter data")
+ logW("newItem was not in adapter data")
}
}
@@ -97,7 +100,7 @@ abstract class PlayingIndicatorAdapter(
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
} else {
- logD("newItem was not in adapter data")
+ logW("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 641e8b2b3..9339f78fc 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 org.oxycblt.auxio.util.logD
/**
* A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
@@ -54,6 +55,7 @@ abstract class SelectionIndicatorAdapter(
// Nothing to do.
return
}
+ logD("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/recycler/DialogRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt
index 96ef1ffcd..9fc255ce1 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt
@@ -29,6 +29,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.MaterialDivider
import org.oxycblt.auxio.R
+import org.oxycblt.auxio.list.recycler.DialogRecyclerView.ViewHolder
import org.oxycblt.auxio.util.getDimenPixels
/**
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 ea5629e78..28112ca61 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
@@ -26,6 +26,7 @@ import androidx.core.view.isInvisible
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
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
@@ -67,7 +68,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 item")
+ logD("Lifting ViewHolder")
val bg = holder.background
val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal)
@@ -109,7 +110,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("Dropping item")
+ logD("Lifting ViewHolder")
val bg = holder.background
val elevation = recyclerView.context.getDimen(R.dimen.elevation_normal)
@@ -136,7 +137,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() {
// Long-press events are too buggy, only allow dragging with the handle.
final override fun isLongPressDragEnabled() = false
- /** Required [RecyclerView.ViewHolder] implementation that exposes the following. */
+ /** Required [RecyclerView.ViewHolder] implementation that exposes required fields */
interface ViewHolder {
/** Whether this [ViewHolder] can be moved right now. */
val enabled: Boolean
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 0c9962996..c829248e5 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
@@ -31,11 +31,16 @@ import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+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.music.areNamesTheSame
+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 [RecyclerView.ViewHolder] that displays a [Song]. Use [from] to create an instance.
@@ -59,7 +64,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
- binding.songAlbumCover.isPlaying = isPlaying
+ binding.songAlbumCover.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@@ -109,7 +114,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
- binding.parentImage.isPlaying = isPlaying
+ binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@@ -169,7 +174,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
- binding.parentImage.isPlaying = isPlaying
+ binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@@ -226,7 +231,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
- binding.parentImage.isPlaying = isPlaying
+ binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@@ -283,7 +288,7 @@ class PlaylistViewHolder private constructor(private val binding: ItemParentBind
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
- binding.parentImage.isPlaying = isPlaying
+ binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@@ -325,7 +330,6 @@ class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderB
* @param basicHeader The new [BasicHeader] to bind.
*/
fun bind(basicHeader: BasicHeader) {
- logD(binding.context.getString(basicHeader.titleRes))
binding.title.text = binding.context.getString(basicHeader.titleRes)
}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt
index cb7bce063..88bdba6d8 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt
@@ -26,6 +26,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
+import org.oxycblt.auxio.util.share
import org.oxycblt.auxio.util.showToast
/**
@@ -79,6 +80,10 @@ abstract class SelectionFragment :
playbackModel.shuffle(selectionModel.take())
true
}
+ R.id.action_selection_share -> {
+ requireContext().share(selectionModel.take())
+ true
+ }
else -> false
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt
index 5329151ad..a5cfd776e 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt
@@ -23,7 +23,16 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+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.MusicRepository
+import org.oxycblt.auxio.music.MusicSettings
+import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.util.logD
/**
* A [ViewModel] that manages the current selection.
@@ -76,10 +85,19 @@ constructor(
* @param music The [Music] item to select.
*/
fun select(music: Music) {
+ if (music is MusicParent && music.songs.isEmpty()) {
+ logD("Cannot select empty parent, ignoring operation")
+ return
+ }
+
val selected = _selected.value.toMutableList()
if (!selected.remove(music)) {
+ logD("Adding $music to selection")
selected.add(music)
+ } else {
+ logD("Removed $music from selection")
}
+
_selected.value = selected
}
@@ -88,8 +106,9 @@ constructor(
*
* @return A list of [Song]s collated from each item selected.
*/
- fun take() =
- _selected.value
+ fun take(): List {
+ logD("Taking selection")
+ return _selected.value
.flatMap {
when (it) {
is Song -> listOf(it)
@@ -99,12 +118,16 @@ constructor(
is Playlist -> it.songs
}
}
- .also { drop() }
+ .also { _selected.value = listOf() }
+ }
/**
* Clear the current selection.
*
* @return true if the prior selection was non-empty, false otherwise.
*/
- fun drop() = _selected.value.isNotEmpty().also { _selected.value = listOf() }
+ fun drop(): Boolean {
+ logD("Dropping selection [empty=${_selected.value.isEmpty()}]")
+ return _selected.value.isNotEmpty().also { _selected.value = listOf() }
+ }
}
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 bcf2fb53e..a9783cfae 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt
@@ -314,21 +314,23 @@ interface Album : MusicParent {
*/
interface Artist : MusicParent {
/**
- * All of the [Album]s this artist is credited to. Note that any [Song] credited to this artist
- * will have it's [Album] considered to be "indirectly" linked to this [Artist], and thus
- * included in this list.
+ * All of the [Album]s this artist is credited to from [explicitAlbums] and [implicitAlbums].
+ * Note that any [Song] credited to this artist will have it's [Album] considered to be
+ * "indirectly" linked to this [Artist], and thus included in this list.
*/
val albums: List
+
+ /** Albums directly credited to this [Artist] via a "Album Artist" tag. */
+ val explicitAlbums: List
+
+ /** Albums indirectly credited to this [Artist] via an "Artist" tag. */
+ val implicitAlbums: List
+
/**
* The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no
* songs.
*/
val durationMs: Long?
- /**
- * Whether this artist is considered a "collaborator", i.e it is not directly credited on any
- * [Album].
- */
- val isCollaborator: Boolean
/** The [Genre]s of this artist. */
val genres: List
}
@@ -339,8 +341,6 @@ interface Artist : MusicParent {
* @author Alexander Capehart (OxygenCobalt)
*/
interface Genre : MusicParent {
- /** The albums indirectly linked to by the [Song]s of this [Genre]. */
- val albums: List
/** The artists indirectly linked to by the [Artist]s of this [Genre]. */
val artists: List
/** The total duration of the songs in this genre, in milliseconds. */
@@ -353,8 +353,6 @@ interface Genre : MusicParent {
* @author Alexander Capehart (OxygenCobalt)
*/
interface Playlist : MusicParent {
- /** The albums indirectly linked to by the [Song]s of this [Playlist]. */
- val albums: List
/** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long
}
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 6fa0b4f79..8d890b218 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
@@ -21,12 +21,18 @@ package org.oxycblt.auxio.music
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
-import java.util.*
+import java.util.LinkedList
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.cache.CacheRepository
import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong
@@ -45,6 +51,9 @@ import org.oxycblt.auxio.util.logW
* music (loading) can be reacted to with [UpdateListener] and [IndexingListener].
*
* @author Alexander Capehart (OxygenCobalt)
+ *
+ * TODO: Switch listeners to set when you can confirm there are no order-dependent listener
+ * configurations
*/
interface MusicRepository {
/** The current music information found on the device. */
@@ -230,24 +239,32 @@ constructor(
@Synchronized
override fun addUpdateListener(listener: MusicRepository.UpdateListener) {
+ logD("Adding $listener to update listeners")
updateListeners.add(listener)
listener.onMusicChanges(MusicRepository.Changes(deviceLibrary = true, userLibrary = true))
}
@Synchronized
override fun removeUpdateListener(listener: MusicRepository.UpdateListener) {
- updateListeners.remove(listener)
+ logD("Removing $listener to update listeners")
+ if (!updateListeners.remove(listener)) {
+ logW("Update listener $listener was not added prior, cannot remove")
+ }
}
@Synchronized
override fun addIndexingListener(listener: MusicRepository.IndexingListener) {
+ logD("Adding $listener to indexing listeners")
indexingListeners.add(listener)
listener.onIndexingStateChanged()
}
@Synchronized
override fun removeIndexingListener(listener: MusicRepository.IndexingListener) {
- indexingListeners.remove(listener)
+ logD("Removing $listener from indexing listeners")
+ if (!indexingListeners.remove(listener)) {
+ logW("Indexing listener $listener was not added prior, cannot remove")
+ }
}
@Synchronized
@@ -256,6 +273,7 @@ constructor(
logW("Worker is already registered")
return
}
+ logD("Registering worker $worker")
indexingWorker = worker
if (indexingState == null) {
worker.requestIndex(true)
@@ -268,6 +286,7 @@ constructor(
logW("Given worker did not match current worker")
return
}
+ logD("Unregistering worker $worker")
indexingWorker = null
currentIndexingState = null
}
@@ -279,44 +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")
userLibrary.createPlaylist(name, songs)
- notifyUserLibraryChange()
+ emitLibraryChange(device = false, user = true)
}
override suspend fun renamePlaylist(playlist: Playlist, name: String) {
val userLibrary = synchronized(this) { userLibrary ?: return }
+ logD("Renaming $playlist to $name")
userLibrary.renamePlaylist(playlist, name)
- notifyUserLibraryChange()
+ emitLibraryChange(device = false, user = true)
}
override suspend fun deletePlaylist(playlist: Playlist) {
val userLibrary = synchronized(this) { userLibrary ?: return }
+ logD("Deleting $playlist")
userLibrary.deletePlaylist(playlist)
- notifyUserLibraryChange()
+ emitLibraryChange(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")
userLibrary.addToPlaylist(playlist, songs)
- notifyUserLibraryChange()
+ emitLibraryChange(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")
userLibrary.rewritePlaylist(playlist, songs)
- notifyUserLibraryChange()
- }
-
- @Synchronized
- private fun notifyUserLibraryChange() {
- for (listener in updateListeners) {
- listener.onMusicChanges(
- MusicRepository.Changes(deviceLibrary = false, userLibrary = true))
- }
+ emitLibraryChange(device = false, user = true)
}
@Synchronized
override fun requestIndex(withCache: Boolean) {
+ logD("Requesting index operation [cache=$withCache]")
indexingWorker?.requestIndex(withCache)
}
@@ -343,7 +360,7 @@ constructor(
private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED) {
- logE("Permission check failed")
+ logE("Permissions were not granted")
// No permissions, signal that we can't do anything.
throw NoAudioPermissionException()
}
@@ -353,14 +370,16 @@ constructor(
emitLoading(IndexingProgress.Indeterminate)
// Do the initial query of the cache and media databases in parallel.
- logD("Starting queries")
+ logD("Starting MediaStore query")
val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() }
val cache =
if (withCache) {
+ logD("Reading cache")
cacheRepository.readCache()
} else {
null
}
+ logD("Awaiting MediaStore query")
val query = mediaStoreQueryJob.await().getOrThrow()
// Now start processing the queried song information in parallel. Songs that can't be
@@ -369,11 +388,13 @@ constructor(
logD("Starting song discovery")
val completeSongs = Channel(Channel.UNLIMITED)
val incompleteSongs = Channel(Channel.UNLIMITED)
+ logD("Started MediaStore discovery")
val mediaStoreJob =
worker.scope.tryAsync {
mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
incompleteSongs.close()
}
+ logD("Started ExoPlayer discovery")
val metadataJob =
worker.scope.tryAsync {
tagExtractor.consume(incompleteSongs, completeSongs)
@@ -386,7 +407,8 @@ constructor(
rawSongs.add(rawSong)
emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
}
- // These should be no-ops
+ logD("Awaiting discovery completion")
+ // These should be no-ops, but we need the error state to see if we should keep going.
mediaStoreJob.await().getOrThrow()
metadataJob.await().getOrThrow()
@@ -401,25 +423,47 @@ constructor(
// TODO: Indicate playlist state in loading process?
emitLoading(IndexingProgress.Indeterminate)
val deviceLibraryChannel = Channel()
+ logD("Starting DeviceLibrary creation")
val deviceLibraryJob =
- worker.scope.tryAsync(Dispatchers.Main) {
+ worker.scope.tryAsync(Dispatchers.Default) {
deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) }
}
+ logD("Starting UserLibrary creation")
val userLibraryJob =
worker.scope.tryAsync {
userLibraryFactory.read(deviceLibraryChannel).also { deviceLibraryChannel.close() }
}
if (cache == null || cache.invalidated) {
+ logD("Writing cache [why=${cache?.invalidated}]")
cacheRepository.writeCache(rawSongs)
}
+ logD("Awaiting library creation")
val deviceLibrary = deviceLibraryJob.await().getOrThrow()
val userLibrary = userLibraryJob.await().getOrThrow()
- withContext(Dispatchers.Main) {
- emitComplete(null)
- emitData(deviceLibrary, userLibrary)
+
+ logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]")
+ emitComplete(null)
+
+ // Comparing the library instances is obscenely expensive, do it within the library
+ val deviceLibraryChanged = this.deviceLibrary != deviceLibrary
+ val userLibraryChanged = this.userLibrary != userLibrary
+ if (!deviceLibraryChanged && !userLibraryChanged) {
+ logD("Library has not changed, skipping update")
+ return
}
+
+ synchronized(this) {
+ this.deviceLibrary = deviceLibrary
+ this.userLibrary = userLibrary
+ }
+
+ emitLibraryChange(deviceLibraryChanged, userLibraryChanged)
}
+ /**
+ * An extension of [async] that forces the outcome to a [Result] to allow exceptions to bubble
+ * upwards instead of crashing the entire app.
+ */
private inline fun CoroutineScope.tryAsync(
context: CoroutineContext = EmptyCoroutineContext,
crossinline block: suspend () -> R
@@ -447,6 +491,7 @@ constructor(
synchronized(this) {
previousCompletedState = IndexingState.Completed(error)
currentIndexingState = null
+ logD("Dispatching completion state [error=$error]")
for (listener in indexingListeners) {
listener.onIndexingStateChanged()
}
@@ -454,14 +499,9 @@ constructor(
}
@Synchronized
- private fun emitData(deviceLibrary: DeviceLibrary, userLibrary: MutableUserLibrary) {
- val deviceLibraryChanged = this.deviceLibrary != deviceLibrary
- val userLibraryChanged = this.userLibrary != userLibrary
- if (!deviceLibraryChanged && !userLibraryChanged) return
-
- this.deviceLibrary = deviceLibrary
- this.userLibrary = userLibrary
- val changes = MusicRepository.Changes(deviceLibraryChanged, userLibraryChanged)
+ private fun emitLibraryChange(device: Boolean, user: Boolean) {
+ val changes = MusicRepository.Changes(device, user)
+ logD("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 48b180388..4274ef4e8 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
@@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.fs.Directory
import org.oxycblt.auxio.music.fs.MusicDirectories
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getSystemServiceCompat
+import org.oxycblt.auxio.util.logD
/**
* User configuration specific to music system.
@@ -231,8 +232,14 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
getString(R.string.set_key_music_dirs),
getString(R.string.set_key_music_dirs_include),
getString(R.string.set_key_separators),
- getString(R.string.set_key_auto_sort_names) -> listener.onIndexingSettingChanged()
- getString(R.string.set_key_observing) -> listener.onObservingChanged()
+ getString(R.string.set_key_auto_sort_names) -> {
+ logD("Dispatching indexing setting change for $key")
+ listener.onIndexingSettingChanged()
+ }
+ getString(R.string.set_key_observing) -> {
+ logD("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 d207bd135..6390929b7 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
+import org.oxycblt.auxio.util.logD
/**
* A [ViewModel] providing data specific to the music loading process.
@@ -89,6 +90,7 @@ constructor(
deviceLibrary.artists.size,
deviceLibrary.genres.size,
deviceLibrary.songs.sumOf { it.durationMs })
+ logD("Updated statistics: ${_statistics.value}")
}
override fun onIndexingStateChanged() {
@@ -97,11 +99,13 @@ constructor(
/** Requests that the music library should be re-loaded while leveraging the cache. */
fun refresh() {
+ logD("Refreshing library")
musicRepository.requestIndex(true)
}
/** Requests that the music library be re-loaded without the cache. */
fun rescan() {
+ logD("Rescanning library")
musicRepository.requestIndex(false)
}
@@ -113,8 +117,10 @@ constructor(
*/
fun createPlaylist(name: String? = null, songs: List = listOf()) {
if (name != null) {
+ logD("Creating $name with ${songs.size} songs]")
viewModelScope.launch(Dispatchers.IO) { musicRepository.createPlaylist(name, songs) }
} else {
+ logD("Launching creation dialog for ${songs.size} songs")
_newPlaylistSongs.put(songs)
}
}
@@ -127,8 +133,10 @@ constructor(
*/
fun renamePlaylist(playlist: Playlist, name: String? = null) {
if (name != null) {
+ logD("Renaming $playlist to $name")
viewModelScope.launch(Dispatchers.IO) { musicRepository.renamePlaylist(playlist, name) }
} else {
+ logD("Launching rename dialog for $playlist")
_playlistToRename.put(playlist)
}
}
@@ -142,8 +150,10 @@ constructor(
*/
fun deletePlaylist(playlist: Playlist, rude: Boolean = false) {
if (rude) {
+ logD("Deleting $playlist")
viewModelScope.launch(Dispatchers.IO) { musicRepository.deletePlaylist(playlist) }
} else {
+ logD("Launching deletion dialog for $playlist")
_playlistToDelete.put(playlist)
}
}
@@ -155,6 +165,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")
addToPlaylist(listOf(song), playlist)
}
@@ -165,6 +176,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")
addToPlaylist(musicSettings.albumSongSort.songs(album.songs), playlist)
}
@@ -175,6 +187,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")
addToPlaylist(musicSettings.artistSongSort.songs(artist.songs), playlist)
}
@@ -185,6 +198,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")
addToPlaylist(musicSettings.genreSongSort.songs(genre.songs), playlist)
}
@@ -196,8 +210,10 @@ constructor(
*/
fun addToPlaylist(songs: List, playlist: Playlist? = null) {
if (playlist != null) {
+ logD("Adding ${songs.size} songs to $playlist")
viewModelScope.launch(Dispatchers.IO) { musicRepository.addToPlaylist(songs, playlist) }
} else {
+ logD("Launching addition dialog for songs=${songs.size}")
_songsToAdd.put(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 967d53d68..0b91c65b3 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,8 @@ package org.oxycblt.auxio.music.cache
import javax.inject.Inject
import org.oxycblt.auxio.music.device.RawSong
-import org.oxycblt.auxio.util.*
+import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.logE
/**
* A repository allowing access to cached metadata obtained in prior music loading operations.
@@ -49,7 +50,9 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached
try {
// Faster to load the whole database into memory than do a query on each
// populate call.
- CacheImpl(cachedSongsDao.readSongs())
+ val songs = cachedSongsDao.readSongs()
+ logD("Successfully read ${songs.size} songs from cache")
+ CacheImpl(songs)
} catch (e: Exception) {
logE("Unable to load cache database.")
logE(e.stackTraceToString())
@@ -60,7 +63,9 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached
try {
// Still write out whatever data was extracted.
cachedSongsDao.nukeSongs()
+ logD("Successfully deleted old cache")
cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw))
+ logD("Successfully wrote ${rawSongs.size} songs to cache")
} catch (e: Exception) {
logE("Unable to save cache database.")
logE(e.stackTraceToString())
@@ -96,7 +101,6 @@ private class CacheImpl(cachedSongs: List) : Cache {
override var invalidated = false
override fun populate(rawSong: RawSong): Boolean {
-
// For a cached raw song to be used, it must exist within the cache and have matching
// addition and modification timestamps. Technically the addition timestamp doesn't
// exist, but to safeguard against possible OEM-specific timestamp incoherence, we
diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt
index af42d85ab..814c15818 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt
@@ -23,7 +23,13 @@ import android.net.Uri
import android.provider.OpenableColumns
import javax.inject.Inject
import org.oxycblt.auxio.list.Sort
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.MusicRepository
+import org.oxycblt.auxio.music.MusicSettings
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.contentResolverSafe
import org.oxycblt.auxio.music.fs.useQuery
import org.oxycblt.auxio.util.logD
@@ -128,20 +134,11 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings
private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } }
private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } }
- override fun equals(other: Any?) =
- other is DeviceLibrary &&
- other.songs == songs &&
- other.albums == albums &&
- other.artists == artists &&
- other.genres == genres
-
- override fun hashCode(): Int {
- var hashCode = songs.hashCode()
- hashCode = hashCode * 31 + albums.hashCode()
- hashCode = hashCode * 31 + artists.hashCode()
- hashCode = hashCode * 31 + genres.hashCode()
- return hashCode
- }
+ // All other music is built from songs, so comparison only needs to check songs.
+ override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs
+ override fun hashCode() = songs.hashCode()
+ override fun toString() =
+ "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, artists=${artists.size}, genres=${genres.size})"
override fun findSong(uid: Music.UID) = songUidMap[uid]
override fun findAlbum(uid: Music.UID) = albumUidMap[uid]
@@ -160,100 +157,69 @@ private class DeviceLibraryImpl(rawSongs: List, settings: MusicSettings
songs.find { it.path.name == displayName && it.size == size }
}
- /**
- * Build a list [SongImpl]s from the given [RawSong].
- *
- * @param rawSongs The [RawSong]s to build the [SongImpl]s from.
- * @param settings [MusicSettings] to obtain user parsing configuration.
- * @return A sorted list of [SongImpl]s derived from the [RawSong] that should be suitable for
- * grouping.
- */
- private fun buildSongs(rawSongs: List, settings: MusicSettings) =
- Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
- .songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid })
+ private fun buildSongs(rawSongs: List, settings: MusicSettings): List {
+ val start = System.currentTimeMillis()
+ val songs =
+ Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
+ .songs(rawSongs.map { SongImpl(it, settings) }.distinctBy { it.uid })
+ logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
+ return songs
+ }
- /**
- * Build a list of [Album]s from the given [Song]s.
- *
- * @param songs The [Song]s to build [Album]s from. These will be linked with their respective
- * [Album]s when created.
- * @param settings [MusicSettings] to obtain user parsing configuration.
- * @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked
- * with parent [Artist] instances in order to be usable.
- */
private fun buildAlbums(songs: List, settings: MusicSettings): List {
+ val start = System.currentTimeMillis()
// Group songs by their singular raw album, then map the raw instances and their
// grouped songs to Album values. Album.Raw will handle the actual grouping rules.
- val songsByAlbum = songs.groupBy { it.rawAlbum }
- val albums = songsByAlbum.map { AlbumImpl(it.key, settings, it.value) }
- logD("Successfully built ${albums.size} albums")
+ val songsByAlbum = songs.groupBy { it.rawAlbum.key }
+ val albums = songsByAlbum.map { AlbumImpl(it.key.value, settings, it.value) }
+ logD("Successfully built ${albums.size} albums in ${System.currentTimeMillis() - start}ms")
return albums
}
- /**
- * Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as
- * they group into [Artist] instances much differently, with [Song]s being grouped primarily by
- * artist names, and [Album]s being grouped primarily by album artist names.
- *
- * @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of
- * one or more [Artist] instances. These will be linked with their respective [Artist]s when
- * created.
- * @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of
- * one or more [Artist] instances. These will be linked with their respective [Artist]s when
- * created.
- * @param settings [MusicSettings] to obtain user parsing configuration.
- * @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings
- * of [Song]s and [Album]s.
- */
private fun buildArtists(
songs: List,
albums: List,
settings: MusicSettings
): List {
+ val start = System.currentTimeMillis()
// Add every raw artist credited to each Song/Album to the grouping. This way,
// different multi-artist combinations are not treated as different artists.
- val musicByArtist = mutableMapOf>()
+ // Songs and albums are grouped by artist and album artist respectively.
+ val musicByArtist = mutableMapOf>()
for (song in songs) {
for (rawArtist in song.rawArtists) {
- musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(song)
+ musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(song)
}
}
for (album in albums) {
for (rawArtist in album.rawArtists) {
- musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(album)
+ musicByArtist.getOrPut(rawArtist.key) { mutableListOf() }.add(album)
}
}
// Convert the combined mapping into artist instances.
- val artists = musicByArtist.map { ArtistImpl(it.key, settings, it.value) }
- logD("Successfully built ${artists.size} artists")
+ val artists = musicByArtist.map { ArtistImpl(it.key.value, settings, it.value) }
+ logD(
+ "Successfully built ${artists.size} artists in ${System.currentTimeMillis() - start}ms")
return artists
}
- /**
- * Group up [Song]s into [Genre] instances.
- *
- * @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of
- * one or more [Genre] instances. These will be linked with their respective [Genre]s when
- * created.
- * @param settings [MusicSettings] to obtain user parsing configuration.
- * @return A non-empty list of [Genre]s.
- */
private fun buildGenres(songs: List, settings: MusicSettings): List {
+ val start = System.currentTimeMillis()
// Add every raw genre credited to each Song to the grouping. This way,
// different multi-genre combinations are not treated as different genres.
- val songsByGenre = mutableMapOf>()
+ val songsByGenre = mutableMapOf>()
for (song in songs) {
for (rawGenre in song.rawGenres) {
- songsByGenre.getOrPut(rawGenre) { mutableListOf() }.add(song)
+ songsByGenre.getOrPut(rawGenre.key) { mutableListOf() }.add(song)
}
}
// Convert the mapping into genre instances.
- val genres = songsByGenre.map { GenreImpl(it.key, settings, it.value) }
- logD("Successfully built ${genres.size} genres")
+ val genres = songsByGenre.map { GenreImpl(it.key.value, settings, it.value) }
+ logD("Successfully built ${genres.size} genres in ${System.currentTimeMillis() - start}ms")
return genres
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt
index 41b69a498..85e8e511e 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt
@@ -26,5 +26,5 @@ import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface DeviceModule {
- @Binds fun deviceLibraryProvider(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory
+ @Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt
index c98049d68..e3ce99928 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt
@@ -20,13 +20,21 @@ package org.oxycblt.auxio.music.device
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Sort
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicSettings
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.MimeType
import org.oxycblt.auxio.music.fs.Path
import org.oxycblt.auxio.music.fs.toAudioUri
import org.oxycblt.auxio.music.fs.toCoverUri
-import org.oxycblt.auxio.music.info.*
import org.oxycblt.auxio.music.info.Date
+import org.oxycblt.auxio.music.info.Disc
+import org.oxycblt.auxio.music.info.Name
+import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
import org.oxycblt.auxio.music.metadata.parseMultiValue
import org.oxycblt.auxio.util.nonZeroOrNull
@@ -85,9 +93,12 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
override val album: Album
get() = unlikelyToBeNull(_album)
- override fun hashCode() = 31 * uid.hashCode() + rawSong.hashCode()
+ private val hashCode = 31 * uid.hashCode() + rawSong.hashCode()
+
+ override fun hashCode() = hashCode
override fun equals(other: Any?) =
other is SongImpl && uid == other.uid && rawSong == other.rawSong
+ override fun toString() = "Song(uid=$uid, name=$name)"
private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings)
private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings)
@@ -237,44 +248,61 @@ class AlbumImpl(
update(rawAlbum.rawArtists.map { it.name })
}
override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings)
-
- override val dates = Date.Range.from(songs.mapNotNull { it.date })
+ override val dates: Date.Range?
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
override val coverUri = rawAlbum.mediaStoreId.toCoverUri()
override val durationMs: Long
override val dateAdded: Long
- override fun hashCode(): Int {
- var hashCode = uid.hashCode()
- hashCode = 31 * hashCode + rawAlbum.hashCode()
- hashCode = 31 * hashCode + songs.hashCode()
- return hashCode
- }
-
- override fun equals(other: Any?) =
- other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs
-
private val _artists = mutableListOf()
override val artists: List
get() = _artists
+ private var hashCode = uid.hashCode()
+
init {
var totalDuration: Long = 0
+ var minDate: Date? = null
+ var maxDate: Date? = null
var earliestDateAdded: Long = Long.MAX_VALUE
// Do linking and value generation in the same loop for efficiency.
for (song in songs) {
song.link(this)
+
+ if (song.date != null) {
+ val min = minDate
+ if (min == null || song.date < min) {
+ minDate = song.date
+ }
+
+ val max = maxDate
+ if (max == null || song.date > max) {
+ maxDate = song.date
+ }
+ }
+
if (song.dateAdded < earliestDateAdded) {
earliestDateAdded = song.dateAdded
}
totalDuration += song.durationMs
}
+ val min = minDate
+ val max = maxDate
+ dates = if (min != null && max != null) Date.Range(min, max) else null
durationMs = totalDuration
dateAdded = earliestDateAdded
+
+ hashCode = 31 * hashCode + rawAlbum.hashCode()
+ hashCode = 31 * hashCode + songs.hashCode()
}
+ override fun hashCode() = hashCode
+ override fun equals(other: Any?) =
+ other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs
+ override fun toString() = "Album(uid=$uid, name=$name)"
+
/**
* The [RawArtist] instances collated by the [Album]. The album artists of the song take
* priority, followed by the artists. If there are no artists, this field will be a single
@@ -336,17 +364,48 @@ class ArtistImpl(
override val songs: List
override val albums: List
+ override val explicitAlbums: List
+ override val implicitAlbums: List
override val durationMs: Long?
- override val isCollaborator: Boolean
+
+ override lateinit var genres: List
+
+ private var hashCode = uid.hashCode()
+
+ init {
+ val distinctSongs = mutableSetOf()
+ val albumMap = mutableMapOf()
+
+ for (music in songAlbums) {
+ when (music) {
+ is SongImpl -> {
+ music.link(this)
+ distinctSongs.add(music)
+ if (albumMap[music.album] == null) {
+ albumMap[music.album] = false
+ }
+ }
+ is AlbumImpl -> {
+ music.link(this)
+ albumMap[music] = true
+ }
+ else -> error("Unexpected input music ${music::class.simpleName}")
+ }
+ }
+
+ songs = distinctSongs.toList()
+ albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(albumMap.keys)
+ explicitAlbums = albums.filter { unlikelyToBeNull(albumMap[it]) }
+ implicitAlbums = albums.filterNot { unlikelyToBeNull(albumMap[it]) }
+ durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull()
+
+ hashCode = 31 * hashCode + rawArtist.hashCode()
+ hashCode = 31 * hashCode + songs.hashCode()
+ }
// Note: Append song contents to MusicParent equality so that artists with
// the same UID but different songs are not equal.
- override fun hashCode(): Int {
- var hashCode = uid.hashCode()
- hashCode = 31 * hashCode + rawArtist.hashCode()
- hashCode = 31 * hashCode + songs.hashCode()
- return hashCode
- }
+ override fun hashCode() = hashCode
override fun equals(other: Any?) =
other is ArtistImpl &&
@@ -354,35 +413,7 @@ class ArtistImpl(
rawArtist == other.rawArtist &&
songs == other.songs
- override lateinit var genres: List
-
- init {
- val distinctSongs = mutableSetOf()
- val distinctAlbums = mutableSetOf()
-
- var noAlbums = true
-
- for (music in songAlbums) {
- when (music) {
- is SongImpl -> {
- music.link(this)
- distinctSongs.add(music)
- distinctAlbums.add(music.album)
- }
- is AlbumImpl -> {
- music.link(this)
- distinctAlbums.add(music)
- noAlbums = false
- }
- else -> error("Unexpected input music ${music::class.simpleName}")
- }
- }
-
- songs = distinctSongs.toList()
- albums = distinctAlbums.toList()
- durationMs = songs.sumOf { it.durationMs }.nonZeroOrNull()
- isCollaborator = noAlbums
- }
+ override fun toString() = "Artist(uid=$uid, name=$name)"
/**
* Returns the original position of this [Artist]'s [RawArtist] within the given [RawArtist]
@@ -393,7 +424,8 @@ class ArtistImpl(
* [RawArtist] will be within the list.
* @return The index of the [Artist]'s [RawArtist] within the list.
*/
- fun getOriginalPositionIn(rawArtists: List) = rawArtists.indexOf(rawArtist)
+ fun getOriginalPositionIn(rawArtists: List) =
+ rawArtists.indexOfFirst { it.key == rawArtist.key }
/**
* Perform final validation and organization on this instance.
@@ -427,19 +459,10 @@ class GenreImpl(
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
?: Name.Unknown(R.string.def_genre)
- override val albums: List
override val artists: List
override val durationMs: Long
- override fun hashCode(): Int {
- var hashCode = uid.hashCode()
- hashCode = 31 * hashCode + rawGenre.hashCode()
- hashCode = 31 * hashCode + songs.hashCode()
- return hashCode
- }
-
- override fun equals(other: Any?) =
- other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs
+ private var hashCode = uid.hashCode()
init {
val distinctAlbums = mutableSetOf()
@@ -453,14 +476,19 @@ class GenreImpl(
totalDuration += song.durationMs
}
- albums =
- Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
- .albums(distinctAlbums)
- .sortedByDescending { album -> album.songs.count { it.genres.contains(this) } }
artists = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING).artists(distinctArtists)
durationMs = totalDuration
+ hashCode = 31 * hashCode + rawGenre.hashCode()
+ hashCode = 31 * hashCode + songs.hashCode()
}
+ override fun hashCode() = hashCode
+
+ override fun equals(other: Any?) =
+ other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs
+
+ override fun toString() = "Genre(uid=$uid, name=$name)"
+
/**
* Returns the original position of this [Genre]'s [RawGenre] within the given [RawGenre] list.
* This can be used to create a consistent ordering within child [Genre] lists based on the
@@ -470,7 +498,8 @@ class GenreImpl(
* [RawGenre] will be within the list.
* @return The index of the [Genre]'s [RawGenre] within the list.
*/
- fun getOriginalPositionIn(rawGenres: List) = rawGenres.indexOf(rawGenre)
+ fun getOriginalPositionIn(rawGenres: List) =
+ rawGenres.indexOfFirst { it.key == rawGenre.key }
/**
* Perform final validation and organization on this instance.
diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt
index 23b02b2f1..2a198c687 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt
@@ -19,7 +19,9 @@
package org.oxycblt.auxio.music.device
import java.util.UUID
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.Directory
import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.ReleaseType
@@ -111,28 +113,35 @@ data class RawAlbum(
/** @see RawArtist.name */
val rawArtists: List
) {
- // Albums are grouped as follows:
- // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
- // same name to be differentiated, which is common in large libraries.
- // - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
- // artist name. This allows for case-insensitive artist/album grouping, which can be common
- // for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
+ val key = Key(this)
- // Cache the hash-code for HashMap efficiency.
- private val hashCode =
- musicBrainzId?.hashCode() ?: (31 * name.lowercase().hashCode() + rawArtists.hashCode())
+ /** Exposed information that denotes [RawAlbum] uniqueness. */
+ data class Key(val value: RawAlbum) {
+ // Albums are grouped as follows:
+ // - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
+ // same name to be differentiated, which is common in large libraries.
+ // - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
+ // artist name. This allows for case-insensitive artist/album grouping, which can be common
+ // for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
- override fun hashCode() = hashCode
+ // Cache the hash-code for HashMap efficiency.
+ private val hashCode =
+ value.musicBrainzId?.hashCode()
+ ?: (31 * value.name.lowercase().hashCode() + value.rawArtists.hashCode())
- override fun equals(other: Any?) =
- other is RawAlbum &&
- when {
- musicBrainzId != null && other.musicBrainzId != null ->
- musicBrainzId == other.musicBrainzId
- musicBrainzId == null && other.musicBrainzId == null ->
- name.equals(other.name, true) && rawArtists == other.rawArtists
- else -> false
- }
+ override fun hashCode() = hashCode
+
+ override fun equals(other: Any?) =
+ other is Key &&
+ when {
+ value.musicBrainzId != null && other.value.musicBrainzId != null ->
+ value.musicBrainzId == other.value.musicBrainzId
+ value.musicBrainzId == null && other.value.musicBrainzId == null ->
+ other.value.name.equals(other.value.name, true) &&
+ other.value.rawArtists == other.value.rawArtists
+ else -> false
+ }
+ }
}
/**
@@ -149,33 +158,42 @@ data class RawArtist(
/** @see Music.name */
val sortName: String? = null
) {
- // Artists are grouped as follows:
- // - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
- // same name to be differentiated, which is common in large libraries.
- // - If we do not have a MusicBrainz ID, compare by the lowercase name. This allows artist
- // grouping to be case-insensitive.
+ val key = Key(this)
- // Cache the hashCode for HashMap efficiency.
- private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
+ /**
+ * Allows [RawArtist]s to be compared by "fundamental" information that is unlikely to change on
+ * an item-by-item
+ */
+ data class Key(val value: RawArtist) {
+ // Artists are grouped as follows:
+ // - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
+ // same name to be differentiated, which is common in large libraries.
+ // - If we do not have a MusicBrainz ID, compare by the lowercase name. This allows artist
+ // grouping to be case-insensitive.
- // Compare names and MusicBrainz IDs in order to differentiate artists with the
- // same name in large libraries.
+ // Cache the hashCode for HashMap efficiency.
+ private val hashCode = value.musicBrainzId?.hashCode() ?: value.name?.lowercase().hashCode()
- override fun hashCode() = hashCode
+ // Compare names and MusicBrainz IDs in order to differentiate artists with the
+ // same name in large libraries.
- override fun equals(other: Any?) =
- other is RawArtist &&
- when {
- musicBrainzId != null && other.musicBrainzId != null ->
- musicBrainzId == other.musicBrainzId
- musicBrainzId == null && other.musicBrainzId == null ->
- when {
- name != null && other.name != null -> name.equals(other.name, true)
- name == null && other.name == null -> true
- else -> false
- }
- else -> false
- }
+ override fun hashCode() = hashCode
+
+ override fun equals(other: Any?) =
+ other is Key &&
+ when {
+ value.musicBrainzId != null && other.value.musicBrainzId != null ->
+ value.musicBrainzId == other.value.musicBrainzId
+ value.musicBrainzId == null && other.value.musicBrainzId == null ->
+ when {
+ value.name != null && other.value.name != null ->
+ value.name.equals(other.value.name, true)
+ value.name == null && other.value.name == null -> true
+ else -> false
+ }
+ else -> false
+ }
+ }
}
/**
@@ -187,20 +205,24 @@ data class RawGenre(
/** @see Music.name */
val name: String? = null
) {
+ val key = Key(this)
- // Cache the hashCode for HashMap efficiency.
- private val hashCode = name?.lowercase().hashCode()
+ data class Key(val value: RawGenre) {
+ // Cache the hashCode for HashMap efficiency.
+ private val hashCode = value.name?.lowercase().hashCode()
- // Only group by the lowercase genre name. This allows Genre grouping to be
- // case-insensitive, which may be helpful in some libraries with different ways of
- // formatting genres.
- override fun hashCode() = hashCode
+ // Only group by the lowercase genre name. This allows Genre grouping to be
+ // case-insensitive, which may be helpful in some libraries with different ways of
+ // formatting genres.
+ override fun hashCode() = hashCode
- override fun equals(other: Any?) =
- other is RawGenre &&
- when {
- name != null && other.name != null -> name.equals(other.name, true)
- name == null && other.name == null -> true
- else -> false
- }
+ override fun equals(other: Any?) =
+ other is Key &&
+ when {
+ value.name != null && other.value.name != null ->
+ value.name.equals(other.value.name, true)
+ value.name == null && other.value.name == null -> true
+ else -> false
+ }
+ }
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt
index 5913c2b8c..5e0799d72 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt
@@ -25,6 +25,7 @@ import org.oxycblt.auxio.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
+import org.oxycblt.auxio.util.logD
/**
* [RecyclerView.Adapter] that manages a list of [Directory] instances.
@@ -54,10 +55,8 @@ class DirectoryAdapter(private val listener: Listener) :
* @param dir The [Directory] to add.
*/
fun add(dir: Directory) {
- if (_dirs.contains(dir)) {
- return
- }
-
+ if (_dirs.contains(dir)) return
+ logD("Adding $dir")
_dirs.add(dir)
notifyItemInserted(_dirs.lastIndex)
}
@@ -65,9 +64,10 @@ class DirectoryAdapter(private val listener: Listener) :
/**
* Add a list of [Directory] instances to the end of the list.
*
- * @param dirs The [Directory instances to add.
+ * @param dirs The [Directory] instances to add.
*/
fun addAll(dirs: List) {
+ logD("Adding ${dirs.size} directories")
val oldLastIndex = dirs.lastIndex
_dirs.addAll(dirs)
notifyItemRangeInserted(oldLastIndex, dirs.size)
@@ -79,6 +79,7 @@ class DirectoryAdapter(private val listener: Listener) :
* @param dir The [Directory] to remove. Must exist in the list.
*/
fun remove(dir: Directory) {
+ logD("Removing $dir")
val idx = _dirs.indexOf(dir)
_dirs.removeAt(idx)
notifyItemRemoved(idx)
@@ -86,6 +87,7 @@ class DirectoryAdapter(private val listener: Listener) :
/** A Listener for [DirectoryAdapter] interactions. */
interface Listener {
+ /** Called when the delete button on a directory item is clicked. */
fun onRemoveDirectory(dir: Directory)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt
index defbb7c3f..93a777a6a 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt
@@ -145,6 +145,8 @@ data class MusicDirectories(val dirs: List, val shouldInclude: Boolea
* @param fromFormat The mime type obtained by analyzing the file format. Null if could not be
* obtained.
* @author Alexander Capehart (OxygenCobalt)
+ *
+ * TODO: Get around to simplifying this
*/
data class MimeType(val fromExtension: String, val fromFormat: String?) {
/**
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 0df80983b..42e29eed2 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
@@ -120,6 +120,7 @@ private abstract class BaseMediaStoreExtractor(
if (dirs.dirs.isNotEmpty()) {
selector += " AND "
if (!dirs.shouldInclude) {
+ logD("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.
@@ -144,14 +145,14 @@ private abstract class BaseMediaStoreExtractor(
}
// Now we can actually query MediaStore.
- logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]")
+ logD("Starting song query [proj=${projection.toList()}, selector=$selector, args=$args]")
val cursor =
context.contentResolverSafe.safeQuery(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
selector,
args.toTypedArray())
- logD("Song query succeeded [Projected total: ${cursor.count}]")
+ logD("Successfully queried for ${cursor.count} songs")
val genreNamesMap = mutableMapOf()
@@ -186,6 +187,7 @@ private abstract class BaseMediaStoreExtractor(
}
}
+ logD("Read ${genreNamesMap.values.distinct().size} genres from MediaStore")
logD("Finished initialization in ${System.currentTimeMillis() - start}ms")
return wrapQuery(cursor, genreNamesMap)
}
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 46e4130f0..860b3e315 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,6 +24,7 @@ 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.nonZeroOrNull
/**
@@ -51,33 +52,30 @@ class Date private constructor(private val tokens: List) : Comparable
* 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
* be properly localized.
*/
- fun resolveDate(context: Context): String {
- if (month != null) {
- // Parse a date format from an ISO-ish format
- val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
- format.applyPattern("yyyy-MM")
- val date =
- try {
- format.parse("$year-$month")
- } catch (e: ParseException) {
- null
- }
-
- if (date != null) {
- // Reformat as a readable month and year
- format.applyPattern("MMM yyyy")
- return format.format(date)
- }
- }
-
+ fun resolve(context: Context) =
// Unable to create fine-grained date, just format as a year.
- return context.getString(R.string.fmt_number, year)
+ month?.let { resolveFineGrained() } ?: context.getString(R.string.fmt_number, year)
+
+ private fun resolveFineGrained(): String? {
+ // We can't directly load a date with our own
+ val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
+ format.applyPattern("yyyy-MM")
+ val date =
+ try {
+ format.parse("$year-$month")
+ } catch (e: ParseException) {
+ logE("Unable to parse fine-grained date: $e")
+ return null
+ }
+
+ // Reformat as a readable month and year
+ format.applyPattern("MMM yyyy")
+ return format.format(date)
}
- override fun hashCode() = tokens.hashCode()
-
override fun equals(other: Any?) = other is Date && compareTo(other) == 0
-
+ override fun hashCode() = tokens.hashCode()
+ override fun toString() = StringBuilder().appendDate().toString()
override fun compareTo(other: Date): Int {
for (i in 0 until max(tokens.size, other.tokens.size)) {
val ai = tokens.getOrNull(i)
@@ -98,8 +96,6 @@ class Date private constructor(private val tokens: List) : Comparable
return 0
}
- override fun toString() = StringBuilder().appendDate().toString()
-
private fun StringBuilder.appendDate(): StringBuilder {
// Construct an ISO-8601 date, dropping precision that doesn't exist.
append(year.toStringFixed(4))
@@ -120,13 +116,15 @@ class Date private constructor(private val tokens: List) : Comparable
*
* @author Alexander Capehart
*/
- class Range
- private constructor(
+ class Range(
/** The earliest [Date] in the range. */
val min: Date,
/** the latest [Date] in the range. May be the same as [min]. ] */
val max: Date
) : Comparable {
+ init {
+ check(min <= max) { "Min date must be <= max date" }
+ }
/**
* Resolve this instance into a human-readable date range.
@@ -139,9 +137,9 @@ class Date private constructor(private val tokens: List) : Comparable
fun resolveDate(context: Context) =
if (min != max) {
context.getString(
- R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context))
+ R.string.fmt_date_range, min.resolve(context), max.resolve(context))
} else {
- min.resolveDate(context)
+ min.resolve(context)
}
override fun equals(other: Any?) = other is Range && min == other.min && max == other.max
@@ -149,35 +147,6 @@ class Date private constructor(private val tokens: List) : Comparable
override fun hashCode() = 31 * max.hashCode() + min.hashCode()
override fun compareTo(other: Range) = min.compareTo(other.min)
-
- companion object {
- /**
- * Create a [Range] from the given list of [Date]s.
- *
- * @param dates The [Date]s to use.
- * @return A [Range] based on the minimum and maximum [Date]s. If there are no [Date]s,
- * null is returned.
- */
- fun from(dates: List): Range? {
- if (dates.isEmpty()) {
- // Nothing to do.
- return null
- }
- // Simultaneously find the minimum and maximum values in the given range.
- // If this list has only one item, then that one date is the minimum and maximum.
- var min = dates.first()
- var max = min
- for (i in 1..dates.lastIndex) {
- if (dates[i] < min) {
- min = dates[i]
- }
- if (dates[i] > max) {
- max = dates[i]
- }
- }
- return Range(min, max)
- }
- }
}
companion object {
diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt
index 759d52b49..52b7ab646 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt
@@ -27,6 +27,7 @@ import org.oxycblt.auxio.list.Item
* @param name The name of the disc group, if any. Null if not present.
*/
class Disc(val number: Int, val name: String?) : Item, Comparable {
+ // We don't want to group discs by differing subtitles, so only compare by the number
override fun equals(other: Any?) = other is Disc && number == other.number
override fun hashCode() = number.hashCode()
override fun compareTo(other: Disc) = number.compareTo(other.number)
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 3b7c3bfc7..838f8f5d5 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
@@ -174,6 +174,8 @@ private data class IntelligentKnownName(override val raw: String, override val s
override val sortTokens = parseTokens(sort ?: raw)
private fun parseTokens(name: String): List {
+ // TODO: This routine is consuming much of the song building runtime, find a way to
+ // optimize it
val stripped =
name
// Remove excess punctuation from the string, as those u
@@ -201,6 +203,7 @@ private data class IntelligentKnownName(override val raw: String, override val s
// Separate each token into their numeric and lexicographic counterparts.
if (token.first().isDigit()) {
// The digit string comparison breaks with preceding zero digits, remove those
+ // TODO: Handle zero digits in other languages
val digits = token.trimStart('0').ifEmpty { token }
// Other languages have other types of digit strings, still use collation keys
collationKey = COLLATOR.getCollationKey(digits)
diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt b/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt
index 20ac60034..c4cb00fd3 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt
@@ -19,6 +19,7 @@
package org.oxycblt.auxio.music.info
import org.oxycblt.auxio.R
+import org.oxycblt.auxio.music.info.ReleaseType.Album
/**
* The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc.
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 acea28744..6e3646b62 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
@@ -104,25 +104,23 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
null
}
- val resolvedMimeType =
- if (song.mimeType.fromFormat != null) {
- // ExoPlayer was already able to populate the format.
- song.mimeType
- } else {
- // ExoPlayer couldn't populate the format somehow, populate it here.
- val formatMimeType =
- try {
- format.getString(MediaFormat.KEY_MIME)
- } catch (e: NullPointerException) {
- logE("Unable to extract mime type field")
- null
- }
-
- MimeType(song.mimeType.fromExtension, formatMimeType)
+ // The song's mime type won't have a populated format field right now, try to
+ // extract it ourselves.
+ val formatMimeType =
+ try {
+ format.getString(MediaFormat.KEY_MIME)
+ } catch (e: NullPointerException) {
+ logE("Unable to extract mime type field")
+ null
}
extractor.release()
- return AudioProperties(bitrate, sampleRate, resolvedMimeType)
+ logD("Finished extracting audio properties")
+
+ return AudioProperties(
+ bitrate,
+ sampleRate,
+ MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType))
}
}
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 5fa612667..3496ea059 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,12 +30,15 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
+import org.oxycblt.auxio.util.logW
/**
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
* split tags with multiple values.
*
* @author Alexander Capehart (OxygenCobalt)
+ *
+ * TODO: Replace with unsplit names dialog
*/
@AndroidEntryPoint
class SeparatorsDialog : ViewBindingDialogFragment() {
@@ -74,7 +77,7 @@ class SeparatorsDialog : ViewBindingDialogFragment() {
Separators.SLASH -> binding.separatorSlash.isChecked = true
Separators.PLUS -> binding.separatorPlus.isChecked = true
Separators.AND -> binding.separatorAnd.isChecked = true
- else -> error("Unexpected separator in settings data")
+ else -> logW("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 bbc2971a0..4cca1a824 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
@@ -23,6 +23,7 @@ import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.device.RawSong
+import org.oxycblt.auxio.util.logD
/**
* The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the
@@ -52,6 +53,8 @@ class TagExtractorImpl @Inject constructor(private val tagWorkerFactory: TagWork
// producing similar throughput's to other kinds of manual metadata extraction.
val tagWorkerPool: Array = arrayOfNulls(TASK_CAPACITY)
+ logD("Beginning primary extraction loop")
+
for (incompleteRawSong in incompleteSongs) {
spin@ while (true) {
for (i in tagWorkerPool.indices) {
@@ -71,6 +74,8 @@ class TagExtractorImpl @Inject constructor(private val tagWorkerFactory: TagWork
}
}
+ logD("All incomplete songs exhausted, starting cleanup loop")
+
do {
var ongoingTasks = false
for (i in tagWorkerPool.indices) {
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 62f19c61b..c4f20df4a 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
@@ -39,6 +39,8 @@ fun List.parseMultiValue(settings: MusicSettings) =
this
}
+// TODO: Remove the escaping checks, it's too expensive to do this for every single tag.
+
/**
* Split a [String] by the given selector, automatically handling escaped characters that satisfy
* the selector.
@@ -106,7 +108,7 @@ fun List.correctWhitespace() = mapNotNull { it.correctWhitespace() }
* @return A list of one or more [String]s that were split up by the user-defined separators.
*/
private fun String.maybeParseBySeparators(settings: MusicSettings): List {
- // Get the separators the user desires. If null, there's nothing to do.
+ if (settings.multiValueSeparators.isEmpty()) return listOf(this)
return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace()
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt
index 115462a8a..b709cb558 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt
@@ -89,12 +89,8 @@ private class TagWorkerImpl(
} catch (e: Exception) {
logW("Unable to extract metadata for ${rawSong.name}")
logW(e.stackTraceToString())
- null
+ return rawSong
}
- if (format == null) {
- logD("Nothing could be extracted for ${rawSong.name}")
- return rawSong
- }
val metadata = format.metadata
if (metadata != null) {
diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt
index 1cdb8b4db..d22f9a5e9 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/picker/AddToPlaylistDialog.kt
@@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
/**
@@ -93,7 +94,7 @@ class AddToPlaylistDialog :
private fun updatePendingSongs(songs: List?) {
if (songs == null) {
- // No songs to feasibly add to a playlist, leave.
+ logD("No songs to show choices for, navigating away")
findNavController().navigateUp()
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt
index afc90c825..15d347199 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/picker/DeletePlaylistDialog.kt
@@ -32,6 +32,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
@@ -76,7 +77,7 @@ class DeletePlaylistDialog : ViewBindingDialogFragment()
private fun updatePendingPlaylist(pendingPlaylist: PendingPlaylist?) {
if (pendingPlaylist == null) {
+ logD("No playlist to create, leaving")
findNavController().navigateUp()
return
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt
index 2181f4605..51a9895cb 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PlaylistPickerViewModel.kt
@@ -31,6 +31,9 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.logE
+import org.oxycblt.auxio.util.logW
/**
* A [ViewModel] managing the state of the playlist picker dialogs.
@@ -84,6 +87,8 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
pendingPlaylist.preferredName,
pendingPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
}
+ logD("Updated pending playlist: ${_currentPendingPlaylist.value?.preferredName}")
+
_currentSongsToAdd.value =
_currentSongsToAdd.value?.let { pendingSongs ->
pendingSongs
@@ -91,6 +96,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
.ifEmpty { null }
.also { refreshChoicesWith = it }
}
+ logD("Updated songs to add: ${_currentSongsToAdd.value?.size} songs")
}
val chosenName = _chosenName.value
@@ -102,6 +108,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
// Nothing to do.
}
}
+ logD("Updated chosen name to $chosenName")
refreshChoicesWith = refreshChoicesWith ?: _currentSongsToAdd.value
}
@@ -119,19 +126,34 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
* @param songUids The [Music.UID]s of songs to be present in the playlist.
*/
fun setPendingPlaylist(context: Context, songUids: Array) {
- val deviceLibrary = musicRepository.deviceLibrary ?: return
- val songs = songUids.mapNotNull(deviceLibrary::findSong)
-
+ logD("Opening ${songUids.size} songs to create a playlist from")
val userLibrary = musicRepository.userLibrary ?: return
- var i = 1
- while (true) {
- val possibleName = context.getString(R.string.fmt_def_playlist, i)
- if (userLibrary.playlists.none { it.name.resolve(context) == possibleName }) {
- _currentPendingPlaylist.value = PendingPlaylist(possibleName, songs)
- return
+ val songs =
+ musicRepository.deviceLibrary
+ ?.let { songUids.mapNotNull(it::findSong) }
+ ?.also(::refreshPlaylistChoices)
+
+ val possibleName =
+ musicRepository.userLibrary?.let {
+ // Attempt to generate a unique default name for the playlist, like "Playlist 1".
+ var i = 1
+ var possibleName: String
+ do {
+ possibleName = context.getString(R.string.fmt_def_playlist, i)
+ logD("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")
+ possibleName
+ }
+
+ _currentPendingPlaylist.value =
+ if (possibleName != null && songs != null) {
+ PendingPlaylist(possibleName, songs)
+ } else {
+ logW("Given song UIDs to create were invalid")
+ null
}
- ++i
- }
}
/**
@@ -140,7 +162,11 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
* @param playlistUid The [Music.UID]s of the [Playlist] to rename.
*/
fun setPlaylistToRename(playlistUid: Music.UID) {
+ logD("Opening playlist $playlistUid to rename")
_currentPlaylistToRename.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
+ if (_currentPlaylistToDelete.value == null) {
+ logW("Given playlist UID to rename was invalid")
+ }
}
/**
@@ -149,7 +175,11 @@ 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")
_currentPlaylistToDelete.value = musicRepository.userLibrary?.findPlaylist(playlistUid)
+ if (_currentPlaylistToDelete.value == null) {
+ logW("Given playlist UID to delete was invalid")
+ }
}
/**
@@ -158,16 +188,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")
_chosenName.value =
when {
- name.isNullOrEmpty() -> ChosenName.Empty
- name.isBlank() -> ChosenName.Blank
+ name.isNullOrEmpty() -> {
+ logE("Chosen name is empty")
+ ChosenName.Empty
+ }
+ name.isBlank() -> {
+ logE("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")
ChosenName.Valid(trimmed)
} else {
+ logD("Chosen name already exists in library")
ChosenName.AlreadyExists(trimmed)
}
}
@@ -180,14 +219,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) {
- val deviceLibrary = musicRepository.deviceLibrary ?: return
- val songs = songUids.mapNotNull(deviceLibrary::findSong)
- _currentSongsToAdd.value = songs
- refreshPlaylistChoices(songs)
+ logD("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")
+ }
}
private fun refreshPlaylistChoices(songs: List) {
val userLibrary = musicRepository.userLibrary ?: return
+ logD("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/picker/RenamePlaylistDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/RenamePlaylistDialog.kt
index fcc8b2538..20ed39bd5 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/picker/RenamePlaylistDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/picker/RenamePlaylistDialog.kt
@@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
@@ -86,7 +87,9 @@ class RenamePlaylistDialog : ViewBindingDialogFragment
) : Playlist {
override val durationMs = songs.sumOf { it.durationMs }
- override val albums =
- songs.groupBy { it.album }.entries.sortedByDescending { it.value.size }.map { it.key }
+ private var hashCode = uid.hashCode()
+
+ init {
+ hashCode = 31 * hashCode + name.hashCode()
+ hashCode = 31 * hashCode + songs.hashCode()
+ }
+
+ override fun equals(other: Any?) =
+ other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs
+ override fun hashCode() = hashCode
+ override fun toString() = "Playlist(uid=$uid, name=$name)"
/**
* Clone the data in this instance to a new [PlaylistImpl] with the given [name].
@@ -55,16 +68,6 @@ private constructor(
*/
inline fun edit(edits: MutableList.() -> Unit) = edit(songs.toMutableList().apply(edits))
- override fun equals(other: Any?) =
- other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs
-
- override fun hashCode(): Int {
- var hashCode = uid.hashCode()
- hashCode = 31 * hashCode + name.hashCode()
- hashCode = 31 * hashCode + songs.hashCode()
- return hashCode
- }
-
companion object {
/**
* Create a new instance with a novel UID.
diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt b/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt
index 96d3b5e77..27cf62554 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt
@@ -18,7 +18,12 @@
package org.oxycblt.auxio.music.user
-import androidx.room.*
+import androidx.room.ColumnInfo
+import androidx.room.Embedded
+import androidx.room.Entity
+import androidx.room.Junction
+import androidx.room.PrimaryKey
+import androidx.room.Relation
import org.oxycblt.auxio.music.Music
/**
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 fc64f5918..412b14fa4 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
@@ -18,10 +18,17 @@
package org.oxycblt.auxio.music.user
+import java.lang.Exception
import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.MusicRepository
+import org.oxycblt.auxio.music.MusicSettings
+import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary
+import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.logE
/**
* Organized library information controlled by the user.
@@ -118,7 +125,14 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus
UserLibrary.Factory {
override suspend fun read(deviceLibraryChannel: Channel): MutableUserLibrary {
// While were waiting for the library, read our playlists out.
- val rawPlaylists = playlistDao.readRawPlaylists()
+ val rawPlaylists =
+ try {
+ playlistDao.readRawPlaylists()
+ } catch (e: Exception) {
+ logE("Unable to read playlists: $e")
+ return UserLibraryImpl(playlistDao, mutableMapOf(), musicSettings)
+ }
+ logD("Successfully read ${rawPlaylists.size} playlists")
val deviceLibrary = deviceLibraryChannel.receive()
// Convert the database playlist information to actual usable playlists.
val playlistMap = mutableMapOf()
@@ -135,6 +149,10 @@ private class UserLibraryImpl(
private val playlistMap: MutableMap,
private val musicSettings: MusicSettings
) : MutableUserLibrary {
+ override fun hashCode() = playlistMap.hashCode()
+ override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap
+ override fun toString() = "UserLibrary(playlists=${playlists.size})"
+
override val playlists: List
get() = playlistMap.values.toList()
@@ -143,40 +161,81 @@ private class UserLibraryImpl(
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
override suspend fun createPlaylist(name: String, songs: List) {
+ // TODO: Use synchronized with value access too
val playlistImpl = PlaylistImpl.from(name, songs, musicSettings)
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
val rawPlaylist =
RawPlaylist(
PlaylistInfo(playlistImpl.uid, playlistImpl.name.raw),
playlistImpl.songs.map { PlaylistSong(it.uid) })
- playlistDao.insertPlaylist(rawPlaylist)
+ try {
+ playlistDao.insertPlaylist(rawPlaylist)
+ logD("Successfully created playlist $name with ${songs.size} songs")
+ } catch (e: Exception) {
+ logE("Unable to create playlist $name with ${songs.size} songs")
+ logE(e.stackTraceToString())
+ synchronized(this) { playlistMap.remove(playlistImpl.uid) }
+ return
+ }
}
override suspend fun renamePlaylist(playlist: Playlist, name: String) {
val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(name, musicSettings) }
- playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name))
+ try {
+ playlistDao.replacePlaylistInfo(PlaylistInfo(playlist.uid, name))
+ logD("Successfully renamed $playlist to $name")
+ } catch (e: Exception) {
+ logE("Unable to rename $playlist to $name: $e")
+ logE(e.stackTraceToString())
+ synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
+ return
+ }
}
override suspend fun deletePlaylist(playlist: Playlist) {
- synchronized(this) {
- requireNotNull(playlistMap.remove(playlist.uid)) { "Cannot remove invalid playlist" }
+ val playlistImpl =
+ requireNotNull(playlistMap[playlist.uid]) { "Cannot remove invalid playlist" }
+ synchronized(this) { playlistMap.remove(playlistImpl.uid) }
+ try {
+ playlistDao.deletePlaylist(playlist.uid)
+ logD("Successfully deleted $playlist")
+ } catch (e: Exception) {
+ logE("Unable to delete $playlist: $e")
+ logE(e.stackTraceToString())
+ synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
+ return
}
- playlistDao.deletePlaylist(playlist.uid)
}
override suspend fun addToPlaylist(playlist: Playlist, songs: List) {
val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" }
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } }
- playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
+ try {
+ playlistDao.insertPlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
+ logD("Successfully added ${songs.size} songs to $playlist")
+ } catch (e: Exception) {
+ logE("Unable to add ${songs.size} songs to $playlist: $e")
+ logE(e.stackTraceToString())
+ synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
+ return
+ }
}
override suspend fun rewritePlaylist(playlist: Playlist, songs: List) {
val playlistImpl =
requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" }
synchronized(this) { playlistMap[playlist.uid] = playlistImpl.edit(songs) }
- playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
+ try {
+ playlistDao.replacePlaylistSongs(playlist.uid, songs.map { PlaylistSong(it.uid) })
+ logD("Successfully rewrote $playlist with ${songs.size} songs")
+ } catch (e: Exception) {
+ logE("Unable to rewrite $playlist with ${songs.size} songs: $e")
+ logE(e.stackTraceToString())
+ synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
+ return
+ }
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt
index 087e46b35..9c46bbe78 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt
@@ -18,7 +18,14 @@
package org.oxycblt.auxio.music.user
-import androidx.room.*
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.RoomDatabase
+import androidx.room.Transaction
+import androidx.room.TypeConverters
import org.oxycblt.auxio.music.Music
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt
index 6e2f43f83..5271e49c7 100644
--- a/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/navigation/NavigationViewModel.kt
@@ -32,7 +32,7 @@ import org.oxycblt.auxio.util.logD
*
* @author Alexander Capehart (OxygenCobalt)
*
- * TODO: This whole system is very jankily designed, perhaps it's time for a refactor?
+ * TODO: Unwind this into ViewModel-specific actions, and then reference those.
*/
class NavigationViewModel : ViewModel() {
private val _mainNavigationAction = MutableEvent()
@@ -96,6 +96,7 @@ class NavigationViewModel : ViewModel() {
* dialog will be shown.
*/
fun exploreNavigateToParentArtist(song: Song) {
+ logD("Navigating to parent artist of $song")
exploreNavigateToParentArtistImpl(song, song.artists)
}
@@ -106,6 +107,7 @@ class NavigationViewModel : ViewModel() {
* dialog will be shown.
*/
fun exploreNavigateToParentArtist(album: Album) {
+ logD("Navigating to parent artist of $album")
exploreNavigateToParentArtistImpl(album, album.artists)
}
diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt
index a8614af77..ade74f930 100644
--- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigateToArtistDialog.kt
@@ -78,7 +78,7 @@ class NavigateToArtistDialog :
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
super.onDestroyBinding(binding)
- choiceAdapter
+ binding.choiceRecycler.adapter = null
}
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {
diff --git a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt
index b09b74ae9..f02621d5b 100644
--- a/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/navigation/picker/NavigationPickerViewModel.kt
@@ -23,7 +23,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+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
/**
* A [ViewModel] that stores the current information required for navigation picker dialogs
@@ -58,6 +63,7 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository:
}
else -> null
}
+ logD("Updated artist choices: ${_artistChoices.value}")
}
override fun onCleared() {
@@ -71,12 +77,22 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository:
* @param itemUid The [Music.UID] of the item to show. Must be a [Song] or [Album].
*/
fun setArtistChoiceUid(itemUid: Music.UID) {
+ logD("Opening navigation choices for $itemUid")
// Support Songs and Albums, which have parent artists.
_artistChoices.value =
when (val music = musicRepository.find(itemUid)) {
- is Song -> SongArtistNavigationChoices(music)
- is Album -> AlbumArtistNavigationChoices(music)
- else -> null
+ is Song -> {
+ logD("Creating navigation choices for song")
+ SongArtistNavigationChoices(music)
+ }
+ is Album -> {
+ logD("Creating navigation choices for album")
+ AlbumArtistNavigationChoices(music)
+ }
+ else -> {
+ logD("Given song/album UID was invalid")
+ null
+ }
}
}
}
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 f49d6df44..05f438c38 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt
@@ -21,6 +21,7 @@ package org.oxycblt.auxio.playback
import android.os.Bundle
import android.view.LayoutInflater
import androidx.fragment.app.activityViewModels
+import com.google.android.material.R as MR
import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
@@ -33,6 +34,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
/**
* A [ViewBindingFragment] that shows the current playback state in a compact manner.
@@ -92,14 +94,16 @@ class PlaybackBarFragment : ViewBindingFragment() {
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, actionMode: ActionMode) {
when (actionMode) {
ActionMode.NEXT -> {
+ logD("Setting up skip next action")
binding.playbackSecondaryAction.apply {
setIconResource(R.drawable.ic_skip_next_24)
contentDescription = getString(R.string.desc_skip_next)
- iconTint = context.getAttrColorCompat(R.attr.colorOnSurfaceVariant)
+ iconTint = context.getAttrColorCompat(MR.attr.colorOnSurfaceVariant)
setOnClickListener { playbackModel.next() }
}
}
ActionMode.REPEAT -> {
+ logD("Setting up repeat mode action")
binding.playbackSecondaryAction.apply {
contentDescription = getString(R.string.desc_change_repeat)
iconTint = context.getColorCompat(R.color.sel_activatable_icon)
@@ -108,6 +112,7 @@ class PlaybackBarFragment : ViewBindingFragment() {
}
}
ActionMode.SHUFFLE -> {
+ logD("Setting up shuffle action")
binding.playbackSecondaryAction.apply {
setIconResource(R.drawable.sel_shuffle_state_24)
contentDescription = getString(R.string.desc_shuffle)
@@ -120,14 +125,17 @@ class PlaybackBarFragment : ViewBindingFragment() {
}
private fun updateSong(song: Song?) {
- if (song != null) {
- val context = requireContext()
- val binding = requireBinding()
- binding.playbackCover.bind(song)
- binding.playbackSong.text = song.name.resolve(context)
- binding.playbackInfo.text = song.artists.resolveNames(context)
- binding.playbackProgressBar.max = song.durationMs.msToDs().toInt()
+ if (song == null) {
+ // Nothing to do.
+ return
}
+
+ val context = requireContext()
+ val binding = requireBinding()
+ binding.playbackCover.bind(song)
+ binding.playbackSong.text = song.name.resolve(context)
+ binding.playbackInfo.text = song.artists.resolveNames(context)
+ binding.playbackProgressBar.max = song.durationMs.msToDs().toInt()
}
private fun updatePlaying(isPlaying: Boolean) {
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 a1a7b86b9..a2ed51882 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt
@@ -24,6 +24,7 @@ import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
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 org.oxycblt.auxio.ui.BaseBottomSheetBehavior
@@ -39,7 +40,7 @@ class PlaybackBottomSheetBehavior(context: Context, attributeSet: Attr
BaseBottomSheetBehavior(context, attributeSet) {
val sheetBackgroundDrawable =
MaterialShapeDrawable.createWithElevationOverlay(context).apply {
- fillColor = context.getAttrColorCompat(R.attr.colorSurface)
+ fillColor = context.getAttrColorCompat(MR.attr.colorSurface)
elevation = context.getDimen(R.dimen.elevation_normal)
}
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 f7bad82e9..6bcc4114e 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt
@@ -43,6 +43,8 @@ import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.ui.StyledSeekBar
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.share
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat
@@ -141,6 +143,7 @@ class PlaybackPanelFragment :
when (item.itemId) {
R.id.action_open_equalizer -> {
// Launch the system equalizer app, if possible.
+ logD("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
@@ -180,6 +183,10 @@ class PlaybackPanelFragment :
}
true
}
+ R.id.action_share -> {
+ playbackModel.song.value?.let { requireContext().share(it) }
+ true
+ }
else -> false
}
@@ -195,6 +202,7 @@ class PlaybackPanelFragment :
val binding = requireBinding()
val context = requireContext()
+ logD("Updating song display: $song")
binding.playbackCover.bind(song)
binding.playbackSong.text = song.name.resolve(context)
binding.playbackArtist.text = song.artists.resolveNames(context)
@@ -228,13 +236,11 @@ class PlaybackPanelFragment :
requireBinding().playbackShuffle.isActivated = isShuffled
}
- /** Navigate to one of the currently playing [Song]'s Artists. */
private fun navigateToCurrentArtist() {
val song = playbackModel.song.value ?: return
navModel.exploreNavigateToParentArtist(song)
}
- /** Navigate to the currently playing [Song]'s albums. */
private fun navigateToCurrentAlbum() {
val song = playbackModel.song.value ?: return
navModel.exploreNavigateTo(song.album)
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 2a65c85a3..8ec5db941 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt
@@ -198,8 +198,14 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont
when (key) {
getString(R.string.set_key_replay_gain),
getString(R.string.set_key_pre_amp_with),
- getString(R.string.set_key_pre_amp_without) -> listener.onReplayGainSettingsChanged()
- getString(R.string.set_key_notif_action) -> listener.onNotificationActionChanged()
+ getString(R.string.set_key_pre_amp_without) -> {
+ logD("Dispatching ReplayGain setting change")
+ listener.onReplayGainSettingsChanged()
+ }
+ getString(R.string.set_key_notif_action) -> {
+ logD("Dispatching notification setting change")
+ listener.onNotificationActionChanged()
+ }
}
}
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 68f2cac16..115d00344 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt
@@ -27,17 +27,30 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicParent
+import org.oxycblt.auxio.music.MusicRepository
+import org.oxycblt.auxio.music.MusicSettings
+import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.playback.queue.Queue
-import org.oxycblt.auxio.playback.state.*
+import org.oxycblt.auxio.playback.state.InternalPlayer
+import org.oxycblt.auxio.playback.state.PlaybackStateManager
+import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
+import org.oxycblt.auxio.util.logD
/**
* An [ViewModel] that provides a safe UI frontend for the current playback state.
*
* @author Alexander Capehart (OxygenCobalt)
+ *
+ * TODO: Debug subtle backwards movement of position on pause
*/
@HiltViewModel
class PlaybackViewModel
@@ -114,27 +127,32 @@ constructor(
}
override fun onIndexMoved(queue: Queue) {
+ logD("Index moved, updating current song")
_song.value = queue.currentSong
}
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
// Other types of queue changes preserve the current song.
if (change.type == Queue.Change.Type.SONG) {
+ logD("Queue changed, updating current song")
_song.value = queue.currentSong
}
}
override fun onQueueReordered(queue: Queue) {
+ logD("Queue completely changed, updating current song")
_isShuffled.value = queue.isShuffled
}
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
+ logD("New playback started, updating playback information")
_song.value = queue.currentSong
_parent.value = parent
_isShuffled.value = queue.isShuffled
}
override fun onStateChanged(state: InternalPlayer.State) {
+ logD("Player state changed, starting new position polling")
_isPlaying.value = state.isPlaying
// Still need to update the position now due to co-routine launch delays
_positionDs.value = state.calculateElapsedPositionMs().msToDs()
@@ -159,6 +177,7 @@ constructor(
/** Shuffle all songs in the music library. */
fun shuffleAll() {
+ logD("Shuffling all songs")
playImpl(null, null, true)
}
@@ -174,6 +193,7 @@ constructor(
* @param playbackMode The [MusicMode] to play from.
*/
fun playFrom(song: Song, playbackMode: MusicMode) {
+ logD("Playing $song from $playbackMode")
when (playbackMode) {
MusicMode.SONGS -> playImpl(song, null)
MusicMode.ALBUMS -> playImpl(song, song.album)
@@ -192,10 +212,13 @@ constructor(
*/
fun playFromArtist(song: Song, artist: Artist? = null) {
if (artist != null) {
+ logD("Playing $song from $artist")
playImpl(song, artist)
} else if (song.artists.size == 1) {
+ logD("$song has one artist, playing from it")
playImpl(song, song.artists[0])
} else {
+ logD("$song has multiple artists, showing choice dialog")
_artistPlaybackPickerSong.put(song)
}
}
@@ -209,10 +232,13 @@ constructor(
*/
fun playFromGenre(song: Song, genre: Genre? = null) {
if (genre != null) {
+ logD("Playing $song from $genre")
playImpl(song, genre)
} else if (song.genres.size == 1) {
+ logD("$song has one genre, playing from it")
playImpl(song, song.genres[0])
} else {
+ logD("$song has multiple genres, showing choice dialog")
_genrePlaybackPickerSong.put(song)
}
}
@@ -224,6 +250,7 @@ constructor(
* @param playlist The [Playlist] to play from. Must be linked to the [Song].
*/
fun playFromPlaylist(song: Song, playlist: Playlist) {
+ logD("Playing $song from $playlist")
playImpl(song, playlist)
}
@@ -232,70 +259,100 @@ constructor(
*
* @param album The [Album] to play.
*/
- fun play(album: Album) = playImpl(null, album, false)
+ fun play(album: Album) {
+ logD("Playing $album")
+ playImpl(null, album, false)
+ }
/**
* Play an [Artist].
*
* @param artist The [Artist] to play.
*/
- fun play(artist: Artist) = playImpl(null, artist, false)
+ fun play(artist: Artist) {
+ logD("Playing $artist")
+ playImpl(null, artist, false)
+ }
/**
* Play a [Genre].
*
* @param genre The [Genre] to play.
*/
- fun play(genre: Genre) = playImpl(null, genre, false)
+ fun play(genre: Genre) {
+ logD("Playing $genre")
+ playImpl(null, genre, false)
+ }
/**
* Play a [Playlist].
*
* @param playlist The [Playlist] to play.
*/
- fun play(playlist: Playlist) = playImpl(null, playlist, false)
+ fun play(playlist: Playlist) {
+ logD("Playing $playlist")
+ playImpl(null, playlist, false)
+ }
/**
* Play a list of [Song]s.
*
* @param songs The [Song]s to play.
*/
- fun play(songs: List) = playbackManager.play(null, null, songs, false)
+ fun play(songs: List) {
+ logD("Playing ${songs.size} songs")
+ playbackManager.play(null, null, songs, false)
+ }
/**
* Shuffle an [Album].
*
* @param album The [Album] to shuffle.
*/
- fun shuffle(album: Album) = playImpl(null, album, true)
+ fun shuffle(album: Album) {
+ logD("Shuffling $album")
+ playImpl(null, album, true)
+ }
/**
* Shuffle an [Artist].
*
* @param artist The [Artist] to shuffle.
*/
- fun shuffle(artist: Artist) = playImpl(null, artist, true)
+ fun shuffle(artist: Artist) {
+ logD("Shuffling $artist")
+ playImpl(null, artist, true)
+ }
/**
* Shuffle a [Genre].
*
* @param genre The [Genre] to shuffle.
*/
- fun shuffle(genre: Genre) = playImpl(null, genre, true)
+ fun shuffle(genre: Genre) {
+ logD("Shuffling $genre")
+ playImpl(null, genre, true)
+ }
/**
* Shuffle a [Playlist].
*
* @param playlist The [Playlist] to shuffle.
*/
- fun shuffle(playlist: Playlist) = playImpl(null, playlist, true)
+ fun shuffle(playlist: Playlist) {
+ logD("Shuffling $playlist")
+ playImpl(null, playlist, true)
+ }
/**
* Shuffle a list of [Song]s.
*
* @param songs The [Song]s to shuffle.
*/
- fun shuffle(songs: List) = playbackManager.play(null, null, songs, true)
+ fun shuffle(songs: List) {
+ logD("Shuffling ${songs.size} songs")
+ playbackManager.play(null, null, songs, true)
+ }
private fun playImpl(
song: Song?,
@@ -324,6 +381,7 @@ constructor(
* @param action The [InternalPlayer.Action] to perform eventually.
*/
fun startAction(action: InternalPlayer.Action) {
+ logD("Starting action $action")
playbackManager.startAction(action)
}
@@ -335,6 +393,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")
playbackManager.seekTo(positionDs.dsToMs())
}
@@ -342,11 +401,13 @@ constructor(
/** Skip to the next [Song]. */
fun next() {
+ logD("Skipping to next song")
playbackManager.next()
}
/** Skip to the previous [Song]. */
fun prev() {
+ logD("Skipping to previous song")
playbackManager.prev()
}
@@ -356,6 +417,7 @@ constructor(
* @param song The [Song] to add.
*/
fun playNext(song: Song) {
+ logD("Playing $song next")
playbackManager.playNext(song)
}
@@ -365,6 +427,7 @@ constructor(
* @param album The [Album] to add.
*/
fun playNext(album: Album) {
+ logD("Playing $album next")
playbackManager.playNext(musicSettings.albumSongSort.songs(album.songs))
}
@@ -374,6 +437,7 @@ constructor(
* @param artist The [Artist] to add.
*/
fun playNext(artist: Artist) {
+ logD("Playing $artist next")
playbackManager.playNext(musicSettings.artistSongSort.songs(artist.songs))
}
@@ -383,6 +447,7 @@ constructor(
* @param genre The [Genre] to add.
*/
fun playNext(genre: Genre) {
+ logD("Playing $genre next")
playbackManager.playNext(musicSettings.genreSongSort.songs(genre.songs))
}
@@ -392,6 +457,7 @@ constructor(
* @param playlist The [Playlist] to add.
*/
fun playNext(playlist: Playlist) {
+ logD("Playing $playlist next")
playbackManager.playNext(playlist.songs)
}
@@ -401,6 +467,7 @@ constructor(
* @param songs The [Song]s to add.
*/
fun playNext(songs: List) {
+ logD("Playing ${songs.size} songs next")
playbackManager.playNext(songs)
}
@@ -410,6 +477,7 @@ constructor(
* @param song The [Song] to add.
*/
fun addToQueue(song: Song) {
+ logD("Adding $song to queue")
playbackManager.addToQueue(song)
}
@@ -419,6 +487,7 @@ constructor(
* @param album The [Album] to add.
*/
fun addToQueue(album: Album) {
+ logD("Adding $album to queue")
playbackManager.addToQueue(musicSettings.albumSongSort.songs(album.songs))
}
@@ -428,6 +497,7 @@ constructor(
* @param artist The [Artist] to add.
*/
fun addToQueue(artist: Artist) {
+ logD("Adding $artist to queue")
playbackManager.addToQueue(musicSettings.artistSongSort.songs(artist.songs))
}
@@ -437,6 +507,7 @@ constructor(
* @param genre The [Genre] to add.
*/
fun addToQueue(genre: Genre) {
+ logD("Adding $genre to queue")
playbackManager.addToQueue(musicSettings.genreSongSort.songs(genre.songs))
}
@@ -446,6 +517,7 @@ constructor(
* @param playlist The [Playlist] to add.
*/
fun addToQueue(playlist: Playlist) {
+ logD("Adding $playlist to queue")
playbackManager.addToQueue(playlist.songs)
}
@@ -455,6 +527,7 @@ constructor(
* @param songs The [Song]s to add.
*/
fun addToQueue(songs: List) {
+ logD("Adding ${songs.size} songs to queue")
playbackManager.addToQueue(songs)
}
@@ -462,11 +535,13 @@ constructor(
/** Toggle [isPlaying] (i.e from playing to paused) */
fun togglePlaying() {
+ logD("Toggling playing state")
playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
}
/** Toggle [isShuffled] (ex. from on to off) */
fun toggleShuffled() {
+ logD("Toggling shuffled state")
playbackManager.reorder(!playbackManager.queue.isShuffled)
}
@@ -476,6 +551,7 @@ constructor(
* @see RepeatMode.increment
*/
fun toggleRepeatMode() {
+ logD("Toggling repeat mode")
playbackManager.repeatMode = playbackManager.repeatMode.increment()
}
@@ -487,6 +563,7 @@ constructor(
* @param onDone Called when the save is completed with true if successful, and false otherwise.
*/
fun savePlaybackState(onDone: (Boolean) -> Unit) {
+ logD("Saving playback state")
viewModelScope.launch {
onDone(persistenceRepository.saveState(playbackManager.toSavedState()))
}
@@ -498,6 +575,7 @@ constructor(
* @param onDone Called when the wipe is completed with true if successful, and false otherwise.
*/
fun wipePlaybackState(onDone: (Boolean) -> Unit) {
+ logD("Wiping playback state")
viewModelScope.launch { onDone(persistenceRepository.saveState(null)) }
}
@@ -508,6 +586,7 @@ constructor(
* otherwise.
*/
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
+ logD("Force-restoring playback state")
viewModelScope.launch {
val savedState = persistenceRepository.readState()
if (savedState != null) {
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt
index 545038207..93e387068 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt
@@ -127,6 +127,7 @@ interface QueueDao {
}
// TODO: Figure out how to get RepeatMode to map to an int instead of a string
+// TODO: Use intrinsic table names rather than custom names
@Entity(tableName = PlaybackState.TABLE_NAME)
data class PlaybackState(
@PrimaryKey val id: Int,
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 a246689fe..00b1a8894 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
@@ -61,7 +61,7 @@ constructor(
heap = queueDao.getHeap()
mapping = queueDao.getMapping()
} catch (e: Exception) {
- logE("Unable to load playback state data")
+ logE("Unable read playback state")
logE(e.stackTraceToString())
return null
}
@@ -74,7 +74,7 @@ constructor(
}
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }
- logD("Read playback state")
+ logD("Successfully read playback state")
return PlaybackStateManager.SavedState(
parent = parent,
@@ -90,8 +90,6 @@ constructor(
}
override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean {
- // Only bother saving a state if a song is actively playing from one.
- // This is not the case with a null state.
try {
playbackStateDao.nukeState()
queueDao.nukeHeap()
@@ -101,7 +99,8 @@ constructor(
logE(e.stackTraceToString())
return false
}
- logD("Cleared state")
+
+ logD("Successfully cleared previous state")
if (state != null) {
// Transform saved state into raw state, which can then be written to the database.
val playbackState =
@@ -118,12 +117,14 @@ constructor(
state.queueState.heap.mapIndexed { i, song ->
QueueHeapItem(i, requireNotNull(song).uid)
}
+
val mapping =
state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed {
i,
pair ->
QueueMappingItem(i, pair.first, pair.second)
}
+
try {
playbackStateDao.insertState(playbackState)
queueDao.insertHeap(heap)
@@ -133,8 +134,10 @@ constructor(
logE(e.stackTraceToString())
return false
}
- logD("Wrote state")
+
+ logD("Successfully wrote new state")
}
+
return true
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt
index 0d477bd8d..c8fc134e7 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromArtistDialog.kt
@@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@@ -72,6 +73,7 @@ class PlayFromArtistDialog :
if (it != null) {
choiceAdapter.update(it.artists, UpdateInstructions.Replace(0))
} else {
+ logD("No song to show choices for, navigating away")
findNavController().navigateUp()
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt
index 0b8914dc2..1f2693a10 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlayFromGenreDialog.kt
@@ -35,6 +35,7 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@@ -72,6 +73,7 @@ class PlayFromGenreDialog :
if (it != null) {
choiceAdapter.update(it.genres, UpdateInstructions.Replace(0))
} else {
+ logD("No song to show choices for, navigating away")
findNavController().navigateUp()
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt
index 577b93c50..644b5a580 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/picker/PlaybackPickerViewModel.kt
@@ -23,7 +23,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import org.oxycblt.auxio.music.*
+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
/**
* A [ViewModel] that stores the choices shown in the playback picker dialogs.
@@ -59,6 +64,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")
_currentPickerSong.value = musicRepository.deviceLibrary?.findSong(uid)
+ if (_currentPickerSong.value != null) {
+ logW("Given song UID was invalid")
+ }
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt
index 434e8f479..c5ebf9904 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt
@@ -23,6 +23,7 @@ import kotlin.random.nextInt
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.util.logD
/**
* A heap-backed play queue.
@@ -106,7 +107,7 @@ interface Queue {
}
}
-class EditableQueue : Queue {
+class MutableQueue : Queue {
@Volatile private var heap = mutableListOf()
@Volatile private var orderedMapping = mutableListOf()
@Volatile private var shuffledMapping = mutableListOf()
@@ -174,6 +175,8 @@ class EditableQueue : Queue {
return
}
+ logD("Reordering queue [shuffled=$shuffled]")
+
if (shuffled) {
val trueIndex =
if (shuffledMapping.isNotEmpty()) {
@@ -190,7 +193,7 @@ class EditableQueue : Queue {
shuffledMapping.add(0, shuffledMapping.removeAt(shuffledMapping.indexOf(trueIndex)))
index = 0
} else if (shuffledMapping.isNotEmpty()) {
- // Un-shuffling, song to preserve is in the shuffled mapping.
+ // Ordering queue, song to preserve is in the shuffled mapping.
index = orderedMapping.indexOf(shuffledMapping[index])
shuffledMapping = mutableListOf()
}
@@ -198,26 +201,30 @@ class EditableQueue : Queue {
}
/**
- * Add [Song]s to the top of the queue. Will start playback if nothing is playing.
+ * Add [Song]s to the "top" of the queue (right next to the currently playing song). Will start
+ * playback if nothing is playing.
*
* @param songs The [Song]s to add.
* @return A [Queue.Change] instance that reflects the changes made.
*/
- fun playNext(songs: List): Queue.Change {
+ fun addToTop(songs: List): Queue.Change {
+ logD("Adding ${songs.size} songs to the front of the queue")
+ val insertAt = index + 1
val heapIndices = songs.map(::addSongToHeap)
if (shuffledMapping.isNotEmpty()) {
// Add the new songs in front of the current index in the shuffled mapping and in front
// of the analogous list song in the ordered mapping.
+ logD("Must append songs to shuffled mapping")
val orderedIndex = orderedMapping.indexOf(shuffledMapping[index])
orderedMapping.addAll(orderedIndex + 1, heapIndices)
- shuffledMapping.addAll(index + 1, heapIndices)
+ shuffledMapping.addAll(insertAt, heapIndices)
} else {
// Add the new song in front of the current index in the ordered mapping.
- orderedMapping.addAll(index + 1, heapIndices)
+ logD("Only appending songs to ordered mapping")
+ orderedMapping.addAll(insertAt, heapIndices)
}
check()
- return Queue.Change(
- Queue.Change.Type.MAPPING, UpdateInstructions.Add(index + 1, songs.size))
+ return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size))
}
/**
@@ -226,16 +233,18 @@ class EditableQueue : Queue {
* @param songs The [Song]s to add.
* @return A [Queue.Change] instance that reflects the changes made.
*/
- fun addToQueue(songs: List): Queue.Change {
+ fun addToBottom(songs: List): Queue.Change {
+ logD("Adding ${songs.size} songs to the back of the queue")
+ val insertAt = orderedMapping.size
val heapIndices = songs.map(::addSongToHeap)
// Can simple append the new songs to the end of both mappings.
orderedMapping.addAll(heapIndices)
if (shuffledMapping.isNotEmpty()) {
+ logD("Appending songs to shuffled mapping")
shuffledMapping.addAll(heapIndices)
}
check()
- return Queue.Change(
- Queue.Change.Type.MAPPING, UpdateInstructions.Add(index + 1, songs.size))
+ return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size))
}
/**
@@ -255,19 +264,33 @@ class EditableQueue : Queue {
orderedMapping.add(dst, orderedMapping.removeAt(src))
}
+ val oldIndex = index
when (index) {
// We are moving the currently playing song, correct the index to it's new position.
- src -> index = dst
+ src -> {
+ logD("Moving current song, shifting index")
+ index = dst
+ }
// We have moved an song from behind the playing song to in front, shift back.
- in (src + 1)..dst -> index -= 1
+ in (src + 1)..dst -> {
+ logD("Moving song from behind -> front, shift backwards")
+ index -= 1
+ }
// We have moved an song from in front of the playing song to behind, shift forward.
- in dst until src -> index += 1
+ in dst until src -> {
+ logD("Moving song from front -> behind, shift forward")
+ index += 1
+ }
else -> {
// Nothing to do.
+ logD("Move preserved index")
check()
return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Move(src, dst))
}
}
+
+ logD("Move changed index: $oldIndex -> $index")
+
check()
return Queue.Change(Queue.Change.Type.INDEX, UpdateInstructions.Move(src, dst))
}
@@ -279,6 +302,7 @@ class EditableQueue : Queue {
* @return A [Queue.Change] instance that reflects the changes made.
*/
fun remove(at: Int): Queue.Change {
+ val lastIndex = orderedMapping.lastIndex
if (shuffledMapping.isNotEmpty()) {
// Remove the specified index in the shuffled mapping and the analogous song in the
// ordered mapping.
@@ -296,15 +320,27 @@ class EditableQueue : Queue {
val type =
when {
// We just removed the currently playing song.
- index == at -> Queue.Change.Type.SONG
+ index == at -> {
+ logD("Removed current song")
+ if (lastIndex == index) {
+ logD("Current song at end of queue, shift back")
+ --index
+ }
+ Queue.Change.Type.SONG
+ }
// Index was ahead of removed song, shift back to preserve consistency.
index > at -> {
- index -= 1
+ logD("Removed before current song, shift back")
+ --index
Queue.Change.Type.INDEX
}
// Nothing to do
- else -> Queue.Change.Type.MAPPING
+ else -> {
+ logD("Removal preserved index")
+ Queue.Change.Type.MAPPING
+ }
}
+ logD("Committing change of type $type")
check()
return Queue.Change(type, UpdateInstructions.Remove(at, 1))
}
@@ -337,6 +373,8 @@ class EditableQueue : Queue {
}
}
+ logD("Created adjustment mapping [max shift=$currentShift]")
+
heap = savedState.heap.filterNotNull().toMutableList()
orderedMapping =
savedState.orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
@@ -352,6 +390,7 @@ class EditableQueue : Queue {
while (currentSong?.uid != savedState.songUid && index > -1) {
index--
}
+ logD("Corrected index: ${savedState.index} -> $index")
check()
}
@@ -371,13 +410,17 @@ class EditableQueue : Queue {
orphanCandidates.add(entry.index)
}
}
+ logD("Found orphans: ${orphanCandidates.map { heap[it] }}")
orphanCandidates.removeAll(currentMapping.toSet())
if (orphanCandidates.isNotEmpty()) {
+ val orphan = orphanCandidates.first()
+ logD("Found an orphan that could be re-used: ${heap[orphan]}")
// There are orphaned songs, return the first one we find.
- return orphanCandidates.first()
+ return orphan
}
}
// Nothing to re-use, add this song to the queue
+ logD("No orphan could be re-used")
heap.add(song)
return heap.lastIndex
}
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 76625a038..350ec3daa 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
@@ -24,16 +24,22 @@ import android.view.View
import android.view.ViewGroup
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.*
+import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
+import org.oxycblt.auxio.list.adapter.PlayingIndicatorAdapter
import org.oxycblt.auxio.list.recycler.MaterialDragCallback
import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames
-import org.oxycblt.auxio.util.*
+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
/**
* A [RecyclerView.Adapter] that shows an editable list of queue items.
@@ -82,9 +88,13 @@ class QueueAdapter(private val listener: EditClickListListener) :
// Have to update not only the currently playing item, but also all items marked
// as playing.
+ // 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")
notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_POSITION)
} else {
+ logD("Moved forwards, update items after index")
notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_POSITION)
}
@@ -110,11 +120,15 @@ 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(R.attr.colorSurface)
+ fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface)
elevation = binding.context.getDimen(R.dimen.elevation_normal) * 5
alpha = 0
}
+ /**
+ * Whether this ViewHolder should be full-opacity to represent a future item, or greyed out to
+ * represent a past item. True if former, false if latter.
+ */
var isFuture: Boolean
get() = binding.songAlbumCover.isEnabled
set(value) {
@@ -128,7 +142,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS
LayerDrawable(
arrayOf(
MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply {
- fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
+ fillColor = binding.context.getAttrColorCompat(MR.attr.colorSurface)
elevation = binding.context.getDimen(R.dimen.elevation_normal)
},
background))
@@ -153,7 +167,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.interactBody.isSelected = isActive
- binding.songAlbumCover.isPlaying = isPlaying
+ binding.songAlbumCover.setPlaying(isPlaying)
}
companion object {
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 d2043f373..ddf70b00d 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
@@ -23,6 +23,7 @@ import android.util.AttributeSet
import android.view.View
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 org.oxycblt.auxio.ui.BaseBottomSheetBehavior
@@ -64,7 +65,7 @@ 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(R.attr.colorSurface)
+ fillColor = context.getAttrColorCompat(MR.attr.colorSurface)
elevation = context.getDimen(R.dimen.elevation_normal)
}
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 414ab0eeb..2db007971 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
@@ -33,6 +33,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
/**
* A [ViewBindingFragment] that displays an editable queue.
@@ -104,9 +105,7 @@ class QueueFragment : ViewBindingFragment(), EditClickList
private fun updateQueue(queue: List, index: Int, isPlaying: Boolean) {
val binding = requireBinding()
- // Replace or diff the queue depending on the type of change it is.
queueAdapter.update(queue, queueModel.queueInstructions.consume())
- // Update position in list (and thus past/future items)
queueAdapter.setPosition(index, isPlaying)
// If requested, scroll to a new item (occurs when the index moves)
@@ -122,13 +121,15 @@ 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")
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.
- binding.queueRecycler.scrollToPosition(
- min(queue.lastIndex, scrollTo + (end - start)))
+ val offset = scrollTo + (end - start)
+ logD("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 13099ef7b..5b1edce73 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
@@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
+import org.oxycblt.auxio.util.logD
/**
* A [ViewModel] that manages the current queue state and allows navigation through the queue.
@@ -60,22 +61,26 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
}
override fun onIndexMoved(queue: Queue) {
+ logD("Index moved, synchronizing and scrolling to new position")
_scrollTo.put(queue.index)
_index.value = queue.index
}
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
+ logD("Updating queue display")
_queueInstructions.put(change.instructions)
_queue.value = queue.resolve()
if (change.type != Queue.Change.Type.MAPPING) {
// Index changed, make sure it remains updated without actually scrolling to it.
+ logD("Index changed with queue, synchronizing new position")
_index.value = queue.index
}
}
override fun onQueueReordered(queue: Queue) {
// Queue changed completely -> Replace queue, update index
+ logD("Queue changed completely, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(queue.index)
_queue.value = queue.resolve()
@@ -84,6 +89,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
// Entirely new queue -> Replace queue, update index
+ logD("New playback, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(queue.index)
_queue.value = queue.resolve()
@@ -102,6 +108,10 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
* range.
*/
fun goto(adapterIndex: Int) {
+ if (adapterIndex !in queue.value.indices) {
+ return
+ }
+ logD("Going to position $adapterIndex in queue")
playbackManager.goto(adapterIndex)
}
@@ -115,6 +125,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
if (adapterIndex !in queue.value.indices) {
return
}
+ logD("Removing item $adapterIndex in queue")
playbackManager.removeQueueItem(adapterIndex)
}
@@ -129,6 +140,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")
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 07815dde4..dcd7db42e 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
@@ -29,6 +29,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
+import org.oxycblt.auxio.util.logD
/**
* aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp].
@@ -61,6 +62,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment() {
// settings. After this, the sliders save their own state, so we do not need to
// do any restore behavior.
val preAmp = playbackSettings.replayGainPreAmp
+ logD("Initializing from $preAmp")
binding.withTagsSlider.value = preAmp.with
binding.withoutTagsSlider.value = preAmp.without
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt
index 7bb57376c..ab86651e0 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt
@@ -125,14 +125,22 @@ constructor(
when (playbackSettings.replayGainMode) {
// User wants track gain to be preferred. Default to album gain only if
// there is no track gain.
- ReplayGainMode.TRACK -> gain.track == 0f
+ ReplayGainMode.TRACK -> {
+ logD("Using track strategy")
+ gain.track == 0f
+ }
// User wants album gain to be preferred. Default to track gain only if
// here is no album gain.
- ReplayGainMode.ALBUM -> gain.album != 0f
+ ReplayGainMode.ALBUM -> {
+ logD("Using album strategy")
+ gain.album != 0f
+ }
// User wants album gain to be used when in an album, track gain otherwise.
- ReplayGainMode.DYNAMIC ->
+ ReplayGainMode.DYNAMIC -> {
+ logD("Using dynamic strategy")
playbackManager.parent is Album &&
playbackManager.queue.currentSong?.album == playbackManager.parent
+ }
}
val resolvedGain =
@@ -184,6 +192,7 @@ constructor(
textTags.vorbis[TAG_RG_TRACK_GAIN]
?.run { first().parseReplayGainAdjustment() }
?.let { albumGain = it }
+
// Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the
// adjustment by 256 to get the gain. This is used alongside the base adjustment
// intrinsic to the format to create the normalized adjustment. This is normally the only
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 63fb85ed2..388087653 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
@@ -22,7 +22,7 @@ import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.playback.queue.EditableQueue
+import org.oxycblt.auxio.playback.queue.MutableQueue
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
@@ -305,10 +305,9 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Volatile private var pendingAction: InternalPlayer.Action? = null
@Volatile private var isInitialized = false
- override val queue = EditableQueue()
+ override val queue = MutableQueue()
@Volatile
- override var parent: MusicParent? =
- null // FIXME: Parent is interpreted wrong when nothing is playing.
+ override var parent: MusicParent? = null
private set
@Volatile
override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
@@ -324,6 +323,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized
override fun addListener(listener: PlaybackStateManager.Listener) {
+ logD("Adding $listener to listeners")
if (isInitialized) {
listener.onNewPlayback(queue, parent)
listener.onRepeatChanged(repeatMode)
@@ -335,7 +335,10 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized
override fun removeListener(listener: PlaybackStateManager.Listener) {
- listeners.remove(listener)
+ logD("Removing $listener from listeners")
+ if (!listeners.remove(listener)) {
+ logW("Listener $listener was not added prior, cannot remove")
+ }
}
@Synchronized
@@ -345,6 +348,8 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
return
}
+ logD("Registering internal player $internalPlayer")
+
if (isInitialized) {
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
internalPlayer.seekTo(playerState.calculateElapsedPositionMs())
@@ -364,6 +369,8 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
return
}
+ logD("Unregistering internal player $internalPlayer")
+
this.internalPlayer = null
}
@@ -372,6 +379,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized
override fun play(song: Song?, parent: MusicParent?, queue: List, shuffled: Boolean) {
val internalPlayer = internalPlayer ?: return
+ logD("Playing $song from $parent in ${queue.size}-song queue [shuffled=$shuffled]")
// Set up parent and queue
this.parent = parent
this.queue.start(song, queue, shuffled)
@@ -391,6 +399,9 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
if (!queue.goto(queue.index + 1)) {
queue.goto(0)
play = repeatMode == RepeatMode.ALL
+ logD("At end of queue, wrapping around to position 0 [play=$play]")
+ } else {
+ logD("Moving to next song")
}
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, play)
@@ -399,12 +410,13 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized
override fun prev() {
val internalPlayer = internalPlayer ?: return
-
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (internalPlayer.shouldRewindWithPrev) {
+ logD("Rewinding current song")
rewind()
setPlaying(true)
} else {
+ logD("Moving to previous song")
if (!queue.goto(queue.index - 1)) {
queue.goto(0)
}
@@ -417,26 +429,33 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
override fun goto(index: Int) {
val internalPlayer = internalPlayer ?: return
if (queue.goto(index)) {
+ logD("Moving to $index")
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
+ } else {
+ logW("$index was not in bounds, could not move to it")
}
}
@Synchronized
override fun playNext(songs: List) {
if (queue.currentSong == null) {
+ logD("Nothing playing, short-circuiting to new playback")
play(songs[0], null, songs, false)
} else {
- notifyQueueChanged(queue.playNext(songs))
+ logD("Adding ${songs.size} songs to start of queue")
+ notifyQueueChanged(queue.addToTop(songs))
}
}
@Synchronized
override fun addToQueue(songs: List) {
if (queue.currentSong == null) {
+ logD("Nothing playing, short-circuiting to new playback")
play(songs[0], null, songs, false)
} else {
- notifyQueueChanged(queue.addToQueue(songs))
+ logD("Adding ${songs.size} songs to end of queue")
+ notifyQueueChanged(queue.addToBottom(songs))
}
}
@@ -459,6 +478,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized
override fun reorder(shuffled: Boolean) {
+ logD("Reordering queue [shuffled=$shuffled]")
queue.reorder(shuffled)
notifyQueueReordered()
}
@@ -503,11 +523,13 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
@Synchronized
override fun setPlaying(isPlaying: Boolean) {
+ logD("Updating playing state to $isPlaying")
internalPlayer?.setPlaying(isPlaying)
}
@Synchronized
override fun seekTo(positionMs: Long) {
+ logD("Seeking to ${positionMs}ms")
internalPlayer?.seekTo(positionMs)
}
@@ -529,10 +551,11 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
destructive: Boolean
) {
if (isInitialized && !destructive) {
+ logW("Already initialized, cannot apply saved state")
return
}
val internalPlayer = internalPlayer ?: return
- logD("Restoring state $savedState")
+ logD("Applying state $savedState")
val lastSong = queue.currentSong
parent = savedState.parent
@@ -544,10 +567,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
// it be. Specifically done so we don't pause on music updates that don't really change
// what's playing (ex. playlist editing)
if (lastSong != queue.currentSong) {
+ logD("Song changed, must reload player")
// Continuing playback while also possibly doing drastic state updates is
// a bad idea, so pause.
internalPlayer.loadSong(queue.currentSong, false)
if (queue.currentSong != null) {
+ logD("Seeking to saved position ${savedState.positionMs}ms")
// Internal player may have reloaded the media item, re-seek to the previous
// position
seekTo(savedState.positionMs)
@@ -559,36 +584,42 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
// --- CALLBACKS ---
private fun notifyIndexMoved() {
+ logD("Dispatching index change")
for (callback in listeners) {
callback.onIndexMoved(queue)
}
}
private fun notifyQueueChanged(change: Queue.Change) {
+ logD("Dispatching queue change $change")
for (callback in listeners) {
callback.onQueueChanged(queue, change)
}
}
private fun notifyQueueReordered() {
+ logD("Dispatching queue reordering")
for (callback in listeners) {
callback.onQueueReordered(queue)
}
}
private fun notifyNewPlayback() {
+ logD("Dispatching new playback")
for (callback in listeners) {
callback.onNewPlayback(queue, parent)
}
}
private fun notifyStateChanged() {
+ logD("Dispatching player state change")
for (callback in listeners) {
callback.onStateChanged(playerState)
}
}
private fun notifyRepeatModeChanged() {
+ logD("Dispatching repeat mode change")
for (callback in listeners) {
callback.onRepeatChanged(repeatMode)
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt
index 6c19d42b6..b875636b8 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt
@@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.playback.state.PlaybackStateManager
+import org.oxycblt.auxio.util.logD
/**
* A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to [PlaybackService].
@@ -43,6 +44,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")
intent.component = ComponentName(context, PlaybackService::class.java)
ContextCompat.startForegroundService(context, intent)
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
index 1d44ebc46..9d273ac98 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
@@ -86,6 +86,7 @@ constructor(
* @param intent The [Intent.ACTION_MEDIA_BUTTON] [Intent] to forward.
*/
fun handleMediaButtonIntent(intent: Intent) {
+ logD("Forwarding $intent to MediaButtonReciever")
MediaButtonReceiver.handleIntent(mediaSession, intent)
}
@@ -283,8 +284,10 @@ constructor(
* playback is currently occuring from all songs.
*/
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
+ logD("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")
mediaSession.setMetadata(emptyMetadata)
return
}
@@ -316,12 +319,17 @@ constructor(
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
// These fields are nullable and so we must check first before adding them to the fields.
song.track?.let {
+ logD("Adding track information")
builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, it.toLong())
}
song.disc?.let {
+ logD("Adding disc information")
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.number.toLong())
}
- song.date?.let { builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) }
+ song.date?.let {
+ logD("Adding date information")
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString())
+ }
// We are normally supposed to use URIs for album art, but that removes some of the
// nice things we can do like square cropping or high quality covers. Instead,
@@ -330,6 +338,8 @@ constructor(
song,
object : BitmapProvider.Target {
override fun onCompleted(bitmap: Bitmap?) {
+ this@MediaSessionComponent.logD(
+ "Bitmap loaded, applying media " + "session and posting notification")
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
val metadata = builder.build()
@@ -364,6 +374,7 @@ constructor(
// playback state.
MediaSessionCompat.QueueItem(description, i.toLong())
}
+ logD("Uploading ${queueItems.size} songs to MediaSession queue")
mediaSession.setQueue(queueItems)
}
@@ -384,7 +395,8 @@ constructor(
// Add the secondary action (either repeat/shuffle depending on the configuration)
val secondaryAction =
when (playbackSettings.notificationAction) {
- ActionMode.SHUFFLE ->
+ ActionMode.SHUFFLE -> {
+ logD("Using shuffle MediaSession action")
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INVERT_SHUFFLE,
context.getString(R.string.desc_shuffle),
@@ -393,11 +405,14 @@ constructor(
} else {
R.drawable.ic_shuffle_off_24
})
- else ->
+ }
+ else -> {
+ logD("Using repeat mode MediaSession action")
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INC_REPEAT_MODE,
context.getString(R.string.desc_change_repeat),
playbackManager.repeatMode.icon)
+ }
}
state.addCustomAction(secondaryAction.build())
@@ -415,14 +430,22 @@ constructor(
/** Invalidate the "secondary" action (i.e shuffle/repeat mode). */
private fun invalidateSecondaryAction() {
+ logD("Invalidating secondary action")
invalidateSessionState()
when (playbackSettings.notificationAction) {
- ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.queue.isShuffled)
- else -> notification.updateRepeatMode(playbackManager.repeatMode)
+ ActionMode.SHUFFLE -> {
+ logD("Using shuffle notification action")
+ notification.updateShuffled(playbackManager.queue.isShuffled)
+ }
+ else -> {
+ logD("Using repeat mode notification action")
+ notification.updateRepeatMode(playbackManager.repeatMode)
+ }
}
if (!bitmapProvider.isBusy) {
+ logD("Not loading a bitmap, post the notification")
listener?.onPostNotification(notification)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
index a6410a274..7b9868072 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
@@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback.system
import android.annotation.SuppressLint
import android.content.Context
-import android.os.Build
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import androidx.annotation.DrawableRes
@@ -31,6 +30,7 @@ import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.service.ForegroundServiceNotification
+import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newBroadcastPendingIntent
import org.oxycblt.auxio.util.newMainPendingIntent
@@ -73,18 +73,11 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
* @param metadata The [MediaMetadataCompat] to display in this notification.
*/
fun updateMetadata(metadata: MediaMetadataCompat) {
+ logD("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))
-
- // Starting in API 24, the subtext field changed semantics from being below the
- // content text to being above the title. Use an appropriate field for both.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- // Display description -> Parent in which playback is occurring
- setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION))
- } else {
- setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_ALBUM))
- }
+ setSubText(metadata.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION))
}
/**
@@ -93,6 +86,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
* @param isPlaying Whether playback should be indicated as ongoing or paused.
*/
fun updatePlaying(isPlaying: Boolean) {
+ logD("Updating playing state: $isPlaying")
mActions[2] = buildPlayPauseAction(context, isPlaying)
}
@@ -102,6 +96,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
* @param repeatMode The current [RepeatMode].
*/
fun updateRepeatMode(repeatMode: RepeatMode) {
+ logD("Applying repeat mode action: $repeatMode")
mActions[0] = buildRepeatAction(context, repeatMode)
}
@@ -111,6 +106,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
* @param isShuffled Whether the queue is currently shuffled or not.
*/
fun updateShuffled(isShuffled: Boolean) {
+ logD("Applying shuffle action: $isShuffled")
mActions[0] = buildShuffleAction(context, isShuffled)
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
index 18c948326..848d47b4d 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
@@ -26,7 +26,11 @@ import android.content.IntentFilter
import android.media.AudioManager
import android.media.audiofx.AudioEffect
import android.os.IBinder
-import androidx.media3.common.*
+import androidx.media3.common.AudioAttributes
+import androidx.media3.common.C
+import androidx.media3.common.MediaItem
+import androidx.media3.common.PlaybackException
+import androidx.media3.common.Player
import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.RenderersFactory
@@ -40,6 +44,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings
@@ -52,6 +57,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider
@@ -102,8 +108,8 @@ class PlaybackService :
// Coroutines
private val serviceJob = Job()
- private val restoreScope = CoroutineScope(serviceJob + Dispatchers.Main)
- private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main)
+ private val restoreScope = CoroutineScope(serviceJob + Dispatchers.IO)
+ private val saveScope = CoroutineScope(serviceJob + Dispatchers.IO)
// --- SERVICE OVERRIDES ---
@@ -221,13 +227,16 @@ class PlaybackService :
if (song == null) {
// No song, stop playback and foreground state.
logD("Nothing playing, stopping playback")
+ // For some reason the player does not mark playWhenReady as false when stopped,
+ // which then completely breaks any re-initialization if playback starts again.
+ // So we manually set it to false here.
+ player.playWhenReady = false
player.stop()
-
stopAndSave()
return
}
- logD("Loading ${song.name}")
+ logD("Loading $song")
player.setMediaItem(MediaItem.fromUri(song.uri))
player.prepare()
player.playWhenReady = play
@@ -239,6 +248,7 @@ class PlaybackService :
}
override fun setPlaying(isPlaying: Boolean) {
+ logD("Updating player state to $isPlaying")
player.playWhenReady = isPlaying
}
@@ -250,14 +260,17 @@ class PlaybackService :
if (player.playWhenReady) {
// Mark that we have started playing so that the notification can now be posted.
hasPlayed = true
+ logD("Player has started playing")
if (!openAudioEffectSession) {
// Convention to start an audioeffect session on play/pause rather than
// start/stop
+ logD("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")
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = false
}
@@ -269,6 +282,7 @@ class PlaybackService :
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY)) {
+ logD("Player state changed, must synchronize state")
playbackManager.synchronizeState(this)
}
}
@@ -277,12 +291,15 @@ class PlaybackService :
if (state == Player.STATE_ENDED) {
// Player ended, repeat the current track if we are configured to.
if (playbackManager.repeatMode == RepeatMode.TRACK) {
+ logD("Looping current track")
playbackManager.rewind()
// May be configured to pause when we repeat a track.
if (playbackSettings.pauseOnRepeat) {
+ logD("Pausing track on loop")
playbackManager.setPlaying(false)
}
} else {
+ logD("Track ended, moving to next track")
playbackManager.next()
}
}
@@ -291,12 +308,15 @@ class PlaybackService :
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 occured")
+ logE(error.stackTraceToString())
playbackManager.next()
}
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")
playbackManager.requestAction(this)
}
}
@@ -304,6 +324,7 @@ class PlaybackService :
// --- OTHER FUNCTIONS ---
private fun broadcastAudioEffectAction(event: String) {
+ logD("Broadcasting AudioEffect event: $event")
sendBroadcast(
Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
@@ -329,24 +350,27 @@ class PlaybackService :
// No library, cannot do anything.
?: return false
- logD("Performing action: $action")
-
when (action) {
// Restore state -> Start a new restoreState job
is InternalPlayer.Action.RestoreState -> {
+ logD("Restoring playback state")
restoreScope.launch {
persistenceRepository.readState()?.let {
- playbackManager.applySavedState(it, false)
+ // Apply the saved state on the main thread to prevent code expecting
+ // state updates on the main thread from crashing.
+ withContext(Dispatchers.Main) { playbackManager.applySavedState(it, false) }
}
}
}
// Shuffle all -> Start new playback from all songs
is InternalPlayer.Action.ShuffleAll -> {
+ logD("Shuffling all tracks")
playbackManager.play(
null, null, musicSettings.songSort.songs(deviceLibrary.songs), true)
}
// Open -> Try to find the Song for the given file and then play it from all songs
is InternalPlayer.Action.Open -> {
+ logD("Opening specified file")
deviceLibrary.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play(
song,
@@ -367,8 +391,9 @@ class PlaybackService :
// where changing a setting would cause the notification to appear in an unfriendly
// manner.
if (hasPlayed) {
- logD("Updating notification")
+ logD("Played before, starting foreground state")
if (!foregroundManager.tryStartForeground(notification)) {
+ logD("Notification changed, re-posting")
notification.post()
}
}
@@ -393,6 +418,7 @@ class PlaybackService :
// 3. Some internal framework thing that also handles bluetooth headsets
// Just use ACTION_HEADSET_PLUG.
AudioManager.ACTION_HEADSET_PLUG -> {
+ logD("Received headset plug event")
when (intent.getIntExtra("state", -1)) {
0 -> pauseFromHeadsetPlug()
1 -> playFromHeadsetPlug()
@@ -400,21 +426,41 @@ class PlaybackService :
initialHeadsetPlugEventHandled = true
}
- AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromHeadsetPlug()
+ AudioManager.ACTION_AUDIO_BECOMING_NOISY -> {
+ logD("Received Headset noise event")
+ pauseFromHeadsetPlug()
+ }
// --- AUXIO EVENTS ---
- ACTION_PLAY_PAUSE ->
+ ACTION_PLAY_PAUSE -> {
+ logD("Received play event")
playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
- ACTION_INC_REPEAT_MODE ->
+ }
+ ACTION_INC_REPEAT_MODE -> {
+ logD("Received repeat mode event")
playbackManager.repeatMode = playbackManager.repeatMode.increment()
- ACTION_INVERT_SHUFFLE -> playbackManager.reorder(!playbackManager.queue.isShuffled)
- ACTION_SKIP_PREV -> playbackManager.prev()
- ACTION_SKIP_NEXT -> playbackManager.next()
+ }
+ ACTION_INVERT_SHUFFLE -> {
+ logD("Received shuffle event")
+ playbackManager.reorder(!playbackManager.queue.isShuffled)
+ }
+ ACTION_SKIP_PREV -> {
+ logD("Received skip previous event")
+ playbackManager.prev()
+ }
+ ACTION_SKIP_NEXT -> {
+ logD("Received skip next event")
+ playbackManager.next()
+ }
ACTION_EXIT -> {
+ logD("Received exit event")
playbackManager.setPlaying(false)
stopAndSave()
}
- WidgetProvider.ACTION_WIDGET_UPDATE -> widgetComponent.update()
+ WidgetProvider.ACTION_WIDGET_UPDATE -> {
+ logD("Received widget update event")
+ widgetComponent.update()
+ }
}
}
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 7023e9361..14d4f78a9 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
@@ -25,6 +25,7 @@ import com.google.android.material.button.MaterialButton
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.RippleFixMaterialButton
import org.oxycblt.auxio.util.getInteger
+import org.oxycblt.auxio.util.logD
/**
* A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when
@@ -32,10 +33,17 @@ import org.oxycblt.auxio.util.getInteger
*
* @author Alexander Capehart (OxygenCobalt)
*/
-class AnimatedMaterialButton
-@JvmOverloads
-constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
- RippleFixMaterialButton(context, attrs, defStyleAttr) {
+class AnimatedMaterialButton : RippleFixMaterialButton {
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr)
+
private var currentCornerRadiusRatio = 0f
private var animator: ValueAnimator? = null
@@ -46,10 +54,12 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
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")
updateCornerRadiusRatio(targetRadius)
return
}
+ logD("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 e37cdf660..20bbd9394 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
@@ -81,6 +81,7 @@ 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]")
// 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) {
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 4c1b2c2a7..a800221fb 100644
--- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt
@@ -20,11 +20,25 @@ package org.oxycblt.auxio.search
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
-import org.oxycblt.auxio.list.*
+import org.oxycblt.auxio.list.BasicHeader
+import org.oxycblt.auxio.list.Divider
+import org.oxycblt.auxio.list.Item
+import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
-import org.oxycblt.auxio.list.recycler.*
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.list.recycler.AlbumViewHolder
+import org.oxycblt.auxio.list.recycler.ArtistViewHolder
+import org.oxycblt.auxio.list.recycler.BasicHeaderViewHolder
+import org.oxycblt.auxio.list.recycler.DividerViewHolder
+import org.oxycblt.auxio.list.recycler.GenreViewHolder
+import org.oxycblt.auxio.list.recycler.PlaylistViewHolder
+import org.oxycblt.auxio.list.recycler.SongViewHolder
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+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
/**
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 ee83b5418..a4471ae5e 100644
--- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt
+++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt
@@ -29,11 +29,14 @@ 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
/**
* Implements the fuzzy-ish searching algorithm used in the search view.
*
* @author Alexander Capehart
+ *
+ * TODO: Handle locale changes
*/
interface SearchEngine {
/**
@@ -65,8 +68,9 @@ interface SearchEngine {
class SearchEngineImpl @Inject constructor(@ApplicationContext private val context: Context) :
SearchEngine {
- override suspend fun search(items: SearchEngine.Items, query: String) =
- SearchEngine.Items(
+ override suspend fun search(items: SearchEngine.Items, query: String): SearchEngine.Items {
+ logD("Launching search for $query")
+ return SearchEngine.Items(
songs =
items.songs?.searchListImpl(query) { q, song ->
song.path.name.contains(q, ignoreCase = true)
@@ -75,6 +79,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
artists = items.artists?.searchListImpl(query),
genres = items.genres?.searchListImpl(query),
playlists = items.playlists?.searchListImpl(query))
+ }
/**
* Search a given [Music] list.
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 a7b29b204..0839d12fb 100644
--- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt
@@ -39,10 +39,23 @@ import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.selection.SelectionViewModel
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.Album
+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.Playlist
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.navigation.NavigationViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
-import org.oxycblt.auxio.util.*
+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.navigateSafe
+import org.oxycblt.auxio.util.setFullWidthLookup
/**
* The [ListFragment] providing search functionality for the music library.
@@ -102,6 +115,7 @@ class SearchFragment : ListFragment() {
if (!launchedKeyboard) {
// Auto-open the keyboard when this view is shown
+ this@SearchFragment.logD("Keyboard is not shown yet")
showKeyboard(this)
launchedKeyboard = true
}
@@ -142,6 +156,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")
item.isChecked = true
searchModel.setFilterOptionId(item.itemId)
return true
@@ -176,6 +191,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")
binding.searchRecycler.scrollToPosition(0)
}
}
@@ -220,6 +236,7 @@ class SearchFragment : ListFragment() {
* @param view The [View] to focus the keyboard on.
*/
private fun showKeyboard(view: View) {
+ logD("Launching keyboard")
view.apply {
requestFocus()
postDelayed(200) {
@@ -231,6 +248,7 @@ class SearchFragment : ListFragment() {
/** Safely hide the keyboard from this view. */
private fun hideKeyboard() {
+ logD("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 ec42ca3cb..8a3aa5a1c 100644
--- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt
@@ -33,7 +33,9 @@ import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.Sort
-import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicRepository
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.user.UserLibrary
import org.oxycblt.auxio.playback.PlaybackSettings
@@ -76,6 +78,7 @@ constructor(
override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.deviceLibrary || changes.userLibrary) {
+ logD("Music changed, re-searching library")
search(lastQuery)
}
}
@@ -94,14 +97,13 @@ constructor(
val deviceLibrary = musicRepository.deviceLibrary
val userLibrary = musicRepository.userLibrary
if (query.isNullOrEmpty() || deviceLibrary == null || userLibrary == null) {
- logD("Search query is not applicable.")
+ logD("Cannot search for the current query, aborting")
_searchResults.value = listOf()
return
}
- logD("Searching music library for $query")
-
// Searching is time-consuming, so do it in the background.
+ logD("Searching music library for $query")
currentSearchJob =
viewModelScope.launch {
_searchResults.value =
@@ -119,6 +121,7 @@ constructor(
val items =
if (filterMode == null) {
// A nulled filter mode means to not filter anything.
+ logD("No filter mode specified, using entire library")
SearchEngine.Items(
deviceLibrary.songs,
deviceLibrary.albums,
@@ -126,6 +129,7 @@ constructor(
deviceLibrary.genres,
userLibrary.playlists)
} else {
+ logD("Filter mode specified, filtering library")
SearchEngine.Items(
songs = if (filterMode == MusicMode.SONGS) deviceLibrary.songs else null,
albums = if (filterMode == MusicMode.ALBUMS) deviceLibrary.albums else null,
@@ -139,11 +143,13 @@ constructor(
return buildList {
results.artists?.let {
+ logD("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")
val header = BasicHeader(R.string.lbl_albums)
if (isNotEmpty()) {
add(Divider(header))
@@ -153,6 +159,7 @@ constructor(
addAll(SORT.albums(it))
}
results.playlists?.let {
+ logD("Adding ${it.size} playlists to search results")
val header = BasicHeader(R.string.lbl_playlists)
if (isNotEmpty()) {
add(Divider(header))
@@ -162,6 +169,7 @@ constructor(
addAll(SORT.playlists(it))
}
results.genres?.let {
+ logD("Adding ${it.size} genres to search results")
val header = BasicHeader(R.string.lbl_genres)
if (isNotEmpty()) {
add(Divider(header))
@@ -171,6 +179,7 @@ constructor(
addAll(SORT.genres(it))
}
results.songs?.let {
+ logD("Adding ${it.size} songs to search results")
val header = BasicHeader(R.string.lbl_songs)
if (isNotEmpty()) {
add(Divider(header))
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
index 0bda7bd9d..8288d7443 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt
@@ -108,6 +108,7 @@ class AboutFragment : ViewBindingFragment() {
// 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")
try {
context.startActivity(browserIntent)
} catch (e: ActivityNotFoundException) {
@@ -119,6 +120,7 @@ class AboutFragment : ViewBindingFragment() {
// 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")
@Suppress("DEPRECATION")
val pkgName =
context.packageManager
@@ -128,16 +130,17 @@ class AboutFragment : ViewBindingFragment() {
if (pkgName != null) {
if (pkgName == "android") {
// No default browser [Must open app chooser, may not be supported]
+ logD("No default browser found")
openAppChooser(browserIntent)
- } else
- try {
- browserIntent.setPackage(pkgName)
- startActivity(browserIntent)
- } catch (e: ActivityNotFoundException) {
- // Not a browser but an app chooser
- browserIntent.setPackage(null)
- openAppChooser(browserIntent)
- }
+ } else logD("Opening browser intent")
+ try {
+ browserIntent.setPackage(pkgName)
+ startActivity(browserIntent)
+ } catch (e: ActivityNotFoundException) {
+ // Not a browser but an app chooser
+ browserIntent.setPackage(null)
+ openAppChooser(browserIntent)
+ }
} else {
// No app installed to open the link
context.showToast(R.string.err_no_app)
@@ -151,6 +154,7 @@ class AboutFragment : ViewBindingFragment() {
* @param intent The [Intent] to show an app chooser for.
*/
private fun openAppChooser(intent: Intent) {
+ logD("Opening app chooser for ${intent.action}")
val chooserIntent =
Intent(Intent.ACTION_CHOOSER)
.putExtra(Intent.EXTRA_INTENT, intent)
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 f739f6b29..14065ea47 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt
@@ -107,9 +107,10 @@ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) :
when (preference) {
is IntListPreference -> {
// Copy the built-in preference dialog launching code into our project so
- // we can automatically use the provided preference class.
+ // we can automatically use the provided preference class. The deprecated code
+ // is largely unavoidable.
val dialog = IntListPreferenceDialog.from(preference)
- dialog.setTargetFragment(this, 0)
+ @Suppress("DEPRECATION") dialog.setTargetFragment(this, 0)
dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG)
}
is WrappedDialogPreference -> {
@@ -128,6 +129,7 @@ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) :
}
if (preference is PreferenceCategory) {
+ // Recurse into preference children to make sure they are set up as well
preference.children.forEach(::setupPreference)
return
}
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 0467abcda..fe2bf0066 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt
@@ -30,6 +30,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
+import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.showToast
@@ -64,18 +65,22 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
// do one.
when (preference.key) {
getString(R.string.set_key_ui) -> {
+ logD("Navigating to UI preferences")
findNavController()
.navigateSafe(RootPreferenceFragmentDirections.goToUiPreferences())
}
getString(R.string.set_key_personalize) -> {
+ logD("Navigating to personalization preferences")
findNavController()
.navigateSafe(RootPreferenceFragmentDirections.goToPersonalizePreferences())
}
getString(R.string.set_key_music) -> {
+ logD("Navigating to music preferences")
findNavController()
.navigateSafe(RootPreferenceFragmentDirections.goToMusicPreferences())
}
getString(R.string.set_key_audio) -> {
+ logD("Navigating to audio preferences")
findNavController()
.navigateSafe(RootPreferenceFragmentDirections.goToAudioPreferences())
}
@@ -85,6 +90,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
playbackModel.savePlaybackState { saved ->
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
+ logD("Showing saving confirmation")
if (saved) {
context?.showToast(R.string.lbl_state_saved)
} else {
@@ -94,6 +100,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
}
getString(R.string.set_key_wipe_state) -> {
playbackModel.wipePlaybackState { wiped ->
+ logD("Showing wipe confirmation")
if (wiped) {
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
@@ -105,6 +112,7 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
}
getString(R.string.set_key_restore_state) ->
playbackModel.tryRestorePlaybackState { restored ->
+ logD("Showing restore confirmation")
if (restored) {
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
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 36594b2de..947362fe7 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt
@@ -22,6 +22,7 @@ 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
@@ -73,15 +74,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")
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
+ logD("Registering listener $listener")
this.listener = listener
}
override fun unregisterListener(listener: L) {
if (this.listener !== listener) {
logW("Given listener was not the current listener.")
+ return
}
+ logD("Unregistering listener $listener")
this.listener = null
// No longer have a listener, detach from the preferences instance.
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
@@ -92,6 +97,7 @@ interface Settings {
key: String
) {
// FIXME: Settings initialization firing the listener.
+ logD("Dispatching settings change $key")
onSettingChanged(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 49eda0656..5bcee531f 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,6 +22,7 @@ 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
/**
@@ -33,6 +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")
findNavController().navigateSafe(AudioPreferenceFragmentDirections.goToPreAmpDialog())
}
}
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 8e60a1b33..b0b59d02b 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,6 +26,7 @@ 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
/**
@@ -39,6 +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")
findNavController()
.navigateSafe(MusicPreferenceFragmentDirections.goToSeparatorsDialog())
}
@@ -46,8 +48,10 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
override fun onSetupPreference(preference: Preference) {
if (preference.key == getString(R.string.set_key_cover_mode)) {
+ logD("Configuring cover mode setting")
preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
+ logD("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 8669c52c3..f284e1d69 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,6 +22,7 @@ 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
/**
@@ -32,6 +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")
findNavController()
.navigateSafe(PersonalizePreferenceFragmentDirections.goToTabDialog())
}
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 b1105f123..8d7ba5114 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,6 +28,7 @@ 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
/**
@@ -41,6 +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")
findNavController().navigateSafe(UIPreferenceFragmentDirections.goToAccentDialog())
}
}
@@ -48,20 +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")
preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, value ->
+ logD("Theme changed, recreating")
AppCompatDelegate.setDefaultNightMode(value as Int)
true
}
}
getString(R.string.set_key_accent) -> {
+ logD("Configuring accent setting")
preference.summary = getString(uiSettings.accent.name)
}
getString(R.string.set_key_black_theme) -> {
+ logD("Configuring black theme setting")
preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
val activity = requireActivity()
if (activity.isNight) {
+ logD("Black theme changed in night mode, recreating")
activity.recreate()
}
diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt
index 10fe7f13e..ff54bcfac 100644
--- a/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt
+++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt
@@ -25,8 +25,8 @@ import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceGroupAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.R
import com.google.android.material.divider.BackportMaterialDividerItemDecoration
-import org.oxycblt.auxio.R
/**
* A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly
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 d0cafd9f0..9937d1eac 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt
@@ -28,6 +28,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
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
/**
@@ -82,6 +83,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")
child.apply {
// Set up compat elevation attributes. These are only shown below API 28.
translationZ = context.getDimen(R.dimen.elevation_normal)
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 eb3560271..c8fc0305c 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt
@@ -26,6 +26,7 @@ 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
@@ -60,10 +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")
return false
}
if (consumed != lastConsumed) {
+ logD("Consumed amount changed, re-applying insets")
lastConsumed = consumed
val insets = lastInsets
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 002f49868..afa60e6fd 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt
@@ -30,6 +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
/**
* An [AppBarLayout] that resolves two issues with the default implementation:
@@ -75,6 +76,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
fun expandWithScrollingRecycler() {
setExpanded(true)
(findScrollingChild() as? RecyclerView)?.let {
+ logD("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 657b5c6ca..137e9abe9 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt
@@ -51,6 +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
+ logD("Switching toolbar visibility from $currentlyVisible -> $index")
return animateToolbarsVisibility(currentlyVisible, index).also { currentlyVisible = index }
}
@@ -61,14 +62,15 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
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()
}
- logD(targetDuration)
-
val fromView = getChildAt(from) as Toolbar
val toView = getChildAt(to) as Toolbar
@@ -80,15 +82,13 @@ 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")
setToolbarsAlpha(fromView, toView, targetFromAlpha)
return false
}
- if (fadeThroughAnimator != null) {
- fadeThroughAnimator?.cancel()
- fadeThroughAnimator = null
- }
-
+ logD("Changing toolbar visibility $from -> 0f, $to -> 1f")
+ fadeThroughAnimator?.cancel()
fadeThroughAnimator =
ValueAnimator.ofFloat(fromView.alpha, targetFromAlpha).apply {
duration = targetDuration
@@ -100,7 +100,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
private fun setToolbarsAlpha(from: Toolbar, to: Toolbar, innerAlpha: Float) {
- logD("${to.id == R.id.detail_edit_toolbar} ${1 - innerAlpha}")
from.apply {
alpha = innerAlpha
isInvisible = innerAlpha == 0f
diff --git a/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt
index c1d1074a9..2586745ae 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt
@@ -21,8 +21,8 @@ package org.oxycblt.auxio.ui
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.AttrRes
+import com.google.android.material.R
import com.google.android.material.button.MaterialButton
-import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.fixDoubleRipple
/**
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 13bcbcbf9..dc699498a 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt
@@ -96,6 +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")
listener.onRoundModeChanged()
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt
index 09eb411ef..41138d718 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt
@@ -18,11 +18,12 @@
package org.oxycblt.auxio.ui.accent
+import android.R as SR
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView
-import org.oxycblt.auxio.R
+import com.google.android.material.R as MR
import org.oxycblt.auxio.databinding.ItemAccentBinding
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.util.getAttrColorCompat
@@ -118,9 +119,9 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
binding.accent.apply {
iconTint =
if (isSelected) {
- context.getAttrColorCompat(R.attr.colorSurface)
+ context.getAttrColorCompat(MR.attr.colorSurface)
} else {
- context.getColorCompat(android.R.color.transparent)
+ context.getColorCompat(SR.color.transparent)
}
}
}
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 d4c058077..bf6d9313b 100644
--- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt
+++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt
@@ -23,7 +23,6 @@ import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
-import android.os.Build
import android.util.TypedValue
import android.view.LayoutInflater
import android.widget.Toast
@@ -184,7 +183,7 @@ fun Context.newMainPendingIntent(): PendingIntent =
this,
IntegerTable.REQUEST_CODE,
Intent(this, MainActivity::class.java),
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
+ PendingIntent.FLAG_IMMUTABLE)
/**
* Create a [PendingIntent] that will broadcast the specified command when launched.
@@ -196,4 +195,4 @@ fun Context.newBroadcastPendingIntent(action: String): PendingIntent =
this,
IntegerTable.REQUEST_CODE,
Intent(action).setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
+ PendingIntent.FLAG_IMMUTABLE)
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 71fc880d6..eb24d8093 100644
--- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt
+++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt
@@ -27,6 +27,7 @@ import android.view.WindowInsets
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.AppCompatButton
import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.app.ShareCompat
import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.DrawableCompat
import androidx.navigation.NavController
@@ -35,6 +36,8 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import java.lang.IllegalArgumentException
+import org.oxycblt.auxio.music.MusicParent
+import org.oxycblt.auxio.music.Song
/**
* Get if this [View] contains the given [PointF], with optional leeway.
@@ -263,3 +266,35 @@ fun WindowInsets.replaceSystemBarInsetsCompat(
}
}
}
+
+/**
+ * Share a single [Song].
+ *
+ * @param song
+ */
+fun Context.share(song: Song) = share(listOf(song))
+
+/**
+ * Share all songs in a [MusicParent].
+ *
+ * @param parent The [MusicParent] to share.
+ */
+fun Context.share(parent: MusicParent) = share(parent.songs)
+
+/**
+ * Share an arbitrary list of [Song]s.
+ *
+ * @param songs The [Song]s to share.
+ */
+fun Context.share(songs: List) {
+ if (songs.isEmpty()) return
+ logD("Showing sharesheet for ${songs.size} songs")
+ val builder = ShareCompat.IntentBuilder(this)
+ val mimeTypes = mutableSetOf()
+ for (song in songs) {
+ builder.addStream(song.uri)
+ mimeTypes.add(song.mimeType.fromFormat ?: song.mimeType.fromExtension)
+ }
+
+ builder.setType(mimeTypes.singleOrNull() ?: "audio/*").startChooser()
+}
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 6c1e9e91b..1cb3f599c 100644
--- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
@@ -22,13 +22,12 @@ import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import coil.request.ImageRequest
-import coil.transform.RoundedCornersTransformation
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.ImageSettings
-import org.oxycblt.auxio.image.extractor.SquareFrameTransform
+import org.oxycblt.auxio.image.RoundedCornersTransformation
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.Queue
@@ -76,6 +75,7 @@ constructor(
val repeatMode = playbackManager.repeatMode
val isShuffled = playbackManager.queue.isShuffled
+ logD("Updating widget with new playback state")
bitmapProvider.load(
song,
object : BitmapProvider.Target {
@@ -83,12 +83,15 @@ 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")
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")
context.getDimenPixels(R.dimen.size_corners_medium)
} else {
// User did not enable round mode.
+ logD("Using no corner radius")
0
}
@@ -97,9 +100,7 @@ constructor(
// rounded corners.
builder
.size(getSafeRemoteViewsImageSize(context, 10f))
- .transformations(
- SquareFrameTransform.INSTANCE,
- RoundedCornersTransformation(cornerRadius.toFloat()))
+ .transformations(RoundedCornersTransformation(cornerRadius.toFloat()))
} else {
builder.size(getSafeRemoteViewsImageSize(context))
}
@@ -107,6 +108,7 @@ constructor(
override fun onCompleted(bitmap: Bitmap?) {
val state = PlaybackState(song, bitmap, isPlaying, repeatMode, isShuffled)
+ logD("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 88e3dffa0..153b7bccf 100644
--- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt
+++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt
@@ -34,7 +34,9 @@ import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.ui.UISettings
-import org.oxycblt.auxio.util.*
+import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.logW
+import org.oxycblt.auxio.util.newBroadcastPendingIntent
/**
* The [AppWidgetProvider] for the "Now Playing" widget. This widget shows the current playback
@@ -79,6 +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")
reset(context)
return
}
@@ -99,6 +102,7 @@ class WidgetProvider : AppWidgetProvider() {
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")
diff --git a/app/src/main/res/drawable-v23/ui_item_ripple.xml b/app/src/main/res/drawable-v23/ui_item_ripple.xml
deleted file mode 100644
index 8f0d43cfb..000000000
--- a/app/src/main/res/drawable-v23/ui_item_ripple.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ui_item_bg.xml b/app/src/main/res/drawable/ui_item_bg.xml
index fb0a9dec3..a8a60879f 100644
--- a/app/src/main/res/drawable/ui_item_bg.xml
+++ b/app/src/main/res/drawable/ui_item_bg.xml
@@ -1,5 +1,6 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ui_queue_drag_handle.xml b/app/src/main/res/drawable/ui_queue_drag_handle.xml
deleted file mode 100644
index 2c907c3d6..000000000
--- a/app/src/main/res/drawable/ui_queue_drag_handle.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
\ 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 a7406768f..f2919eacc 100644
--- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml
+++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml
@@ -16,15 +16,16 @@
app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" />
-
+ app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
-
+ app:enablePlaybackIndicator="false"
+ app:enableSelectionBadge="false"
+ tools:ignore="ContentDescription" />
-
+ app:enablePlaybackIndicator="false"
+ app:enableSelectionBadge="false"
+ tools:ignore="ContentDescription" />
-
+ app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
-
+ tools:ignore="ContentDescription" />
-
+ app:enablePlaybackIndicator="false"
+ app:enableSelectionBadge="false"
+ tools:ignore="ContentDescription" />
-
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent" />
diff --git a/app/src/main/res/layout/fragment_playback_bar.xml b/app/src/main/res/layout/fragment_playback_bar.xml
index 4e9e2cd5a..eb95d8968 100644
--- a/app/src/main/res/layout/fragment_playback_bar.xml
+++ b/app/src/main/res/layout/fragment_playback_bar.xml
@@ -6,14 +6,15 @@
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
-
+ app:layout_constraintTop_toTopOf="parent" />
-
+ app:layout_constraintVertical_chainStyle="packed" />
diff --git a/app/src/main/res/layout/item_album_song.xml b/app/src/main/res/layout/item_album_song.xml
index 2505dcf32..3d495dd40 100644
--- a/app/src/main/res/layout/item_album_song.xml
+++ b/app/src/main/res/layout/item_album_song.xml
@@ -16,15 +16,25 @@
with us only overlaying the track number (and other elements) onto it.
-->
-
+ app:layout_constraintTop_toTopOf="parent">
+
+
-
+
@@ -68,9 +78,9 @@
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_medium"
android:textColor="?android:attr/textColorSecondary"
- app:layout_constraintBottom_toBottomOf="@+id/song_track_bg"
+ app:layout_constraintBottom_toBottomOf="@+id/song_track_cover"
app:layout_constraintEnd_toStartOf="@+id/song_menu"
- app:layout_constraintStart_toEndOf="@+id/song_track_bg"
+ app:layout_constraintStart_toEndOf="@+id/song_track_cover"
app:layout_constraintTop_toBottomOf="@+id/song_name"
tools:text="16:16" />
diff --git a/app/src/main/res/layout/item_detail_header.xml b/app/src/main/res/layout/item_detail_header.xml
index 99c4e17d2..56c756f38 100644
--- a/app/src/main/res/layout/item_detail_header.xml
+++ b/app/src/main/res/layout/item_detail_header.xml
@@ -10,14 +10,15 @@
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_mid_medium">
-
+ tools:ignore="ContentDescription" />
-
+ tools:ignore="ContentDescription">
+
+
+
+
@@ -43,7 +55,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="gone"
- app:layout_constraintStart_toEndOf="@+id/disc_icon"
+ app:layout_constraintStart_toEndOf="@+id/disc_cover"
app:layout_constraintTop_toBottomOf="@+id/disc_number"
tools:text="Part 1" />
diff --git a/app/src/main/res/layout/item_editable_song.xml b/app/src/main/res/layout/item_editable_song.xml
index 93fe6f0de..dbda5a44a 100644
--- a/app/src/main/res/layout/item_editable_song.xml
+++ b/app/src/main/res/layout/item_editable_song.xml
@@ -34,7 +34,7 @@
android:layout_height="wrap_content"
android:background="@drawable/ui_item_ripple">
-
+ app:layout_constraintTop_toTopOf="parent" />
-
+ app:enablePlaybackIndicator="false"
+ app:enableSelectionBadge="false">
+
+
+
+
-
+ app:layout_constraintTop_toTopOf="parent" />
-
+ app:layout_constraintTop_toTopOf="parent" />
-
+ app:layout_constraintTop_toTopOf="parent" />
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_album_detail.xml b/app/src/main/res/menu/menu_album_detail.xml
index 34de6eb5e..7cc2b4b79 100644
--- a/app/src/main/res/menu/menu_album_detail.xml
+++ b/app/src/main/res/menu/menu_album_detail.xml
@@ -12,4 +12,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_album_song_actions.xml b/app/src/main/res/menu/menu_album_song_actions.xml
index 256322f3e..7325144c0 100644
--- a/app/src/main/res/menu/menu_album_song_actions.xml
+++ b/app/src/main/res/menu/menu_album_song_actions.xml
@@ -15,4 +15,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_artist_album_actions.xml b/app/src/main/res/menu/menu_artist_album_actions.xml
index c94d6886f..a39b0127e 100644
--- a/app/src/main/res/menu/menu_artist_album_actions.xml
+++ b/app/src/main/res/menu/menu_artist_album_actions.xml
@@ -18,4 +18,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_artist_song_actions.xml b/app/src/main/res/menu/menu_artist_song_actions.xml
index 4b20abd21..78442df43 100644
--- a/app/src/main/res/menu/menu_artist_song_actions.xml
+++ b/app/src/main/res/menu/menu_artist_song_actions.xml
@@ -15,4 +15,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_parent_actions.xml b/app/src/main/res/menu/menu_parent_actions.xml
index 4e6112035..6de2527de 100644
--- a/app/src/main/res/menu/menu_parent_actions.xml
+++ b/app/src/main/res/menu/menu_parent_actions.xml
@@ -15,4 +15,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_parent_detail.xml b/app/src/main/res/menu/menu_parent_detail.xml
index 3a2225ea3..e73829b41 100644
--- a/app/src/main/res/menu/menu_parent_detail.xml
+++ b/app/src/main/res/menu/menu_parent_detail.xml
@@ -9,4 +9,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_playback.xml b/app/src/main/res/menu/menu_playback.xml
index 0c24bb5da..92264d881 100644
--- a/app/src/main/res/menu/menu_playback.xml
+++ b/app/src/main/res/menu/menu_playback.xml
@@ -21,4 +21,7 @@
android:id="@+id/action_song_detail"
android:title="@string/lbl_song_detail"
app:showAsAction="never" />
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_playlist_actions.xml b/app/src/main/res/menu/menu_playlist_actions.xml
index 395ec387b..6a165da6a 100644
--- a/app/src/main/res/menu/menu_playlist_actions.xml
+++ b/app/src/main/res/menu/menu_playlist_actions.xml
@@ -18,4 +18,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_playlist_detail.xml b/app/src/main/res/menu/menu_playlist_detail.xml
index 05a11b388..666629234 100644
--- a/app/src/main/res/menu/menu_playlist_detail.xml
+++ b/app/src/main/res/menu/menu_playlist_detail.xml
@@ -12,4 +12,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_playlist_song_actions.xml b/app/src/main/res/menu/menu_playlist_song_actions.xml
index 28c508681..e55d8e3f6 100644
--- a/app/src/main/res/menu/menu_playlist_song_actions.xml
+++ b/app/src/main/res/menu/menu_playlist_song_actions.xml
@@ -15,4 +15,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_selection_actions.xml b/app/src/main/res/menu/menu_selection_actions.xml
index 568d04a62..e596b97a6 100644
--- a/app/src/main/res/menu/menu_selection_actions.xml
+++ b/app/src/main/res/menu/menu_selection_actions.xml
@@ -21,4 +21,8 @@
android:id="@+id/action_selection_shuffle"
android:title="@string/lbl_shuffle_selected"
app:showAsAction="never"/>
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_song_actions.xml b/app/src/main/res/menu/menu_song_actions.xml
index abb176fb5..b892ba7c3 100644
--- a/app/src/main/res/menu/menu_song_actions.xml
+++ b/app/src/main/res/menu/menu_song_actions.xml
@@ -18,4 +18,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 833d8340e..561b05892 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -40,7 +40,7 @@
Зборнікі
Зборнік
Жывая зборка
- Міксы
+ DJ Міксы
Светлая
Жывы сінгл
Рэмікс сінгла
@@ -48,7 +48,7 @@
Саўндтрэкі
Мікстэйпы
Мікстэйп
- Мікс
+ DJ Мікс
Цёмная
Канцэрт
Выканаўца
@@ -281,4 +281,16 @@
Плэйліст створаны
Паведамленні ў плэйліст
Без трэкаў
+ Выдаліць
+ Выдаліць %s\? Гэтае дзеянне не можа быць адменена.
+ Перайменаваць
+ Перайменаваць плэйліст
+ Выдаліць плэйліст\?
+ Змяніць
+ Плэйліст перайменаван
+ Плэйліст выдален
+ Падзяліцца
+ Няма дыска
+ З\'яўляецца на
+ Рэдагаванне %s
\ 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 900a8e440..956f74699 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -238,9 +238,9 @@
Středník (;)
Lomítko (/)
Remixová kompilace
- Mixy
+ DJ mixy
Živá kompilace
- Mix
+ DJ mix
Varování: Při použití tohoto nastavení mohou být některé značky nesprávně interpretovány jako vícehodnotové. Tento problém můžete vyřešit přidáním zpětného lomítka (\\) před nechtěné oddělovací znaky.
Vyloučit nehudební obsah
Ignorovat zvukové soubory, které nejsou hudbou, například podcasty
@@ -299,4 +299,9 @@
Seznam skladeb přejmenován
Seznam skladeb odstraněn
Přejmenovat seznam skladeb
+ Upravit
+ Také v
+ Žádný disk
+ Úprava seznamu %s
+ Sdílet
\ 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 72c42e4a7..e70e04dfd 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -224,8 +224,8 @@
Wiedergabe anhalten
Live-Kompilation
Remix-Kompilation
- Mixes
- Mix
+ DJ-Mixes
+ DJ-Mix
Mehrfachwert-Trenner
Zeichen ändern, welche mehrere Tag-Werte trennt
Und-Zeichen (&)
@@ -290,4 +290,9 @@
Wiedergabeliste umbenennen
Wiedergabeliste umbenannt
Wiedergabeliste gelöscht
+ Bearbeiten
+ Teilen
+ Keine Disc
+ Erscheint in
+ %s bearbeiten
\ 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 de9f77187..3a7e94380 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -229,8 +229,8 @@
Barra oblicua (/)
Recopilación en directo
Compilaciones de remezclas
- Mezclas
- Mezcla
+ Mezclas del DJ
+ Mezclas del DJ
Ecualizador
Portadas de álbumes
Apagado
@@ -295,4 +295,8 @@
¿Borrar %s\? Esto no se puede deshacer.
¿Borrar la lista de reproducción\?
Editar
+ Editando %s
+ Aparece en
+ Compartir
+ Sin disco
\ No newline at end of file
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 060647abe..8ade1b2d6 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -2,70 +2,296 @@
Újra
- Engedélyezés
- Műfajo
+ Engedélyez
+ Műfajok
Előadók
Albumok
Dalok
- Összes Dalok
+ Összes dal
Keresés
- Filter
+ Szűrő
Összes
- Összes
+ Rendezés
Növekvő
Lejátszás
Keverés
- Most Játszott
- Lejátszási sor
- Lejátszás következőnek
- Lejátszás sorhoz adás
+ Most játszott
+ Várósor
+ Következő lejátszása
+ Várósorhoz ad
Sorbaállítva
- Ugrás az előadóhoz
- Ugrás az albumhoz
- Névjegy
+ Ugrás előadóhoz
+ Ugrás albumhoz
+ Rólunk
Verzió
- Megtekintés GitHubon
- Engedélyek
+ Forráskód
+ Licencek
Beállítások
- Megjelenés
+ Megjelenés és érzés
Téma
Automatikus
Világos
Sötét
- Kiemelés
+ Színséma
Hang
- Működés
+ Testreszabás
Nem található zene
Keresés a könyvtárban…
Sáv %d
- Lejátszás/Szünet
+ Lejátszás/szünet
- Piros
- Rózsaszínű
- Lila
- Sötétlila
+ Vörös
+ Pink
+ Bíbor
+ Mély bíbor
Indigókék
Kék
- Világoskék
+ Sötétkék
Kékeszöld
Zöld
- Világoszöld
- Sárgazöld
+ Sötétzöld
+ Lime
Sárga
Narancs
Barna
Szürke
- - %d Dal
- - %d Dalok
+ - %d dal
+ - %d dal
- - %d Album
- - %d Albumok
+ - %d album
+ - %d album
+ EP-k
+ EP
+ Élő EP
+ Remix EP
+ Név
+ Dátum
+ Csökkenő
+ Kiválasztott lejátszása
+ Új lejátszólista
+ Ismeretlen műfaj
+ Ugrás a következő dalra
+ Ugrás az utolsó dalra
+ Ismétlő mód módosítása
+ Keverés be/ki kapcsolása
+ %s album borítója
+ Visszajátszás
+ Szülő útvonal
+ Mappa eltávolítása
+ Playlistához ad
+ Formátum
+ Wiki
+ OK
+ Méret
+ Bitráta
+ Figyelem: Ennek a beállításnak a használata azt eredményezheti, hogy egyes címkék helytelenül több értéket tartalmaznak. Ezt úgy oldhatja fel, hogy a nem kívánt elválasztó karakterek elé egy backslash-t (\\) helyez.
+ Szünet, amikor egy dal ismétlődik
+ Visszajátszás stratégia
+ Inkább album
+ Inkább hangsáv
+ Ez a mappa nem támogatott
+ %s playlista képe
+ Nincs hangsáv
+ Az aktuális lejátszási állapot mentése most
+ Nincs dal
+ MPEG-1 hang
+ MPEG-4 hang
+ %s műfaj képe
+ %d Hz
+ Többértékű elválasztók
+ Betöltött album: %d
+ Élő összeállítás
+ Remix összeállítás
+ Gyűjtemények
+ DJ Mixek
+ DJ Mix
+ Műfaj
+ Dal tulajdonságai
+ Nincs mappa
+ Tiszta fekete sötét téma használata
+ Dinamikus
+ Album borítók
+ Visszatekerés az előző dalra való ugrás előtt
+ Figyelem: Az előerősítő magas pozitív értékre módosítása egyes hangsávoknál csúcsosodást eredményezhet.
+ Könyvtár
+ Kitartás
+ Lejátszólista
+ Lejátszólisták
+ Töröl
+ Zenelejátszás megtekintése és vezérlése
+ Zenei könyvtár betöltése…
+ Playlistához adva
+ Visszatekerés visszaugrás előtt
+ Ismétlés szünet
+ %s törlése\? Ez nem fordítható vissza.
+ Hangsáv
+ Új dal lejátszásakor a keverési mód bekapcsolva tartása
+ A számokkal vagy \"the\" típusú szavakkal kezdődő nevek helyes rendezése (legjobban angol nyelvű zenékkel működik)
+ Az összes keverése
+ A korábban elmentett lejátszási állapot visszaállítása (ha van ilyen)
+ Visszajátszás előerősítő
+ Az előerősítő a lejátszás során a létező beállítással kerül alkalmazásra
+ Helyezze át ezt a dalt
+ %s előadó fotója
+ Teljes időtartam: %s
+ Kiválasztottak keverése
+ UI vezérlők és viselkedés testreszabása
+ A könyvtárfülek láthatóságának és sorrendjének módosítása
+ A tétel részleteiből történő lejátszáskor
+ Lejátszás albumból
+ A zene és a képek betöltésének vezérlése
+ Képek
+ Időtartam
+ Dal szám
+ Tulajdonságok
+ Mintavétel
+ Zenekönyvtár változás figyelése…
+ Az app téma és színeinek módosítása
+ Lekerekítés
+ Ismétlő mód
+ Viselkedés
+ Emlékezzen a keverésre
+ Inkább album, ha egyet játszik
+ Zene frissítése
+ Mégse
+ Fájl név
+ Egyéni lejátszási sáv művelet
+ A lejátszás mindig akkor indul el, ha a fejhallgató csatlakoztatva van (nem minden eszközön működik)
+ Automatikus újratöltés
+ Cián
+ Lejátszólista átnevezés
+ Átnevez
+ Hozzáadás dátuma
+ Mappák
+ Ment
+ Alaphelyzet
+ Állapot törölve
+ Fejlesztő Alexander Capehart
+ Lejátszás az összes dalból
+ Lejátszás műfajból
+ Tartalom
+ A zenei könyvtár újratöltése, ha változik (állandó értesítést igényel)
+ Zene könyvtárak
+ A zene nem töltődik be a hozzáadott mappákból.
+ Kizárva
+ A zene betöltése sikertelen
+ Plusz (+)
+ Nem találtunk olyan alkalmazást, amely képes lenne kezelni ezt a feladatot
+ Állapot helyreállítás nem lehetséges
+ Ismeretlen előadó
+ %1$s, %2$s
+ Egy új playlista készítése
+ Várósor megnyitás
+ Mozgassa ezt a lapot
+ Keresési lekérdezés törlése
+ Nincs zenelejátszás
+ Egyéni értesítési művelet
+ Nincs dátum
+ Gyors
+ A nem zenei anyagok kizárása
+ Állapot törlés nem lehetséges
+ Állapot mentés nem lehetséges
+ Keverés minden dalból
+ Ogg hang
+ Megjelenítés
+ Hangsáv
+ Szerkeszt
+ Lemez
+ Playlista létrehozva
+ Fekete téma
+ Lekerekített sarkok engedélyezése további UI elemeken (az albumborítók lekerekítése szükséges)
+ Könyvtár fülek
+ Mód
+ Free Lossless Audio Codec (FLAC)
+ Beállítás címkékkel
+ A könyvtárból történő lejátszáskor
+ Lejátszás a megjelenő elemről
+ %d kbps
+ Betöltött műfaj: %d
+ Zene betöltés
+ Zene betöltése
+ Zene könyvtár figyelése
+ Állapot mentve
+ Lejátszás megállítása
+ Egyszerű, racionális zene lejátszó androidra.
+ Matroska hang
+ Album
+ Kislemezek
+ Kislemez
+ Élő album
+ Összeállítások
+ Album remix
+ Összeállítás
+ Hangsáv
+ Élő kislemez
+ Remixelt kislemez
+ Gyűjtemény
+ Élő
+ Keverés
+ Előadó
+ Remixek
+ Hozzáad
+ +%.1f dB
+ Per jel (/)
+
+ - %d előadó
+ - %d előadó
+
+ Equalizer
+ Könyvtári statisztika
+ Playlista átnevezve
+ Playlista törölve
+ Ugrás a következőre
+ Lejátszás előadótól
+ Zene
+ A nem zenei fájlok, például podcastok figyelmen kívül hagyása
+ Több címkeértéket jelölő karakterek konfigurálása
+ Vessző (,)
+ Ponosvessző (;)
+ És (&)
+ Intelligens rendezés
+ Közreműködők elrejtése
+ Csak az albumon közvetlenül feltüntetett előadók megjelenítése (a jól címkézett könyvtárakban működik a legjobban)
+ Ki
+ Magas minőség
+ Hang és lejátszási viselkedés konfigurálása
+ Lejátszás
+ Fejhallgató auto. lejátszás
+ Beállítás címkék nélkül
+ Kezelje, hogy honnan töltsön be zenét
+ Zene újraolvasása
+ A címkék gyorsítótárának törlése és a zenei könyvtár teljes újratöltése (lassabb, de teljesebb)
+ Lejátszási állapot mentése
+ Lejátszási állapot törlése
+ A zene csak az Ön által hozzáadott mappákból töltődik be.
+ A zenei könyvtár újratöltése, lehetőség szerint a gyorstárazott címkék használatával
+ %d kiválasztott
+ %d lemez
+ %d playlista
+ -%.1f dB
+ Zenei könyvtár betöltése… (%1$d/%2$d)
+ Betöltött dal: %d
+ Betöltött előadó: %d
+ Album borító
+ Advanced Audio Coding (AAC)
+ Megjelenik itt,
+ Megoszt
+ Lejátszólista törlése\?
+ Állapot helyreállítva
+ A korábban elmentett lejátszási állapot törlése (ha van ilyen)
+ Lejátszási állapot visszaállítása
+ Tartalmaz
+ Az Auxio engedélyt kér a zenei könyvtár olvasásához
+ Távolítsa el ezt a dalt
+ Auxio ikon
+ Nincs lemez
+ %s szerkesztése
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 19d059cd1..964385c34 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -63,7 +63,7 @@
Comincia la riproduzione ogni volta che le cuffie sono inserite (potrebbe non funzionare su tutti i dispositivi)
Strategia ReplayGain
Preferisci traccia
- Preferisci disco
+ Preferisci album
Preferisci l\'album se in riproduzione
Comportamento
Quando in riproduzione dalla libreria
@@ -85,7 +85,7 @@
Nessuna app può completare questa azione
Questa cartella non è supportata
- Cerca nella tua libreria…
+ Cerca nella libreria…
Canzone %d
Riproduci o pausa
@@ -109,7 +109,7 @@
Genere sconosciuto
Data sconosciuta
Nessuna traccia
- Nessuna canzone riproduzione
+ Nessuna canzone in riproduzione
Rosso
Rosa
@@ -156,10 +156,10 @@
Caricamento libreria musicale… (%1$d/%2$d)
Quando in riproduzione dai dettagli dell\'elemento
Attenzione: impostare valore positivi alti può provocare distorsioni su alcune tracce.
- Regolazione senza tags
+ Regolazione senza tag
Mescola
Mescola tutto
- Regolazione con tags
+ Regolazione con tag
Il pre-amp è applicato alla regolazione esistente durante la riproduzione
Riproduci dall\'elemento mostrato
Gestisci le cartelle da dove caricare la musica
@@ -185,7 +185,7 @@
Directory superiore
Formato
Dimensione
- Velocità di trasmissione
+ Bitrate
Cancella
Pre-amp ReplayGain
Sto monitorando i cambiamenti nella tua libreria musicale…
@@ -277,8 +277,8 @@
Discendente
Playlist
Playlist
- Ignora gli articoli durante l\'ordinamento
- Ignora parole come \"the\" durante l\'ordinamento per nome (funziona meglio con la musica in lingua inglese)
+ Ordinazione intelligente
+ Ordina correttamente i nomi che iniziano con numeri o parole come \"the\" (funziona meglio con i titoli in inglese)
Crea una nuova playlist
Immagine della playlist per %s
Nuova playlist
@@ -287,4 +287,12 @@
Aggiunto alla playlist
Niente canzoni
Playlist %d
+ Elimina
+ Eliminare la playlist\?
+ Rinomina
+ Rinomina playlist
+ Modifica
+ Eliminare %s\? L\'operazione non può essere annullata.
+ Playlist eliminata
+ Playlist rinominata
\ No newline at end of file
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index cd948d6b6..1e1db4996 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -1,9 +1,9 @@
- מוזיקה נטענת
- מוזיקה נטענת
- נסה~י שוב
- מפקח על ספריית המוזיקה
+ מוזיקה בטעינה
+ מוזיקה בטעינה
+ לנסות שוב
+ מתבצעת סריקה בספריית המוזיקה שלך
כל השירים
אלבומים
אלבום חי
@@ -26,8 +26,8 @@
רמיקסים
אומן
אומנים
- ז\'אנר
- ז\'אנרים
+ סוגה
+ סוגות
סינון
הכל
תאריך
@@ -37,87 +37,87 @@
מיון
עולה
יורד
- מנוגן כעת
+ מושמע כעת
איקוולייזר
- נגנ~י
- נגנ~י נבחרים
+ ניגון
+ ניגון הנבחרים
ערבוב
- ערבוב נבחרים
- נגנ~י את הבא
- הוספ~י לתור
+ ערבוב הנבחרים
+ ניגון הבא
+ הוספה לתור
מעבר לאלבום
הצגת מאפיינים
מאפייני שיר
- פורמט
+ תבנית
גודל
- קצב סיביות (ביטרייט)
- קצב דגימה (סאמפל רייט)
- ערבב~י הכל
+ קצב סיביות
+ קצב דגימה
+ ערבוב הכול
אישור
ביטול
שמירה
אתחול
- הוספ~י
- המצב שנשמר
+ הוספה
+ המצב נשמר
גרסה
קוד מקור
ויקי
- רשיונות
+ רישיונות
סטטיסטיקות ספרייה
צפייה ושליטה בהשמעת המוזיקה
טוען את ספריית המוזיקה שלך…
- משגיח על ספריית המוזיקה שלך כדי לאתר שינויים…
- התווסף לרשימה
+ סורק את ספריית המוזיקה שלך כדי לאתר שינויים…
+ התווסף לתור
מפותח על ידי אלכסנדר קייפהארט
- חפש~י בספרייה שלך…
+ חיפוש בספרייה שלך…
מראה ותחושה
- שנה~י את ערכת הנושא והצבעים של היישום
+ שינוי ערכת הנושא והצבעים של היישום
ערכת נושא
בהיר
כהה
- סכמת צבעים
+ ערכת צבעים
ערכת נושא שחורה
- השתמש~י בערכת נושא שחורה לגמרי
- מצב עגול
+ שימוש בערכת נושא שחורה לגמרי
+ מצב מעוגל
התאמה אישית
- התאמ~י את בקרי והתנהגות הממשק
- צג
+ התאמת רכיבים והתנהגות ממשק המשתמש
+ תצוגה
לשוניות ספרייה
פעולת התראות מותאמת אישית
- דלג~י לבא
+ דילוג לבא
מצב חזרה
התנהגות
כאשר מנוגן מהספרייה
כאשר מנוגן מפרטי הפריט
- נגנ~י מהפריט המוצג
- נגנ~י מכל השירים
- נגנ~י מאלבום
- נגנ~י מהאומן
- נגנ~י מז\'אנר
- זכור~י ערבוב
- שמור~י על ערבוב פועל בעת הפעלת שיר חדש
+ ניגון מהפריט המוצג
+ ניגון מכל השירים
+ ניגון מאלבום
+ ניגון מהאומן
+ ניגון מסוגה
+ לזכור ערבוב
+ המשך ערבוב בעת הפעלת שיר חדש
תוכן
טעינה מחדש אוטומטית
- טענ~י את הספריה מחדש בכל פעם שהיא משתנה (דורש התראה קבועה)
- התעלמ~י מקבצי אודיו שאינם מוזיקה, כמו פודקאסטים (הסכתים)
+ לטעון מחדש את הספרייה בכל פעם שהיא משתנה (דורש התראה קבועה)
+ התעלמות מקובצי שמע שאינם מוזיקה, כמו הסכתים
מפרידים רבי-ערכים
פסיק (,)
נקודה-פסיק (;)
פלוס (+)
- ו- (&)
- החבא~י משתפי~ות פעולה
- הראה~י רק אומנים שמצויינים ישירות בקרדיטים של אלבום (עובד באופן הטוב ביותר על ספריות מתוייגות היטב)
- עטיפות אלבומים
+ גם (&)
+ הסתרת שיתופי פעולה
+ הצגת אומנים שמצויינים ישירות בקרדיטים של אלבום בלבד (עובד באופן מיטבי על ספריות מתויגות היטב)
+ עטיפות אלבום
כבוי
מהיר
- אודיו
- השמעה
+ שמע
+ ניגון
ניגון אוטומטי באוזניות
הרצה לאחור לפני דילוג אחורה
- הריצ~י לאחור לפני דילוג לשיר הקודם
+ הרצה לאחור לפני דילוג לשיר הקודם
עצירה בעת חזרה
- עוצמת נגינה מחדש
- העדפ~י אלבום
+ ReplayGain
+ העדפת אלבום
מגבר עוצמת נגינה מחדש
התאמה עם תגיות
מיקסטייפ
@@ -128,35 +128,136 @@
סינגל רמיקס
מיקסים
חיפוש
- אורך
+ משך
שם
רצועה
תור
מעבר לאומן
שם קובץ
- ערבב~י
- מצב שוחזר
- אודות
+ ערבוב
+ המצב שוחזר
+ על אודות
הגדרות
אוטומטי
- הפעל~י פינות מעוגלות ברכיבי ממשק נוספים (עטיפות אלבומים נדרשות להיות מעוגלות)
- שנה~י את הנראות והסדר של לשוניות הספרייה
+ הפעלת פינות מעוגלות ברכיבי ממשק נוספים (עטיפות אלבומים נדרשות להיות מעוגלות)
+ שינוי מראה וסדר לשוניות הספרייה
פעולת סרגל השמעה מותאמת אישית
- קבע~י איך מוזיקה ותמונות נטענים
+ הגדרת טעינת המוזיקה והתמונות
מוזיקה
אי-הכללת תוכן שאינו מוזיקה
- התאמ~י תווים המציינים ערכי תגית מרובים
+ התאמת תווים המציינים ערכי תגית מרובים
קו נטוי (/)
אזהרה: השימוש בהגדרה זו עלול לגרום לחלק מהתגיות להיות מפורשות באופן שגוי כבעלות מספר ערכים. ניתן לפתור זאת על ידי הכנסת קו נטוי אחורי (\\) לפני תווים מפרידים לא רצויים.
איכות גבוהה
- התעלמ~י ממילים כמו \"The\" (\"ה-\") בעת סידור על פי שם (עובד באופן הכי טוב עם מוזיקה בשפה האנגלית)
+ התעלמות ממילים כמו \"The\" (\"ה׳ היידוע\") בעת סידור על פי שם (עובד באופן מיטבי עם מוזיקה בשפה האנגלית)
תמונות
- התאמ~י התנהגות צליל והשמעה
- התחל~י לנגן תמיד ברגע שמחוברות אוזניות (עלול לא לעבוד בכל המערכות)
- עצר~י כאשר שיר חוזר
- העדפ~י רצועה
- אסטרטגיית עוצמת נגינה מחדש
- העדפ~י אלבום אם אחד מופעל
+ הגדרת הצליל והניגון
+ תמיד להתחיל לנגן ברגע שמחוברות אזניות (עלול לא לעבוד בכל המערכות)
+ השהיה עם חזרה על שיר
+ העדפת רצועה
+ אסטרטגיית ReplayGain
+ העדפת אלבום אם אחד מופעל
התאמה ללא תגיות
המגבר מוחל על ההתאמה הקיימת בזמן השמעה
+ רשימת השמעה חדשה
+ הוספה לרשימת השמעה
+ לתת
+ רשימת השמעה
+ רשימות השמעה
+ מחיקה
+ שינוי שם
+ שינוי שם רשימת השמעה
+ למחוק את רשימת ההשמעה\?
+ עריכה
+ לא ניתן לנקות את המצב
+ כתום
+ תיקיות מוזיקה
+ טעינה מחדש של ספריית המוזיקה, במידה וניתן יעשה שימוש במטמון תגיות
+ סריקה מחדש אחר מוזיקה
+ שמירת מצב הנגינה
+ לא ניתן לשמור את המצב
+ Auxio צריך הרשאות על מנת לקרוא את ספריית המוזיקה שלך
+ פתיחת התור
+ סך הכל משך: %s
+ רשימת השמעה %d
+ אומנים טעונים: %d
+ שירים טעונים: %d
+ אלבומים טעונים: %d
+ סוגות טעונות: %d
+ המצב נוקה
+ ספרייה
+ שמירת מצב הנגינה הנוכחי כעת
+ לא נמצא יישום שיכול לטפל במשימה זו
+ אין תיקיות
+ תיקייה זו אינה נתמכת
+ דילוג לשיר האחרון
+ שינוי מצב חזרה
+ ניגון או השהיה
+ דילוג לשיר הבאה
+ הפעלת או כיבוי של מצב ערבוב
+ עטיפת אלבום עבור %s
+ תמונת אומן עבור %s
+ יצירת תמונה עבור %s
+ אומן לא ידוע
+ סוגה לא ידועה
+ אין תאריך
+ אין רצועה
+ אך מוזיקה אינה מתנגנת
+ כחול
+ כחול עמוק
+ אפור
+ דינמי
+ המוזיקה שלך בטעינה (%1$d/%2$d)…
+ דיסק %d
+ ניהול תיקיות המוזיקה לטעינה
+ אין שירים
+ ורוד
+ נוצרה רשימת השמעה
+ תיקיות
+
+ - אומן אחד
+ - שני אומנים
+ - %d אומנים
+
+ לכלול
+ רענון מוזיקה
+ לא נמצאה מוזיקה
+ אירע כשל בטעינה מוזיקה
+ עטיפת אלבום
+ ניקוי מצב הנגינה
+
+ - שיר אחד
+ - שני שירים
+ - %d שירים
+
+
+ - אלבום אחד
+ - שני אלבומים
+ - %d אלבומים
+
+ שונה שם לרשימת השמעה
+ רשימת השמעה נמחקה
+ נוסף לרשימת השמעה
+ ערבוב כל השירים
+ סמל Auxio
+ הסרת תיקייה
+ תמונת רשימת השמעה עבור %s
+ אדום
+ ירוק
+ ניתוב הורה
+ לא ניתן לשחזר את המצב
+ רצועה %d
+ יצירת רשימת השמעה חדשה
+ עצירת הנגינה
+ הסרת שיר זה
+ שיתוף
+ מצב
+ החרגה
+ העברת שיר זה
+ העברת לשונית זו
+ ניקוי תור החיפוש
+ אין דיסק
+ ירוק עמוק
+ צהוב
+ מחיקת %s\? פעולה זו לא ניתן לביטול.
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 62b5683b4..8117c1f3a 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -272,4 +272,16 @@
曲がありません
プレイリスト %d
新しいプレイリストを作成する
+ 消去
+ 名前の変更
+ プレイリストの名前を変更する
+ プレイリストを削除しますか\?
+ プレイリストが削除されました
+ 編集
+ プレイリストの名前が変更されました
+ %sを削除しますか\? この操作は元に戻すことができません。
+ 共有
+ ディスクがありません
+ %sを編集中
+ に登場します
\ 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 c99c87e16..51108beaa 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -172,10 +172,10 @@
Ogg 오디오
마트로스카 오디오
%d Hz
- 믹스
+ DJ믹스
라이브 컴필레이션
리믹스 편집
- 믹스
+ DJ믹스
이퀄라이저
셔플
표시된 항목에서 재생
@@ -283,4 +283,16 @@
재생목록에 추가됨
재생목록 %d
노래 없음
+ 삭제
+ %s를 삭제하시겠습니까\? 이 취소 할 수 없습니다.
+ 이름 바꾸기
+ 재생목록 이름 바꾸기
+ 재생목록을 삭제하시겠습니까\?
+ 편집하다
+ 에 나타납니다
+ 공유하다
+ 재생목록의 이름이 변경됨
+ 디스크 없음
+ 재생목록이 삭제되었습니다
+ %s 수정 중
\ 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 9cc1f6ec5..6f9d600e2 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -128,20 +128,20 @@
Jokių aplankų
Šis aplankas nepalaikomas
Groti arba pristabdyti
- Pereiti į kitą dainą
- Pereiti į paskutinę dainą
+ Peršokti į kitą dainą
+ Peršokti į paskutinę dainą
Mikstapas
Mikstapai
Bibliotekos skirtukai
Keisti bibliotekos skirtukų matomumą ir tvarką
Pageidaujamas albumui
Pageidaujamas albumui, jei vienas groja
- Jokių programų nerasta, kurios galėtų atlikti šią užduotį
- „Auxio“ piktograma
- Perkelti šią eilės dainą
+ Jokią programą nerasta, kuri galėtų atlikti šią užduotį
+ Auxio piktograma
+ Perkelti šią dainą
Perkelti šį skirtuką
Muzikos krovimas nepavyko
- „Auxio“ reikia leidimo skaityti jūsų muzikos biblioteką
+ Auxio reikia leidimo skaityti jūsų muzikos biblioteką
Diskas %d
+%.1f dB
-%.1f dB
@@ -173,7 +173,7 @@
Išvalyti paieškos užklausą
Muzika nebus įkeliama iš pridėtų aplankų jūs pridėsite.
Įtraukti
- Pašalinti šią eilės dainą
+ Pašalinti šią dainą
Groti iš visų dainų
Groti iš parodyto elemento
Groti iš albumo
@@ -210,10 +210,10 @@
Keisti kartojimo režimą
Indigos
%d kbps
- Miksai
- Miksas
+ DJ\'ų Miksai
+ DJ\'o Miksas
Gyvai kompiliacija
- Remikso kompiliacijos
+ Remikso kompiliacija
Pagrindinis aplankas
Išvalyti anksčiau išsaugotą grojimo būseną (jei yra)
Daugiareikšmiai separatoriai
@@ -275,4 +275,22 @@
Grojaraščiai
Grojaraščio vaizdas %s
Sukurti naują grojaraštį
+ Naujas grojaraštis
+ Įtraukti į grojaraštį
+ Įtraukta į grojaraštį
+ Ištrinti
+ Ištrinti %s\? To negalima atšaukti.
+ Pervadinti
+ Pervadinti grojaraštį
+ Ištrinti grojaraštį\?
+ Nėra dainų
+ Redaguoti
+ Bendrinti
+ Pervadintas grojaraštis
+ Grojaraštis %d
+ Sukurtas grojaraštis
+ Ištrintas grojaraštis
+ Nėra disko
+ Redaguojama %s
+ Rodoma
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index c835f1de5..8016b72f2 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -288,4 +288,16 @@
Brak utworów
Dodano do playlisty
Playlista %d
+ Usuwać
+ Usunąć %s\? Tego nie da się cofnąć.
+ Przemianować
+ Przemianować playlistę
+ Usunąć playlistę\?
+ Edytować
+ Pojawia się
+ Udział
+ Zmieniono nazwę playlisty
+ Playlista usunięta
+ Brak dysku
+ Edytowanie %s
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 449dc9662..26f8b19bc 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -23,7 +23,7 @@
Сейчас играет
Играть
Перемешать
- Играть все треки
+ Играть все композиции
Играть альбом
Играть исполнителя
Очередь
@@ -55,7 +55,7 @@
Вкладки
Изменить видимость и порядок вкладок
Скруглённые обложки
- Включить скруглённые края на некоторых элементах интерфейса
+ Скруглить края на некоторых элементах интерфейса
Кнопка в уведомлении
Звук
Воспроизводить при подключении
@@ -63,7 +63,7 @@
Выравнивание громкости
По треку
По альбому
- Предпочитать альбом, если он воспроизводится
+ Предпочитать по альбому, если он воспроизводится
Поведение
При воспроизведении из библиотеки
Запоминать перемешивание
@@ -76,7 +76,7 @@
Запоминать позицию
Запоминать позицию в треке
Обновить музыку
- Обновлять библиотеку, при возможности используя кеш тегов
+ Обновлять библиотеку, при возможности используя кэш тегов
Треков нет
Ошибка чтения библиотеки
@@ -106,13 +106,13 @@
Неизвестный исполнитель
Неизвестный жанр
- Дата отсутствует
+ Дата не указана
Музыка не играет
Красный
Розовый
Пурпурный
- Темно-фиолетовый
+ Тёмно-фиолетовый
Индиго
Синий
Тёмно-синий
@@ -126,7 +126,7 @@
Коричневый
Серый
- Всего композиций: %d
+ Композиций загружено: %d
- %d трек
- %d трека
@@ -152,16 +152,16 @@
Битрейт
Диск
Трек
- Состояние восстановлено
+ Позиция восстановлена
Отмена
- Внимание: Изменение предусилителя на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках.
+ Внимание: Изменение предусиления на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках.
Свойства
Свойства песни
Путь
Формат
Размер
Частота дискретизации
- Предусилитель применяется к существующей настройке во время воспроизведения
+ Предусиление применяется к существующей настройке во время воспроизведения
Статистика библиотеки
Восстановить состояние воспроизведения
Продолжительность
@@ -171,12 +171,12 @@
Сингл
Дата добавления
Синглы
- Предусилитель ReplayGain
+ Предусиление ReplayGain
Исключить
AAC
Очистить состояние воспроизведения
Музыка не будет загружена из указанных папок.
- Укажите, откуда загружать музыку
+ Укажите, откуда надо загружать музыку
%d кбит/с
Автоматическая перезагрузка
MPEG-1
@@ -191,7 +191,7 @@
Концертный альбом
Концертный
Мониторинг изменений в музыкальной библиотеке…
- Состояние очищено
+ Позиция очищена
Папки с музыкой
Включить
Альбом ремиксов
@@ -203,7 +203,7 @@
Сборники
Очистить ранее сохраненное состояние воспроизведения (если есть)
Перезагружать библиотеку при изменении (требует постоянное уведомление)
- -%.1f дБ
+ −%.1f дБ
Жанров загружено: %d
Восстановить предыдущее состояние воспроизведения (если есть)
Режим
@@ -234,20 +234,20 @@
Точка с запятой (;)
Плюс (+)
Амперсанд (&)
- Миксы
+ DJ Миксы
Остановить воспроизведение
- Игнорировать аудиофайлы, которые не являются музыкой, например, подкасты
+ Игнорировать аудиофайлы, которые не являются музыкой. Например, подкасты
Обложки альбомов
- Микс
+ DJ Микс
Изменение по тегам
Изменения без тегов
Отключены
Исходные (Быстрая загрузка)
Повышенного качества (Медленная загрузка)
- Подборка в реальном времени
+ Сборник концертных записей
Пользовательское поведение панели воспроизведения
Пересканировать музыку
- Очистить кеш тегов и полностью обновить библиотеку (медленно, но более эффективно)
+ Очистить кэш тегов и полностью обновить библиотеку (медленно, но более точно)
Сборник ремиксов
- %d исполнитель
@@ -260,7 +260,7 @@
Предупреждение: Использование этой настройки может привести к тому, что некоторые теги будут неправильно интерпретироваться как имеющие несколько значений. Вы можете решить эту проблему, добавив к нежелательным символам-разделителям обратную косую черту (\\).
Воспроизвести выбранное
Перемешать выбранное
- %d Выбрано
+ %d выбрано
Вики
Сбросить
%1$s,%2$s
@@ -271,7 +271,7 @@
Изображения
Библиотека
Настройка темы и цветов приложения
- Настройка элементов управления и поведения пользовательского интерфейса
+ Настроить элементы управления и поведение интерфейса
Управляйте загрузкой музыки и изображений
Настройка звука и поведения при воспроизведении
Воспроизведение
@@ -290,4 +290,16 @@
Без треков
Добавлено в плейлист
Плейлист создан
+ Удалить
+ Переименовать
+ Переименовать плейлист
+ Удалить плейлист\?
+ Плейлист переименован
+ Плейлист удалён
+ Редактировать
+ Удалить %s\? Это действие не может быть отменено.
+ Появляется на
+ Редактирование %s
+ Нет диска
+ Поделиться
\ No newline at end of file
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index bd7e5ac5e..314de547a 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -6,4 +6,23 @@
Једноставан, рационалан музички плејер за android.
Музика се учитава
Учитавање музике
+ Песме
+ Све песме
+ Албуми
+ Ремикси епизода
+ Синглес
+ Сингл
+ Ливе сингл
+ Жива компилација
+ Компилације
+ Компилација
+ Ремик компилација
+ Соундтрацкс
+ Соундтрацк
+ Албум
+ Ливе албум
+ Албум ремикса
+ Ливе епизод
+ Епизоде
+ Епизод
\ 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 e29bd03a1..038e65384 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -95,8 +95,8 @@
Мікстейпи
Виконавець
Тривалість
- Мікси
- Мікс
+ DJ мікси
+ DJ мікс
Заокруглені обкладинки
Завантажено жанрів: %d
Властивості пісні
@@ -204,7 +204,7 @@
Зупинити відтворення
Вільний аудіокодек без втрат (FLAC)
Темно-фіолетовий
- Вибрано %d
+ Вибрано: %d
Завантаження музичної бібліотеки… (%1$d/%2$d)
%d кбіт/с
%d Гц
@@ -295,4 +295,8 @@
Перейменувати список відтворення
Список відтворення перейменовано
Редагувати
+ Поділитись
+ Редагування %s
+ Немає диску
+ З\'являється на
\ No newline at end of file
diff --git a/app/src/main/res/values-v23/styles_core.xml b/app/src/main/res/values-v23/styles_core.xml
deleted file mode 100644
index c3bd17685..000000000
--- a/app/src/main/res/values-v23/styles_core.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
\ 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 60ff2a42b..52243b67a 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -231,8 +231,8 @@
分号 (;)
重混集
实况音乐集
- 混音
- 混音
+ DJ 混音
+ DJ 混音
警告:使用此设置可能会导致某些标签被错误地阐释为具有多个值。要解决这个问题,你可以在不想要的分隔符前加上反斜杠 (\\)。
忽略不是音乐的音频文件,例如播客
排除非音乐
@@ -289,4 +289,8 @@
已重命名播放列表
已删除播放列表
编辑
+ 出现于
+ 分享
+ 无唱片
+ 正在编辑 %s
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 4ca96d8e0..a320f9ea5 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -10,10 +10,15 @@
200
100
-
+
-
-
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 0600e089c..7dffd1917 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -20,13 +20,14 @@
16dp
24dp
+
48dp
56dp
64dp
72dp
24dp
- 32dp
+ 32dp
56dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf01fa3dd..676133e13 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -61,14 +61,16 @@
Mixtape
- Mixes
+ DJ Mixes
- Mix
+ DJ Mix
Live
Remixes
+
+ Appears on
Artist
Artists
@@ -120,6 +122,7 @@
Go to artist
Go to album
View properties
+ Share
Song properties
File name
@@ -331,10 +334,10 @@
Unknown artist
Unknown genre
No date
+ No disc
No track
No songs
No music playing
- There\'s nothing here yet
@@ -385,6 +388,8 @@
%d Selected
+
+ Editing %s
Disc %d
diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml
index bbe18c90c..446c3064f 100644
--- a/app/src/main/res/values/styles_core.xml
+++ b/app/src/main/res/values/styles_core.xml
@@ -50,9 +50,15 @@
- false
- true
+
+ - @color/sel_compat_ripple
- ?attr/colorOnSurfaceVariant
- ?attr/colorPrimary
+
+ - @color/overlay_text_highlight
+ - @color/overlay_text_highlight_inverse
+
- @style/PreferenceTheme.Auxio
- @style/Preference.Auxio
- @style/Preference.Auxio.PreferenceCategory
diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml
index 6e0ee0001..97611ce8d 100644
--- a/app/src/main/res/values/styles_ui.xml
+++ b/app/src/main/res/values/styles_ui.xml
@@ -34,42 +34,38 @@